mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 02:32:47 +00:00
fix(app): keep short styled terminal runs complete
Give native text layout rounding room inside the existing clipping cell boxes. Matching glyph advance alone still lets one-line layout replace short styled segments with ellipses. Co-authored-by: lucas <lucas@withvus.ai>
This commit is contained in:
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { TerminalViewportState } from "./headless-terminal-state";
|
||||
import { TerminalGridView } from "./terminal-grid-view.native";
|
||||
|
||||
const fixtureStyle = { width: 1000 };
|
||||
const output = "#".repeat(49);
|
||||
const state: TerminalViewportState = {
|
||||
rows: 1,
|
||||
@@ -16,20 +17,74 @@ const state: TerminalViewportState = {
|
||||
cursor: { row: 0, col: 48, hidden: true },
|
||||
};
|
||||
|
||||
afterEach(cleanup);
|
||||
const styledRuns = ["a", "bc", "defg", "hijkl"];
|
||||
const styledState: TerminalViewportState = {
|
||||
...state,
|
||||
cols: 12,
|
||||
grid: [
|
||||
styledRuns.flatMap((text, index) =>
|
||||
[...text].map((char) => ({ char, fg: index + 1, fgMode: 1 })),
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
const measuredCellWidths: number[] = [];
|
||||
function onCellMetricsChange(metrics: { cellWidth: number }) {
|
||||
measuredCellWidths.push(metrics.cellWidth);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
measuredCellWidths.length = 0;
|
||||
});
|
||||
|
||||
describe("native terminal full-width text", () => {
|
||||
it.each([9, 11, 12, 14, 18])(
|
||||
"keeps short colored runs complete despite native layout rounding at %s px",
|
||||
async (fontSize) => {
|
||||
const view = render(
|
||||
<TerminalGridView state={styledState} fontSize={fontSize} style={fixtureStyle} />,
|
||||
);
|
||||
for (const runText of styledRuns) {
|
||||
const text = view.getByText(runText);
|
||||
const run = text.parentElement!;
|
||||
await expect.poll(() => run.clientWidth).toBeGreaterThan(0);
|
||||
// Native one-line layout rounds independently of the cell box. Leave text
|
||||
// layout at least one physical pixel of slack, while the cell box clips paint.
|
||||
await expect
|
||||
.poll(() => text.getBoundingClientRect().width - run.getBoundingClientRect().width)
|
||||
.toBeGreaterThanOrEqual(1 / window.devicePixelRatio);
|
||||
expect(getComputedStyle(run).overflowX).toBe("hidden");
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(text);
|
||||
expect(range.getBoundingClientRect().width).toBeLessThanOrEqual(
|
||||
text.getBoundingClientRect().width,
|
||||
);
|
||||
}
|
||||
expect(view.getByTestId("terminal-row-0").getAttribute("aria-label")).toBe(
|
||||
styledRuns.join(""),
|
||||
);
|
||||
},
|
||||
);
|
||||
it.each([9, 11, 12, 14, 18])(
|
||||
"fits every character in a full-width ASCII row at %s px",
|
||||
async (fontSize) => {
|
||||
const view = render(<TerminalGridView state={state} fontSize={fontSize} />);
|
||||
const view = render(
|
||||
<TerminalGridView
|
||||
state={state}
|
||||
style={fixtureStyle}
|
||||
fontSize={fontSize}
|
||||
onCellMetricsChange={onCellMetricsChange}
|
||||
/>,
|
||||
);
|
||||
await expect.poll(() => measuredCellWidths.at(-1) ?? 0).toBeGreaterThan(0);
|
||||
const text = view.getByText(output);
|
||||
await expect.poll(() => text.clientWidth).toBeGreaterThan(0);
|
||||
// The native Text adapter truncates overflowing one-line content. Compare the actual
|
||||
// font layout with the available width, rather than asserting a particular CSS policy.
|
||||
await expect.poll(() => text.scrollWidth - text.clientWidth).toBeLessThanOrEqual(1);
|
||||
const run = text.parentElement!;
|
||||
expect(run.getBoundingClientRect().width).toBeCloseTo(text.getBoundingClientRect().width, 1);
|
||||
expect(run.getBoundingClientRect().width).toBeCloseTo(49 * measuredCellWidths.at(-1)!, 1);
|
||||
expect(view.getByTestId("terminal-row-0").getAttribute("aria-label")).toBe(output);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -115,8 +115,15 @@ function TerminalGridRun({ run, cellWidth, cellHeight, textStyle }: TerminalGrid
|
||||
[cellHeight, cellWidth, run.cellCount, run.style.backgroundColor],
|
||||
);
|
||||
const runTextStyle = useMemo<StyleProp<TextStyle>>(
|
||||
() => [textStyle, run.style],
|
||||
[run.style, textStyle],
|
||||
() => [
|
||||
textStyle,
|
||||
run.style,
|
||||
styles.runText,
|
||||
// Native text layout rounds independently of the cell box. One extra cell
|
||||
// prevents rounding from truncating a run that otherwise fits its cells.
|
||||
{ minWidth: (run.cellCount + 1) * cellWidth },
|
||||
],
|
||||
[cellWidth, run.cellCount, run.style, textStyle],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -124,7 +131,7 @@ function TerminalGridRun({ run, cellWidth, cellHeight, textStyle }: TerminalGrid
|
||||
{run.renderKind === "custom-glyph" ? (
|
||||
<TerminalGridCustomGlyphRun run={run} cellWidth={cellWidth} cellHeight={cellHeight} />
|
||||
) : (
|
||||
<Text numberOfLines={1} style={runTextStyle}>
|
||||
<Text numberOfLines={1} ellipsizeMode="clip" style={runTextStyle}>
|
||||
{run.text}
|
||||
</Text>
|
||||
)}
|
||||
@@ -458,6 +465,12 @@ const styles = StyleSheet.create({
|
||||
run: {
|
||||
overflow: "hidden",
|
||||
},
|
||||
runText: {
|
||||
backgroundColor: "transparent",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
customGlyphRun: {
|
||||
flex: 1,
|
||||
position: "relative",
|
||||
|
||||
Reference in New Issue
Block a user