Skip to content
Merged
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
49 changes: 46 additions & 3 deletions ui-tui/packages/hermes-ink/src/ink/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ export type DOMElement = {
// Only set on ink-root. The document owns focus — any node can
// reach it by walking parentNode, like browser getRootNode().
focusManager?: FocusManager
// Measurement cache for ink-text nodes: avoids re-squashing and re-wrapping
// text when yoga calls measureFunc multiple times per frame with different
// widths during flex re-pass. Keyed by `${width}|${widthMode}`.
_textMeasureCache?: { gen: number; entries: Map<string, { _gen: number; result: { width: number; height: number } }> }
} & InkNode

export type TextNode = {
Expand Down Expand Up @@ -311,10 +315,42 @@ export const createTextNode = (text: string): TextNode => {
return node
}

const MEASURE_CACHE_CAP = 16

const measureTextNode = function (
node: DOMNode,
width: number,
widthMode: LayoutMeasureMode
): { width: number; height: number } {
const elem = node.nodeName !== '#text' ? (node as DOMElement) : node.parentNode
if (elem && elem.nodeName === 'ink-text') {
let cache = elem._textMeasureCache
if (!cache) {
cache = { gen: 0, entries: new Map() }
elem._textMeasureCache = cache
}
const key = `${width}|${widthMode}`
const hit = cache.entries.get(key)
if (hit && hit._gen === cache.gen) {
return hit.result
}
const result = computeTextMeasure(node, width, widthMode)
// Enforce cap with FIFO eviction to avoid unbounded growth during
// pathological frames where yoga probes many widths.
if (cache.entries.size >= MEASURE_CACHE_CAP) {
const firstKey = cache.entries.keys().next().value
cache.entries.delete(firstKey)
}
cache.entries.set(key, { _gen: cache.gen, result })
return result
}
return computeTextMeasure(node, width, widthMode)
}

const computeTextMeasure = function (
node: DOMNode,
width: number,
widthMode: LayoutMeasureMode
): { width: number; height: number } {
const rawText = node.nodeName === '#text' ? node.nodeValue : squashTextNodes(node)

Expand Down Expand Up @@ -378,13 +414,19 @@ export const markDirty = (node?: DOMNode): void => {

while (current) {
if (current.nodeName !== '#text') {
;(current as DOMElement).dirty = true
const elem = current as DOMElement
elem.dirty = true

// Only mark yoga dirty on leaf nodes that have measure functions
if (!markedYoga && (current.nodeName === 'ink-text' || current.nodeName === 'ink-raw-ansi') && current.yogaNode) {
current.yogaNode.markDirty()
if (!markedYoga && (elem.nodeName === 'ink-text' || elem.nodeName === 'ink-raw-ansi') && elem.yogaNode) {
elem.yogaNode.markDirty()
markedYoga = true
}

// Invalidate text measurement cache — child text or style changed.
if (elem._textMeasureCache) {
elem._textMeasureCache.gen++
}
}

current = current.parentNode
Expand Down Expand Up @@ -433,6 +475,7 @@ export const clearYogaNodeReferences = (node: DOMElement | TextNode): void => {
for (const child of node.childNodes) {
clearYogaNodeReferences(child)
}
node._textMeasureCache = undefined
}

node.yogaNode = undefined
Expand Down
Loading