@textui/core 0.1.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 (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -0
  3. package/dist/adapters/index.d.ts +12 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +11 -0
  6. package/dist/app/app.d.ts +200 -0
  7. package/dist/app/app.d.ts.map +1 -0
  8. package/dist/app/app.js +980 -0
  9. package/dist/core/animation.d.ts +34 -0
  10. package/dist/core/animation.d.ts.map +1 -0
  11. package/dist/core/animation.js +125 -0
  12. package/dist/core/clipboard.d.ts +29 -0
  13. package/dist/core/clipboard.d.ts.map +1 -0
  14. package/dist/core/clipboard.js +30 -0
  15. package/dist/core/commands.d.ts +42 -0
  16. package/dist/core/commands.d.ts.map +1 -0
  17. package/dist/core/commands.js +146 -0
  18. package/dist/core/components.d.ts +27 -0
  19. package/dist/core/components.d.ts.map +1 -0
  20. package/dist/core/components.js +78 -0
  21. package/dist/core/events.d.ts +23 -0
  22. package/dist/core/events.d.ts.map +1 -0
  23. package/dist/core/events.js +77 -0
  24. package/dist/core/focus.d.ts +69 -0
  25. package/dist/core/focus.d.ts.map +1 -0
  26. package/dist/core/focus.js +336 -0
  27. package/dist/core/i18n.d.ts +27 -0
  28. package/dist/core/i18n.d.ts.map +1 -0
  29. package/dist/core/i18n.js +88 -0
  30. package/dist/core/keybindings.d.ts +53 -0
  31. package/dist/core/keybindings.d.ts.map +1 -0
  32. package/dist/core/keybindings.js +163 -0
  33. package/dist/core/layers.d.ts +28 -0
  34. package/dist/core/layers.d.ts.map +1 -0
  35. package/dist/core/layers.js +84 -0
  36. package/dist/core/manifest.d.ts +22 -0
  37. package/dist/core/manifest.d.ts.map +1 -0
  38. package/dist/core/manifest.js +85 -0
  39. package/dist/core/navigation.d.ts +47 -0
  40. package/dist/core/navigation.d.ts.map +1 -0
  41. package/dist/core/navigation.js +110 -0
  42. package/dist/core/resources.d.ts +115 -0
  43. package/dist/core/resources.d.ts.map +1 -0
  44. package/dist/core/resources.js +321 -0
  45. package/dist/core/services.d.ts +21 -0
  46. package/dist/core/services.d.ts.map +1 -0
  47. package/dist/core/services.js +63 -0
  48. package/dist/core/store.d.ts +60 -0
  49. package/dist/core/store.d.ts.map +1 -0
  50. package/dist/core/store.js +593 -0
  51. package/dist/core/surfaces.d.ts +81 -0
  52. package/dist/core/surfaces.d.ts.map +1 -0
  53. package/dist/core/surfaces.js +237 -0
  54. package/dist/core/syntax.d.ts +49 -0
  55. package/dist/core/syntax.d.ts.map +1 -0
  56. package/dist/core/syntax.js +172 -0
  57. package/dist/core/when.d.ts +15 -0
  58. package/dist/core/when.d.ts.map +1 -0
  59. package/dist/core/when.js +224 -0
  60. package/dist/index.d.ts +49 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +54 -0
  63. package/dist/jsx/factory.d.ts +27 -0
  64. package/dist/jsx/factory.d.ts.map +1 -0
  65. package/dist/jsx/factory.js +102 -0
  66. package/dist/jsx/intrinsics.d.ts +96 -0
  67. package/dist/jsx/intrinsics.d.ts.map +1 -0
  68. package/dist/jsx/intrinsics.js +1 -0
  69. package/dist/jsx/jsx-dev-runtime.d.ts +13 -0
  70. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -0
  71. package/dist/jsx/jsx-dev-runtime.js +12 -0
  72. package/dist/jsx/jsx-runtime.d.ts +42 -0
  73. package/dist/jsx/jsx-runtime.d.ts.map +1 -0
  74. package/dist/jsx/jsx-runtime.js +16 -0
  75. package/dist/render/buffer.d.ts +77 -0
  76. package/dist/render/buffer.d.ts.map +1 -0
  77. package/dist/render/buffer.js +275 -0
  78. package/dist/render/color.d.ts +35 -0
  79. package/dist/render/color.d.ts.map +1 -0
  80. package/dist/render/color.js +160 -0
  81. package/dist/render/diff.d.ts +37 -0
  82. package/dist/render/diff.d.ts.map +1 -0
  83. package/dist/render/diff.js +66 -0
  84. package/dist/render/layout.d.ts +72 -0
  85. package/dist/render/layout.d.ts.map +1 -0
  86. package/dist/render/layout.js +633 -0
  87. package/dist/render/static.d.ts +63 -0
  88. package/dist/render/static.d.ts.map +1 -0
  89. package/dist/render/static.js +209 -0
  90. package/dist/runtime/bindings.d.ts +46 -0
  91. package/dist/runtime/bindings.d.ts.map +1 -0
  92. package/dist/runtime/bindings.js +116 -0
  93. package/dist/runtime/hooks.d.ts +284 -0
  94. package/dist/runtime/hooks.d.ts.map +1 -0
  95. package/dist/runtime/hooks.js +846 -0
  96. package/dist/runtime/instance.d.ts +106 -0
  97. package/dist/runtime/instance.d.ts.map +1 -0
  98. package/dist/runtime/instance.js +181 -0
  99. package/dist/runtime/paint.d.ts +27 -0
  100. package/dist/runtime/paint.d.ts.map +1 -0
  101. package/dist/runtime/paint.js +567 -0
  102. package/dist/runtime/reconcile.d.ts +23 -0
  103. package/dist/runtime/reconcile.d.ts.map +1 -0
  104. package/dist/runtime/reconcile.js +260 -0
  105. package/dist/runtime/runtime.d.ts +42 -0
  106. package/dist/runtime/runtime.d.ts.map +1 -0
  107. package/dist/runtime/runtime.js +1 -0
  108. package/dist/runtime/style.d.ts +55 -0
  109. package/dist/runtime/style.d.ts.map +1 -0
  110. package/dist/runtime/style.js +143 -0
  111. package/dist/themes/borders.d.ts +10 -0
  112. package/dist/themes/borders.d.ts.map +1 -0
  113. package/dist/themes/borders.js +88 -0
  114. package/dist/themes/builtin.d.ts +31 -0
  115. package/dist/themes/builtin.d.ts.map +1 -0
  116. package/dist/themes/builtin.js +291 -0
  117. package/dist/themes/glyphs.d.ts +15 -0
  118. package/dist/themes/glyphs.d.ts.map +1 -0
  119. package/dist/themes/glyphs.js +97 -0
  120. package/dist/themes/index.d.ts +5 -0
  121. package/dist/themes/index.d.ts.map +1 -0
  122. package/dist/themes/index.js +4 -0
  123. package/dist/themes/registry.d.ts +20 -0
  124. package/dist/themes/registry.d.ts.map +1 -0
  125. package/dist/themes/registry.js +204 -0
  126. package/dist/types/adapter.d.ts +38 -0
  127. package/dist/types/adapter.d.ts.map +1 -0
  128. package/dist/types/adapter.js +1 -0
  129. package/dist/types/animation.d.ts +31 -0
  130. package/dist/types/animation.d.ts.map +1 -0
  131. package/dist/types/animation.js +1 -0
  132. package/dist/types/app.d.ts +148 -0
  133. package/dist/types/app.d.ts.map +1 -0
  134. package/dist/types/app.js +1 -0
  135. package/dist/types/async.d.ts +30 -0
  136. package/dist/types/async.d.ts.map +1 -0
  137. package/dist/types/async.js +1 -0
  138. package/dist/types/capabilities.d.ts +44 -0
  139. package/dist/types/capabilities.d.ts.map +1 -0
  140. package/dist/types/capabilities.js +33 -0
  141. package/dist/types/cells.d.ts +72 -0
  142. package/dist/types/cells.d.ts.map +1 -0
  143. package/dist/types/cells.js +10 -0
  144. package/dist/types/command.d.ts +127 -0
  145. package/dist/types/command.d.ts.map +1 -0
  146. package/dist/types/command.js +1 -0
  147. package/dist/types/component-registry.d.ts +77 -0
  148. package/dist/types/component-registry.d.ts.map +1 -0
  149. package/dist/types/component-registry.js +1 -0
  150. package/dist/types/disposable.d.ts +8 -0
  151. package/dist/types/disposable.d.ts.map +1 -0
  152. package/dist/types/disposable.js +1 -0
  153. package/dist/types/focus.d.ts +72 -0
  154. package/dist/types/focus.d.ts.map +1 -0
  155. package/dist/types/focus.js +1 -0
  156. package/dist/types/geometry.d.ts +28 -0
  157. package/dist/types/geometry.d.ts.map +1 -0
  158. package/dist/types/geometry.js +14 -0
  159. package/dist/types/graph.d.ts +129 -0
  160. package/dist/types/graph.d.ts.map +1 -0
  161. package/dist/types/graph.js +31 -0
  162. package/dist/types/i18n.d.ts +23 -0
  163. package/dist/types/i18n.d.ts.map +1 -0
  164. package/dist/types/i18n.js +1 -0
  165. package/dist/types/index.d.ts +32 -0
  166. package/dist/types/index.d.ts.map +1 -0
  167. package/dist/types/index.js +31 -0
  168. package/dist/types/input.d.ts +65 -0
  169. package/dist/types/input.d.ts.map +1 -0
  170. package/dist/types/input.js +1 -0
  171. package/dist/types/keybinding.d.ts +49 -0
  172. package/dist/types/keybinding.d.ts.map +1 -0
  173. package/dist/types/keybinding.js +1 -0
  174. package/dist/types/layer.d.ts +54 -0
  175. package/dist/types/layer.d.ts.map +1 -0
  176. package/dist/types/layer.js +1 -0
  177. package/dist/types/manifest.d.ts +114 -0
  178. package/dist/types/manifest.d.ts.map +1 -0
  179. package/dist/types/manifest.js +1 -0
  180. package/dist/types/markdown.d.ts +58 -0
  181. package/dist/types/markdown.d.ts.map +1 -0
  182. package/dist/types/markdown.js +1 -0
  183. package/dist/types/navigation.d.ts +40 -0
  184. package/dist/types/navigation.d.ts.map +1 -0
  185. package/dist/types/navigation.js +1 -0
  186. package/dist/types/render.d.ts +72 -0
  187. package/dist/types/render.d.ts.map +1 -0
  188. package/dist/types/render.js +1 -0
  189. package/dist/types/resource.d.ts +170 -0
  190. package/dist/types/resource.d.ts.map +1 -0
  191. package/dist/types/resource.js +1 -0
  192. package/dist/types/services.d.ts +24 -0
  193. package/dist/types/services.d.ts.map +1 -0
  194. package/dist/types/services.js +3 -0
  195. package/dist/types/shell.d.ts +32 -0
  196. package/dist/types/shell.d.ts.map +1 -0
  197. package/dist/types/shell.js +1 -0
  198. package/dist/types/store.d.ts +124 -0
  199. package/dist/types/store.d.ts.map +1 -0
  200. package/dist/types/store.js +1 -0
  201. package/dist/types/stream.d.ts +18 -0
  202. package/dist/types/stream.d.ts.map +1 -0
  203. package/dist/types/stream.js +1 -0
  204. package/dist/types/style.d.ts +166 -0
  205. package/dist/types/style.d.ts.map +1 -0
  206. package/dist/types/style.js +1 -0
  207. package/dist/types/surface.d.ts +97 -0
  208. package/dist/types/surface.d.ts.map +1 -0
  209. package/dist/types/surface.js +1 -0
  210. package/dist/types/syntax.d.ts +68 -0
  211. package/dist/types/syntax.d.ts.map +1 -0
  212. package/dist/types/syntax.js +5 -0
  213. package/dist/types/terminal.d.ts +52 -0
  214. package/dist/types/terminal.d.ts.map +1 -0
  215. package/dist/types/terminal.js +1 -0
  216. package/dist/types/theme.d.ts +117 -0
  217. package/dist/types/theme.d.ts.map +1 -0
  218. package/dist/types/theme.js +1 -0
  219. package/dist/types/when.d.ts +19 -0
  220. package/dist/types/when.d.ts.map +1 -0
  221. package/dist/types/when.js +1 -0
  222. package/dist/ui/primitives.d.ts +56 -0
  223. package/dist/ui/primitives.d.ts.map +1 -0
  224. package/dist/ui/primitives.js +108 -0
  225. package/dist/ui/screen.d.ts +23 -0
  226. package/dist/ui/screen.d.ts.map +1 -0
  227. package/dist/ui/screen.js +18 -0
  228. package/dist/util/disposable.d.ts +6 -0
  229. package/dist/util/disposable.d.ts.map +1 -0
  230. package/dist/util/disposable.js +49 -0
  231. package/dist/util/markdown.d.ts +24 -0
  232. package/dist/util/markdown.d.ts.map +1 -0
  233. package/dist/util/markdown.js +220 -0
  234. package/dist/util/paths.d.ts +47 -0
  235. package/dist/util/paths.d.ts.map +1 -0
  236. package/dist/util/paths.js +134 -0
  237. package/dist/util/stream.d.ts +32 -0
  238. package/dist/util/stream.d.ts.map +1 -0
  239. package/dist/util/stream.js +206 -0
  240. package/dist/util/text.d.ts +65 -0
  241. package/dist/util/text.d.ts.map +1 -0
  242. package/dist/util/text.js +419 -0
  243. package/package.json +70 -0
  244. package/src/adapters/index.ts +11 -0
  245. package/src/app/app.ts +1096 -0
  246. package/src/core/animation.ts +144 -0
  247. package/src/core/clipboard.ts +40 -0
  248. package/src/core/commands.ts +169 -0
  249. package/src/core/components.ts +91 -0
  250. package/src/core/events.ts +95 -0
  251. package/src/core/focus.ts +358 -0
  252. package/src/core/i18n.ts +107 -0
  253. package/src/core/keybindings.ts +184 -0
  254. package/src/core/layers.ts +94 -0
  255. package/src/core/manifest.ts +84 -0
  256. package/src/core/navigation.ts +135 -0
  257. package/src/core/resources.ts +362 -0
  258. package/src/core/services.ts +69 -0
  259. package/src/core/store.ts +640 -0
  260. package/src/core/surfaces.ts +292 -0
  261. package/src/core/syntax.ts +200 -0
  262. package/src/core/when.ts +238 -0
  263. package/src/index.ts +76 -0
  264. package/src/jsx/factory.ts +124 -0
  265. package/src/jsx/intrinsics.ts +99 -0
  266. package/src/jsx/jsx-dev-runtime.ts +23 -0
  267. package/src/jsx/jsx-runtime.ts +76 -0
  268. package/src/render/buffer.ts +318 -0
  269. package/src/render/color.ts +180 -0
  270. package/src/render/diff.ts +99 -0
  271. package/src/render/layout.ts +764 -0
  272. package/src/render/static.ts +290 -0
  273. package/src/runtime/bindings.ts +150 -0
  274. package/src/runtime/hooks.ts +1057 -0
  275. package/src/runtime/instance.ts +284 -0
  276. package/src/runtime/paint.ts +737 -0
  277. package/src/runtime/reconcile.ts +335 -0
  278. package/src/runtime/runtime.ts +47 -0
  279. package/src/runtime/style.ts +205 -0
  280. package/src/themes/borders.ts +95 -0
  281. package/src/themes/builtin.ts +301 -0
  282. package/src/themes/glyphs.ts +101 -0
  283. package/src/themes/index.ts +4 -0
  284. package/src/themes/registry.ts +224 -0
  285. package/src/types/adapter.ts +41 -0
  286. package/src/types/animation.ts +34 -0
  287. package/src/types/app.ts +149 -0
  288. package/src/types/async.ts +35 -0
  289. package/src/types/capabilities.ts +79 -0
  290. package/src/types/cells.ts +79 -0
  291. package/src/types/command.ts +139 -0
  292. package/src/types/component-registry.ts +82 -0
  293. package/src/types/disposable.ts +8 -0
  294. package/src/types/focus.ts +77 -0
  295. package/src/types/geometry.ts +50 -0
  296. package/src/types/graph.ts +174 -0
  297. package/src/types/i18n.ts +25 -0
  298. package/src/types/index.ts +31 -0
  299. package/src/types/input.ts +81 -0
  300. package/src/types/keybinding.ts +51 -0
  301. package/src/types/layer.ts +46 -0
  302. package/src/types/manifest.ts +101 -0
  303. package/src/types/markdown.ts +47 -0
  304. package/src/types/navigation.ts +42 -0
  305. package/src/types/render.ts +86 -0
  306. package/src/types/resource.ts +185 -0
  307. package/src/types/services.ts +28 -0
  308. package/src/types/shell.ts +30 -0
  309. package/src/types/store.ts +148 -0
  310. package/src/types/stream.ts +24 -0
  311. package/src/types/style.ts +205 -0
  312. package/src/types/surface.ts +119 -0
  313. package/src/types/syntax.ts +93 -0
  314. package/src/types/terminal.ts +58 -0
  315. package/src/types/theme.ts +121 -0
  316. package/src/types/when.ts +21 -0
  317. package/src/ui/primitives.ts +118 -0
  318. package/src/ui/screen.ts +41 -0
  319. package/src/util/disposable.ts +49 -0
  320. package/src/util/markdown.ts +225 -0
  321. package/src/util/paths.ts +138 -0
  322. package/src/util/stream.ts +213 -0
  323. package/src/util/text.ts +428 -0
@@ -0,0 +1,737 @@
1
+ import type { Rect } from '../types/geometry.js';
2
+ import type { Style } from '../types/style.js';
3
+ import type { ResolvedTheme } from '../types/theme.js';
4
+ import type { TerminalCapabilities } from '../types/capabilities.js';
5
+ import type { Cell, Color } from '../types/cells.js';
6
+ import { ATTR_DIM } from '../types/cells.js';
7
+ import type { CellStyle, PaintSurface, RenderContext } from '../types/render.js';
8
+ import type { LayoutBox } from '../render/layout.js';
9
+ import { overflowOn } from '../render/layout.js';
10
+ import type { Instance } from './instance.js';
11
+ import type { Buffer } from '../render/buffer.js';
12
+ import { COLOR_DEFAULT, mix, packColor, type PackedColor } from '../render/color.js';
13
+ import { rectIntersect } from '../types/geometry.js';
14
+ import {
15
+ graphemes, graphemeWidth, sanitize, stringWidth, truncate,
16
+ truncateSideOf, wrapModeOf, wrapText,
17
+ } from '../util/text.js';
18
+ import {
19
+ attrsFromStyle, NO_INTERACTION, packStyleColor, resolveBorder,
20
+ resolveStyle, type InteractionState, type ResolvedBorder,
21
+ } from './style.js';
22
+
23
+ /**
24
+ * Painting.
25
+ *
26
+ * The tree is walked twice per frame: once to build layout boxes, once to
27
+ * paint them. Both walks skip function components entirely - they contribute
28
+ * their children and nothing else - so the layout engine and the painter only
29
+ * ever see `box`, `text` and `canvas`.
30
+ */
31
+
32
+ // ------------------------------------------------------------ paint surface
33
+
34
+ class Surface implements PaintSurface {
35
+ /**
36
+ * What a cell gets when the caller does not name a background.
37
+ *
38
+ * `COLOR_DEFAULT` for the painters that own the whole box, and the box's own
39
+ * resolved background for a `canvas`, which does not. A canvas is drawn
40
+ * *onto* something - a panel, a selected row, a themed surface - and a glyph
41
+ * written with only an `fg` was resetting the cell behind it to the
42
+ * terminal's default, punching a hole through whatever it was sitting on.
43
+ *
44
+ * A painter that really wants the terminal's own background can still say
45
+ * so, because `'default'` is a colour it can pass.
46
+ */
47
+ constructor(
48
+ private buffer: Buffer,
49
+ readonly rect: Rect,
50
+ private clipRect: Rect,
51
+ private defaultBg: PackedColor = COLOR_DEFAULT,
52
+ ) {}
53
+
54
+ private visible(ax: number, ay: number): boolean {
55
+ return (
56
+ ax >= this.clipRect.x && ax < this.clipRect.x + this.clipRect.width &&
57
+ ay >= this.clipRect.y && ay < this.clipRect.y + this.clipRect.height
58
+ );
59
+ }
60
+
61
+ put(x: number, y: number, char: string, style?: CellStyle): void {
62
+ const ax = this.rect.x + x;
63
+ const ay = this.rect.y + y;
64
+ if (!this.visible(ax, ay)) return;
65
+ this.buffer.put(
66
+ ax, ay, char,
67
+ style?.fg === undefined ? COLOR_DEFAULT : packColor(style.fg),
68
+ style?.bg === undefined ? this.defaultBg : packColor(style.bg),
69
+ style?.attrs ?? 0,
70
+ style?.link,
71
+ );
72
+ }
73
+
74
+ /*
75
+ * One style, packed once.
76
+ *
77
+ * `put` packs both colours out of the style it is handed, which is right for
78
+ * one cell and wrong for the two callers that write a *run* of them: a
79
+ * twenty-character label was twenty pairs of `packColor` calls on the same
80
+ * two values, and a filled box was one pair per cell of the box. Packing is
81
+ * what a colour becomes, and it becomes the same thing every time.
82
+ */
83
+ text(x: number, y: number, text: string, style?: CellStyle): number {
84
+ const fg = style?.fg === undefined ? COLOR_DEFAULT : packColor(style.fg);
85
+ const bg = style?.bg === undefined ? this.defaultBg : packColor(style.bg);
86
+ const attrs = style?.attrs ?? 0;
87
+ const link = style?.link;
88
+ const oy = this.rect.y + y;
89
+
90
+ let cx = x;
91
+ for (const g of graphemes(sanitize(text))) {
92
+ const w = graphemeWidth(g);
93
+ if (w === 0) continue;
94
+ const ax = this.rect.x + cx;
95
+ if (this.visible(ax, oy)) this.buffer.put(ax, oy, g, fg, bg, attrs, link);
96
+ cx += w;
97
+ }
98
+ return cx - x;
99
+ }
100
+
101
+ fill(rect: Rect | undefined, char: string, style?: CellStyle): void {
102
+ const r = rect ?? { x: 0, y: 0, width: this.rect.width, height: this.rect.height };
103
+ const fg = style?.fg === undefined ? COLOR_DEFAULT : packColor(style.fg);
104
+ const bg = style?.bg === undefined ? this.defaultBg : packColor(style.bg);
105
+ const attrs = style?.attrs ?? 0;
106
+ const link = style?.link;
107
+
108
+ for (let y = r.y; y < r.y + r.height; y++) {
109
+ const ay = this.rect.y + y;
110
+ for (let x = r.x; x < r.x + r.width; x++) {
111
+ const ax = this.rect.x + x;
112
+ if (this.visible(ax, ay)) this.buffer.put(ax, ay, char, fg, bg, attrs, link);
113
+ }
114
+ }
115
+ }
116
+
117
+ cell(x: number, y: number, cell: Cell): void {
118
+ this.put(x, y, cell.char, {
119
+ fg: cell.fg, bg: cell.bg, attrs: cell.attrs, link: cell.link,
120
+ });
121
+ }
122
+
123
+ clip(rect: Rect): PaintSurface {
124
+ const absolute = {
125
+ x: this.rect.x + rect.x,
126
+ y: this.rect.y + rect.y,
127
+ width: rect.width,
128
+ height: rect.height,
129
+ };
130
+ return new Surface(this.buffer, absolute, rectIntersect(this.clipRect, absolute), this.defaultBg);
131
+ }
132
+ }
133
+
134
+ /** Direct writer for the runtime itself, in absolute buffer coordinates. */
135
+ function rawSurface(buffer: Buffer, clip: Rect): PaintSurface {
136
+ return new Surface(buffer, { x: 0, y: 0, width: buffer.width, height: buffer.height }, clip);
137
+ }
138
+
139
+ // ------------------------------------------------------------ render context
140
+
141
+ export function createRenderContext(
142
+ theme: ResolvedTheme,
143
+ capabilities: TerminalCapabilities,
144
+ state: InteractionState = NO_INTERACTION,
145
+ ): RenderContext {
146
+ return {
147
+ theme,
148
+ capabilities,
149
+ focused: state.focused,
150
+ hovered: state.hovered,
151
+ active: state.active,
152
+ selected: state.selected,
153
+ disabled: state.disabled,
154
+ color: (c, fallback = 'default') =>
155
+ c === undefined ? fallback : (theme.color(String(c)) as Color),
156
+ glyph: (name) => {
157
+ const g = (theme.glyphs as unknown as Record<string, unknown>)[name];
158
+ return typeof g === 'string' ? g : '';
159
+ },
160
+ measureText: stringWidth,
161
+ };
162
+ }
163
+
164
+ // ------------------------------------------------------------- layout build
165
+
166
+ export interface PaintEnv {
167
+ theme: ResolvedTheme;
168
+ capabilities: TerminalCapabilities;
169
+ /** Interaction state for an instance, resolved by the runtime. */
170
+ stateOf(instance: Instance): InteractionState;
171
+ }
172
+
173
+ interface HostVisual {
174
+ style: Style;
175
+ border: ResolvedBorder;
176
+ /** Effective colour: this box's own, or the one it inherited. */
177
+ fg: PackedColor;
178
+ bg: PackedColor;
179
+ attrs: number;
180
+ /** True when the background is this box's rather than an ancestor's. */
181
+ ownBg: boolean;
182
+ /** The same question for the foreground, which the border has to ask. */
183
+ ownFg: boolean;
184
+ }
185
+
186
+ /** Colour and attributes handed down to a box's children. */
187
+ interface Inherited {
188
+ fg: PackedColor;
189
+ bg: PackedColor;
190
+ attrs: number;
191
+ }
192
+
193
+ const NO_INHERITANCE: Inherited = { fg: COLOR_DEFAULT, bg: COLOR_DEFAULT, attrs: 0 };
194
+
195
+ const VISUAL = Symbol('textui.visual');
196
+
197
+ interface VisualBox extends LayoutBox {
198
+ [VISUAL]?: HostVisual;
199
+ instance?: Instance;
200
+ }
201
+
202
+ /**
203
+ * Colour is inherited; a box's own always wins.
204
+ *
205
+ * Without this a `text` inside a coloured box is drawn in the terminal's
206
+ * default colours - and because a cell holds one background, the glyphs punch
207
+ * a hole through the box behind them. That is a label in the wrong colour on a
208
+ * button, a selected row whose text stays the unselected colour, and a filled
209
+ * block with a ragged bar of default background across the middle of it.
210
+ */
211
+ function visualFor(
212
+ instance: Instance,
213
+ env: PaintEnv,
214
+ inherited: Inherited = NO_INHERITANCE,
215
+ ): HostVisual {
216
+ const state = env.stateOf(instance);
217
+ const style = resolveStyle(
218
+ instance.props,
219
+ env.theme,
220
+ instance.component,
221
+ instance.definition?.defaultStyle,
222
+ state,
223
+ );
224
+ const border = resolveBorder(style.border, env.theme);
225
+
226
+ const ownFg = packStyleColor(style.fg, env.theme);
227
+ const ownBg = packStyleColor(style.bg, env.theme);
228
+
229
+ return {
230
+ style,
231
+ border,
232
+ fg: ownFg === COLOR_DEFAULT ? inherited.fg : ownFg,
233
+ bg: ownBg === COLOR_DEFAULT ? inherited.bg : ownBg,
234
+ // Attributes accumulate: `bold` on a row is bold for what is in the row.
235
+ attrs: attrsFromStyle(style) | inherited.attrs,
236
+ // Stating a background makes a box opaque, even when the background it
237
+ // states is the terminal's own. The two are not the same thing: a box
238
+ // that says nothing inherits what is behind it, and a box that says
239
+ // `bg: 'default'` fills itself with spaces at the terminal's colour.
240
+ //
241
+ // Every colour in the `mono` theme is `default`, so the old test - is the
242
+ // packed colour something other than the default? - made every surface in
243
+ // that theme transparent. A palette opened over a document drew its border
244
+ // and its rows and left the document showing through the gaps between
245
+ // them, which is unreadable and looked like a compositing bug.
246
+ ownBg: style.bg !== undefined,
247
+ ownFg: style.fg !== undefined,
248
+ };
249
+ }
250
+
251
+ /** Boxes contributed by an instance. Function components contribute none. */
252
+ export function buildBoxes(
253
+ instance: Instance,
254
+ env: PaintEnv,
255
+ inherited: Inherited = NO_INHERITANCE,
256
+ ): LayoutBox[] {
257
+ if (instance.kind !== 'host') {
258
+ // A function component contributes no box, and no colour of its own.
259
+ const out: LayoutBox[] = [];
260
+ for (const child of instance.children) out.push(...buildBoxes(child, env, inherited));
261
+ return out;
262
+ }
263
+
264
+ const visual = visualFor(instance, env, inherited);
265
+ const box: VisualBox = {
266
+ style: visual.style,
267
+ borderEdges: visual.border.edges,
268
+ children: [],
269
+ rect: { x: 0, y: 0, width: 0, height: 0 },
270
+ content: { x: 0, y: 0, width: 0, height: 0 },
271
+ scrollTop: typeof instance.props.scrollTop === 'number' ? instance.props.scrollTop : 0,
272
+ scrollLeft: typeof instance.props.scrollLeft === 'number' ? instance.props.scrollLeft : 0,
273
+ [VISUAL]: visual,
274
+ instance,
275
+ };
276
+
277
+ switch (instance.component) {
278
+ case 'text':
279
+ box.measure = textMeasure(instance, visual.style);
280
+ break;
281
+ case 'canvas': {
282
+ const intrinsic = instance.props.intrinsic as { width?: number; height?: number } | undefined;
283
+ box.measure = () => ({ width: intrinsic?.width ?? 0, height: intrinsic?.height ?? 0 });
284
+ break;
285
+ }
286
+ case 'spacer': {
287
+ const size = typeof instance.props.size === 'number' ? instance.props.size : undefined;
288
+ // No size and no flex means take what is left. A spacer that measures
289
+ // zero and does not grow is a node that does nothing, which is never
290
+ // what anyone wrote one for - and it is what `size` has always been
291
+ // documented to mean when it is unset.
292
+ if (size === undefined && box.style.flex === undefined) {
293
+ box.style = { ...box.style, flex: 1 };
294
+ }
295
+ box.measure = () => ({ width: size ?? 0, height: size ?? 0 });
296
+ break;
297
+ }
298
+ default:
299
+ break;
300
+ }
301
+
302
+ const handDown: Inherited = { fg: visual.fg, bg: visual.bg, attrs: visual.attrs };
303
+ for (const child of instance.children) {
304
+ box.children.push(...buildBoxes(child, env, handDown));
305
+ }
306
+
307
+ // Painting and hit-testing order within one parent.
308
+ box.children.sort((a, b) => (a.style.zIndex ?? 0) - (b.style.zIndex ?? 0));
309
+
310
+ instance.box = box;
311
+ return [box];
312
+ }
313
+
314
+ function textContent(instance: Instance): string {
315
+ const { content, children } = instance.props;
316
+ if (typeof content === 'string') return content;
317
+ if (typeof content === 'number') return String(content);
318
+ if (typeof children === 'string') return children;
319
+ if (typeof children === 'number') return String(children);
320
+ if (Array.isArray(children)) {
321
+ return children
322
+ .filter((c) => typeof c === 'string' || typeof c === 'number')
323
+ .join('');
324
+ }
325
+ return '';
326
+ }
327
+
328
+ /**
329
+ * Flatten to one row, for the truncating wrap modes.
330
+ *
331
+ * A newline in a text that has one row to live in has nowhere to go. Joining
332
+ * with a space keeps the sentence readable and keeps the cut where the reader
333
+ * can see it; dropping everything after the first newline would silently lose
334
+ * text that the ellipsis then claims was merely too long.
335
+ */
336
+ function oneLine(text: string): string {
337
+ return text.includes('\n') ? text.split('\n').join(' ') : text;
338
+ }
339
+
340
+ function textMeasure(instance: Instance, style: Style): (w: number, h: number) => { width: number; height: number } {
341
+ return (maxWidth: number) => {
342
+ const text = sanitize(textContent(instance));
343
+ if (text === '') return { width: 0, height: text.includes('\n') ? 1 : 1 };
344
+
345
+ const wrap = style.wrap ?? 'none';
346
+
347
+ // A truncating text is one row, always. It still asks for the width it
348
+ // would like - a box sizing itself around it gets the whole string when
349
+ // there is room, and only cuts when there is not.
350
+ if (truncateSideOf(wrap) !== undefined) {
351
+ return { width: stringWidth(oneLine(text)), height: 1 };
352
+ }
353
+
354
+ if (wrap === 'none') {
355
+ const lines = text.split('\n');
356
+ return {
357
+ width: Math.max(...lines.map(stringWidth)),
358
+ height: lines.length,
359
+ };
360
+ }
361
+ const limit = Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : stringWidth(text);
362
+ const lines = wrapText(text, limit, wrapModeOf(wrap));
363
+ return {
364
+ width: Math.min(limit, Math.max(0, ...lines.map(stringWidth))),
365
+ height: Math.max(1, lines.length),
366
+ };
367
+ };
368
+ }
369
+
370
+ // ------------------------------------------------------------------- paint
371
+
372
+ export function paintTree(
373
+ buffer: Buffer,
374
+ instance: Instance,
375
+ env: PaintEnv,
376
+ clip: Rect,
377
+ ): void {
378
+ if (instance.kind !== 'host') {
379
+ for (const child of instance.children) paintTree(buffer, child, env, clip);
380
+ return;
381
+ }
382
+
383
+ const box = instance.box as VisualBox | undefined;
384
+ if (!box) return;
385
+
386
+ const visual = box[VISUAL] ?? visualFor(instance, env);
387
+ const rect = box.rect;
388
+ if (rect.width <= 0 || rect.height <= 0) return;
389
+
390
+ const own = rectIntersect(clip, rect);
391
+ if (own.width <= 0 || own.height <= 0) {
392
+ // Fully clipped, but absolutely-positioned descendants may still be visible
393
+ // if they escaped - they do not, so nothing to do.
394
+ return;
395
+ }
396
+
397
+ const surface = rawSurface(buffer, own);
398
+
399
+ // 1. background
400
+ //
401
+ // Only when this box states one: an inherited background has already been
402
+ // painted by whoever owns it, and filling it again per nested box would be
403
+ // the same cells written several times a frame.
404
+ if (visual.ownBg || visual.style.fill) {
405
+ /*
406
+ * Straight to the buffer, with the colours it already has.
407
+ *
408
+ * This loop used to build a style object per cell and call `colorOf` twice
409
+ * to fill it - unpacking two packed colours into `Color` values so that
410
+ * `put` could pack them straight back. On a full-screen background that is
411
+ * five allocations a cell and sixty thousand a frame, all of it to arrive
412
+ * at the two integers `visual.fg` and `visual.bg` already held.
413
+ *
414
+ * Iterating the clipped rect rather than the box's own also drops the
415
+ * per-cell visibility test: `own` is the intersection, so every cell in it
416
+ * is visible by construction.
417
+ */
418
+ const fill = visual.style.fill ?? ' ';
419
+ for (let y = own.y; y < own.y + own.height; y++) {
420
+ for (let x = own.x; x < own.x + own.width; x++) {
421
+ buffer.put(x, y, fill, visual.fg, visual.bg);
422
+ }
423
+ }
424
+ }
425
+
426
+ // 2. scrim: recede what is already here, rather than cover it
427
+ if (visual.style.scrim !== undefined && visual.style.scrim !== false) {
428
+ washRegion(
429
+ buffer,
430
+ own,
431
+ packStyleColor(visual.style.scrim === true ? 'scrim' : visual.style.scrim, env.theme),
432
+ visual.style.scrimStrength ?? 0.55,
433
+ );
434
+ }
435
+
436
+ // 3. border, with its title and footer
437
+ if (visual.border.style !== 'none') {
438
+ paintBorder(surface, rect, visual, instance, env);
439
+ }
440
+
441
+ // 4. content
442
+ switch (instance.component) {
443
+ case 'text':
444
+ paintText(surface, box, visual, instance, env);
445
+ break;
446
+ case 'canvas':
447
+ paintCanvas(buffer, box, instance, env, own, visual.bg);
448
+ break;
449
+ default:
450
+ break;
451
+ }
452
+
453
+ // 5. children, clipped to this box's content when overflow is contained -
454
+ // per axis, so a row that scrolls sideways can still spill downwards.
455
+ const clipX = overflowOn(visual.style, 'x') !== 'visible';
456
+ const clipY = overflowOn(visual.style, 'y') !== 'visible';
457
+ const childClip = clipX || clipY
458
+ ? rectIntersect(clip, {
459
+ x: clipX ? box.content.x : clip.x,
460
+ y: clipY ? box.content.y : clip.y,
461
+ width: clipX ? box.content.width : clip.width,
462
+ height: clipY ? box.content.height : clip.height,
463
+ })
464
+ : clip;
465
+
466
+ for (const child of instance.children) {
467
+ paintTree(buffer, child, env, childClip);
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Blend a region toward one colour.
473
+ *
474
+ * A terminal has no alpha, so the choice is between covering the content
475
+ * behind a modal and moving it toward the scrim colour. Moving it keeps the
476
+ * screen recognisable - you can still see what the dialog is about - and it is
477
+ * what dimming means on a display that cannot dim.
478
+ *
479
+ * A cell left at the terminal's default colour cannot be blended, because we
480
+ * do not know what colour it is; it gets the dim attribute instead.
481
+ */
482
+ function washRegion(buffer: Buffer, rect: Rect, color: PackedColor, strength: number): void {
483
+ const amount = Math.max(0, Math.min(1, strength));
484
+
485
+ /*
486
+ * Straight at the cells, not through `get()`.
487
+ *
488
+ * `Buffer.get` builds a friendly object - unpacking both colours into `Color`
489
+ * values - and this immediately packs them back to compare and mix. That is
490
+ * an allocation and a round trip per cell, and a scrim covers a whole dialog
491
+ * area: an 80x30 modal is 2,400 objects per frame to answer a question the
492
+ * packed integers already answer. `get()` stays the public shape; the
493
+ * renderer's inner loop is not the place for it.
494
+ */
495
+ for (let y = rect.y; y < rect.y + rect.height; y++) {
496
+ for (let x = rect.x; x < rect.x + rect.width; x++) {
497
+ const at = buffer.packedAt(x, y);
498
+ if (!at) continue;
499
+
500
+ buffer.put(
501
+ x, y, at.char,
502
+ at.fg === COLOR_DEFAULT ? at.fg : mix(at.fg, color, amount),
503
+ at.bg === COLOR_DEFAULT ? at.bg : mix(at.bg, color, amount),
504
+ at.fg === COLOR_DEFAULT || at.bg === COLOR_DEFAULT ? at.attrs | ATTR_DIM : at.attrs,
505
+ at.link,
506
+ );
507
+ }
508
+ }
509
+ }
510
+
511
+ function colorOf(packed: PackedColor): Color {
512
+ if (packed === COLOR_DEFAULT) return 'default';
513
+ if (packed >= 0x1000000) {
514
+ return { rgb: [(packed >> 16) & 0xff, (packed >> 8) & 0xff, packed & 0xff] };
515
+ }
516
+ return { palette: packed };
517
+ }
518
+
519
+ function paintBorder(
520
+ surface: PaintSurface,
521
+ rect: Rect,
522
+ visual: HostVisual,
523
+ instance: Instance,
524
+ env: PaintEnv,
525
+ ): void {
526
+ const { chars, sides, colors } = visual.border;
527
+ /*
528
+ * A frame is the theme's `border` unless this box asked for something else.
529
+ *
530
+ * "Something else" is a colour on the border spec, or a colour on the box -
531
+ * `<box fg="danger" border="single">` means a danger frame, and that is
532
+ * worth keeping. What it must *not* mean is a colour the box merely
533
+ * inherited: the test used to be `visual.fg === COLOR_DEFAULT`, and
534
+ * `visual.fg` is the effective colour, so any box under an ancestor that set
535
+ * one took that instead of the border token. An application whose shell
536
+ * states its text colour - which is most of them - drew every frame in the
537
+ * text colour, as bright as the words inside it, and the `border` token in
538
+ * the theme went unused.
539
+ *
540
+ * `ownFg` is the same question `ownBg` already asks for the background.
541
+ */
542
+ const fg = visual.border.color !== undefined
543
+ ? colorOf(packStyleColor(visual.border.color, env.theme))
544
+ : visual.ownFg
545
+ ? colorOf(visual.fg)
546
+ : colorOf(packStyleColor('border', env.theme));
547
+ const bg = colorOf(visual.bg);
548
+ // `dim` on the border spec dims the frame and nothing else. Setting it on
549
+ // the box would take the title, the content and every child with it.
550
+ const attrs = visual.attrs | (visual.border.dim ? ATTR_DIM : 0);
551
+ const style: CellStyle = { fg, bg, attrs };
552
+
553
+ /** One edge's style: its own colour if it named one, the frame's otherwise. */
554
+ const edge = (side: 'top' | 'right' | 'bottom' | 'left'): CellStyle => {
555
+ const own = colors[side];
556
+ return own === undefined ? style : { fg: colorOf(packStyleColor(own, env.theme)), bg, attrs };
557
+ };
558
+ const top = edge('top');
559
+ const bottomStyle = edge('bottom');
560
+
561
+ const { x, y, width: w, height: h } = rect;
562
+ const right = x + w - 1;
563
+ const bottom = y + h - 1;
564
+
565
+ if (sides.top) {
566
+ for (let i = x; i <= right; i++) surface.put(i, y, chars.top, top);
567
+ }
568
+ if (sides.bottom && h > 1) {
569
+ for (let i = x; i <= right; i++) surface.put(i, bottom, chars.bottom, bottomStyle);
570
+ }
571
+ if (sides.left) {
572
+ const left = edge('left');
573
+ for (let i = y; i <= bottom; i++) surface.put(x, i, chars.left, left);
574
+ }
575
+ if (sides.right && w > 1) {
576
+ const rightStyle = edge('right');
577
+ for (let i = y; i <= bottom; i++) surface.put(right, i, chars.right, rightStyle);
578
+ }
579
+
580
+ // A corner is one cell and two edges meet in it, so one of them has to win.
581
+ // The horizontal rule takes it: a box reads as a top and a bottom line with
582
+ // sides between them, and breaking a long rule's colour at its own ends is
583
+ // the more visible mistake.
584
+ if (sides.top && sides.left) surface.put(x, y, chars.topLeft, top);
585
+ if (sides.top && sides.right && w > 1) surface.put(right, y, chars.topRight, top);
586
+ if (sides.bottom && sides.left && h > 1) surface.put(x, bottom, chars.bottomLeft, bottomStyle);
587
+ if (sides.bottom && sides.right && w > 1 && h > 1) {
588
+ surface.put(right, bottom, chars.bottomRight, bottomStyle);
589
+ }
590
+
591
+ const inner = w - 2;
592
+ if (inner <= 0) return;
593
+
594
+ /*
595
+ * A second label on the top rule, hard against the right.
596
+ *
597
+ * Painted *before* the title, and the title is then given the width it
598
+ * leaves, so the two cannot land on the same cells whatever `titleAlign`
599
+ * says. That order also decides which one loses when there is not enough
600
+ * room: the right label is the short one - a count, a shortcut, a state -
601
+ * and the title is the one that can be truncated and still be read.
602
+ */
603
+ const rightTitle = instance.props.rightTitle;
604
+ let reserved = 0;
605
+ if (sides.top && typeof rightTitle === 'string' && rightTitle !== '') {
606
+ const label = ` ${rightTitle} `;
607
+ reserved = Math.min(inner, stringWidth(sanitize(label)));
608
+ paintBorderLabel(
609
+ surface, x, y, inner, label, 'right',
610
+ { ...style, fg: colorOf(packStyleColor('muted', env.theme)), attrs: visual.attrs },
611
+ env.theme.glyphs.ellipsis,
612
+ );
613
+ }
614
+
615
+ const title = instance.props.title;
616
+ if (sides.top && typeof title === 'string' && title !== '' && inner - reserved > 0) {
617
+ paintBorderLabel(
618
+ surface, x, y, inner - reserved, ` ${title} `,
619
+ (instance.props.titleAlign as 'left' | 'center' | 'right') ?? 'left',
620
+ { ...style, fg: colorOf(visual.fg === COLOR_DEFAULT ? packStyleColor('text', env.theme) : visual.fg), attrs: visual.attrs },
621
+ env.theme.glyphs.ellipsis,
622
+ );
623
+ }
624
+
625
+ const footer = instance.props.footer;
626
+ if (sides.bottom && h > 1 && typeof footer === 'string' && footer !== '') {
627
+ paintBorderLabel(
628
+ surface, x, bottom, inner, ` ${footer} `,
629
+ (instance.props.footerAlign as 'left' | 'center' | 'right') ?? 'left',
630
+ { ...style, fg: colorOf(packStyleColor('muted', env.theme)), attrs: visual.attrs },
631
+ env.theme.glyphs.ellipsis,
632
+ );
633
+ }
634
+ }
635
+
636
+ function paintBorderLabel(
637
+ surface: PaintSurface,
638
+ x: number,
639
+ y: number,
640
+ inner: number,
641
+ label: string,
642
+ align: 'left' | 'center' | 'right',
643
+ style: CellStyle,
644
+ ellipsis: string,
645
+ ): void {
646
+ const text = truncate(sanitize(label), inner, ellipsis);
647
+ const w = stringWidth(text);
648
+ const offset =
649
+ align === 'center' ? Math.max(0, Math.floor((inner - w) / 2))
650
+ : align === 'right' ? Math.max(0, inner - w)
651
+ : 0;
652
+ surface.text(x + 1 + offset, y, text, style);
653
+ }
654
+
655
+ function paintText(
656
+ surface: PaintSurface,
657
+ box: LayoutBox,
658
+ visual: HostVisual,
659
+ instance: Instance,
660
+ env: PaintEnv,
661
+ ): void {
662
+ const content = sanitize(textContent(instance));
663
+ if (content === '') return;
664
+
665
+ const { content: area } = box;
666
+ if (area.width <= 0 || area.height <= 0) return;
667
+
668
+ const style: CellStyle = {
669
+ fg: colorOf(visual.fg),
670
+ bg: colorOf(visual.bg),
671
+ attrs: visual.attrs,
672
+ link: typeof instance.props.link === 'string' ? instance.props.link : undefined,
673
+ };
674
+
675
+ const wrap = visual.style.wrap ?? 'none';
676
+ const align = visual.style.textAlign ?? 'left';
677
+ // The ellipsis is a glyph like any other: on an ascii terminal it is '...'.
678
+ const ellipsis = typeof instance.props.ellipsis === 'string'
679
+ ? instance.props.ellipsis
680
+ : env.theme.glyphs.ellipsis;
681
+
682
+ // Where the cut goes, if there is one. `wrap` names it for the whole text;
683
+ // the older `truncate` prop still wins where both are set, so a caller that
684
+ // wants word wrap *and* an ellipsis on the last row can still ask for it -
685
+ // that is a combination `wrap` alone cannot express.
686
+ const wrapSide = truncateSideOf(wrap);
687
+ const explicit = instance.props.truncate as 'end' | 'start' | 'middle' | false | undefined;
688
+ const truncateSide = explicit ?? wrapSide;
689
+
690
+ const raw = wrapSide !== undefined
691
+ ? [oneLine(content)]
692
+ : wrap === 'none' ? content.split('\n') : wrapText(content, area.width, wrapModeOf(wrap));
693
+
694
+ for (let i = 0; i < raw.length && i < area.height; i++) {
695
+ let line = raw[i] as string;
696
+ if (stringWidth(line) > area.width) {
697
+ line = truncateSide === false
698
+ ? line
699
+ : truncate(line, area.width, ellipsis, truncateSide ?? 'end');
700
+ }
701
+ const w = stringWidth(line);
702
+ const offset =
703
+ align === 'center' ? Math.max(0, Math.floor((area.width - w) / 2))
704
+ : align === 'right' ? Math.max(0, area.width - w)
705
+ : 0;
706
+ surface.text(area.x + offset, area.y + i, line, style);
707
+ }
708
+ }
709
+
710
+ /**
711
+ * The escape hatch. A canvas draws in its own coordinate space starting at
712
+ * 0,0, clipped to its content box - so a chart can be written without knowing
713
+ * where on screen it landed.
714
+ */
715
+ function paintCanvas(
716
+ buffer: Buffer,
717
+ box: LayoutBox,
718
+ instance: Instance,
719
+ env: PaintEnv,
720
+ clip: Rect,
721
+ /** The box's own resolved background, which is what it is drawn onto. */
722
+ background: PackedColor,
723
+ ): void {
724
+ const draw = instance.props.draw;
725
+ if (typeof draw !== 'function') return;
726
+
727
+ const area = box.content;
728
+ if (area.width <= 0 || area.height <= 0) return;
729
+
730
+ const surface = new Surface(buffer, area, rectIntersect(clip, area), background);
731
+ const ctx = createRenderContext(env.theme, env.capabilities, env.stateOf(instance));
732
+ try {
733
+ (draw as (s: PaintSurface, c: RenderContext) => void)(surface, ctx);
734
+ } catch (err) {
735
+ instance.runtime.onError(err, `canvas draw in <${instance.component}>`);
736
+ }
737
+ }