From fccb5ea69220d92cee6568c22bb073215f788fbe Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:27:51 +0800 Subject: [PATCH] fix(mobile): keep the terminal's scrollbar on screen while panning Panning a pane wider than the phone scrolled xterm's own box sideways with the text. The box was only the view's width, so its vertical scrollbar panned off with the columns and its scrollback stopped taking touches past the first screen. The box now spans every column, and the bar is shifted to the visible right edge as the view pans. The bar is also drawn like the indicator WebKit shows for the pan, thin, rounded and translucent, instead of VS Code's 14px square slider, so the two axes match. Co-Authored-By: Claude --- mobile/src/main.ts | 24 ++++++++++++++++++++++++ mobile/src/style.css | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/mobile/src/main.ts b/mobile/src/main.ts index b6cf4740..17fb4ba8 100644 --- a/mobile/src/main.ts +++ b/mobile/src/main.ts @@ -778,6 +778,12 @@ function terminalTheme(): ITheme { const theme: Record = dark ? { background: "#191b20", foreground: "#e2e5eb", cursor: "#78a8f5", cursorAccent: "#191b20", selectionBackground: "#78a8f555" } : { background: "#ffffff", foreground: "#0f1419", cursor: "#1f6bf0", cursorAccent: "#ffffff", selectionBackground: "#1f6bf033" }; + // xterm's scrollbar, drawn to match the system indicator WebKit gives the + // pan (style.css shapes it). + const bar = dark ? "#ffffff" : "#000000"; + theme.scrollbarSliderBackground = `${bar}59`; + theme.scrollbarSliderHoverBackground = `${bar}59`; + theme.scrollbarSliderActiveBackground = `${bar}73`; NAMES.forEach((n, i) => { theme[n] = ansi[i]; theme[`bright${n[0].toUpperCase()}${n.slice(1)}`] = ansi[i + 8]; @@ -1080,7 +1086,25 @@ function terminalScreen(host: Host, place: Place, pane: PaneView, title: string) zoom.replaceChildren(ico(readable ? "fit" : "zoom")); zoom.ariaLabel = readable ? "Fit the whole width" : "Make the text readable"; follow(); + // The new font size is laid out on the next frame. + requestAnimationFrame(pinScrollbar); }; + // xterm puts its scrollbar at the right of its own box, which pans with + // the text; shift it so it stays at the right of what is on screen. The + // `translate` property, not `transform`, so it never fights xterm's own + // styling of the bar. + const pinScrollbar = () => { + const box = screenEl.querySelector(".xterm"); + const bar = box?.querySelector(".xterm-scrollable-element > .scrollbar.vertical"); + if (!box || !bar) return; + if (!screenEl.classList.contains("panning")) { + bar.style.translate = ""; + return; + } + const edge = screenEl.scrollLeft + screenEl.clientWidth - parseFloat(getComputedStyle(screenEl).paddingRight); + bar.style.translate = `${Math.min(0, edge - (box.offsetLeft + box.offsetWidth))}px 0`; + }; + screenEl.addEventListener("scroll", pinScrollbar, { passive: true }); const follow = () => { if (!screenEl.classList.contains("panning")) return; const cell = screenEl.scrollWidth / cols; diff --git a/mobile/src/style.css b/mobile/src/style.css index 79bab1ac..648384ba 100644 --- a/mobile/src/style.css +++ b/mobile/src/style.css @@ -1064,6 +1064,24 @@ code { overflow-x: auto; } +/* Panning, xterm's box has to span every column: at the view's width it ends + partway across the pane, so its scrollback stops taking touches past that + point and its scrollbar pans off with the text. `pinScrollbar` keeps the + bar at the visible edge. */ +.term.panning .xterm { + width: max-content; +} + +/* xterm's vertical scrollbar is VS Code's: a 14px square slider. Shape it like + the thin rounded indicator WebKit draws for the sideways pan, so the two + axes read as one kind of thing. The colours come from the theme. */ +.term .xterm-scrollable-element > .scrollbar.vertical > .slider { + left: auto !important; + right: 2px; + width: 3px !important; + border-radius: 1.5px; +} + .term .xterm { -webkit-user-select: text; user-select: text;