flexily 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/chunk-CBBoxR_p.mjs +26 -0
  2. package/dist/constants-BNURa6H7.mjs +65 -0
  3. package/dist/constants-BNURa6H7.mjs.map +1 -0
  4. package/dist/constants-D7ythAJC.d.mts +64 -0
  5. package/dist/constants-D7ythAJC.d.mts.map +1 -0
  6. package/{src/classic/node.ts → dist/index-classic.d.mts} +118 -619
  7. package/dist/index-classic.d.mts.map +1 -0
  8. package/dist/index-classic.mjs +1909 -0
  9. package/dist/index-classic.mjs.map +1 -0
  10. package/dist/index.d.mts +195 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +3279 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/node-zero-75maLs2s.d.mts +762 -0
  15. package/dist/node-zero-75maLs2s.d.mts.map +1 -0
  16. package/dist/src-BWyhokNZ.mjs +692 -0
  17. package/dist/src-BWyhokNZ.mjs.map +1 -0
  18. package/dist/src-DdSLylRA.mjs +816 -0
  19. package/dist/src-DdSLylRA.mjs.map +1 -0
  20. package/dist/testing.d.mts +55 -0
  21. package/dist/testing.d.mts.map +1 -0
  22. package/dist/testing.mjs +154 -0
  23. package/dist/testing.mjs.map +1 -0
  24. package/dist/types--IozHd4V.mjs +283 -0
  25. package/dist/types--IozHd4V.mjs.map +1 -0
  26. package/dist/types-DG1H4DVR.d.mts +157 -0
  27. package/dist/types-DG1H4DVR.d.mts.map +1 -0
  28. package/package.json +33 -24
  29. package/src/CLAUDE.md +0 -527
  30. package/src/classic/layout.ts +0 -1843
  31. package/src/constants.ts +0 -82
  32. package/src/create-flexily.ts +0 -153
  33. package/src/index-classic.ts +0 -110
  34. package/src/index.ts +0 -133
  35. package/src/layout-flex-lines.ts +0 -413
  36. package/src/layout-helpers.ts +0 -160
  37. package/src/layout-measure.ts +0 -259
  38. package/src/layout-stats.ts +0 -41
  39. package/src/layout-traversal.ts +0 -70
  40. package/src/layout-zero.ts +0 -2219
  41. package/src/logger.ts +0 -68
  42. package/src/monospace-measurer.ts +0 -68
  43. package/src/node-zero.ts +0 -1508
  44. package/src/pretext-measurer.ts +0 -86
  45. package/src/test-measurer.ts +0 -219
  46. package/src/testing.ts +0 -215
  47. package/src/text-layout.ts +0 -75
  48. package/src/trace.ts +0 -252
  49. package/src/types.ts +0 -236
  50. package/src/utils.ts +0 -243
@@ -1,86 +0,0 @@
1
- /**
2
- * Pretext text measurement plugin.
3
- *
4
- * Integrates with @chenglou/pretext for proportional font measurement.
5
- * Pretext is a peer dependency — users must install it separately.
6
- *
7
- * See silvery-internal/design/v05-layout/pretext-integration.md
8
- */
9
-
10
- import type { FlexilyPlugin } from "./create-flexily.js"
11
- import type { TextLayoutService, PreparedText, TextLayout, IntrinsicSizes } from "./text-layout.js"
12
-
13
- /** Pretext API shape (from @chenglou/pretext). Defined here to avoid a hard dependency. */
14
- export interface PretextAPI {
15
- prepare(text: string, font: string): PretextPrepared
16
- }
17
-
18
- export interface PretextPrepared {
19
- layout(maxWidth: number, lineHeight?: number): PretextLayout
20
- }
21
-
22
- export interface PretextLayout {
23
- width: number
24
- height: number
25
- lines?: Array<{ text: string; width: number }>
26
- }
27
-
28
- /**
29
- * Create a Pretext-based text measurement service.
30
- *
31
- * @param pretext - The pretext module (import from "@chenglou/pretext")
32
- */
33
- export function createPretextMeasurer(pretext: PretextAPI): TextLayoutService {
34
- return {
35
- prepare(input) {
36
- const { text, style } = input
37
- const pretextPrepared = pretext.prepare(text, style.fontShorthand)
38
- const lineHeight = style.lineHeight || style.fontSize
39
-
40
- let cachedIntrinsic: IntrinsicSizes | null = null
41
-
42
- const prepared: PreparedText = {
43
- intrinsicSizes(): IntrinsicSizes {
44
- if (cachedIntrinsic) return cachedIntrinsic
45
- const unconstrained = pretextPrepared.layout(Infinity, lineHeight)
46
- const minLayout = pretextPrepared.layout(0, lineHeight)
47
- cachedIntrinsic = {
48
- minContentWidth: minLayout.width,
49
- maxContentWidth: unconstrained.width,
50
- }
51
- return cachedIntrinsic
52
- },
53
-
54
- layout(constraints): TextLayout {
55
- const maxWidth = constraints.maxWidth ?? Infinity
56
- const result = pretextPrepared.layout(maxWidth, lineHeight)
57
- const lineCount = result.lines?.length ?? 1
58
- const width = constraints.shrinkWrap ? result.width : Math.min(maxWidth, result.width)
59
-
60
- return {
61
- width,
62
- height: result.height,
63
- lineCount,
64
- firstBaseline: lineHeight,
65
- lastBaseline: lineCount > 0 ? (lineCount - 1) * lineHeight + lineHeight : lineHeight,
66
- truncated: false,
67
- }
68
- },
69
- }
70
-
71
- return prepared
72
- },
73
- }
74
- }
75
-
76
- /**
77
- * Plugin: add Pretext-based proportional text measurement.
78
- *
79
- * @param pretext - The pretext module (import from "@chenglou/pretext")
80
- */
81
- export function withPretext(pretext: PretextAPI): FlexilyPlugin {
82
- return (engine) => {
83
- engine.textLayout = createPretextMeasurer(pretext)
84
- return engine
85
- }
86
- }
@@ -1,219 +0,0 @@
1
- /**
2
- * Deterministic test text measurer.
3
- *
4
- * Fixed grapheme width table: Latin 0.8, CJK 1.0, emoji 1.8 (relative to fontSize).
5
- * Deterministic across platforms — use in tests and CI.
6
- * Supports word wrapping for realistic text layout testing.
7
- */
8
-
9
- import type { FlexilyPlugin } from "./create-flexily.js"
10
- import type { TextLayoutService, PreparedText, TextLayout, TextLine, IntrinsicSizes } from "./text-layout.js"
11
-
12
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" })
13
-
14
- /** Measure a single grapheme cluster with the deterministic width table. */
15
- function graphemeWidth(grapheme: string, fontSize: number): number {
16
- const cp = grapheme.codePointAt(0) ?? 0
17
-
18
- // Emoji: ZWJ sequences, variation selectors, regional indicators
19
- if (
20
- (cp >= 0x1f300 && cp <= 0x1faff) ||
21
- (cp >= 0x2600 && cp <= 0x27bf) ||
22
- (cp >= 0xfe00 && cp <= 0xfe0f) ||
23
- cp === 0x200d ||
24
- (cp >= 0x1f900 && cp <= 0x1f9ff) ||
25
- grapheme.length > 2
26
- ) {
27
- return fontSize * 1.8
28
- }
29
-
30
- // CJK: Chinese, Japanese, Korean ideographs + fullwidth
31
- if (
32
- (cp >= 0x4e00 && cp <= 0x9fff) ||
33
- (cp >= 0x3000 && cp <= 0x303f) ||
34
- (cp >= 0x3040 && cp <= 0x309f) ||
35
- (cp >= 0x30a0 && cp <= 0x30ff) ||
36
- (cp >= 0xac00 && cp <= 0xd7af) ||
37
- (cp >= 0xff00 && cp <= 0xffef)
38
- ) {
39
- return fontSize * 1.0
40
- }
41
-
42
- // Latin and everything else
43
- return fontSize * 0.8
44
- }
45
-
46
- /** A word or whitespace segment for line breaking. */
47
- interface TextSegment {
48
- text: string
49
- width: number
50
- isWhitespace: boolean
51
- }
52
-
53
- function segmentText(text: string, fontSize: number): TextSegment[] {
54
- const segments: TextSegment[] = []
55
- let current = ""
56
- let currentWidth = 0
57
- let isWhitespace = false
58
-
59
- for (const { segment } of segmenter.segment(text)) {
60
- const isSpace = segment === " " || segment === "\t"
61
-
62
- if (segments.length === 0 && current === "") {
63
- isWhitespace = isSpace
64
- current = segment
65
- currentWidth = isSpace ? fontSize * 0.8 : graphemeWidth(segment, fontSize)
66
- continue
67
- }
68
-
69
- if (isSpace !== isWhitespace) {
70
- if (current) segments.push({ text: current, width: currentWidth, isWhitespace })
71
- current = segment
72
- currentWidth = isSpace ? fontSize * 0.8 : graphemeWidth(segment, fontSize)
73
- isWhitespace = isSpace
74
- } else {
75
- current += segment
76
- currentWidth += isSpace ? fontSize * 0.8 : graphemeWidth(segment, fontSize)
77
- }
78
- }
79
-
80
- if (current) segments.push({ text: current, width: currentWidth, isWhitespace })
81
- return segments
82
- }
83
-
84
- function measureString(text: string, fontSize: number): number {
85
- let width = 0
86
- for (const { segment } of segmenter.segment(text)) {
87
- width += graphemeWidth(segment, fontSize)
88
- }
89
- return width
90
- }
91
-
92
- /**
93
- * Create a deterministic text measurement service for testing.
94
- *
95
- * Uses fixed grapheme widths: Latin 0.8, CJK 1.0, emoji 1.8 (relative to fontSize).
96
- */
97
- export function createTestMeasurer(): TextLayoutService {
98
- return {
99
- prepare(input) {
100
- const { text, style } = input
101
- const fontSize = style.fontSize
102
- const lineHeight = style.lineHeight || fontSize
103
-
104
- const segments = segmentText(text, fontSize)
105
-
106
- let maxContentWidth = 0
107
- let minContentWidth = 0
108
- for (const seg of segments) {
109
- maxContentWidth += seg.width
110
- if (!seg.isWhitespace && seg.width > minContentWidth) {
111
- minContentWidth = seg.width
112
- }
113
- }
114
-
115
- const prepared: PreparedText = {
116
- intrinsicSizes(): IntrinsicSizes {
117
- return { minContentWidth, maxContentWidth }
118
- },
119
-
120
- layout(constraints, options?): TextLayout {
121
- const maxWidth = constraints.maxWidth ?? Infinity
122
- const maxLines = constraints.maxLines ?? Infinity
123
- const wrap = constraints.wrap ?? "normal"
124
- const includeLines = options?.includeLines ?? false
125
-
126
- if (wrap === "none" || maxWidth >= maxContentWidth) {
127
- const truncated = maxLines < 1
128
- const width = constraints.shrinkWrap ? maxContentWidth : Math.min(maxWidth, maxContentWidth)
129
- return {
130
- width,
131
- height: truncated ? 0 : lineHeight,
132
- lineCount: truncated ? 0 : 1,
133
- firstBaseline: lineHeight,
134
- lastBaseline: lineHeight,
135
- truncated: maxWidth < maxContentWidth,
136
- lines: includeLines
137
- ? [{ text, width: maxContentWidth, startIndex: 0, endIndex: text.length }]
138
- : undefined,
139
- }
140
- }
141
-
142
- // Word wrapping
143
- const lines: TextLine[] = []
144
- let lineWidth = 0
145
- let lineText = ""
146
- let lineStart = 0
147
- let charIndex = 0
148
-
149
- for (const seg of segments) {
150
- if (seg.isWhitespace) {
151
- if (lineText) {
152
- lineWidth += seg.width
153
- lineText += seg.text
154
- }
155
- charIndex += seg.text.length
156
- continue
157
- }
158
-
159
- if (lineWidth + seg.width > maxWidth && lineText) {
160
- const trimmed = lineText.trimEnd()
161
- lines.push({
162
- text: trimmed,
163
- width: measureString(trimmed, fontSize),
164
- startIndex: lineStart,
165
- endIndex: charIndex,
166
- })
167
- if (lines.length >= maxLines) break
168
-
169
- lineText = seg.text
170
- lineWidth = seg.width
171
- lineStart = charIndex
172
- } else {
173
- lineText += seg.text
174
- lineWidth += seg.width
175
- }
176
- charIndex += seg.text.length
177
- }
178
-
179
- if (lineText && lines.length < maxLines) {
180
- const trimmed = lineText.trimEnd()
181
- lines.push({
182
- text: trimmed,
183
- width: measureString(trimmed, fontSize),
184
- startIndex: lineStart,
185
- endIndex: charIndex,
186
- })
187
- }
188
-
189
- const lineCount = lines.length
190
- const height = lineCount * lineHeight
191
- const widestLine = lines.reduce((max, l) => Math.max(max, l.width), 0)
192
- const width = constraints.shrinkWrap ? widestLine : Math.min(maxWidth, widestLine)
193
-
194
- return {
195
- width,
196
- height,
197
- lineCount,
198
- firstBaseline: lineHeight,
199
- lastBaseline: lineCount > 0 ? (lineCount - 1) * lineHeight + lineHeight : lineHeight,
200
- truncated: lines.length >= maxLines && charIndex < text.length,
201
- lines: includeLines ? lines : undefined,
202
- }
203
- },
204
- }
205
-
206
- return prepared
207
- },
208
- }
209
- }
210
-
211
- /**
212
- * Plugin: add deterministic test text measurement to the engine.
213
- */
214
- export function withTestMeasurer(): FlexilyPlugin {
215
- return (engine) => {
216
- engine.textLayout = createTestMeasurer()
217
- return engine
218
- }
219
- }
package/src/testing.ts DELETED
@@ -1,215 +0,0 @@
1
- /**
2
- * Flexily Testing Utilities
3
- *
4
- * Diagnostic helpers for verifying layout correctness, especially
5
- * incremental re-layout consistency. Used by downstream consumers
6
- * (silvery, km-tui) and flexily's own test suite.
7
- *
8
- * @example
9
- * ```typescript
10
- * import { Node, DIRECTION_LTR } from 'flexily';
11
- * import { getLayout, diffLayouts, assertLayoutSanity } from 'flexily/testing';
12
- *
13
- * const root = Node.create();
14
- * root.setWidth(80);
15
- * root.calculateLayout(80, 24, DIRECTION_LTR);
16
- * assertLayoutSanity(root);
17
- * const layout = getLayout(root);
18
- * ```
19
- */
20
-
21
- import { DIRECTION_LTR, MEASURE_MODE_AT_MOST, MEASURE_MODE_EXACTLY } from "./constants.js"
22
- import type { MeasureFunc } from "./types.js"
23
- import { Node } from "./node-zero.js"
24
-
25
- // ============================================================================
26
- // Types
27
- // ============================================================================
28
-
29
- export interface LayoutResult {
30
- left: number
31
- top: number
32
- width: number
33
- height: number
34
- children: LayoutResult[]
35
- }
36
-
37
- export interface BuildTreeResult {
38
- root: Node
39
- dirtyTargets: Node[]
40
- direction?: number // DIRECTION_LTR or DIRECTION_RTL, defaults to LTR
41
- }
42
-
43
- // ============================================================================
44
- // Layout Inspection
45
- // ============================================================================
46
-
47
- /** Recursively extract computed layout from a node tree. */
48
- export function getLayout(node: Node): LayoutResult {
49
- return {
50
- left: node.getComputedLeft(),
51
- top: node.getComputedTop(),
52
- width: node.getComputedWidth(),
53
- height: node.getComputedHeight(),
54
- children: Array.from({ length: node.getChildCount() }, (_, i) => getLayout(node.getChild(i)!)),
55
- }
56
- }
57
-
58
- /** Format a layout tree as an indented string for debugging. */
59
- export function formatLayout(layout: LayoutResult, indent = 0): string {
60
- const pad = " ".repeat(indent)
61
- let result = `${pad}{ left: ${layout.left}, top: ${layout.top}, width: ${layout.width}, height: ${layout.height} }`
62
- if (layout.children.length > 0) {
63
- result += ` [\n${layout.children.map((c) => formatLayout(c, indent + 1)).join(",\n")}\n${pad}]`
64
- }
65
- return result
66
- }
67
-
68
- /**
69
- * Collect node-by-node diffs between two layout trees.
70
- * Returns empty array if layouts match.
71
- */
72
- export function diffLayouts(a: LayoutResult, b: LayoutResult, path = "root"): string[] {
73
- const diffs: string[] = []
74
- // Use Object.is for NaN-safe comparison (NaN === NaN is false, Object.is(NaN, NaN) is true)
75
- if (!Object.is(a.left, b.left)) diffs.push(`${path}: left ${a.left} vs ${b.left}`)
76
- if (!Object.is(a.top, b.top)) diffs.push(`${path}: top ${a.top} vs ${b.top}`)
77
- if (!Object.is(a.width, b.width)) diffs.push(`${path}: width ${a.width} vs ${b.width}`)
78
- if (!Object.is(a.height, b.height)) diffs.push(`${path}: height ${a.height} vs ${b.height}`)
79
- for (let i = 0; i < Math.max(a.children.length, b.children.length); i++) {
80
- if (a.children[i] && b.children[i]) {
81
- diffs.push(...diffLayouts(a.children[i]!, b.children[i]!, `${path}[${i}]`))
82
- } else if (a.children[i]) {
83
- diffs.push(`${path}[${i}]: missing in incremental`)
84
- } else {
85
- diffs.push(`${path}[${i}]: missing in reference`)
86
- }
87
- }
88
- return diffs
89
- }
90
-
91
- // ============================================================================
92
- // Measure Functions
93
- // ============================================================================
94
-
95
- /**
96
- * Wrapping text measure function factory.
97
- * Simulates text of given width that wraps to multiple lines when constrained.
98
- */
99
- export function textMeasure(textWidth: number): MeasureFunc {
100
- return (width: number, widthMode: number, _height: number, _heightMode: number) => {
101
- if (widthMode === MEASURE_MODE_EXACTLY || widthMode === MEASURE_MODE_AT_MOST) {
102
- if (width >= textWidth) return { width: textWidth, height: 1 }
103
- const lines = Math.ceil(textWidth / width)
104
- return { width: Math.min(textWidth, width), height: lines }
105
- }
106
- return { width: textWidth, height: 1 }
107
- }
108
- }
109
-
110
- // ============================================================================
111
- // Assertions (throw on failure, no vitest dependency)
112
- // ============================================================================
113
-
114
- /**
115
- * Assert that all layout values are non-negative and width is finite.
116
- * Height may be NaN for auto-height trees with unconstrained height.
117
- * Throws a descriptive error on failure.
118
- */
119
- export function assertLayoutSanity(node: Node, path = "root"): void {
120
- const w = node.getComputedWidth()
121
- const h = node.getComputedHeight()
122
- const l = node.getComputedLeft()
123
- const t = node.getComputedTop()
124
-
125
- // Width and height should be non-negative when finite.
126
- // They can be NaN for auto-sized nodes in unconstrained containers (e.g., absolute children).
127
- if (Number.isFinite(w) && w < 0) throw new Error(`${path}: width is negative (${w})`)
128
- if (Number.isFinite(h) && h < 0) throw new Error(`${path}: height is negative (${h})`)
129
- // Position values can be negative (absolute positioning) or non-finite (unconstrained containers).
130
- // Only check that width is not Infinity (NaN is ok for auto-sized).
131
- if (w === Infinity || w === -Infinity) throw new Error(`${path}: width is Infinity`)
132
-
133
- for (let i = 0; i < node.getChildCount(); i++) {
134
- assertLayoutSanity(node.getChild(i)!, `${path}[${i}]`)
135
- }
136
- }
137
-
138
- /**
139
- * Differential oracle: re-layout of partially-dirty tree must match fresh layout.
140
- * Throws a descriptive error with node-by-node diff on failure.
141
- */
142
- export function expectRelayoutMatchesFresh(
143
- buildTree: () => BuildTreeResult,
144
- layoutWidth: number,
145
- layoutHeight: number,
146
- ): void {
147
- // 1. Build, layout, mark dirty, re-layout
148
- const { root, dirtyTargets, direction: dir } = buildTree()
149
- const layoutDir = dir ?? DIRECTION_LTR
150
- root.calculateLayout(layoutWidth, layoutHeight, layoutDir)
151
- for (const t of dirtyTargets) t.markDirty()
152
- root.calculateLayout(layoutWidth, layoutHeight, layoutDir)
153
- const incremental = getLayout(root)
154
-
155
- // 2. Build identical fresh tree, layout once
156
- const fresh = buildTree()
157
- fresh.root.calculateLayout(layoutWidth, layoutHeight, layoutDir)
158
- const reference = getLayout(fresh.root)
159
-
160
- // 3. Must be identical — show detailed diff on failure
161
- const diffs = diffLayouts(reference, incremental)
162
- if (diffs.length > 0) {
163
- const detail = diffs.map((d) => ` ${d}`).join("\n")
164
- throw new Error(
165
- `Incremental layout differs from fresh (${diffs.length} diffs):\n${detail}\n\nFresh:\n${formatLayout(reference)}\n\nIncremental:\n${formatLayout(incremental)}`,
166
- )
167
- }
168
- }
169
-
170
- /**
171
- * Assert that laying out twice with identical constraints produces identical results.
172
- * Catches non-determinism or state corruption from a single layout pass.
173
- */
174
- export function expectIdempotent(buildTree: () => BuildTreeResult, layoutWidth: number, layoutHeight: number): void {
175
- const { root, direction: dir } = buildTree()
176
- const layoutDir = dir ?? DIRECTION_LTR
177
- root.calculateLayout(layoutWidth, layoutHeight, layoutDir)
178
- const first = getLayout(root)
179
- root.calculateLayout(layoutWidth, layoutHeight, layoutDir)
180
- const second = getLayout(root)
181
-
182
- const diffs = diffLayouts(first, second)
183
- if (diffs.length > 0) {
184
- const detail = diffs.map((d) => ` ${d}`).join("\n")
185
- throw new Error(`Layout not idempotent (${diffs.length} diffs after 2nd pass):\n${detail}`)
186
- }
187
- }
188
-
189
- /**
190
- * Assert that layout at width W, then different width W', then back to W
191
- * produces the same result as fresh layout at W.
192
- * Catches stale cache entries that don't update on constraint change.
193
- */
194
- export function expectResizeRoundTrip(buildTree: () => BuildTreeResult, widths: number[]): void {
195
- const { root, direction: dir } = buildTree()
196
- const layoutDir = dir ?? DIRECTION_LTR
197
- for (const w of widths) {
198
- root.setWidth(w)
199
- root.calculateLayout(w, NaN, layoutDir)
200
- }
201
- const incremental = getLayout(root)
202
-
203
- // Fresh reference at final width
204
- const finalWidth = widths[widths.length - 1]!
205
- const fresh = buildTree()
206
- fresh.root.setWidth(finalWidth)
207
- fresh.root.calculateLayout(finalWidth, NaN, layoutDir)
208
- const reference = getLayout(fresh.root)
209
-
210
- const diffs = diffLayouts(reference, incremental)
211
- if (diffs.length > 0) {
212
- const detail = diffs.map((d) => ` ${d}`).join("\n")
213
- throw new Error(`Resize round-trip differs (widths: ${widths.join("→")}, ${diffs.length} diffs):\n${detail}`)
214
- }
215
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * Text Layout Service — pluggable text measurement for Flexily.
3
- *
4
- * The TextLayoutService interface decouples text measurement from the layout engine.
5
- * Different backends handle different environments:
6
- * - MonospaceMeasurer: terminal (1 char = 1 cell)
7
- * - DeterministicTestMeasurer: tests/CI (fixed grapheme widths)
8
- * - PretextMeasurer: proportional fonts (Canvas measureText)
9
- *
10
- * See silvery-internal/design/v05-layout/pretext-integration.md for design rationale.
11
- */
12
-
13
- /** Resolved text style — consumed by both measurement and painting. */
14
- export interface ResolvedTextStyle {
15
- fontShorthand: string // e.g. "14px 'Inter', sans-serif"
16
- fontFamily: string
17
- fontSize: number
18
- fontWeight: number
19
- fontStyle: string
20
- lineHeight: number
21
- }
22
-
23
- /** Input for text preparation. */
24
- export interface TextPrepareInput {
25
- text: string
26
- style: ResolvedTextStyle
27
- direction?: "auto" | "ltr" | "rtl"
28
- locale?: string
29
- }
30
-
31
- /** Intrinsic text sizes for flexbox min/max-content. */
32
- export interface IntrinsicSizes {
33
- minContentWidth: number // longest unbreakable segment
34
- maxContentWidth: number // unwrapped total width
35
- }
36
-
37
- /** Constraints for text layout. */
38
- export interface TextConstraints {
39
- maxWidth?: number
40
- maxHeight?: number
41
- maxLines?: number
42
- wrap?: "normal" | "anywhere" | "none"
43
- overflow?: "clip" | "ellipsis"
44
- shrinkWrap?: boolean
45
- }
46
-
47
- /** A single laid-out line of text. */
48
- export interface TextLine {
49
- text: string
50
- width: number
51
- startIndex: number
52
- endIndex: number
53
- }
54
-
55
- /** Result of laying out prepared text at a specific width. */
56
- export interface TextLayout {
57
- width: number
58
- height: number
59
- lineCount: number
60
- firstBaseline: number
61
- lastBaseline: number
62
- truncated: boolean
63
- lines?: readonly TextLine[]
64
- }
65
-
66
- /** Prepared text — measured and segmented, ready for layout at any width. */
67
- export interface PreparedText {
68
- intrinsicSizes(): IntrinsicSizes
69
- layout(constraints: TextConstraints, options?: { includeLines?: boolean }): TextLayout
70
- }
71
-
72
- /** Pluggable text measurement backend. */
73
- export interface TextLayoutService {
74
- prepare(input: TextPrepareInput): PreparedText
75
- }