Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/text-measure-seam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix: **Labels no longer wrap their last glyph**: render-time pixel snapping rounds each frame edge on its own and can shave a whole device pixel off a label sized exactly to its text, but the wrap budget only handed back half of that, so labels such as "Start" painted as "Star" / "t". The Native SDK now hands back the full pixel, and a capture fixture pins one laid-out line per text node.
2 changes: 1 addition & 1 deletion scripts/release-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { dirname, extname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const expectedNativeCommit = "9a499f16ec01e498208def9f239fca4780eeecad";
const expectedNativeCommit = "464ff65faa02d36179b34e2252fa2a1cf46f7541";

function filesBelow(root) {
const output = [];
Expand Down
17 changes: 17 additions & 0 deletions test/capture-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,20 @@ try {
assert.match(snapshotText(noro), /role=button name="Seek"/);
assert.match(snapshotText(noro), /name="00:42"/);

// Content-sized labels must hold their run on one line: the number of
// laid-out text lines equals the number of text nodes.
const textWrap = capture("text-wrap", "test/fixtures/text-wrap");
assert.equal(textNodeCount(textWrap), 5);
assert.equal(textLineCount(textWrap), 5);

const noroSignal = capture("noro-signal", "examples/noro-signal", [
"--provider-fixture", join(root, "test", "capture", "noro.provider.json"),
]);
assert.match(snapshotText(noroSignal), /name="Capture Proof"/);
assert.match(snapshotText(noroSignal), /role=button name="Seek"/);
assert.match(snapshotText(noroSignal), /role=button name="PAUSE"/);
assert.match(snapshotText(noroSignal), /name="00:42"/);
assert.equal(textLineCount(noroSignal), textNodeCount(noroSignal));

const noroSignalAction = capture("noro-signal-action", "examples/noro-signal", [
"--provider-fixture", join(root, "test", "capture", "noro-action.provider.json"),
Expand Down Expand Up @@ -269,3 +276,13 @@ function recordClockSession() {
function snapshotText(receipt) {
return readFileSync(receipt.output.snapshot, "utf8");
}

function textLineCount(receipt) {
const match = /text_layout_lines=(\d+)\//.exec(snapshotText(receipt));
assert.ok(match, "snapshot header is missing text_layout_lines");
return Number(match[1]);
}

function textNodeCount(receipt) {
return snapshotText(receipt).split("\n").filter((line) => line.includes(" role=text ")).length;
}
19 changes: 19 additions & 0 deletions test/fixtures/text-wrap/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"jsxImportSource": "@weaver/sdk",
"types": [],
"baseUrl": ".",
"paths": {
"@weaver/sdk": ["../../../sdk/index.d.ts"],
"@weaver/sdk/jsx-runtime": ["../../../sdk/jsx-runtime.d.ts"]
}
},
"include": ["widget.tsx"]
}
28 changes: 28 additions & 0 deletions test/fixtures/text-wrap/widget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { widget } from "@weaver/sdk";

// Regression fixture: labels sized exactly to their measured text, placed
// at fractional x positions by centering. Each <text> must lay out on one
// line. Render-time pixel snapping rounds each frame edge on its own and
// can shave up to a whole device pixel off such a frame; the wrap budget
// used to hand back only half of that, so "Start" painted as "Star" / "t".
// The chip row is sized with room to spare so no flex shrink applies:
// a row that overflows its container shrinks its children and wraps them
// legitimately.
export default widget({
name: "Text Wrap",
size: [300, 140],
}, () => (
<column class="size-full p-[12px] gap-[8px] items-center bg-[#101010]">
<row class="gap-[6px]">
<row class="h-[28px] px-[12px] items-center bg-[#333333]"><text class="text-[11px] text-[#ffffff]">Focus</text></row>
<row class="h-[28px] px-[12px] items-center bg-[#333333]"><text class="text-[11px] text-[#ffffff]">Short break</text></row>
<row class="h-[28px] px-[12px] items-center bg-[#333333]"><text class="text-[11px] text-[#ffffff]">Long break</text></row>
</row>
<row class="w-[197px] h-[46px] bg-[#333333]">
<row class="size-full items-center justify-center"><text class="text-[14px] font-semibold text-[#ffffff]">Start</text></row>
</row>
<row class="w-[183px] h-[30px] bg-[#333333]">
<row class="size-full items-center justify-center"><text class="text-[11px] text-[#ffffff]">fixture</text></row>
</row>
</column>
));
Loading