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 <noreply@anthropic.com>
This commit is contained in:
l0ng-ai
2026-09-28 17:27:51 +08:00
co-authored by Claude
parent 0522b7666d
commit fccb5ea692
2 changed files with 42 additions and 0 deletions
+24
View File
@@ -778,6 +778,12 @@ function terminalTheme(): ITheme {
const theme: Record<string, string> = 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<HTMLElement>(".xterm");
const bar = box?.querySelector<HTMLElement>(".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;
+18
View File
@@ -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;