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(); + } + }); +});