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
5 changes: 5 additions & 0 deletions .changeset/text-measure-float-slack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning-plugin-flexbox': patch
---

Allow 0.01px of slack on the width limit when measuring text. Yoga hands the width back as a float32, which can land a hair under the text's own width and split a short word across lines ("TV-14" as "TV-1" / "4").
7 changes: 6 additions & 1 deletion packages/plugin-flexbox/src/YogaManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const MAX_SIZEOF_UPDATE = 1024 * 10;
// Grow-only text remeasure converges monotonically (1-3 passes in practice).
// This is a runaway backstop, not an expected limit.
const MAX_LAYOUT_PASSES = 10;
const TEXT_MEASURE_SLACK = 0.01;

export class YogaManager {
private _elementMap: Map<number, ManagerNode> = new Map();
Expand Down Expand Up @@ -206,8 +207,12 @@ export class YogaManager {
return { width: 0, height: 0 };
}

// Yoga hands the width back as a float32, which can land a hair under the
// text's own width and wrap a single word ("TV-14" -> "TV-1" / "4").
const maxWidth =
widthMode === 0 || !Number.isFinite(availableWidth) ? Infinity : availableWidth;
widthMode === 0 || !Number.isFinite(availableWidth)
? Infinity
: availableWidth + TEXT_MEASURE_SLACK;

return layoutText(font, text.props, maxWidth);
}
Expand Down
52 changes: 52 additions & 0 deletions packages/plugin-flexbox/src/measureText.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,55 @@ describe('Yoga text measurement (real yoga)', () => {
}
});
});

describe('Yoga text measurement float32 widths', () => {
// Fractional advances at a non-integer font scale (20 / 21), so the text's
// own width isn't representable as a float32 and rounds down.
const tvAtlas: AtlasData = {
info: { size: 21, face: 'TV' },
common: { lineHeight: 12, base: 8 },
lightningMetrics: {
ascender: 800,
descender: -200,
lineGap: 0,
unitsPerEm: 1000,
},
chars: [
{ id: 84, xadvance: 11.7, xoffset: 0, yoffset: 0, width: 8, height: 8 }, // T
{ id: 86, xadvance: 12.9, xoffset: 0, yoffset: 0, width: 8, height: 8 }, // V
{ id: 45, xadvance: 7.3, xoffset: 0, yoffset: 0, width: 6, height: 2 }, // -
{ id: 49, xadvance: 11.1, xoffset: 0, yoffset: 0, width: 8, height: 8 }, // 1
{ id: 52, xadvance: 11, xoffset: 0, yoffset: 0, width: 8, height: 8 }, // 4
{ id: 32, xadvance: 5, xoffset: 0, yoffset: 0, width: 0, height: 0 }, // (space)
],
kernings: [],
};

it('keeps a word on one line when yoga hands back its own width as a float32', async () => {
const manager = new YogaManager();
await manager.init();

const internals = manager as unknown as {
_fontStore: { register: (f: string, d: AtlasData) => void };
_measureText: (
id: number,
width: number,
widthMode: number,
) => { width: number; height: number };
};

internals._fontStore.register('TV', tvAtlas);
manager.addNode(1);
manager.setTextMeasure(1, 'TV', { ...textProps, text: 'TV-14' });

const own = internals._measureText(1, Infinity, 0);
const float32Width = Math.fround(own.width);

expect(float32Width).toBeLessThan(own.width);

const measured = internals._measureText(1, float32Width, 2);

expect(measured.height).toBe(20);
expect(measured.width).toBe(own.width);
});
});
Loading