@effect-tui/react 0.15.2 → 2.0.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 (313) hide show
  1. package/README.md +11 -2
  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/ListView.d.ts +4 -4
  7. package/dist/src/components/ListView.d.ts.map +1 -1
  8. package/dist/src/components/ListView.js +16 -17
  9. package/dist/src/components/ListView.js.map +1 -1
  10. package/dist/src/components/Markdown.js +3 -3
  11. package/dist/src/components/Markdown.js.map +1 -1
  12. package/dist/src/components/MultilineTextInput.d.ts.map +1 -1
  13. package/dist/src/components/MultilineTextInput.js +133 -305
  14. package/dist/src/components/MultilineTextInput.js.map +1 -1
  15. package/dist/src/components/TextInput.d.ts.map +1 -1
  16. package/dist/src/components/TextInput.js +51 -98
  17. package/dist/src/components/TextInput.js.map +1 -1
  18. package/dist/src/components/text-editing.d.ts +61 -0
  19. package/dist/src/components/text-editing.d.ts.map +1 -1
  20. package/dist/src/components/text-editing.js +131 -0
  21. package/dist/src/components/text-editing.js.map +1 -1
  22. package/dist/src/console/ConsolePopover.d.ts +7 -1
  23. package/dist/src/console/ConsolePopover.d.ts.map +1 -1
  24. package/dist/src/console/ConsolePopover.js +55 -74
  25. package/dist/src/console/ConsolePopover.js.map +1 -1
  26. package/dist/src/debug/DebugOverlay.d.ts.map +1 -1
  27. package/dist/src/debug/DebugOverlay.js +3 -57
  28. package/dist/src/debug/DebugOverlay.js.map +1 -1
  29. package/dist/src/debug/DiagnosticsPanel.js +1 -1
  30. package/dist/src/debug/DiagnosticsPanel.js.map +1 -1
  31. package/dist/src/dev.d.ts +5 -117
  32. package/dist/src/dev.d.ts.map +1 -1
  33. package/dist/src/dev.js +3 -333
  34. package/dist/src/dev.js.map +1 -1
  35. package/dist/src/hooks/use-scroll.d.ts +31 -35
  36. package/dist/src/hooks/use-scroll.d.ts.map +1 -1
  37. package/dist/src/hooks/use-scroll.js +51 -90
  38. package/dist/src/hooks/use-scroll.js.map +1 -1
  39. package/dist/src/hosts/base.d.ts +13 -2
  40. package/dist/src/hosts/base.d.ts.map +1 -1
  41. package/dist/src/hosts/base.js +74 -2
  42. package/dist/src/hosts/base.js.map +1 -1
  43. package/dist/src/hosts/box.d.ts +2 -2
  44. package/dist/src/hosts/box.d.ts.map +1 -1
  45. package/dist/src/hosts/box.js +29 -2
  46. package/dist/src/hosts/box.js.map +1 -1
  47. package/dist/src/hosts/canvas.d.ts +24 -4
  48. package/dist/src/hosts/canvas.d.ts.map +1 -1
  49. package/dist/src/hosts/canvas.js +107 -41
  50. package/dist/src/hosts/canvas.js.map +1 -1
  51. package/dist/src/hosts/codeblock.d.ts +10 -12
  52. package/dist/src/hosts/codeblock.d.ts.map +1 -1
  53. package/dist/src/hosts/codeblock.js +38 -35
  54. package/dist/src/hosts/codeblock.js.map +1 -1
  55. package/dist/src/hosts/flex-container.d.ts +3 -3
  56. package/dist/src/hosts/flex-container.d.ts.map +1 -1
  57. package/dist/src/hosts/flex-container.js +20 -5
  58. package/dist/src/hosts/flex-container.js.map +1 -1
  59. package/dist/src/hosts/index.d.ts +3 -2
  60. package/dist/src/hosts/index.d.ts.map +1 -1
  61. package/dist/src/hosts/index.js +2 -1
  62. package/dist/src/hosts/index.js.map +1 -1
  63. package/dist/src/hosts/layout-helpers.d.ts +10 -0
  64. package/dist/src/hosts/layout-helpers.d.ts.map +1 -0
  65. package/dist/src/hosts/layout-helpers.js +10 -0
  66. package/dist/src/hosts/layout-helpers.js.map +1 -0
  67. package/dist/src/hosts/leaf.d.ts +14 -0
  68. package/dist/src/hosts/leaf.d.ts.map +1 -0
  69. package/dist/src/hosts/leaf.js +31 -0
  70. package/dist/src/hosts/leaf.js.map +1 -0
  71. package/dist/src/hosts/overlay-item.d.ts +2 -2
  72. package/dist/src/hosts/overlay-item.d.ts.map +1 -1
  73. package/dist/src/hosts/overlay-item.js +7 -2
  74. package/dist/src/hosts/overlay-item.js.map +1 -1
  75. package/dist/src/hosts/overlay.d.ts +2 -2
  76. package/dist/src/hosts/overlay.d.ts.map +1 -1
  77. package/dist/src/hosts/overlay.js +6 -9
  78. package/dist/src/hosts/overlay.js.map +1 -1
  79. package/dist/src/hosts/scroll.d.ts +54 -26
  80. package/dist/src/hosts/scroll.d.ts.map +1 -1
  81. package/dist/src/hosts/scroll.js +185 -87
  82. package/dist/src/hosts/scroll.js.map +1 -1
  83. package/dist/src/hosts/single-child.d.ts.map +1 -1
  84. package/dist/src/hosts/single-child.js +2 -0
  85. package/dist/src/hosts/single-child.js.map +1 -1
  86. package/dist/src/hosts/spacer.d.ts +3 -3
  87. package/dist/src/hosts/spacer.d.ts.map +1 -1
  88. package/dist/src/hosts/spacer.js +8 -3
  89. package/dist/src/hosts/spacer.js.map +1 -1
  90. package/dist/src/hosts/text.d.ts +22 -18
  91. package/dist/src/hosts/text.d.ts.map +1 -1
  92. package/dist/src/hosts/text.js +108 -131
  93. package/dist/src/hosts/text.js.map +1 -1
  94. package/dist/src/hosts/vstack.js +1 -1
  95. package/dist/src/hosts/vstack.js.map +1 -1
  96. package/dist/src/hosts/zstack.d.ts +3 -3
  97. package/dist/src/hosts/zstack.d.ts.map +1 -1
  98. package/dist/src/hosts/zstack.js +13 -8
  99. package/dist/src/hosts/zstack.js.map +1 -1
  100. package/dist/src/index.d.ts +2 -2
  101. package/dist/src/index.d.ts.map +1 -1
  102. package/dist/src/internal/dev/hmr.d.ts +20 -0
  103. package/dist/src/internal/dev/hmr.d.ts.map +1 -0
  104. package/dist/src/internal/dev/hmr.js +93 -0
  105. package/dist/src/internal/dev/hmr.js.map +1 -0
  106. package/dist/src/internal/dev/runtime.d.ts +24 -0
  107. package/dist/src/internal/dev/runtime.d.ts.map +1 -0
  108. package/dist/src/internal/dev/runtime.js +135 -0
  109. package/dist/src/internal/dev/runtime.js.map +1 -0
  110. package/dist/src/internal/dev/ui.d.ts +13 -0
  111. package/dist/src/internal/dev/ui.d.ts.map +1 -0
  112. package/dist/src/internal/dev/ui.js +51 -0
  113. package/dist/src/internal/dev/ui.js.map +1 -0
  114. package/dist/src/internal/renderer/context.d.ts +9 -0
  115. package/dist/src/internal/renderer/context.d.ts.map +1 -0
  116. package/dist/src/internal/renderer/context.js +22 -0
  117. package/dist/src/internal/renderer/context.js.map +1 -0
  118. package/dist/src/internal/renderer/core/FrameBuilder.d.ts +18 -0
  119. package/dist/src/internal/renderer/core/FrameBuilder.d.ts.map +1 -0
  120. package/dist/src/internal/renderer/core/FrameBuilder.js +40 -0
  121. package/dist/src/internal/renderer/core/FrameBuilder.js.map +1 -0
  122. package/dist/src/internal/renderer/core/RendererState.d.ts +41 -0
  123. package/dist/src/internal/renderer/core/RendererState.d.ts.map +1 -0
  124. package/dist/src/internal/renderer/core/RendererState.js +70 -0
  125. package/dist/src/internal/renderer/core/RendererState.js.map +1 -0
  126. package/dist/src/internal/renderer/core/index.d.ts +3 -0
  127. package/dist/src/internal/renderer/core/index.d.ts.map +1 -0
  128. package/dist/src/internal/renderer/core/index.js +3 -0
  129. package/dist/src/internal/renderer/core/index.js.map +1 -0
  130. package/dist/src/internal/renderer/index.d.ts +40 -0
  131. package/dist/src/internal/renderer/index.d.ts.map +1 -0
  132. package/dist/src/internal/renderer/index.js +543 -0
  133. package/dist/src/internal/renderer/index.js.map +1 -0
  134. package/dist/src/internal/renderer/input/InputProcessor.d.ts +30 -0
  135. package/dist/src/internal/renderer/input/InputProcessor.d.ts.map +1 -0
  136. package/dist/src/internal/renderer/input/InputProcessor.js +122 -0
  137. package/dist/src/internal/renderer/input/InputProcessor.js.map +1 -0
  138. package/dist/src/internal/renderer/input/index.d.ts +2 -0
  139. package/dist/src/internal/renderer/input/index.d.ts.map +1 -0
  140. package/dist/src/internal/renderer/input/index.js +2 -0
  141. package/dist/src/internal/renderer/input/index.js.map +1 -0
  142. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts +42 -0
  143. package/dist/src/internal/renderer/lifecycle/EventBus.d.ts.map +1 -0
  144. package/dist/src/internal/renderer/lifecycle/EventBus.js +97 -0
  145. package/dist/src/internal/renderer/lifecycle/EventBus.js.map +1 -0
  146. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts +13 -0
  147. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.d.ts.map +1 -0
  148. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js +111 -0
  149. package/dist/src/internal/renderer/lifecycle/ProcessLifecycle.js.map +1 -0
  150. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts +3 -0
  151. package/dist/src/internal/renderer/lifecycle/RenderCache.d.ts.map +1 -0
  152. package/dist/src/internal/renderer/lifecycle/RenderCache.js +9 -0
  153. package/dist/src/internal/renderer/lifecycle/RenderCache.js.map +1 -0
  154. package/dist/src/internal/renderer/lifecycle/index.d.ts +4 -0
  155. package/dist/src/internal/renderer/lifecycle/index.d.ts.map +1 -0
  156. package/dist/src/internal/renderer/lifecycle/index.js +4 -0
  157. package/dist/src/internal/renderer/lifecycle/index.js.map +1 -0
  158. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts +12 -0
  159. package/dist/src/internal/renderer/modes/FullscreenRenderer.d.ts.map +1 -0
  160. package/dist/src/internal/renderer/modes/FullscreenRenderer.js +54 -0
  161. package/dist/src/internal/renderer/modes/FullscreenRenderer.js.map +1 -0
  162. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts +25 -0
  163. package/dist/src/internal/renderer/modes/InlineRenderer.d.ts.map +1 -0
  164. package/dist/src/internal/renderer/modes/InlineRenderer.js +166 -0
  165. package/dist/src/internal/renderer/modes/InlineRenderer.js.map +1 -0
  166. package/dist/src/internal/renderer/modes/RendererMode.d.ts +42 -0
  167. package/dist/src/internal/renderer/modes/RendererMode.d.ts.map +1 -0
  168. package/dist/src/internal/renderer/modes/RendererMode.js +2 -0
  169. package/dist/src/internal/renderer/modes/RendererMode.js.map +1 -0
  170. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts +25 -0
  171. package/dist/src/internal/renderer/modes/StaticContentRenderer.d.ts.map +1 -0
  172. package/dist/src/internal/renderer/modes/StaticContentRenderer.js +49 -0
  173. package/dist/src/internal/renderer/modes/StaticContentRenderer.js.map +1 -0
  174. package/dist/src/internal/renderer/modes/index.d.ts +5 -0
  175. package/dist/src/internal/renderer/modes/index.d.ts.map +1 -0
  176. package/dist/src/internal/renderer/modes/index.js +4 -0
  177. package/dist/src/internal/renderer/modes/index.js.map +1 -0
  178. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts +13 -0
  179. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.d.ts.map +1 -0
  180. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js +75 -0
  181. package/dist/src/internal/renderer/terminal/KeyboardCapabilityProbe.js.map +1 -0
  182. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts +29 -0
  183. package/dist/src/internal/renderer/terminal/TerminalSetup.d.ts.map +1 -0
  184. package/dist/src/internal/renderer/terminal/TerminalSetup.js +82 -0
  185. package/dist/src/internal/renderer/terminal/TerminalSetup.js.map +1 -0
  186. package/dist/src/internal/renderer/terminal/index.d.ts +3 -0
  187. package/dist/src/internal/renderer/terminal/index.d.ts.map +1 -0
  188. package/dist/src/internal/renderer/terminal/index.js +3 -0
  189. package/dist/src/internal/renderer/terminal/index.js.map +1 -0
  190. package/dist/src/internal/renderer/types.d.ts +122 -0
  191. package/dist/src/internal/renderer/types.d.ts.map +1 -0
  192. package/dist/src/internal/renderer/types.js +2 -0
  193. package/dist/src/internal/renderer/types.js.map +1 -0
  194. package/dist/src/motion/hooks.d.ts +1 -1
  195. package/dist/src/motion/hooks.js +1 -1
  196. package/dist/src/reconciler/host-config.js +2 -2
  197. package/dist/src/reconciler/host-config.js.map +1 -1
  198. package/dist/src/reconciler/types.d.ts +5 -1
  199. package/dist/src/reconciler/types.d.ts.map +1 -1
  200. package/dist/src/renderer-context.d.ts +1 -8
  201. package/dist/src/renderer-context.d.ts.map +1 -1
  202. package/dist/src/renderer-context.js +1 -21
  203. package/dist/src/renderer-context.js.map +1 -1
  204. package/dist/src/renderer-types.d.ts +1 -115
  205. package/dist/src/renderer-types.d.ts.map +1 -1
  206. package/dist/src/renderer.d.ts +1 -31
  207. package/dist/src/renderer.d.ts.map +1 -1
  208. package/dist/src/renderer.js +1 -495
  209. package/dist/src/renderer.js.map +1 -1
  210. package/dist/src/test/render-tui.d.ts +3 -3
  211. package/dist/src/test/render-tui.d.ts.map +1 -1
  212. package/dist/src/test/render-tui.js +16 -9
  213. package/dist/src/test/render-tui.js.map +1 -1
  214. package/dist/src/utils/alignment.d.ts +1 -1
  215. package/dist/src/utils/alignment.d.ts.map +1 -1
  216. package/dist/src/utils/alignment.js +0 -2
  217. package/dist/src/utils/alignment.js.map +1 -1
  218. package/dist/src/utils/border.d.ts +1 -1
  219. package/dist/src/utils/border.d.ts.map +1 -1
  220. package/dist/src/utils/border.js +2 -0
  221. package/dist/src/utils/border.js.map +1 -1
  222. package/dist/src/utils/console-helpers.d.ts +19 -0
  223. package/dist/src/utils/console-helpers.d.ts.map +1 -0
  224. package/dist/src/utils/console-helpers.js +61 -0
  225. package/dist/src/utils/console-helpers.js.map +1 -0
  226. package/dist/src/utils/index.d.ts +2 -1
  227. package/dist/src/utils/index.d.ts.map +1 -1
  228. package/dist/src/utils/index.js +2 -1
  229. package/dist/src/utils/index.js.map +1 -1
  230. package/dist/src/utils/styles.d.ts +8 -1
  231. package/dist/src/utils/styles.d.ts.map +1 -1
  232. package/dist/src/utils/styles.js +10 -8
  233. package/dist/src/utils/styles.js.map +1 -1
  234. package/dist/src/utils/text-layout.d.ts +22 -0
  235. package/dist/src/utils/text-layout.d.ts.map +1 -0
  236. package/dist/src/utils/text-layout.js +37 -0
  237. package/dist/src/utils/text-layout.js.map +1 -0
  238. package/dist/src/utils/text-wrap.d.ts +31 -1
  239. package/dist/src/utils/text-wrap.d.ts.map +1 -1
  240. package/dist/src/utils/text-wrap.js +205 -48
  241. package/dist/src/utils/text-wrap.js.map +1 -1
  242. package/dist/src/visualize/index.js +1 -1
  243. package/dist/src/visualize/index.js.map +1 -1
  244. package/dist/tsconfig.tsbuildinfo +1 -1
  245. package/package.json +2 -2
  246. package/src/codeblock.tsx +2 -2
  247. package/src/components/ListView.tsx +21 -23
  248. package/src/components/Markdown.tsx +3 -3
  249. package/src/components/MultilineTextInput.tsx +138 -344
  250. package/src/components/TextInput.tsx +54 -99
  251. package/src/components/text-editing.ts +180 -0
  252. package/src/console/ConsolePopover.tsx +124 -107
  253. package/src/debug/DebugOverlay.ts +15 -74
  254. package/src/debug/DiagnosticsPanel.tsx +1 -1
  255. package/src/dev.tsx +5 -458
  256. package/src/hooks/use-scroll.ts +85 -145
  257. package/src/hosts/base.ts +86 -3
  258. package/src/hosts/box.ts +37 -2
  259. package/src/hosts/canvas.ts +128 -42
  260. package/src/hosts/codeblock.ts +48 -35
  261. package/src/hosts/flex-container.ts +25 -6
  262. package/src/hosts/index.ts +11 -2
  263. package/src/hosts/layout-helpers.ts +20 -0
  264. package/src/hosts/leaf.ts +36 -0
  265. package/src/hosts/overlay-item.ts +8 -2
  266. package/src/hosts/overlay.ts +13 -11
  267. package/src/hosts/scroll.ts +228 -106
  268. package/src/hosts/single-child.ts +2 -0
  269. package/src/hosts/spacer.ts +8 -3
  270. package/src/hosts/text.ts +126 -132
  271. package/src/hosts/vstack.ts +1 -1
  272. package/src/hosts/zstack.ts +14 -9
  273. package/src/index.ts +2 -2
  274. package/src/internal/dev/hmr.ts +101 -0
  275. package/src/internal/dev/runtime.ts +170 -0
  276. package/src/internal/dev/ui.tsx +87 -0
  277. package/src/internal/renderer/context.ts +27 -0
  278. package/src/{renderer → internal/renderer}/core/FrameBuilder.ts +2 -2
  279. package/src/internal/renderer/index.ts +689 -0
  280. package/src/{renderer → internal/renderer}/input/InputProcessor.ts +10 -1
  281. package/src/{renderer → internal/renderer}/lifecycle/EventBus.ts +9 -1
  282. package/src/internal/renderer/lifecycle/ProcessLifecycle.ts +125 -0
  283. package/src/internal/renderer/lifecycle/index.ts +3 -0
  284. package/src/{renderer → internal/renderer}/modes/InlineRenderer.ts +5 -2
  285. package/src/{renderer → internal/renderer}/modes/RendererMode.ts +1 -1
  286. package/src/{renderer → internal/renderer}/modes/StaticContentRenderer.ts +5 -2
  287. package/src/internal/renderer/terminal/KeyboardCapabilityProbe.ts +91 -0
  288. package/src/{renderer/lifecycle → internal/renderer/terminal}/TerminalSetup.ts +4 -22
  289. package/src/internal/renderer/terminal/index.ts +2 -0
  290. package/src/internal/renderer/types.ts +129 -0
  291. package/src/motion/hooks.ts +1 -1
  292. package/src/reconciler/host-config.ts +2 -2
  293. package/src/reconciler/types.ts +7 -1
  294. package/src/renderer-context.ts +1 -27
  295. package/src/renderer-types.ts +10 -123
  296. package/src/renderer.ts +1 -619
  297. package/src/test/render-tui.ts +16 -10
  298. package/src/utils/alignment.ts +1 -3
  299. package/src/utils/border.ts +11 -1
  300. package/src/utils/console-helpers.ts +86 -0
  301. package/src/utils/index.ts +15 -1
  302. package/src/utils/styles.ts +16 -4
  303. package/src/utils/text-layout.ts +65 -0
  304. package/src/utils/text-wrap.ts +261 -48
  305. package/src/visualize/index.tsx +1 -1
  306. package/src/renderer/lifecycle/ResizeManager.ts +0 -65
  307. package/src/renderer/lifecycle/index.ts +0 -4
  308. /package/src/{renderer → internal/renderer}/core/RendererState.ts +0 -0
  309. /package/src/{renderer → internal/renderer}/core/index.ts +0 -0
  310. /package/src/{renderer → internal/renderer}/input/index.ts +0 -0
  311. /package/src/{renderer → internal/renderer}/lifecycle/RenderCache.ts +0 -0
  312. /package/src/{renderer → internal/renderer}/modes/FullscreenRenderer.ts +0 -0
  313. /package/src/{renderer → internal/renderer}/modes/index.ts +0 -0
@@ -1,16 +1,17 @@
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,
7
8
  drawBorder,
9
+ fillRectWithInheritedBg,
8
10
  resolveBgStyle,
9
- resolveInheritedBgStyle,
10
11
  styleIdFromProps,
11
12
  toColorValue,
12
13
  } from "../utils/index.js"
13
- import { BaseHost } from "./base.js"
14
+ import { LeafHost } from "./leaf.js"
14
15
 
15
16
  export type { BorderKind }
16
17
 
@@ -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 {
@@ -67,7 +85,7 @@ export interface CanvasProps extends CommonProps {
67
85
  inheritBg?: boolean
68
86
  }
69
87
 
70
- export class CanvasHost extends BaseHost {
88
+ export class CanvasHost extends LeafHost {
71
89
  draw: CanvasProps["draw"] = () => {}
72
90
  fixedWidth?: number
73
91
  fixedHeight?: number
@@ -78,7 +96,7 @@ export class CanvasHost extends BaseHost {
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,
@@ -91,26 +109,36 @@ export class CanvasHost extends BaseHost {
91
109
  if (!this.rect) return
92
110
  const { x: ox, y: oy, w, h } = this.rect
93
111
 
94
- // Get inherited background for use in drawing functions
95
- const { value: inheritedBgValue, styleId: inheritedBgStyleId } = this.inheritBg
96
- ? resolveInheritedBgStyle(palette, undefined, this.parent)
97
- : { value: undefined, styleId: 0 }
98
-
99
112
  // Pre-fill with inherited background if requested
100
- if (this.inheritBg && inheritedBgValue !== undefined) {
101
- buffer.fillRect(ox, oy, w, h, " ".codePointAt(0)!, inheritedBgStyleId)
102
- }
113
+ const inheritedBgValue = this.inheritBg
114
+ ? fillRectWithInheritedBg(buffer, palette, { x: ox, y: oy, w, h }, undefined, this.parent, {
115
+ skipWhenUndefined: true,
116
+ }).value
117
+ : undefined
103
118
 
104
119
  // Create draw context
105
120
  const ctx: DrawContext = {
106
121
  width: w,
107
122
  height: h,
108
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
+
109
136
  text: (x, y, str, opts) => {
110
137
  const px = Math.round(ox + x)
111
138
  const py = Math.round(oy + y)
112
139
  // Use inherited bg when inheritBg is enabled and no explicit bg provided
113
140
  const effectiveBg = opts?.bg ?? (this.inheritBg ? inheritedBgValue : undefined)
141
+ const styleT = Prof.startPhase()
114
142
  const style = styleIdFromProps(palette, {
115
143
  fg: opts?.fg,
116
144
  bg: effectiveBg,
@@ -119,20 +147,40 @@ export class CanvasHost extends BaseHost {
119
147
  underline: opts?.underline,
120
148
  inverse: opts?.inverse,
121
149
  })
122
- let col = px
123
- for (const char of str) {
124
- if (col >= ox + w) break
125
- buffer.drawCP(col, py, char.codePointAt(0)!, style)
126
- 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)
127
174
  }
128
175
  },
129
176
 
130
- fill: (x, y, fw, fh, char = " ", opts) => {
177
+ fillRect: (x, y, fw, fh, char = " ", opts) => {
131
178
  const px = Math.round(ox + x)
132
179
  const py = Math.round(oy + y)
133
- const cp = char.codePointAt(0)!
180
+ const cp = char.length > 0 ? char.codePointAt(0)! : " ".codePointAt(0)!
134
181
  // Use inherited bg when inheritBg is enabled and no explicit bg provided
135
182
  const effectiveBg = opts?.bg ?? (this.inheritBg ? inheritedBgValue : undefined)
183
+ const styleT = Prof.startPhase()
136
184
  const style = styleIdFromProps(palette, {
137
185
  fg: opts?.fg,
138
186
  bg: effectiveBg,
@@ -141,65 +189,103 @@ export class CanvasHost extends BaseHost {
141
189
  underline: opts?.underline,
142
190
  inverse: opts?.inverse,
143
191
  })
144
- for (let row = 0; row < fh; row++) {
145
- const yy = py + row
146
- for (let col = 0; col < fw; col++) {
147
- const xx = px + col
148
- buffer.drawCP(xx, yy, cp, style)
149
- }
150
- }
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)
151
198
  },
152
199
 
153
200
  box: (x, y, bw, bh, opts) => {
154
201
  const px = Math.round(ox + x)
155
202
  const py = Math.round(oy + y)
203
+ const boxW = Math.ceil(bw)
204
+ const boxH = Math.ceil(bh)
156
205
  const border = opts?.border ?? "none"
206
+ const bgStyleT = Prof.startPhase()
157
207
  const { value: bgValue, styleId: bgStyleId } = resolveBgStyle(palette, opts?.bg)
208
+ Prof.endPhase("canvas.box.bg.style", bgStyleT)
158
209
 
159
210
  // Fill background (with clipping)
160
211
  if (bgValue !== undefined) {
161
- for (let row = 0; row < bh; row++) {
162
- const yy = py + row
163
- for (let col = 0; col < bw; col++) {
164
- const xx = px + col
165
- buffer.drawCP(xx, yy, " ".codePointAt(0)!, bgStyleId)
166
- }
167
- }
212
+ const bgDrawT = Prof.startPhase()
213
+ buffer.fillRect(px, py, boxW, boxH, " ".codePointAt(0)!, bgStyleId)
214
+ Prof.endPhase("canvas.box.bg.draw", bgDrawT)
168
215
  }
169
216
 
170
217
  // Draw border (with clipping)
171
- if (border !== "none" && bw >= 2 && bh >= 2) {
218
+ if (border !== "none" && boxW >= 2 && boxH >= 2) {
219
+ const borderStyleT = Prof.startPhase()
172
220
  const chars = borderChars(border)
173
221
  const borderFg = toColorValue(opts?.borderColor) ?? toColorValue(opts?.fg) ?? Colors.ansi.gray(8)
174
222
  const borderStyle = palette.id({ fg: borderFg })
175
- 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)
176
227
  }
177
228
  },
178
229
 
179
230
  clear: () => {
231
+ const clearT = Prof.startPhase()
180
232
  const style = palette.id({})
181
- for (let row = 0; row < h; row++) {
182
- for (let col = 0; col < w; col++) {
183
- buffer.drawCP(ox + col, oy + row, " ".codePointAt(0)!, style)
184
- }
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)
185
253
  }
254
+ Prof.endPhase("canvas.cells.draw", drawT)
186
255
  },
187
256
  }
188
257
 
189
258
  buffer.withClip(ox, oy, w, h, () => {
190
259
  // Call user's draw function within the canvas clip region
260
+ const drawT = Prof.startPhase()
191
261
  this.draw(ctx)
262
+ Prof.endPhase("canvas.draw", drawT)
192
263
  })
193
264
  }
194
265
 
195
266
  override updateProps(props: Record<string, unknown>): void {
196
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
+
197
273
  if (props.draw !== undefined) {
198
274
  this.draw = props.draw as CanvasProps["draw"]
199
- this.ctx.requestRender() // trigger repaint when draw function changes
200
275
  }
201
276
  if (props.width !== undefined) this.fixedWidth = props.width as number
202
277
  if (props.height !== undefined) this.fixedHeight = props.height as number
203
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
+ }
204
290
  }
205
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"
5
- import { BaseHost } from "./base.js"
4
+ import {
5
+ type Padding,
6
+ type PaddingInput,
7
+ resolveBgStyle,
8
+ resolvePadding,
9
+ spansDisplayWidth,
10
+ styleIdFromProps,
11
+ } from "../utils/index.js"
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
18
+ bg?: Color
19
+ lineNumberFg?: Color
20
+ lineNumberBg?: Color
14
21
  }
15
22
 
16
- function lineDisplayWidth(line: HighlightLine): number {
17
- return line.reduce((w, token) => w + displayWidth(token.text), 0)
18
- }
19
-
20
- export class CodeBlockHost extends BaseHost {
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 BaseHost {
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 BaseHost {
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 BaseHost {
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 BaseHost {
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 BaseHost {
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
  }
@@ -15,7 +15,7 @@ import {
15
15
  import { BaseHost } from "./base.js"
16
16
 
17
17
  export type CrossAlignment<A extends FlexAxis> = A extends "vertical"
18
- ? "leading" | "center" | "trailing"
18
+ ? "left" | "center" | "right"
19
19
  : "top" | "center" | "bottom"
20
20
 
21
21
  export interface FlexContainerProps<A extends FlexAxis> extends CommonProps {
@@ -30,7 +30,7 @@ export interface FlexContainerProps<A extends FlexAxis> extends CommonProps {
30
30
  function toFlexAlignment<A extends FlexAxis>(axis: A, alignment: CrossAlignment<A>): FlexAlignment {
31
31
  if (alignment === "center") return "center"
32
32
  if (axis === "vertical") {
33
- return alignment === "leading" ? "start" : "end"
33
+ return alignment === "left" ? "start" : "end"
34
34
  } else {
35
35
  return alignment === "top" ? "start" : "end"
36
36
  }
@@ -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,9 +102,9 @@ 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
- const stretchCross = this.axis === "vertical" ? this.alignment === "leading" : this.alignment === "top"
107
+ const stretchCross = this.axis === "vertical" ? this.alignment === "left" : this.alignment === "top"
108
108
  const insetX = this.padding.left + this.padding.right
109
109
  const insetY = this.padding.top + this.padding.bottom
110
110
  const innerRect: Rect = {
@@ -142,12 +142,31 @@ 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 =
148
153
  (props.alignment as CrossAlignment<A> | undefined) ??
149
- ((this.axis === "vertical" ? "leading" : "top") as CrossAlignment<A>)
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,13 +14,22 @@ 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"
21
21
  export { OverlayItemHost, type OverlayItemProps } from "./overlay-item.js"
22
- export { ScrollHost, type ScrollProps } from "./scroll.js"
22
+ export {
23
+ ScrollHost,
24
+ type ScrollAlign,
25
+ type ScrollAlignX,
26
+ type ScrollAlignY,
27
+ type ScrollAxis,
28
+ type ScrollLayoutChange,
29
+ type ScrollProps,
30
+ } from "./scroll.js"
23
31
  export { SingleChildHost } from "./single-child.js"
32
+ export { LeafHost } from "./leaf.js"
24
33
  export { SpacerHost, type SpacerProps } from "./spacer.js"
25
34
  export {
26
35
  RawTextHost,
@@ -0,0 +1,20 @@
1
+ import type { Rect, Size } from "../reconciler/types.js"
2
+ import type { HostInstance } from "../reconciler/types.js"
3
+ import { alignedChildRect, type HAlign, type VAlign } from "../utils/index.js"
4
+
5
+ type Alignment = { h?: HAlign; v?: VAlign }
6
+
7
+ export function layoutAlignedChildren(
8
+ layoutRect: Rect,
9
+ children: HostInstance[],
10
+ cachedSizes: Size[],
11
+ alignmentForChild: (child: HostInstance, index: number) => Alignment,
12
+ startIndex = 0,
13
+ ): void {
14
+ for (let i = startIndex; i < children.length; i++) {
15
+ const child = children[i]
16
+ const size = cachedSizes[i] ?? child.measure(layoutRect.w, layoutRect.h)
17
+ const alignment = alignmentForChild(child, i)
18
+ child.layout(alignedChildRect(layoutRect, size, alignment.h ?? "center", alignment.v ?? "center"))
19
+ }
20
+ }
@@ -0,0 +1,36 @@
1
+ import type { HostInstance } from "../reconciler/types.js"
2
+ import { BaseHost } from "./base.js"
3
+
4
+ /**
5
+ * Base host that rejects children.
6
+ * Any attempted child insertion is ignored with a warning (once).
7
+ */
8
+ export abstract class LeafHost extends BaseHost {
9
+ private warned = false
10
+
11
+ override appendChild(child: HostInstance): void {
12
+ this.rejectChild(child)
13
+ }
14
+
15
+ override insertBefore(child: HostInstance, _before: HostInstance): void {
16
+ this.rejectChild(child)
17
+ }
18
+
19
+ override removeChild(child: HostInstance): void {
20
+ const idx = this.children.indexOf(child)
21
+ if (idx >= 0) {
22
+ this.children.splice(idx, 1)
23
+ }
24
+ if (child.parent === this) {
25
+ child.parent = null
26
+ }
27
+ }
28
+
29
+ private rejectChild(child: HostInstance): void {
30
+ if (!this.warned) {
31
+ console.warn(`[effect-tui] <${this.type}> does not support children; child ignored.`)
32
+ this.warned = true
33
+ }
34
+ child.parent = null
35
+ }
36
+ }
@@ -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
  }
@@ -18,8 +18,8 @@
18
18
 
19
19
  import type { CellBuffer, Palette, Rect, Size } from "@effect-tui/core"
20
20
  import type { CommonProps, HostContext } from "../reconciler/types.js"
21
- import { alignedChildRect } from "../utils/index.js"
22
21
  import { BaseHost } from "./base.js"
22
+ import { layoutAlignedChildren } from "./layout-helpers.js"
23
23
  import type { OverlayItemHost } from "./overlay-item.js"
24
24
 
25
25
  export interface OverlayProps extends CommonProps {}
@@ -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
@@ -66,14 +66,16 @@ export class OverlayHost extends BaseHost {
66
66
  }
67
67
 
68
68
  // Layout overlay children with their alignment
69
- for (let i = 1; i < this.children.length; i++) {
70
- const child = this.children[i]
71
- const size = this.cachedSizes[i] ?? child.measure(layoutRect.w, layoutRect.h)
72
-
73
- // Read alignment from OverlayItemHost (use type check, not instanceof, for bundler compatibility)
74
- const alignment = child.type === "overlayItem" ? (child as OverlayItemHost).alignment : {}
75
- child.layout(alignedChildRect(layoutRect, size, alignment.h ?? "center", alignment.v ?? "center"))
76
- }
69
+ layoutAlignedChildren(
70
+ layoutRect,
71
+ this.children,
72
+ this.cachedSizes,
73
+ (child) => {
74
+ // Read alignment from OverlayItemHost (use type check, not instanceof, for bundler compatibility)
75
+ return child.type === "overlayItem" ? (child as OverlayItemHost).alignment : {}
76
+ },
77
+ 1,
78
+ )
77
79
  }
78
80
 
79
81
  override render(buffer: CellBuffer, palette: Palette): void {