import { Node as YogaNode } from "../core/yoga.ts"; import { createRenderer } from "./index.ts"; /** Screen-space rectangle cached during paint for hit-testing. */ export interface ScreenRect { bottom: number; left: number; right: number; top: number; } export interface TermNode { /** Cached collectText() result — cleared on markDirty. */ _cachedText: string | null; children: TermNode[]; dirty: boolean; parent: TermNode | null; props: Record; /** Screen-space bounds, updated each paint pass. */ screenRect: ScreenRect | null; /** Scroll offset for overflow="scroll" containers. */ scrollTop: number; type: string; yogaNode: YogaNode | null; } function createTermNode(type: string): TermNode { const yogaNode = type === "_text" || type === "_slot" ? null : new YogaNode(); return { type, props: {}, children: [], parent: null, yogaNode, screenRect: null, scrollTop: 0, dirty: true, _cachedText: null, }; } function markDirty(node: TermNode) { node.dirty = true; node._cachedText = null; let current = node.parent; while (current && !current.dirty) { current.dirty = true; current._cachedText = null; current = current.parent; } } export const { render, effect, memo, createComponent, createElement, createTextNode, insert, insertNode, spread, setProp, mergeProps, ref, } = createRenderer({ createElement(type: string) { return createTermNode(type); }, createTextNode(value: string) { const node = createTermNode("_text"); node.props.content = value; return node; }, createSlotNode() { return createTermNode("_slot"); }, isTextNode(node: TermNode) { return node.type === "_text"; }, replaceText(node: TermNode, value: string) { node.props.content = value; // Walk up to the nearest ancestor with a measure function and mark its // yoga node dirty so layout re-measures the text height. Without this, // yoga caches the old measurement and content-driven scrolling breaks. let ancestor: TermNode | null = node.parent; while (ancestor) { if (ancestor.yogaNode?.measureFunc) { ancestor.yogaNode.markDirty(); break; } ancestor = ancestor.parent; } markDirty(node); }, setProperty(node: TermNode, key: string, value: T) { node.props[key] = value; // Mark yoga dirty so measure functions re-run (e.g. input value changes height) if (node.yogaNode?.measureFunc) { node.yogaNode.markDirty(); } markDirty(node); }, insertNode(parent: TermNode, node: TermNode, anchor?: TermNode) { if (anchor) { const idx = parent.children.indexOf(anchor); parent.children.splice(idx, 0, node); } else { parent.children.push(node); } node.parent = parent; // Attach yoga child if both have yoga nodes if (parent.yogaNode && node.yogaNode) { let yogaIndex = 0; for (const c of parent.children) { if (c === node) break; if (c.yogaNode) yogaIndex++; } parent.yogaNode.insertChild(node.yogaNode, yogaIndex); } markDirty(parent); }, removeNode(parent: TermNode, node: TermNode) { const idx = parent.children.indexOf(node); if (idx >= 0) parent.children.splice(idx, 1); if (parent.yogaNode && node.yogaNode) { parent.yogaNode.removeChild(node.yogaNode); } node.parent = null; markDirty(parent); }, getParentNode(node: TermNode) { return node.parent ?? undefined; }, getFirstChild(node: TermNode) { return node.children[0]; }, getNextSibling(node: TermNode) { if (!node.parent) return undefined; const idx = node.parent.children.indexOf(node); return node.parent.children[idx + 1]; }, });