From cfa9ca7e15d2bb13460a7227575b075600fda29a Mon Sep 17 00:00:00 2001 From: Pierre Kisters <1524059+lhns@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:06:24 +0200 Subject: [PATCH] launcher-menu grid: scroll indicators follow the element that scrolls With maxRows capping the scroller, the inner grid panel scrolls (as in Steam's own design, where the outer element holds the fixed edge shadows). The fade/shadow state is now read from whichever of the two overflows and updated on the panel's scroll events too, so the shadows show only where more content is. --- modules/launcher-menu/grid/patch.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/modules/launcher-menu/grid/patch.js b/modules/launcher-menu/grid/patch.js index 1322229..5a32805 100644 --- a/modules/launcher-menu/grid/patch.js +++ b/modules/launcher-menu/grid/patch.js @@ -39,7 +39,10 @@ // shadows (box-shadow in the background colour at the top and // bottom edge), shown by Steam's can-scroll-up/-down classes, which // go stale the same way. The same state goes in data-sfui-shadow -// on the scroller and sets the pseudo-elements' opacity. +// on the scroller and sets the pseudo-elements' opacity. Once +// maxRows caps the scroller, the inner panel is what scrolls (as in +// Steam's own design; the outer element keeps the shadows fixed), +// so the state is read from whichever of the two overflows. // Steam's gamepad navigation derives a panel's layout from its computed // style (display: grid), so up/down/left/right move between tiles. // With the pinned-desktop patch, its pinned row (.sfui-pinned-desktop, @@ -51,7 +54,7 @@ // which removes markers, stylesheets, observers and the timer. ((find, sigs, opts) => { const NAME = 'launcher-menu-grid'; - const VERSION = 5; + const VERSION = 6; const COLS = opts.columns; const ROWS = opts.maxRows ?? null; if (!(Number.isInteger(COLS) && COLS >= 1) || !(ROWS === null || (Number.isInteger(ROWS) && ROWS >= 1))) @@ -191,11 +194,16 @@ ${PINNED} > [role=button] * { flex-grow: 0 !important; text-align: center !impor // Scroll indicator state from the actual scroll position (Steam's // thresholds), for both the mask fade and the edge shadows. + // The element that actually scrolls: the inner panel once maxRows caps + // the scroller, otherwise the scroller. + const scrolling = (sc, panel) => + panel && (panel.scrollHeight > panel.clientHeight + 1 || panel.scrollTop > 0) ? panel : sc; const fade = (sc) => { const r = regions.get(sc); if (!r) return; - const top = sc.scrollTop > 1; - const bottom = sc.scrollHeight - sc.scrollTop > sc.clientHeight + 1; + const el = scrolling(sc, r.panel); + const top = el.scrollTop > 1; + const bottom = el.scrollHeight - el.scrollTop > el.clientHeight + 1; const v = top && bottom ? 'both' : top ? 'top' : bottom ? 'bottom' : 'none'; if (sc.getAttribute(SHADOW) !== v) sc.setAttribute(SHADOW, v); if (r.fade && r.fade.getAttribute(FADE) !== v) r.fade.setAttribute(FADE, v); @@ -234,12 +242,16 @@ ${PINNED} > [role=button] * { flex-grow: 0 !important; text-align: center !impor const ro = docs.get(d)?.ro; const r = regions.get(sc); if (r) { - if (r.panel !== panel) { ro?.unobserve(r.panel); r.panel = panel; ro?.observe(panel); } + if (r.panel !== panel) { + ro?.unobserve(r.panel); r.panel.removeEventListener('scroll', r.onScroll); + r.panel = panel; ro?.observe(panel); panel.addEventListener('scroll', r.onScroll, { passive: true }); + } return; } const onScroll = guard(() => fade(sc)); regions.set(sc, { d, list, panel, fade: fadeOf(sc, list), onScroll }); sc.addEventListener('scroll', onScroll, { passive: true }); + panel.addEventListener('scroll', onScroll, { passive: true }); ro?.observe(sc); ro?.observe(panel); }; @@ -249,6 +261,7 @@ ${PINNED} > [role=button] * { flex-grow: 0 !important; text-align: center !impor if (!r) return; regions.delete(sc); sc.removeEventListener('scroll', r.onScroll); + r.panel.removeEventListener('scroll', r.onScroll); const ro = docs.get(r.d)?.ro; ro?.unobserve(sc); ro?.unobserve(r.panel);