Skip to content
Open
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
10 changes: 10 additions & 0 deletions src/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,16 @@ describe('layout invariants', () => {
expect(rich.lines.at(-1)!.end).toEqual({ segmentIndex: 1, graphemeIndex: 0 })
})

test('NaN maxWidth falls back to zero-width layout instead of disabling wrapping', () => {
const prepared = prepareWithSegments('hello world', FONT)
const zeroWidth = layoutWithLines(prepared, 0, LINE_HEIGHT)

expect(layout(prepared, Number.NaN, LINE_HEIGHT)).toEqual(layout(prepared, 0, LINE_HEIGHT))
expect(layoutWithLines(prepared, Number.NaN, LINE_HEIGHT)).toEqual(zeroWidth)
expect(layoutNextLine(prepared, { segmentIndex: 0, graphemeIndex: 0 }, Number.NaN)).toEqual(zeroWidth.lines[0]!)
expect(measureLineStats(prepared, Number.NaN)).toEqual(measureLineStats(prepared, 0))
})

test('mixed-direction text is a stable smoke test', () => {
const prepared = prepareWithSegments('According to محمد الأحمد, the results improved.', FONT)
const result = layoutWithLines(prepared, 120, LINE_HEIGHT)
Expand Down
17 changes: 11 additions & 6 deletions src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,10 @@ function getInternalPrepared(prepared: PreparedText): InternalPreparedText {
return prepared as InternalPreparedText
}

function normalizeMaxWidth(maxWidth: number): number {
return Number.isNaN(maxWidth) ? 0 : maxWidth
}

// Layout prepared text at a given max width and caller-provided lineHeight.
// Pure arithmetic on cached widths — no canvas calls, no DOM reads, no string
// operations, no allocations.
Expand All @@ -706,7 +710,7 @@ export function layout(prepared: PreparedText, maxWidth: number, lineHeight: num
// Keep the resize hot path specialized. `layoutWithLines()` shares the same
// break semantics but also tracks line ranges; the extra bookkeeping is too
// expensive to pay on every hot-path `layout()` call.
const lineCount = countPreparedLines(getInternalPrepared(prepared), maxWidth)
const lineCount = countPreparedLines(getInternalPrepared(prepared), normalizeMaxWidth(maxWidth))
return { lineCount, height: lineCount * lineHeight }
}

Expand Down Expand Up @@ -786,7 +790,7 @@ export function walkLineRanges(

return walkPreparedLinesRaw(
getInternalPrepared(prepared),
maxWidth,
normalizeMaxWidth(maxWidth),
(width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) => {
onLine(createLayoutLineRange(
width,
Expand All @@ -803,7 +807,7 @@ export function measureLineStats(
prepared: PreparedTextWithSegments,
maxWidth: number,
): LineStats {
return measurePreparedLineGeometry(getInternalPrepared(prepared), maxWidth)
return measurePreparedLineGeometry(getInternalPrepared(prepared), normalizeMaxWidth(maxWidth))
}

// Intrinsic-width helper for rich/userland layout work. This asks "how wide is
Expand Down Expand Up @@ -832,7 +836,7 @@ export function layoutNextLine(

const lineStartSegmentIndex = end.segmentIndex
const lineStartGraphemeIndex = end.graphemeIndex
const width = stepPreparedLineGeometryFromChunk(internal, end, chunkIndex, maxWidth)
const width = stepPreparedLineGeometryFromChunk(internal, end, chunkIndex, normalizeMaxWidth(maxWidth))
if (width === null) return null

return createLayoutLine(
Expand Down Expand Up @@ -861,7 +865,7 @@ export function layoutNextLineRange(

const lineStartSegmentIndex = end.segmentIndex
const lineStartGraphemeIndex = end.graphemeIndex
const width = stepPreparedLineGeometryFromChunk(internal, end, chunkIndex, maxWidth)
const width = stepPreparedLineGeometryFromChunk(internal, end, chunkIndex, normalizeMaxWidth(maxWidth))
if (width === null) return null

return createLayoutLineRange(
Expand All @@ -881,10 +885,11 @@ export function layoutWithLines(prepared: PreparedTextWithSegments, maxWidth: nu
const lines: LayoutLine[] = []
if (prepared.widths.length === 0) return { lineCount: 0, height: 0, lines }

const normalizedMaxWidth = normalizeMaxWidth(maxWidth)
const graphemeCache = getLineTextCache(prepared)
const lineCount = walkPreparedLinesRaw(
getInternalPrepared(prepared),
maxWidth,
normalizedMaxWidth,
(width, startSegmentIndex, startGraphemeIndex, endSegmentIndex, endGraphemeIndex) => {
lines.push(createLayoutLine(
prepared,
Expand Down