@effect-tui/react 0.16.0 → 2.0.1

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 (126) hide show
  1. package/README.md +9 -0
  2. package/dist/src/codeblock.d.ts +1 -1
  3. package/dist/src/codeblock.d.ts.map +1 -1
  4. package/dist/src/codeblock.js +2 -2
  5. package/dist/src/codeblock.js.map +1 -1
  6. package/dist/src/components/Markdown.js +3 -3
  7. package/dist/src/components/Markdown.js.map +1 -1
  8. package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
  9. package/dist/src/components/MultilineTextInput.js +133 -305
  10. package/dist/src/components/MultilineTextInput.js.map +1 -1
  11. package/dist/src/components/TextInput.d.ts.map +1 -1
  12. package/dist/src/components/TextInput.js +51 -98
  13. package/dist/src/components/TextInput.js.map +1 -1
  14. package/dist/src/components/text-editing.d.ts +61 -0
  15. package/dist/src/components/text-editing.d.ts.map +1 -1
  16. package/dist/src/components/text-editing.js +131 -0
  17. package/dist/src/components/text-editing.js.map +1 -1
  18. package/dist/src/hosts/base.d.ts +13 -2
  19. package/dist/src/hosts/base.d.ts.map +1 -1
  20. package/dist/src/hosts/base.js +74 -2
  21. package/dist/src/hosts/base.js.map +1 -1
  22. package/dist/src/hosts/box.d.ts +2 -2
  23. package/dist/src/hosts/box.d.ts.map +1 -1
  24. package/dist/src/hosts/box.js +29 -2
  25. package/dist/src/hosts/box.js.map +1 -1
  26. package/dist/src/hosts/canvas.d.ts +22 -2
  27. package/dist/src/hosts/canvas.d.ts.map +1 -1
  28. package/dist/src/hosts/canvas.js +99 -31
  29. package/dist/src/hosts/canvas.js.map +1 -1
  30. package/dist/src/hosts/codeblock.d.ts +8 -10
  31. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  32. package/dist/src/hosts/codeblock.js +36 -33
  33. package/dist/src/hosts/codeblock.js.map +1 -1
  34. package/dist/src/hosts/flex-container.d.ts +2 -2
  35. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  36. package/dist/src/hosts/flex-container.js +17 -2
  37. package/dist/src/hosts/flex-container.js.map +1 -1
  38. package/dist/src/hosts/index.d.ts +1 -1
  39. package/dist/src/hosts/index.d.ts.map +1 -1
  40. package/dist/src/hosts/index.js.map +1 -1
  41. package/dist/src/hosts/overlay-item.d.ts +2 -2
  42. package/dist/src/hosts/overlay-item.d.ts.map +1 -1
  43. package/dist/src/hosts/overlay-item.js +7 -2
  44. package/dist/src/hosts/overlay-item.js.map +1 -1
  45. package/dist/src/hosts/overlay.d.ts +2 -2
  46. package/dist/src/hosts/overlay.d.ts.map +1 -1
  47. package/dist/src/hosts/overlay.js +2 -2
  48. package/dist/src/hosts/overlay.js.map +1 -1
  49. package/dist/src/hosts/scroll.d.ts +7 -2
  50. package/dist/src/hosts/scroll.d.ts.map +1 -1
  51. package/dist/src/hosts/scroll.js +126 -45
  52. package/dist/src/hosts/scroll.js.map +1 -1
  53. package/dist/src/hosts/single-child.d.ts.map +1 -1
  54. package/dist/src/hosts/single-child.js +2 -0
  55. package/dist/src/hosts/single-child.js.map +1 -1
  56. package/dist/src/hosts/spacer.d.ts +1 -1
  57. package/dist/src/hosts/spacer.d.ts.map +1 -1
  58. package/dist/src/hosts/spacer.js +6 -1
  59. package/dist/src/hosts/spacer.js.map +1 -1
  60. package/dist/src/hosts/text.d.ts +20 -15
  61. package/dist/src/hosts/text.d.ts.map +1 -1
  62. package/dist/src/hosts/text.js +104 -71
  63. package/dist/src/hosts/text.js.map +1 -1
  64. package/dist/src/hosts/zstack.d.ts +2 -2
  65. package/dist/src/hosts/zstack.d.ts.map +1 -1
  66. package/dist/src/hosts/zstack.js +7 -2
  67. package/dist/src/hosts/zstack.js.map +1 -1
  68. package/dist/src/index.d.ts +1 -1
  69. package/dist/src/index.d.ts.map +1 -1
  70. package/dist/src/internal/renderer/index.d.ts.map +1 -1
  71. package/dist/src/internal/renderer/index.js +41 -16
  72. package/dist/src/internal/renderer/index.js.map +1 -1
  73. package/dist/src/internal/renderer/types.d.ts +4 -0
  74. package/dist/src/internal/renderer/types.d.ts.map +1 -1
  75. package/dist/src/motion/hooks.d.ts +1 -1
  76. package/dist/src/motion/hooks.js +1 -1
  77. package/dist/src/reconciler/host-config.js +2 -2
  78. package/dist/src/reconciler/host-config.js.map +1 -1
  79. package/dist/src/reconciler/types.d.ts +5 -1
  80. package/dist/src/reconciler/types.d.ts.map +1 -1
  81. package/dist/src/utils/border.d.ts +1 -1
  82. package/dist/src/utils/border.d.ts.map +1 -1
  83. package/dist/src/utils/border.js +2 -0
  84. package/dist/src/utils/border.js.map +1 -1
  85. package/dist/src/utils/index.d.ts +2 -1
  86. package/dist/src/utils/index.d.ts.map +1 -1
  87. package/dist/src/utils/index.js +2 -1
  88. package/dist/src/utils/index.js.map +1 -1
  89. package/dist/src/utils/text-layout.d.ts +22 -0
  90. package/dist/src/utils/text-layout.d.ts.map +1 -0
  91. package/dist/src/utils/text-layout.js +37 -0
  92. package/dist/src/utils/text-layout.js.map +1 -0
  93. package/dist/src/utils/text-wrap.d.ts +26 -1
  94. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  95. package/dist/src/utils/text-wrap.js +106 -11
  96. package/dist/src/utils/text-wrap.js.map +1 -1
  97. package/dist/tsconfig.tsbuildinfo +1 -1
  98. package/package.json +2 -2
  99. package/src/codeblock.tsx +2 -2
  100. package/src/components/Markdown.tsx +3 -3
  101. package/src/components/MultilineTextInput.tsx +138 -344
  102. package/src/components/TextInput.tsx +54 -99
  103. package/src/components/text-editing.ts +180 -0
  104. package/src/hosts/base.ts +86 -3
  105. package/src/hosts/box.ts +37 -2
  106. package/src/hosts/canvas.ts +120 -31
  107. package/src/hosts/codeblock.ts +46 -33
  108. package/src/hosts/flex-container.ts +21 -2
  109. package/src/hosts/index.ts +1 -1
  110. package/src/hosts/overlay-item.ts +8 -2
  111. package/src/hosts/overlay.ts +2 -2
  112. package/src/hosts/scroll.ts +142 -45
  113. package/src/hosts/single-child.ts +2 -0
  114. package/src/hosts/spacer.ts +6 -1
  115. package/src/hosts/text.ts +122 -75
  116. package/src/hosts/zstack.ts +7 -2
  117. package/src/index.ts +1 -1
  118. package/src/internal/renderer/index.ts +53 -20
  119. package/src/internal/renderer/types.ts +4 -0
  120. package/src/motion/hooks.ts +1 -1
  121. package/src/reconciler/host-config.ts +2 -2
  122. package/src/reconciler/types.ts +7 -1
  123. package/src/utils/border.ts +11 -1
  124. package/src/utils/index.ts +15 -1
  125. package/src/utils/text-layout.ts +65 -0
  126. package/src/utils/text-wrap.ts +135 -13
@@ -1,6 +1,7 @@
1
1
  import type { CellBuffer, Color, Palette } from "@effect-tui/core"
2
- import { Colors } from "@effect-tui/core"
2
+ import { Colors, displayWidth } from "@effect-tui/core"
3
3
  import type { CommonProps, HostContext, Size } from "../reconciler/types.js"
4
+ import * as Prof from "../profiler.js"
4
5
  import {
5
6
  type BorderKind,
6
7
  borderChars,
@@ -20,6 +21,9 @@ export interface DrawContext {
20
21
  /** Canvas height in cells */
21
22
  height: number
22
23
 
24
+ /** Resolve a style id for reuse across many draw calls */
25
+ style(opts?: { fg?: Color; bg?: Color; bold?: boolean; italic?: boolean; underline?: boolean; inverse?: boolean }): number
26
+
23
27
  /** Draw text at position */
24
28
  text(
25
29
  x: number,
@@ -29,7 +33,7 @@ export interface DrawContext {
29
33
  ): void
30
34
 
31
35
  /** Fill rectangle with character */
32
- fill(
36
+ fillRect(
33
37
  x: number,
34
38
  y: number,
35
39
  w: number,
@@ -54,6 +58,20 @@ export interface DrawContext {
54
58
 
55
59
  /** Clear entire canvas */
56
60
  clear(): void
61
+
62
+ /** Draw a single cell with a codepoint */
63
+ cell(x: number, y: number, cp: number, style?: number, width?: number): void
64
+
65
+ /** Draw many single-cell codepoints efficiently */
66
+ cells(cells: Array<CanvasCell>): void
67
+ }
68
+
69
+ export interface CanvasCell {
70
+ x: number
71
+ y: number
72
+ cp: number
73
+ style?: number
74
+ width?: number
57
75
  }
58
76
 
59
77
  export interface CanvasProps extends CommonProps {
@@ -78,7 +96,7 @@ export class CanvasHost extends LeafHost {
78
96
  this.updateProps(props as unknown as Record<string, unknown>)
79
97
  }
80
98
 
81
- measure(maxW: number, maxH: number): Size {
99
+ protected measureSelf(maxW: number, maxH: number): Size {
82
100
  const constrained = this.constrainProposal(maxW, maxH)
83
101
  const size = {
84
102
  w: this.fixedWidth ?? constrained.w,
@@ -103,11 +121,24 @@ export class CanvasHost extends LeafHost {
103
121
  width: w,
104
122
  height: h,
105
123
 
124
+ style: (opts) => {
125
+ const effectiveBg = opts?.bg ?? (this.inheritBg ? inheritedBgValue : undefined)
126
+ return styleIdFromProps(palette, {
127
+ fg: opts?.fg,
128
+ bg: effectiveBg,
129
+ bold: opts?.bold,
130
+ italic: opts?.italic,
131
+ underline: opts?.underline,
132
+ inverse: opts?.inverse,
133
+ })
134
+ },
135
+
106
136
  text: (x, y, str, opts) => {
107
137
  const px = Math.round(ox + x)
108
138
  const py = Math.round(oy + y)
109
139
  // Use inherited bg when inheritBg is enabled and no explicit bg provided
110
140
  const effectiveBg = opts?.bg ?? (this.inheritBg ? inheritedBgValue : undefined)
141
+ const styleT = Prof.startPhase()
111
142
  const style = styleIdFromProps(palette, {
112
143
  fg: opts?.fg,
113
144
  bg: effectiveBg,
@@ -116,20 +147,40 @@ export class CanvasHost extends LeafHost {
116
147
  underline: opts?.underline,
117
148
  inverse: opts?.inverse,
118
149
  })
119
- let col = px
120
- for (const char of str) {
121
- if (col >= ox + w) break
122
- buffer.drawCP(col, py, char.codePointAt(0)!, style)
123
- col++
150
+ Prof.endPhase("canvas.text.style", styleT)
151
+ const widthT = Prof.startPhase()
152
+ let textWidth = 0
153
+ let asciiCp: number | null = null
154
+ if (str.length === 1) {
155
+ const code = str.charCodeAt(0)
156
+ if (code >= 0x20 && code <= 0x7e) {
157
+ textWidth = 1
158
+ asciiCp = code
159
+ } else {
160
+ textWidth = displayWidth(str)
161
+ }
162
+ } else {
163
+ textWidth = displayWidth(str)
164
+ }
165
+ Prof.endPhase("canvas.text.width", widthT)
166
+ if (textWidth > 0) {
167
+ const drawT = Prof.startPhase()
168
+ if (asciiCp !== null) {
169
+ buffer.drawCP(px, py, asciiCp, style, 1)
170
+ } else {
171
+ buffer.drawText(px, py, str, style, textWidth)
172
+ }
173
+ Prof.endPhase("canvas.text.draw", drawT)
124
174
  }
125
175
  },
126
176
 
127
- fill: (x, y, fw, fh, char = " ", opts) => {
177
+ fillRect: (x, y, fw, fh, char = " ", opts) => {
128
178
  const px = Math.round(ox + x)
129
179
  const py = Math.round(oy + y)
130
- const cp = char.codePointAt(0)!
180
+ const cp = char.length > 0 ? char.codePointAt(0)! : " ".codePointAt(0)!
131
181
  // Use inherited bg when inheritBg is enabled and no explicit bg provided
132
182
  const effectiveBg = opts?.bg ?? (this.inheritBg ? inheritedBgValue : undefined)
183
+ const styleT = Prof.startPhase()
133
184
  const style = styleIdFromProps(palette, {
134
185
  fg: opts?.fg,
135
186
  bg: effectiveBg,
@@ -138,65 +189,103 @@ export class CanvasHost extends LeafHost {
138
189
  underline: opts?.underline,
139
190
  inverse: opts?.inverse,
140
191
  })
141
- for (let row = 0; row < fh; row++) {
142
- const yy = py + row
143
- for (let col = 0; col < fw; col++) {
144
- const xx = px + col
145
- buffer.drawCP(xx, yy, cp, style)
146
- }
147
- }
192
+ Prof.endPhase("canvas.fillRect.style", styleT)
193
+ const fillW = Math.ceil(fw)
194
+ const fillH = Math.ceil(fh)
195
+ const drawT = Prof.startPhase()
196
+ buffer.fillRect(px, py, fillW, fillH, cp, style)
197
+ Prof.endPhase("canvas.fillRect.draw", drawT)
148
198
  },
149
199
 
150
200
  box: (x, y, bw, bh, opts) => {
151
201
  const px = Math.round(ox + x)
152
202
  const py = Math.round(oy + y)
203
+ const boxW = Math.ceil(bw)
204
+ const boxH = Math.ceil(bh)
153
205
  const border = opts?.border ?? "none"
206
+ const bgStyleT = Prof.startPhase()
154
207
  const { value: bgValue, styleId: bgStyleId } = resolveBgStyle(palette, opts?.bg)
208
+ Prof.endPhase("canvas.box.bg.style", bgStyleT)
155
209
 
156
210
  // Fill background (with clipping)
157
211
  if (bgValue !== undefined) {
158
- for (let row = 0; row < bh; row++) {
159
- const yy = py + row
160
- for (let col = 0; col < bw; col++) {
161
- const xx = px + col
162
- buffer.drawCP(xx, yy, " ".codePointAt(0)!, bgStyleId)
163
- }
164
- }
212
+ const bgDrawT = Prof.startPhase()
213
+ buffer.fillRect(px, py, boxW, boxH, " ".codePointAt(0)!, bgStyleId)
214
+ Prof.endPhase("canvas.box.bg.draw", bgDrawT)
165
215
  }
166
216
 
167
217
  // Draw border (with clipping)
168
- if (border !== "none" && bw >= 2 && bh >= 2) {
218
+ if (border !== "none" && boxW >= 2 && boxH >= 2) {
219
+ const borderStyleT = Prof.startPhase()
169
220
  const chars = borderChars(border)
170
221
  const borderFg = toColorValue(opts?.borderColor) ?? toColorValue(opts?.fg) ?? Colors.ansi.gray(8)
171
222
  const borderStyle = palette.id({ fg: borderFg })
172
- drawBorder(buffer, px, py, bw, bh, chars, borderStyle, { ox, oy, w, h })
223
+ Prof.endPhase("canvas.box.border.style", borderStyleT)
224
+ const borderDrawT = Prof.startPhase()
225
+ drawBorder(buffer, px, py, boxW, boxH, chars, borderStyle, { ox, oy, w, h })
226
+ Prof.endPhase("canvas.box.border.draw", borderDrawT)
173
227
  }
174
228
  },
175
229
 
176
230
  clear: () => {
231
+ const clearT = Prof.startPhase()
177
232
  const style = palette.id({})
178
- for (let row = 0; row < h; row++) {
179
- for (let col = 0; col < w; col++) {
180
- buffer.drawCP(ox + col, oy + row, " ".codePointAt(0)!, style)
181
- }
233
+ buffer.fillRect(ox, oy, w, h, " ".codePointAt(0)!, style)
234
+ Prof.endPhase("canvas.clear", clearT)
235
+ },
236
+
237
+ cell: (x, y, cp, style = 0, width) => {
238
+ const px = Math.round(ox + x)
239
+ const py = Math.round(oy + y)
240
+ const drawT = Prof.startPhase()
241
+ buffer.drawCP(px, py, cp, style, width)
242
+ Prof.endPhase("canvas.cell.draw", drawT)
243
+ },
244
+
245
+ cells: (cells) => {
246
+ const drawT = Prof.startPhase()
247
+ const baseX = ox
248
+ const baseY = oy
249
+ for (const cell of cells) {
250
+ const px = Math.round(baseX + cell.x)
251
+ const py = Math.round(baseY + cell.y)
252
+ buffer.drawCP(px, py, cell.cp, cell.style ?? 0, cell.width)
182
253
  }
254
+ Prof.endPhase("canvas.cells.draw", drawT)
183
255
  },
184
256
  }
185
257
 
186
258
  buffer.withClip(ox, oy, w, h, () => {
187
259
  // Call user's draw function within the canvas clip region
260
+ const drawT = Prof.startPhase()
188
261
  this.draw(ctx)
262
+ Prof.endPhase("canvas.draw", drawT)
189
263
  })
190
264
  }
191
265
 
192
266
  override updateProps(props: Record<string, unknown>): void {
193
267
  super.updateProps(props)
268
+ const prevDraw = this.draw
269
+ const prevWidth = this.fixedWidth
270
+ const prevHeight = this.fixedHeight
271
+ const prevInheritBg = this.inheritBg
272
+
194
273
  if (props.draw !== undefined) {
195
274
  this.draw = props.draw as CanvasProps["draw"]
196
- this.ctx.requestRender() // trigger repaint when draw function changes
197
275
  }
198
276
  if (props.width !== undefined) this.fixedWidth = props.width as number
199
277
  if (props.height !== undefined) this.fixedHeight = props.height as number
200
278
  if (props.inheritBg !== undefined) this.inheritBg = props.inheritBg as boolean
279
+
280
+ const layoutChanged = prevWidth !== this.fixedWidth || prevHeight !== this.fixedHeight
281
+ if (layoutChanged) {
282
+ this.invalidateLayout()
283
+ return
284
+ }
285
+
286
+ const renderChanged = prevDraw !== this.draw || prevInheritBg !== this.inheritBg
287
+ if (renderChanged) {
288
+ this.invalidateRender()
289
+ }
201
290
  }
202
291
  }
@@ -1,33 +1,35 @@
1
1
  import { type CellBuffer, type Color, Colors, displayWidth, type Palette } from "@effect-tui/core"
2
2
  import type { HighlightLine } from "../highlight.js"
3
3
  import type { CommonProps, HostContext, Size } from "../reconciler/types.js"
4
- import { type Padding, type PaddingInput, resolveBgStyle, resolvePadding, styleIdFromProps } from "../utils/index.js"
4
+ import {
5
+ type Padding,
6
+ type PaddingInput,
7
+ resolveBgStyle,
8
+ resolvePadding,
9
+ spansDisplayWidth,
10
+ styleIdFromProps,
11
+ } from "../utils/index.js"
5
12
  import { LeafHost } from "./leaf.js"
6
13
 
7
14
  export interface CodeBlockProps extends CommonProps {
8
15
  lines: HighlightLine[]
9
16
  lineNumbers?: boolean
10
17
  padding?: PaddingInput
11
- background?: Color
12
- lineNumberColor?: Color
13
- lineNumberBackground?: Color
14
- }
15
-
16
- function lineDisplayWidth(line: HighlightLine): number {
17
- return line.reduce((w, token) => w + displayWidth(token.text), 0)
18
+ bg?: Color
19
+ lineNumberFg?: Color
20
+ lineNumberBg?: Color
18
21
  }
19
22
 
20
23
  export class CodeBlockHost extends LeafHost {
21
24
  lines: HighlightLine[] = [[]]
22
25
  lineNumbers = false
23
26
  padding: Padding = { top: 0, right: 0, bottom: 0, left: 0 }
24
- background?: Color
25
- lineNumberColor?: Color
26
- lineNumberBackground?: Color
27
+ bg?: Color
28
+ lineNumberFg?: Color
29
+ lineNumberBg?: Color
27
30
 
28
31
  private cachedLineWidths: number[] = []
29
32
  private gutterWidth = 0
30
- private prepared = false
31
33
 
32
34
  constructor(props: CodeBlockProps, ctx: HostContext) {
33
35
  super("codeblock", props, ctx)
@@ -51,21 +53,15 @@ export class CodeBlockHost extends LeafHost {
51
53
  }
52
54
 
53
55
  private prepareMetrics(): void {
54
- this.cachedLineWidths = this.lines.map((line) => lineDisplayWidth(line))
56
+ this.cachedLineWidths = this.lines.map((line) => spansDisplayWidth(line))
55
57
  this.gutterWidth = this.computeGutterWidth()
56
- this.prepared = true
57
58
  }
58
59
 
59
- private ensurePrepared(): void {
60
- if (this.prepared) return
60
+ protected override prepareSelf(_layoutDirty: boolean, _renderDirty: boolean): void {
61
61
  this.prepareMetrics()
62
62
  }
63
63
 
64
- protected override prepareSelf(): void {
65
- this.ensurePrepared()
66
- }
67
-
68
- measure(maxW: number, maxH: number): Size {
64
+ protected measureSelf(maxW: number, maxH: number): Size {
69
65
  const constrained = this.constrainProposal(maxW, maxH)
70
66
  this.ensurePrepared()
71
67
 
@@ -89,7 +85,7 @@ export class CodeBlockHost extends LeafHost {
89
85
  const startX = x + this.padding.left + this.gutterWidth
90
86
  const startY = y + this.padding.top
91
87
 
92
- const { value: bgValue, styleId: bgStyleId } = resolveBgStyle(palette, this.background)
88
+ const { value: bgValue, styleId: bgStyleId } = resolveBgStyle(palette, this.bg)
93
89
  if (bgValue !== undefined && w > 0 && h > 0) {
94
90
  buffer.fillRect(x, y, w, h, " ".codePointAt(0)!, bgStyleId)
95
91
  }
@@ -100,8 +96,8 @@ export class CodeBlockHost extends LeafHost {
100
96
 
101
97
  if (this.lineNumbers) {
102
98
  const gutterStyle = styleIdFromProps(palette, {
103
- fg: this.lineNumberColor ?? Colors.ansi.gray(11),
104
- bg: this.lineNumberBackground ?? this.background,
99
+ fg: this.lineNumberFg ?? Colors.ansi.gray(11),
100
+ bg: this.lineNumberBg ?? this.bg,
105
101
  })
106
102
  const digits = String(i + 1).padStart(this.gutterWidth - 1, " ")
107
103
  buffer.drawText(x + this.padding.left, lineY, `${digits} `, gutterStyle, this.gutterWidth)
@@ -116,7 +112,7 @@ export class CodeBlockHost extends LeafHost {
116
112
  const style = token.style ?? {}
117
113
  const styleId = styleIdFromProps(palette, {
118
114
  fg: style.fg,
119
- bg: style.bg ?? this.background,
115
+ bg: style.bg ?? this.bg,
120
116
  bold: style.bold,
121
117
  italic: style.italic,
122
118
  underline: style.underline,
@@ -131,19 +127,36 @@ export class CodeBlockHost extends LeafHost {
131
127
 
132
128
  override updateProps(props: Record<string, unknown>): void {
133
129
  super.updateProps(props)
134
- let invalidate = false
130
+ let layoutChanged = false
131
+ let renderChanged = false
135
132
  if (props.lines !== undefined) {
136
133
  this.lines = props.lines as HighlightLine[]
137
- invalidate = true
134
+ layoutChanged = true
138
135
  }
139
136
  if (props.lineNumbers !== undefined) {
140
137
  this.lineNumbers = !!props.lineNumbers
141
- invalidate = true
138
+ layoutChanged = true
139
+ }
140
+ if (props.padding !== undefined) {
141
+ this.padding = resolvePadding(props.padding as CodeBlockProps["padding"])
142
+ layoutChanged = true
143
+ }
144
+ if (props.bg !== undefined) {
145
+ this.bg = props.bg as Color
146
+ renderChanged = true
147
+ }
148
+ if (props.lineNumberFg !== undefined) {
149
+ this.lineNumberFg = props.lineNumberFg as Color
150
+ renderChanged = true
151
+ }
152
+ if (props.lineNumberBg !== undefined) {
153
+ this.lineNumberBg = props.lineNumberBg as Color
154
+ renderChanged = true
155
+ }
156
+ if (layoutChanged) {
157
+ this.invalidateLayout()
158
+ } else if (renderChanged) {
159
+ this.invalidateRender()
142
160
  }
143
- if (props.padding !== undefined) this.padding = resolvePadding(props.padding as CodeBlockProps["padding"])
144
- if (props.background !== undefined) this.background = props.background as Color
145
- if (props.lineNumberColor !== undefined) this.lineNumberColor = props.lineNumberColor as Color
146
- if (props.lineNumberBackground !== undefined) this.lineNumberBackground = props.lineNumberBackground as Color
147
- if (invalidate) this.prepared = false
148
161
  }
149
162
  }
@@ -79,7 +79,7 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
79
79
  * </vstack>
80
80
  * ```
81
81
  */
82
- measure(maxW: number, maxH: number): Size {
82
+ protected measureSelf(maxW: number, maxH: number): Size {
83
83
  // Apply frame constraints to what we propose to children
84
84
  const constrained = this.constrainProposal(maxW, maxH)
85
85
  const insetX = this.padding.left + this.padding.right
@@ -102,7 +102,7 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
102
102
  return this.constrainResult(paddedSize)
103
103
  }
104
104
 
105
- override layout(rect: Rect): void {
105
+ protected override layoutSelf(rect: Rect): void {
106
106
  const layoutRect = this.layoutWithConstraints(rect)
107
107
  const stretchCross = this.axis === "vertical" ? this.alignment === "left" : this.alignment === "top"
108
108
  const insetX = this.padding.left + this.padding.right
@@ -142,6 +142,11 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
142
142
 
143
143
  override updateProps(props: Record<string, unknown>): void {
144
144
  super.updateProps(props)
145
+ const prevSpacing = this.spacing
146
+ const prevAlignment = this.alignment
147
+ const prevPadding = this.padding
148
+ const prevBg = this.bg
149
+
145
150
  this.spacing = (props.spacing as number | undefined) ?? 0
146
151
  // Reset to axis-specific default when undefined
147
152
  this.alignment =
@@ -149,5 +154,19 @@ export class FlexContainerHost<A extends FlexAxis> extends BaseHost {
149
154
  ((this.axis === "vertical" ? "left" : "top") as CrossAlignment<A>)
150
155
  this.padding = resolvePadding(props.padding as FlexContainerProps<A>["padding"])
151
156
  this.bg = props.bg as Color | undefined
157
+
158
+ const paddingChanged =
159
+ prevPadding.top !== this.padding.top ||
160
+ prevPadding.right !== this.padding.right ||
161
+ prevPadding.bottom !== this.padding.bottom ||
162
+ prevPadding.left !== this.padding.left
163
+
164
+ const layoutChanged = prevSpacing !== this.spacing || prevAlignment !== this.alignment || paddingChanged
165
+
166
+ if (layoutChanged) {
167
+ this.invalidateLayout()
168
+ } else if (prevBg !== this.bg) {
169
+ this.invalidateRender()
170
+ }
152
171
  }
153
172
  }
@@ -14,7 +14,7 @@ import { ZStackHost } from "./zstack.js"
14
14
 
15
15
  export { BaseHost } from "./base.js"
16
16
  export { BoxHost, type BoxProps } from "./box.js"
17
- export { CanvasHost, type CanvasProps, type DrawContext } from "./canvas.js"
17
+ export { CanvasHost, type CanvasCell, type CanvasProps, type DrawContext } from "./canvas.js"
18
18
  export { CodeBlockHost, type CodeBlockProps } from "./codeblock.js"
19
19
  export { HStackHost, type HStackProps } from "./hstack.js"
20
20
  export { OverlayHost, type OverlayProps } from "./overlay.js"
@@ -26,7 +26,7 @@ export class OverlayItemHost extends SingleChildHost {
26
26
  this.updateProps(props as unknown as Record<string, unknown>)
27
27
  }
28
28
 
29
- override measure(maxW: number, maxH: number): Size {
29
+ protected override measureSelf(maxW: number, maxH: number): Size {
30
30
  const constrained = this.constrainProposal(maxW, maxH)
31
31
 
32
32
  if (!this.child) {
@@ -37,7 +37,7 @@ export class OverlayItemHost extends SingleChildHost {
37
37
  return this.constrainResult(childSize)
38
38
  }
39
39
 
40
- override layout(rect: Rect): void {
40
+ protected override layoutSelf(rect: Rect): void {
41
41
  const layoutRect = this.layoutWithConstraints(rect)
42
42
  this.child?.layout(layoutRect)
43
43
  }
@@ -50,8 +50,14 @@ export class OverlayItemHost extends SingleChildHost {
50
50
 
51
51
  override updateProps(props: Record<string, unknown>): void {
52
52
  super.updateProps(props)
53
+ const prevAlignment = this.alignment
53
54
  if (props.alignment !== undefined) {
54
55
  this.alignment = props.alignment as { h?: HAlign; v?: VAlign }
55
56
  }
57
+ const layoutChanged =
58
+ prevAlignment.h !== this.alignment.h || prevAlignment.v !== this.alignment.v
59
+ if (layoutChanged) {
60
+ this.invalidateLayout()
61
+ }
56
62
  }
57
63
  }
@@ -32,7 +32,7 @@ export class OverlayHost extends BaseHost {
32
32
  this.updateProps(props as unknown as Record<string, unknown>)
33
33
  }
34
34
 
35
- override measure(maxW: number, maxH: number): Size {
35
+ protected override measureSelf(maxW: number, maxH: number): Size {
36
36
  // Apply frame constraints to what we propose to children
37
37
  const constrained = this.constrainProposal(maxW, maxH)
38
38
 
@@ -56,7 +56,7 @@ export class OverlayHost extends BaseHost {
56
56
  return this.constrainResult(baseSize)
57
57
  }
58
58
 
59
- override layout(rect: Rect): void {
59
+ protected override layoutSelf(rect: Rect): void {
60
60
  const layoutRect = this.layoutWithConstraints(rect)
61
61
 
62
62
  // Layout base child to fill our rect