Jarasłaŭ Viktorčyk: 2 Integrate plugins into gamepadui Restyle plugins page to match Steam Deck 10 files changed, 263 insertions(+), 102 deletions(-)
crankshaft/patches: FAILED in 1m30s [Integrate plugins into gamepadui][0] from [Jarasłaŭ Viktorčyk][1] [0]: https://lists.sr.ht/~avery/public-inbox/patches/35041 [1]: mailto:ugzuzg@gmail.com ✗ #835368 FAILED crankshaft/patches/mirror.yml https://builds.sr.ht/~avery/job/835368 ✓ #835367 SUCCESS crankshaft/patches/linux.yml https://builds.sr.ht/~avery/job/835367
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~avery/public-inbox/patches/35041/mbox | git am -3Learn more about email & git
--- .../menu-manager/deck/menu-injector-deck.css | 13 +--- .../menu-manager/deck/menu-injector-deck.tsx | 70 +++++-------------- injected/src/smm.ts | 18 +++++ injected/src/tab-observer.ts | 5 ++ injected/src/types/global.d.ts | 2 + 5 files changed, 46 insertions(+), 62 deletions(-) diff --git a/injected/src/menu-manager/deck/menu-injector-deck.css b/injected/src/menu-manager/deck/menu-injector-deck.css index 4f78a23..2621f37 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.css +++ b/injected/src/menu-manager/deck/menu-injector-deck.css @@ -1,21 +1,12 @@ [data-smm-menu-page-container] { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: auto; - z-index: 999; - width: 100%; - height: calc(100% - 40px); + height: 100%; + padding-top: calc(0px + var(--basicui-header-height)); display: flex; overflow: auto; background-color: #23262e; - opacity: 0; - - pointer-events: none; } [data-smm-menu-page-container] > ul:first-child { diff --git a/injected/src/menu-manager/deck/menu-injector-deck.tsx b/injected/src/menu-manager/deck/menu-injector-deck.tsx index 67b7497..f6949a9 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.tsx +++ b/injected/src/menu-manager/deck/menu-injector-deck.tsx @@ -34,6 +34,7 @@ export class MenuInjectorDeck implements MenuInjector { this.createPageContainer(); this.addPluginsMenuItem(); this.listenToClickEvents(); + this.listenToNavigationChanges(); } private injectMenuStyles() { @@ -56,21 +57,24 @@ export class MenuInjectorDeck implements MenuInjector { {this.menuPage} </div> ); - - document - .querySelector<HTMLDivElement>(DECK_SELECTORS.mainNavMenu) - ?.appendChild(this.pageContainer); } private listenToClickEvents() { this.smm.IPC.on<{ id: string }>( 'csMenuItemClicked', async ({ data: { id: _id } }) => { - this.openPluginsPage(); + window.coolClass.Navigate('/blank/cs-plugins'); + window.coolClass.CloseSideMenus(); } ); } + private listenToNavigationChanges() { + this.smm.addEventListener('switchToPlugins', () => { + this.openPluginsPage(); + }); + } + private addPluginsMenuItem() { window.csMenuItems = [ { @@ -82,23 +86,11 @@ export class MenuInjectorDeck implements MenuInjector { } private openPluginsPage() { - window.csMenuActiveItem = 'plugins'; - - // Close menu - window.coolClass.OpenSideMenu(); + if (this.pageContainer.isConnected) return; - // Make sure we're on a page where we can show the plugin page - // (we'll navigate back when the page is closed) - if ( - document.querySelector(DECK_SELECTORS.topLevelTransitionSwitch)?.children - ?.length === 0 - ) { - window.coolClass.NavigateToLibraryTab(); - this.enteredWithNavigate = true; - } + window.csMenuActiveItem = 'plugins'; this.showPageContainer(); - this.menuListGamepad = new GamepadHandler({ smm: this.smm, root: this.menuList, @@ -110,44 +102,20 @@ export class MenuInjectorDeck implements MenuInjector { }); } - private async closePluginsPage() { - // Fade out the plugin page before removing it - const animation = await this.pageContainer.animate([{ opacity: 0 }], { - duration: 300, - fill: 'forwards', - }).finished; - this.hidePageContainer(); - animation.cancel(); - + private async closePluginsPage(forward = false) { // Clear active menu item window.csMenuActiveItem = undefined; window.csMenuUpdate?.(); - if (this.enteredWithNavigate) { - window.coolClass.NavigateBackOrOpenMenu(); - } - } + deleteAll('[data-smm-menu-page-container]'); - private showPageContainer() { - this.pageContainer.style.opacity = '1'; - this.pageContainer.style.pointerEvents = 'all'; - const header = document.querySelector<HTMLDivElement>( - DECK_SELECTORS.header - ); - if (header) { - header.style.display = 'none'; - } + if (!forward) window.coolClass.NavigateBackOrOpenMenu(); } - private hidePageContainer() { - this.pageContainer.style.opacity = '0'; - this.pageContainer.style.pointerEvents = 'none'; - const header = document.querySelector<HTMLDivElement>( - DECK_SELECTORS.header - ); - if (header) { - header.style.display = 'flex'; - } + private showPageContainer() { + document + .querySelector<HTMLDivElement>(DECK_SELECTORS.topLevelTransitionSwitch) + ?.appendChild(this.pageContainer); } createMenuItem({ id, label, render }: MenuItem) { @@ -181,7 +149,7 @@ export class MenuInjectorDeck implements MenuInjector { closeActivePage() { this.activePluginGamepad?.cleanup(); this.menuListGamepad?.cleanup(); - this.closePluginsPage(); + this.closePluginsPage(true); } private openPluginPage(render: MenuItem['render']) { diff --git a/injected/src/smm.ts b/injected/src/smm.ts index a375f9b..2689405 100644 --- a/injected/src/smm.ts +++ b/injected/src/smm.ts @@ -29,6 +29,7 @@ type SMMEventType = | typeof eventTypeSwitchToCollections | typeof eventTypeSwitchToAppDetails | typeof eventTypeSwitchToAppProperties + | typeof eventTypeSwitchToPlugins | typeof eventTypeLockScreenOpened | typeof eventTypeLockScreenClosed; @@ -38,6 +39,7 @@ type SMMEvent = | EventSwitchToCollections | EventSwitchToAppDetails | EventSwitchToAppProperties + | EventSwitchToPlugins | EventLockScreenOpened | EventLockScreenClosed; @@ -77,6 +79,13 @@ class EventSwitchToAppProperties extends CustomEvent<AppPropsApp> { } } +const eventTypeSwitchToPlugins = 'switchToPlugins' as const; +class EventSwitchToPlugins extends CustomEvent<void> { + constructor() { + super(eventTypeSwitchToPlugins); + } +} + const eventTypeLockScreenOpened = 'lockScreenOpened' as const; class EventLockScreenOpened extends CustomEvent<void> { constructor() { @@ -275,6 +284,15 @@ export class SMM extends EventTarget { this.dispatchEvent(new EventSwitchToAppProperties(app)); } + /** + * @internal + */ + switchToPlugins() { + info('Switched to plugins'); + + this.dispatchEvent(new EventSwitchToPlugins()); + } + /** * @internal */ diff --git a/injected/src/tab-observer.ts b/injected/src/tab-observer.ts index c66c3eb..d8ea9ee 100644 --- a/injected/src/tab-observer.ts +++ b/injected/src/tab-observer.ts @@ -81,6 +81,11 @@ export const createTabObserver = (smm: SMM, mainLibraryEl: HTMLElement) => { } } + if (location.pathname === '/routes/blank/cs-plugins') { + smm.switchToPlugins(); + return; + } + smm.switchToUnknownPage(); }); diff --git a/injected/src/types/global.d.ts b/injected/src/types/global.d.ts index 0b0c628..f082cbf 100644 --- a/injected/src/types/global.d.ts +++ b/injected/src/types/global.d.ts @@ -68,9 +68,11 @@ declare global { // other/none = close menu OpenSideMenu: (menu?: number) => void; ToggleSideMenu: (menu?: number) => void; + CloseSideMenus: () => void; // Currently open menu (same number values as above) m_eOpenSideMenu?: number; + Navigate: (target: string) => void; NavigateToLibraryTab: () => void; NavigateBackOrOpenMenu: () => void; -- 2.37.3
--- injected/src/gamepad/gamepad.ts | 16 +- .../menu-manager/deck/menu-injector-deck.css | 138 +++++++++++++++--- .../menu-manager/deck/menu-injector-deck.tsx | 98 +++++++++++-- injected/src/ui/buttons.tsx | 2 +- injected/src/ui/index.ts | 3 +- 5 files changed, 217 insertions(+), 40 deletions(-) diff --git a/injected/src/gamepad/gamepad.ts b/injected/src/gamepad/gamepad.ts index a7577c5..fbd2b67 100644 --- a/injected/src/gamepad/gamepad.ts +++ b/injected/src/gamepad/gamepad.ts @@ -79,23 +79,27 @@ export class GamepadHandler { async cleanup() { await this.smm.ButtonInterceptors.removeAfter(gamepadRoot(this.id)); await this.smm.ButtonInterceptors.removeInterceptor(gamepadRoot(this.id)); - this.root - .querySelectorAll('.cs-gp-focus') - .forEach((node) => node.classList.remove('cs-gp-focus')); + this.root.querySelectorAll<HTMLElement>('.cs-gp-focus').forEach((node) => { + node.classList.remove('cs-gp-focus'); + node.blur(); + }); } updateFocused(newFocusPath: string) { const curFocus = this.tree[this.focusPath]; if (curFocus) { curFocus.el.classList.remove('cs-gp-focus'); + curFocus.el.blur(); } - document - .querySelectorAll('.cs-gp-focus') - .forEach((node) => node.classList.remove('cs-gp-focus')); + document.querySelectorAll<HTMLElement>('.cs-gp-focus').forEach((node) => { + node.classList.remove('cs-gp-focus'); + node.blur(); + }); const newFocusEl = this.tree[newFocusPath].el; newFocusEl.classList.add('cs-gp-focus'); + newFocusEl.focus(); this.focusPath = newFocusPath; diff --git a/injected/src/menu-manager/deck/menu-injector-deck.css b/injected/src/menu-manager/deck/menu-injector-deck.css index 2621f37..c2f0150 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.css +++ b/injected/src/menu-manager/deck/menu-injector-deck.css @@ -9,47 +9,110 @@ background-color: #23262e; } -[data-smm-menu-page-container] > ul:first-child { - flex: 1 0 auto; +[data-smm-menu-page-container].animate-in { + transition-property: opacity, transform; + transition-duration: 400ms; + transition-timing-function: cubic-bezier(0, 0, 0.1, 1); + transition-delay: 100ms; +} +[data-smm-menu-page-container].animate-out { + transition-property: opacity, transform; + transition-duration: 100ms; + transition-timing-function: cubic-bezier(0.6, 0, 1, 1); +} +[data-smm-menu-page-container].animate-in-end, +[data-smm-menu-page-container].animate-out-start { + opacity: 1; + transform: scale(1); +} +[data-smm-menu-page-container].animate-out-end, +[data-smm-menu-page-container].animate-in-start { + opacity: 0; + transform: scale(0.9); +} - max-width: 200px; - height: 100%; +[data-smm-menu-page-container] h1 { + font-size: 22px !important; +} + +[data-smm-menu-page-container] h2 { + font-size: 16px; +} + +[data-smm-menu-page-container] > ul:first-child { + min-width: 240px; + max-width: 40%; + height: calc(100% - 16px * 2); margin: 0; - padding: 0; + padding: 16px 0; list-style: none; - background-color: rgba(255, 255, 255, 2%); + + background: #23262e; + border-right: 1px solid #0e141b; + + flex: 1; + display: flex; + flex-direction: column; + position: relative; + overflow-x: hidden; } [data-smm-menu-page-container] > ul:first-child > li { cursor: pointer; border: solid 1px transparent; - - transition: all 200ms; } [data-smm-menu-page-container] > ul:first-child > li.cs-gp-focus { outline: none; - border-color: white; +} + +[data-smm-menu-page-container] + > ul:first-child + > li.cs-gp-focus + > .smm-menu-item-button { + transform: scale(1.1); + background: #3d4450; } .smm-menu-item-button { width: 100%; - padding: 8px 24px; + border: none; + background: none; - background-color: rgba(255, 255, 255, 2%); - color: rgba(255, 255, 255, 90%); + font-weight: normal; font-size: 16px; - border: none; + font-style: normal; + line-height: 20px; + text-align: left; + text-decoration: none; + text-indent: 0; + text-shadow: none; + text-transform: none; + letter-spacing: 0px; + color: #fff; + padding: 10px calc(12px + 1.4vw); + color: #b8bcbf; cursor: pointer; - - transition: all 150ms; + display: flex; + flex-direction: row; + transform: scale(1) rotateX(1deg); + line-height: 22px; + scroll-margin: 2.5em 0; + transition-property: transform, background-color; + transition-duration: 0.32s, 0s; + transition-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83); + transform-origin: 12% 50%; + animation-timing-function: cubic-bezier(0.17, 0.45, 0.14, 0.83); + animation-duration: 0.5s; + animation-fill-mode: forwards; + transform: scale(1) rotateX(1deg); } .smm-menu-item-button:hover { - background-color: rgba(255, 255, 255, 4%); + background-color: #23262e; } .smm-menu-item-button.active { @@ -60,5 +123,46 @@ width: 100%; height: 100%; overflow: auto; - background-color: #23262e; + background: #0e141b; + position: relative; +} + +[data-smm-plugin-page] { + position: absolute; + top: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +[data-smm-plugin-page].animate-out-top, +[data-smm-plugin-page].animate-out-bottom { + transition-property: opacity, transform; + transition-duration: 80ms; + transition-timing-function: cubic-bezier(0.6, 0, 1, 1); +} +[data-smm-plugin-page].animate-in-top, +[data-smm-plugin-page].animate-in-bottom { + transition-property: opacity, transform; + transition-duration: 320ms; + transition-timing-function: cubic-bezier(0, 0, 0.1, 1); + transition-delay: 80ms; +} + +[data-smm-plugin-page].animate-out-top-start, +[data-smm-plugin-page].animate-out-bottom-start, +[data-smm-plugin-page].animate-in-top-end, +[data-smm-plugin-page].animate-in-bottom-end { + opacity: 1; + transform: translateY(0); +} +[data-smm-plugin-page].animate-out-bottom-end, +[data-smm-plugin-page].animate-in-top-start { + opacity: 0; + transform: translateY(-8%); +} +[data-smm-plugin-page].animate-out-top-end, +[data-smm-plugin-page].animate-in-bottom-start { + opacity: 0; + transform: translateY(8%); } diff --git a/injected/src/menu-manager/deck/menu-injector-deck.tsx b/injected/src/menu-manager/deck/menu-injector-deck.tsx index f6949a9..f8a82a8 100644 --- a/injected/src/menu-manager/deck/menu-injector-deck.tsx +++ b/injected/src/menu-manager/deck/menu-injector-deck.tsx @@ -8,6 +8,21 @@ import styles from './menu-injector-deck.css'; // @use-dom-chef +const animate = ( + node: HTMLElement, + animationName: string, + duration: number +) => { + node.classList.add(animationName, `${animationName}-start`); + setTimeout(() => { + node.classList.remove(`${animationName}-start`); + node.classList.add(`${animationName}-end`); + }, 0); + setTimeout(() => { + node.classList.remove(animationName, `${animationName}-end`); + }, duration); +}; + export class MenuInjectorDeck implements MenuInjector { private readonly smm: SMM; private readonly menuManager: MenuManager; @@ -18,17 +33,21 @@ export class MenuInjectorDeck implements MenuInjector { // List of plugins in page private menuList!: HTMLUListElement; private menuItemNodes: Record<string, HTMLLIElement>; + private menuItems: MenuItem[]; // Root to render plugin contents private menuPage!: HTMLDivElement; private menuListGamepad?: GamepadHandler; private activePluginGamepad?: GamepadHandler; + private activePluginId?: string; + private activePluginEl?: HTMLDivElement; constructor(smm: SMM, menuManager: MenuManager) { this.smm = smm; this.menuManager = menuManager; this.enteredWithNavigate = false; this.menuItemNodes = {}; + this.menuItems = []; this.injectMenuStyles(); this.createPageContainer(); @@ -103,11 +122,13 @@ export class MenuInjectorDeck implements MenuInjector { } private async closePluginsPage(forward = false) { + this.activePluginId = undefined; + this.activePluginEl = undefined; // Clear active menu item window.csMenuActiveItem = undefined; window.csMenuUpdate?.(); - deleteAll('[data-smm-menu-page-container]'); + this.hidePageContainer(); if (!forward) window.coolClass.NavigateBackOrOpenMenu(); } @@ -116,11 +137,19 @@ export class MenuInjectorDeck implements MenuInjector { document .querySelector<HTMLDivElement>(DECK_SELECTORS.topLevelTransitionSwitch) ?.appendChild(this.pageContainer); + animate(this.pageContainer, 'animate-in', 600); } - createMenuItem({ id, label, render }: MenuItem) { + private hidePageContainer() { + animate(this.pageContainer, 'animate-out', 100); + setTimeout(() => deleteAll('[data-smm-menu-page-container]'), 100); + } + + createMenuItem(menuItem: MenuItem) { + const { id, label, render } = menuItem; const newMenuItem = dcCreateElement<HTMLLIElement>( <li + tabIndex={0} smm-menu-item={id} data-cs-gp-in-group="root" data-cs-gp-item={id} @@ -128,7 +157,10 @@ export class MenuInjectorDeck implements MenuInjector { Object.values(this.menuItemNodes).length === 0 ? 'true' : 'false' } onClick={() => { - this.openPluginPage(render); + this.openPluginPage(id, render, true); + }} + onFocus={() => { + this.openPluginPage(id, render); }} > <button className="smm-menu-item-button" data-smm-menu-item-button={id}> @@ -139,11 +171,13 @@ export class MenuInjectorDeck implements MenuInjector { this.menuItemNodes[id] = newMenuItem; this.menuList.appendChild(newMenuItem); + this.menuItems.push(menuItem); } removeMenuItem(id: string) { this.menuItemNodes[id]?.remove(); delete this.menuItemNodes[id]; + this.menuItems = this.menuItems.filter((item) => item.id !== id); } closeActivePage() { @@ -152,17 +186,51 @@ export class MenuInjectorDeck implements MenuInjector { this.closePluginsPage(true); } - private openPluginPage(render: MenuItem['render']) { - [...this.menuPage.children].forEach((node) => node.remove()); - render(this.smm, this.menuPage); - this.activePluginGamepad = new GamepadHandler({ - smm: this.smm, - root: this.menuPage, - rootExitCallback: () => { - this.activePluginGamepad?.cleanup(); - [...this.menuPage.children].forEach((node) => node.remove()); - this.menuListGamepad?.updateFocused(this.menuListGamepad.focusPath); - }, - }); + private openPluginPage( + id: string, + render: MenuItem['render'], + navigateIntoView = false + ) { + if (this.activePluginId !== id || !this.activePluginEl) { + const oldIndex = this.menuItems.findIndex( + (item) => item.id === this.activePluginId + ); + const newIndex = this.menuItems.findIndex((item) => item.id === id); + const animationDirection = newIndex > oldIndex ? 'bottom' : 'top'; + + ([...this.menuPage.children] as HTMLElement[]).forEach( + (node: HTMLElement) => { + animate(node, `animate-out-${animationDirection}`, 80); + setTimeout(() => { + // reset scroll after out animation is over + this.menuPage.scrollTop = 0; + node.remove(); + }, 80); + } + ); + + const shouldAnimate = this.activePluginId != null; + this.activePluginId = id; + + this.activePluginEl = dcCreateElement<HTMLDivElement>( + <div data-smm-plugin-page /> + ); + render(this.smm, this.activePluginEl); + this.menuPage.appendChild(this.activePluginEl); + + if (shouldAnimate) + animate(this.activePluginEl, `animate-in-${animationDirection}`, 500); + } + + if (navigateIntoView) { + this.activePluginGamepad = new GamepadHandler({ + smm: this.smm, + root: this.activePluginEl, + rootExitCallback: () => { + this.activePluginGamepad?.cleanup(); + this.menuListGamepad?.updateFocused(this.menuListGamepad.focusPath); + }, + }); + } } } diff --git a/injected/src/ui/buttons.tsx b/injected/src/ui/buttons.tsx index 6103d97..dfd89d5 100644 --- a/injected/src/ui/buttons.tsx +++ b/injected/src/ui/buttons.tsx @@ -25,7 +25,7 @@ const buttonStyles = ` } .cs-button:focus { - outline: none; + outline: white solid 2px; } .cs-button:focus-visible { diff --git a/injected/src/ui/index.ts b/injected/src/ui/index.ts index 0eb5849..1eb9864 100644 --- a/injected/src/ui/index.ts +++ b/injected/src/ui/index.ts @@ -9,7 +9,8 @@ export const registerCustomElements = async () => { // TODO: load these styles from a .css file // Had an issue with esbuild loading the text from the file let styleSheetContents = ` -.cs-gp-focus { +.cs-gp-focus, +.cs-gp-focus:focus { outline: solid 2px white; } `; -- 2.37.3
builds.sr.ht <builds@sr.ht>crankshaft/patches: FAILED in 1m30s [Integrate plugins into gamepadui][0] from [Jarasłaŭ Viktorčyk][1] [0]: https://lists.sr.ht/~avery/public-inbox/patches/35041 [1]: mailto:ugzuzg@gmail.com ✗ #835368 FAILED crankshaft/patches/mirror.yml https://builds.sr.ht/~avery/job/835368 ✓ #835367 SUCCESS crankshaft/patches/linux.yml https://builds.sr.ht/~avery/job/835367