From 01144e94969d45136696362e022a029bdf95a94d Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Tue, 30 Jun 2026 18:58:47 -0500 Subject: [PATCH 1/3] Fix spurious horizontal scroll on the output column track MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The font is fitted from a measured 200-char glyph run so exactly MinColumns (78) columns span the content box. But the min-width track that holds those columns used the CSS `ch` unit (`calc(var(--sc-cols) * 1ch)`), whose single- glyph advance rounds ~2px wider than the averaged run. So `78ch` exceeded the box by a couple px and .sc-output-area's overflow-x:auto showed a permanent tiny horizontal scrollbar — you had to center the scroll to see all 78, and long lines scrolled instead of wrapping at the column edge. measureGrid now publishes the track width in pixels (--sc-cols-width), measured from a real run of `cols` glyphs at the fitted size — the same basis the font was fitted to. .sc-output min-width uses that px value instead of 1ch. In the fitting case the track stays inside the box (no scroll; lines wrap at column 78); it only exceeds the box and scrolls when the screen genuinely can't fit the columns at the minimum font, which is the intended fallback. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp --- src/SharpClient.UI/wwwroot/app.css | 12 +++++++----- src/SharpClient.UI/wwwroot/sc-interop.js | 9 +++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/SharpClient.UI/wwwroot/app.css b/src/SharpClient.UI/wwwroot/app.css index fe8ac03..ed7781f 100644 --- a/src/SharpClient.UI/wwwroot/app.css +++ b/src/SharpClient.UI/wwwroot/app.css @@ -74,12 +74,14 @@ body { /* ── OutputView component classes (.sc-output / .sc-line) ──────── */ .sc-output { display: block; - /* Hold a floor of --sc-cols character columns (set by measureGrid alongside --out-fs). On a - screen too narrow to fit them at the minimum font, the output scrolls horizontally instead of - wrapping the server's NAWS-width lines — so what the player sees matches what the server wrapped - to. font-size must equal the line font so 1ch is the real column width. */ + /* Hold a floor of --sc-cols character columns. The track width is published in pixels by + measureGrid as --sc-cols-width, measured from a real glyph run on the SAME basis the font was + fitted to — NOT the CSS `ch` unit, whose single-glyph rounding ran ~2px wider than the fit and + produced a spurious horizontal scrollbar even when the columns fit. In the fitting case the + track stays inside the box, so lines wrap at the column edge with no scroll; only on a screen + too narrow to fit the columns at the minimum font does it exceed the box and scroll. */ font-size: var(--out-fs); - min-width: calc(var(--sc-cols, 0) * 1ch); + min-width: var(--sc-cols-width, 0px); } .sc-line { diff --git a/src/SharpClient.UI/wwwroot/sc-interop.js b/src/SharpClient.UI/wwwroot/sc-interop.js index 7d2ca49..d5519da 100644 --- a/src/SharpClient.UI/wwwroot/sc-interop.js +++ b/src/SharpClient.UI/wwwroot/sc-interop.js @@ -143,8 +143,17 @@ export function measureGrid(element, targetCols, minFont, maxFont) { cols = clampGrid(Math.floor(contentW / (advanceRatio * fontPx))); } + // Publish the column-track width in PIXELS, measured on the same basis the font was fitted to + // (a real run of `cols` glyphs). The CSS `ch` unit can't be used for the track: its single-glyph + // advance rounds a couple px wider than this averaged run, which pushed `cols * 1ch` past the + // content box and produced a spurious horizontal scrollbar even though the columns fit. Measured + // px keeps the track <= the box in the fitting case (no scroll, lines wrap at the column edge) and + // only exceeds it when the screen genuinely can't fit `cols` at the min font (then it scrolls). + const trackW = runWidth(fontPx, '0'.repeat(cols)); + element.style.setProperty('--out-fs', fontPx + 'px'); element.style.setProperty('--sc-cols', String(cols)); + element.style.setProperty('--sc-cols-width', trackW + 'px'); return { cols, rows: clampGrid(Math.floor(contentH / (lineRatio * fontPx))) }; } From 91c0925530bd19a5cccbfe8f581a4406cb0804d4 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Tue, 30 Jun 2026 19:14:34 -0500 Subject: [PATCH 2/3] Stop the output area from scrolling horizontally; wrap instead MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier ch→px change was a no-op: verified in-browser that for JetBrains Mono `78ch` and the measured 78-column run are sub-pixel identical, so the column track was never the ~2px too wide I'd assumed. The horizontal scroll came from the page *allowing* it — `.sc-output` had a forced `min-width` column track and `.sc-output-area` had `overflow-x: auto`, so any excess width (sub-pixel rounding, or Android WebView font-boosting inflating text past the size the grid was fitted to) became a left/right scrollbar you had to center to read all 78 columns. Fix, matching the intent (wrap, never scroll sideways): - Remove the `min-width` track from `.sc-output` — nothing forces the block wider than its box, so lines wrap at the fitted column width. - `.sc-output-area` overflow-x: auto → hidden — the page can no longer scroll horizontally; any residual excess is clipped, not scrolled. - Add `text-size-adjust: 100%` on html/body to stop the Android WebView from auto-inflating text (the device-only reason real lines exceeded the fitted grid), and let `.sc-link` buttons wrap so a long command link can't overflow. - Revert the unused `--sc-cols-width` JS. Verified in Chrome across 360/393/412px widths with a long link + simulated boosting: no horizontal scrollbar; content wraps. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp --- src/SharpClient.UI/wwwroot/app.css | 28 +++++++++++++++--------- src/SharpClient.UI/wwwroot/sc-interop.js | 9 -------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/SharpClient.UI/wwwroot/app.css b/src/SharpClient.UI/wwwroot/app.css index ed7781f..90e8308 100644 --- a/src/SharpClient.UI/wwwroot/app.css +++ b/src/SharpClient.UI/wwwroot/app.css @@ -31,6 +31,11 @@ html, body { height: 100%; width: 100%; overflow-x: hidden; + /* Stop the Android WebView from auto-inflating ("boosting") text in block containers based on the + system font-size setting — it renders lines wider than the size measureGrid fitted the column + grid to, breaking the NAWS column math and forcing horizontal overflow. Keep text at the CSS px. */ + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; } body { @@ -74,14 +79,12 @@ body { /* ── OutputView component classes (.sc-output / .sc-line) ──────── */ .sc-output { display: block; - /* Hold a floor of --sc-cols character columns. The track width is published in pixels by - measureGrid as --sc-cols-width, measured from a real glyph run on the SAME basis the font was - fitted to — NOT the CSS `ch` unit, whose single-glyph rounding ran ~2px wider than the fit and - produced a spurious horizontal scrollbar even when the columns fit. In the fitting case the - track stays inside the box, so lines wrap at the column edge with no scroll; only on a screen - too narrow to fit the columns at the minimum font does it exceed the box and scroll. */ + /* No forced min-width track: the font is fitted (measureGrid) so --sc-cols columns span the + content box, so the block just fills its container and lines wrap at the column edge. A + min-width track only ever pushed the block WIDER than the box (by sub-pixel/rounding or Android + WebView font-boosting), which .sc-output-area then turned into a horizontal scrollbar — the bug + this removes. Wrapping is preferred over horizontal scrolling. */ font-size: var(--out-fs); - min-width: var(--sc-cols-width, 0px); } .sc-line { @@ -387,9 +390,9 @@ body { .sc-output-area { flex: 1; overflow-y: auto; - /* Horizontal scroll when the --sc-cols track is wider than the viewport (narrow screen at min - font), so NAWS-width lines are shown intact rather than wrapped. */ - overflow-x: auto; + /* Never scroll horizontally — output wraps at the fitted column width instead. Any sub-pixel or + font-boosting excess is clipped rather than turned into a left/right scrollbar. */ + overflow-x: hidden; background: var(--outbg); padding: 0.75rem 1rem; } @@ -583,6 +586,11 @@ body { color: var(--acc2); text-decoration: underline; text-underline-offset: 2px; + /* A clickable command link is an inline-block button; let its text wrap (and break long tokens) + so it can never be the one element that refuses to wrap and forces horizontal overflow. */ + white-space: normal; + overflow-wrap: anywhere; + text-align: left; } .sc-link:hover { color: var(--acc); diff --git a/src/SharpClient.UI/wwwroot/sc-interop.js b/src/SharpClient.UI/wwwroot/sc-interop.js index d5519da..7d2ca49 100644 --- a/src/SharpClient.UI/wwwroot/sc-interop.js +++ b/src/SharpClient.UI/wwwroot/sc-interop.js @@ -143,17 +143,8 @@ export function measureGrid(element, targetCols, minFont, maxFont) { cols = clampGrid(Math.floor(contentW / (advanceRatio * fontPx))); } - // Publish the column-track width in PIXELS, measured on the same basis the font was fitted to - // (a real run of `cols` glyphs). The CSS `ch` unit can't be used for the track: its single-glyph - // advance rounds a couple px wider than this averaged run, which pushed `cols * 1ch` past the - // content box and produced a spurious horizontal scrollbar even though the columns fit. Measured - // px keeps the track <= the box in the fitting case (no scroll, lines wrap at the column edge) and - // only exceeds it when the screen genuinely can't fit `cols` at the min font (then it scrolls). - const trackW = runWidth(fontPx, '0'.repeat(cols)); - element.style.setProperty('--out-fs', fontPx + 'px'); element.style.setProperty('--sc-cols', String(cols)); - element.style.setProperty('--sc-cols-width', trackW + 'px'); return { cols, rows: clampGrid(Math.floor(contentH / (lineRatio * fontPx))) }; } From 14c698eaf4941a816c78f821775afc485fe280d2 Mon Sep 17 00:00:00 2001 From: Harry Cordewener Date: Tue, 30 Jun 2026 19:27:03 -0500 Subject: [PATCH 3/3] Use full width for the column grid; shorten bottom nav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the horizontal-scroll fix, addressing two things reported on device: the output was inset ~5 columns short of the screen width (78-col lines wrapped after 73), and the bottom nav was taller than needed. - .sc-output-area horizontal padding 1rem → 4px: a wide inset wasted ~5 columns at the fitted font size. When the box is small enough that the font floors at 6px, reclaimed raw width is the only way to fit the advertised columns, so this directly recovers them. Vertical padding unchanged. - observeResize now re-fires the fit callback on document.fonts.ready and after a couple of animation frames. The first measurement can run before the native WindowInsets bridge has applied the real safe-area padding (which shrinks the box); re-measuring once the layout settles re-fits the grid to the true width instead of leaving lines wrapping a few chars short. - .sc-nav-link: padding 8/7 → 4/3, icon 21 → 19px, gap 3 → 2px — a shorter Session/World/etc bar. Web builds clean. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HHCRc5CJ6595iMzEgYYdQp --- src/SharpClient.UI/wwwroot/app.css | 10 ++++++---- src/SharpClient.UI/wwwroot/sc-interop.js | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/SharpClient.UI/wwwroot/app.css b/src/SharpClient.UI/wwwroot/app.css index 90e8308..a7b69a2 100644 --- a/src/SharpClient.UI/wwwroot/app.css +++ b/src/SharpClient.UI/wwwroot/app.css @@ -394,7 +394,9 @@ body { font-boosting excess is clipped rather than turned into a left/right scrollbar. */ overflow-x: hidden; background: var(--outbg); - padding: 0.75rem 1rem; + /* Tight horizontal inset so the column grid uses nearly the full screen width (a wide inset here + wasted ~5 columns at the fitted font size). Vertical padding stays comfortable. */ + padding: 0.75rem 4px; } /* ── Empty State ───────────────────────────────────────────────── */ @@ -614,8 +616,8 @@ body { flex-direction: column; align-items: center; justify-content: center; - gap: 3px; - padding: 8px 0 7px; + gap: 2px; + padding: 4px 0 3px; color: var(--faint); text-decoration: none; font-family: var(--ui); @@ -625,7 +627,7 @@ body { margin-top: -1px; transition: color .12s ease; } -.sc-nav-link svg { width: 21px; height: 21px; display: block; } +.sc-nav-link svg { width: 19px; height: 19px; display: block; } .sc-nav-link:hover { color: var(--dim); } .sc-nav-link.active { color: var(--acc2); border-top-color: var(--acc2); } diff --git a/src/SharpClient.UI/wwwroot/sc-interop.js b/src/SharpClient.UI/wwwroot/sc-interop.js index 7d2ca49..83e381e 100644 --- a/src/SharpClient.UI/wwwroot/sc-interop.js +++ b/src/SharpClient.UI/wwwroot/sc-interop.js @@ -29,6 +29,27 @@ export function observeResize(dotNetRef, element) { observer.observe(element); _observers.set(element, observer); + // Re-fire the callback once the layout has actually settled. The first measurement can happen + // before (a) the JetBrains Mono webfont has loaded — the fallback font has a different advance, so + // the grid would be fitted to the wrong column width and lines wrap a few chars short — and (b) the + // native WindowInsets bridge has pushed the real safe-area padding, which shrinks this box. Both + // resolve asynchronously; re-measuring on fonts.ready and after a couple of frames re-fits the grid + // to the true width. (document.fonts.ready is the same guard SharpMUSH.Client's metrics use.) + const refire = () => { + const cs2 = getComputedStyle(element); + const px = parseFloat(cs2.paddingLeft || '0') + parseFloat(cs2.paddingRight || '0'); + const py = parseFloat(cs2.paddingTop || '0') + parseFloat(cs2.paddingBottom || '0'); + const w = Math.floor(element.clientWidth - px); + const h = Math.floor(element.clientHeight - py); + if (w > 0) { + dotNetRef.invokeMethodAsync('OnResized', w, h); + } + }; + if (document.fonts && document.fonts.ready) { + document.fonts.ready.then(refire); + } + requestAnimationFrame(() => requestAnimationFrame(refire)); + // Return the *content-box* size (padding excluded) to match what the ResizeObserver reports via // contentRect, so the initial NAWS/font calc and subsequent resizes use the same basis. const cs = getComputedStyle(element);