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 <lucas@withvus.ai>
This commit is contained in:
paseo-bot[bot]
2026-10-24 16:40:41 +00:00
co-authored by lucas
parent 31f9657679
commit c532f32227
2 changed files with 47 additions and 16 deletions
@@ -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<StyleProp<TextStyle>>(
() =>
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" ? (
<TerminalGridCustomGlyphRun run={run} cellWidth={cellWidth} cellHeight={cellHeight} />
) : (
<Text numberOfLines={1} style={runTextStyle}>
<Text numberOfLines={1} ellipsizeMode="clip" style={runTextStyle}>
{renderTerminalTextSegments(run, wideGlyphStyles)}
</Text>
)}
@@ -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",
@@ -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(<TerminalGridView state={terminal.getViewportState()} />);
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();
}
});
});