From c532f32227eed1baf8c31602dcc34c458ec0e2bf Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:40:41 +0000 Subject: [PATCH] fix(app): preserve short terminal runs between accented glyphs Short ASCII runs split by fallback glyphs can wrap or ellipsize in Android Text when their layout width equals their cell reservation. Give text the same extra layout room already used for isolated glyphs, while ordinary run boxes keep clipping paint at their original cell boundaries. Co-authored-by: lucas --- .../terminal-grid-view.native.tsx | 30 ++++++++--------- .../terminal-grid-view.test.tsx | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 packages/app/src/terminal/native-renderer/terminal-grid-view.test.tsx diff --git a/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx b/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx index 99307f8606..c5fc19381b 100644 --- a/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx +++ b/packages/app/src/terminal/native-renderer/terminal-grid-view.native.tsx @@ -44,8 +44,9 @@ const MEASURE_TEXT = "mmmmmmmmmm"; const DEFAULT_FONT_SIZE = 12; const INITIAL_CELL_WIDTH_RATIO = 0.62; const INITIAL_CELL_HEIGHT_RATIO = 1.35; -// Room a fallback glyph (emoji, symbols) gets to spill past its cells instead of being clipped. -const ISOLATED_GLYPH_OVERFLOW_CELLS = 2; +// Keep native line layout clear of the cell boundary: rounding and trailing letter spacing can +// otherwise wrap a short run. The run box clips ordinary text; isolated fallback glyphs can spill. +const TEXT_LAYOUT_OVERFLOW_CELLS = 2; interface CellMetrics { cellWidth: number; @@ -152,16 +153,13 @@ function TerminalGridRun({ [cellHeight, cellWidth, isolated, run.cellCount, run.style.backgroundColor], ); const runTextStyle = useMemo>( - () => - isolated - ? [ - textStyle, - run.style, - styles.isolatedText, - { width: (run.cellCount + ISOLATED_GLYPH_OVERFLOW_CELLS) * cellWidth }, - ] - : [textStyle, run.style], - [cellWidth, isolated, run.cellCount, run.style, textStyle], + () => [ + textStyle, + run.style, + styles.runText, + { width: (run.cellCount + TEXT_LAYOUT_OVERFLOW_CELLS) * cellWidth }, + ], + [cellWidth, run.cellCount, run.style, textStyle], ); return ( @@ -169,7 +167,7 @@ function TerminalGridRun({ {run.renderKind === "custom-glyph" ? ( ) : ( - + {renderTerminalTextSegments(run, wideGlyphStyles)} )} @@ -577,9 +575,9 @@ const styles = StyleSheet.create({ isolatedRun: { overflow: "visible", }, - // The run box keeps the cell width and paints the background; the wider text only draws the - // glyph, so a fallback glyph wider than its cells spills over the next cell like in a terminal. - isolatedText: { + // The run box owns cell dimensions and background. Text gets extra layout room without + // changing those dimensions; only isolated fallback glyphs paint beyond the run box. + runText: { backgroundColor: "transparent", left: 0, position: "absolute", diff --git a/packages/app/src/terminal/native-renderer/terminal-grid-view.test.tsx b/packages/app/src/terminal/native-renderer/terminal-grid-view.test.tsx new file mode 100644 index 0000000000..a0e5c6276c --- /dev/null +++ b/packages/app/src/terminal/native-renderer/terminal-grid-view.test.tsx @@ -0,0 +1,33 @@ +// @vitest-environment jsdom +import React from "react"; +import { cleanup, render } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { createNativeHeadlessTerminal } from "./headless-terminal-state"; +import { TerminalGridView } from "./terminal-grid-view.native"; + +afterEach(cleanup); + +describe("native terminal grid text layout", () => { + it("gives short runs between accented glyphs layout room without widening their cells", async () => { + const terminal = createNativeHeadlessTerminal({ rows: 1, cols: 30 }); + try { + await terminal.write("café naïve|END"); + const view = render(); + const text = view.getByText(" na", { normalizer: (value) => value }); + const run = text.parentElement!; + const accentRun = view.getByText("é").parentElement!; + const cellWidth = parseFloat(getComputedStyle(accentRun).width); + expect(parseFloat(getComputedStyle(run).width)).toBe(3 * cellWidth); + + // Native Text wraps or ellipsizes when rounding or trailing letter spacing puts this + // leading-space run just past its layout width. Layout has room; the cell box clips paint. + expect(parseFloat(getComputedStyle(text).width)).toBeGreaterThan(3 * cellWidth); + expect(getComputedStyle(text).position).toBe("absolute"); + expect(getComputedStyle(run).overflowX).toBe("hidden"); + expect(view.getByTestId("terminal-row-0").getAttribute("aria-label")).toBe("café naïve|END"); + } finally { + terminal.dispose(); + } + }); +});