@typecad/ui 1.0.0-alpha.11 → 1.0.0-alpha.13
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.
- package/README.md +21 -1
- package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
- package/assets/fonts/dejavu/LICENSE +187 -0
- package/assets/fonts/dejavu/README.md +49 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +74 -0
- package/dist/engine-index.js +2 -0
- package/dist/preview/build-program.d.ts +7 -1
- package/dist/preview/build-program.js +94 -15
- package/dist/preview/host-gfx.d.ts +3 -0
- package/dist/preview/host-gfx.js +30 -3
- package/dist/preview/host-ui-runtime.d.ts +115 -0
- package/dist/preview/host-ui-runtime.js +928 -46
- package/dist/ui-engine/block-layout.js +25 -11
- package/dist/ui-engine/color.d.ts +6 -0
- package/dist/ui-engine/color.js +80 -2
- package/dist/ui-engine/compat-report.d.ts +8 -0
- package/dist/ui-engine/compat-report.js +124 -0
- package/dist/ui-engine/css-imports.d.ts +4 -0
- package/dist/ui-engine/css-imports.js +70 -0
- package/dist/ui-engine/css-parser.js +58 -14
- package/dist/ui-engine/default-font.d.ts +25 -0
- package/dist/ui-engine/default-font.js +80 -0
- package/dist/ui-engine/font-assets.d.ts +2 -2
- package/dist/ui-engine/font-assets.js +22 -5
- package/dist/ui-engine/html-parser.d.ts +4 -0
- package/dist/ui-engine/html-parser.js +241 -26
- package/dist/ui-engine/image-assets.d.ts +8 -1
- package/dist/ui-engine/image-assets.js +35 -2
- package/dist/ui-engine/image-decode.d.ts +22 -0
- package/dist/ui-engine/image-decode.js +194 -0
- package/dist/ui-engine/inline-parser.js +1 -1
- package/dist/ui-engine/layout-engine.d.ts +19 -0
- package/dist/ui-engine/layout-engine.js +59 -3
- package/dist/ui-engine/model.d.ts +18 -0
- package/dist/ui-engine/model.js +76 -8
- package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
- package/dist/ui-engine/runtime-header/canvas-helpers.js +5 -2
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
- package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
- package/dist/ui-engine/runtime-header/keyboard.js +682 -380
- package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1377
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
- package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
- package/dist/ui-engine/runtime-header/structs.js +228 -216
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +79 -23
- package/dist/ui-engine/runtime-header/types-defines.js +110 -93
- package/dist/ui-engine/shadcn-kit.d.ts +1 -0
- package/dist/ui-engine/shadcn-kit.js +502 -0
- package/dist/ui-engine/style-resolver.d.ts +4 -0
- package/dist/ui-engine/style-resolver.js +58 -6
- package/dist/ui-engine/transpile-ui.js +12 -5
- package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
- package/dist/ui-engine/ua-stylesheet.js +119 -32
- package/dist/ui-engine/ui-lowering.js +37 -3
- package/dist/ui-engine/ui-registry.js +32 -4
- package/dist/ui-engine/yoga-layout.js +57 -52
- package/dist/wizard/config-writer.d.ts +49 -0
- package/dist/wizard/config-writer.js +366 -0
- package/dist/wizard/display-catalog.d.ts +75 -0
- package/dist/wizard/display-catalog.js +207 -0
- package/dist/wizard/index.d.ts +7 -0
- package/dist/wizard/index.js +10 -0
- package/dist/wizard/integration-wizard.d.ts +15 -0
- package/dist/wizard/integration-wizard.js +486 -0
- package/dist/wizard/prompts.d.ts +32 -0
- package/dist/wizard/prompts.js +95 -0
- package/dist/wizard/starter-ui.d.ts +1 -0
- package/dist/wizard/starter-ui.js +56 -0
- package/package.json +18 -5
- package/src/cli.ts +87 -0
- package/src/engine-index.ts +53 -51
- package/src/preview/build-program.ts +810 -734
- package/src/preview/host-gfx.ts +30 -3
- package/src/preview/host-ui-runtime.ts +4133 -3289
- package/src/ui-engine/block-layout.ts +24 -11
- package/src/ui-engine/color.ts +77 -2
- package/src/ui-engine/compat-report.ts +139 -0
- package/src/ui-engine/css-imports.ts +69 -0
- package/src/ui-engine/css-parser.ts +64 -15
- package/src/ui-engine/default-font.ts +95 -0
- package/src/ui-engine/font-assets.ts +545 -525
- package/src/ui-engine/html-parser.ts +615 -397
- package/src/ui-engine/image-assets.ts +37 -2
- package/src/ui-engine/image-decode.ts +244 -0
- package/src/ui-engine/inline-parser.ts +1 -1
- package/src/ui-engine/layout-engine.ts +61 -3
- package/src/ui-engine/model.ts +1317 -1230
- package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
- package/src/ui-engine/runtime-header/canvas-helpers.ts +5 -2
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
- package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
- package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
- package/src/ui-engine/runtime-header/keyboard.ts +689 -386
- package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1402
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
- package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
- package/src/ui-engine/runtime-header/structs.ts +234 -222
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +79 -23
- package/src/ui-engine/runtime-header/types-defines.ts +116 -99
- package/src/ui-engine/shadcn-kit.ts +503 -0
- package/src/ui-engine/style-resolver.ts +471 -416
- package/src/ui-engine/transpile-ui.ts +12 -5
- package/src/ui-engine/ua-stylesheet.ts +137 -31
- package/src/ui-engine/ui-lowering.ts +486 -452
- package/src/ui-engine/ui-registry.ts +556 -524
- package/src/ui-engine/yoga-layout.ts +307 -309
- package/src/wizard/config-writer.ts +404 -0
- package/src/wizard/display-catalog.ts +273 -0
- package/src/wizard/index.ts +38 -0
- package/src/wizard/integration-wizard.ts +619 -0
- package/src/wizard/prompts.ts +145 -0
- package/src/wizard/starter-ui.ts +58 -0
|
@@ -1,452 +1,486 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// UI lowering — styled tree + computed boxes → C++ node/binding/transition
|
|
3
|
-
// tables + a .ui.d.html.ts declaration file.
|
|
4
|
-
//
|
|
5
|
-
// This is the bridge between the host-side parse/layout pipeline and the
|
|
6
|
-
// device-side retained runtime. Colors are resolved to the target color format
|
|
7
|
-
// here (once, at transpile time) so the device never converts colors.
|
|
8
|
-
//
|
|
9
|
-
// Output shape:
|
|
10
|
-
// - nodeTable: `static const UINode __ui_nodes[] = { ... };`
|
|
11
|
-
// - transitionTable: `static const UITransition __ui_trans[] = { ... };`
|
|
12
|
-
// - typeDecl: TypeScript declarations so .ui.html imports are typed.
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
import { StyledNode } from "./style-resolver.js";
|
|
16
|
-
import { Box } from "./layout-engine.js";
|
|
17
|
-
import { lowerUIToModel, type UIProgram, type UINodeModel, type KeyframeSetModel } from "./model.js";
|
|
18
|
-
import { resolveColor } from "./color.js";
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import type {
|
|
22
|
-
import type {
|
|
23
|
-
import type {
|
|
24
|
-
import type {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
type
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
n.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
for (const
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
case "
|
|
232
|
-
case "
|
|
233
|
-
case "
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
lines.push(
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
`
|
|
397
|
-
`
|
|
398
|
-
|
|
399
|
-
`
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
}).
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
`
|
|
451
|
-
|
|
452
|
-
}
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// UI lowering — styled tree + computed boxes → C++ node/binding/transition
|
|
3
|
+
// tables + a .ui.d.html.ts declaration file.
|
|
4
|
+
//
|
|
5
|
+
// This is the bridge between the host-side parse/layout pipeline and the
|
|
6
|
+
// device-side retained runtime. Colors are resolved to the target color format
|
|
7
|
+
// here (once, at transpile time) so the device never converts colors.
|
|
8
|
+
//
|
|
9
|
+
// Output shape:
|
|
10
|
+
// - nodeTable: `static const UINode __ui_nodes[] = { ... };`
|
|
11
|
+
// - transitionTable: `static const UITransition __ui_trans[] = { ... };`
|
|
12
|
+
// - typeDecl: TypeScript declarations so .ui.html imports are typed.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
import { StyledNode } from "./style-resolver.js";
|
|
16
|
+
import { Box } from "./layout-engine.js";
|
|
17
|
+
import { lowerUIToModel, type UIProgram, type UINodeModel, type KeyframeSetModel } from "./model.js";
|
|
18
|
+
import { resolveColor } from "./color.js";
|
|
19
|
+
import { defaultFontAttribution } from "./default-font.js";
|
|
20
|
+
import { DEFAULT_ALPHA_KEYBOARD, DEFAULT_NUMBER_KEYBOARD } from "./default-keyboards.js";
|
|
21
|
+
import type { KeyboardTemplate, UIKeyTemplate } from "./html-parser.js";
|
|
22
|
+
import type { CSSRule, CSSProperty } from "./css-parser.js";
|
|
23
|
+
import type { DisplayProfile } from "@typecad/cuttlefish/api/shared";
|
|
24
|
+
import type { UIFontAssetModel } from "./font-assets.js";
|
|
25
|
+
import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
|
|
26
|
+
import { getListBindings } from "@typecad/cuttlefish/ir/transformers/ui-reactive";
|
|
27
|
+
|
|
28
|
+
export interface LoweredUI {
|
|
29
|
+
fontTables: string;
|
|
30
|
+
nodeTable: string;
|
|
31
|
+
transitionTable: string;
|
|
32
|
+
typeDecl: string;
|
|
33
|
+
/** C++ keyboard loader function bodies (one per keyboard in use). */
|
|
34
|
+
keyboardLoaders: string;
|
|
35
|
+
/** C++ dispatch table mapping input node index → loader function. */
|
|
36
|
+
keyboardDispatch: string;
|
|
37
|
+
/** Number of distinct screens (for multi-screen navigation). */
|
|
38
|
+
screenCount: number;
|
|
39
|
+
/** C++ image data arrays + index table (for <img> support). */
|
|
40
|
+
imageTables: string;
|
|
41
|
+
/** C++ keyframe data arrays + animation table. */
|
|
42
|
+
keyframeTables: string;
|
|
43
|
+
/** Layout-time diagnostics (viewport overflow, text overflow). Populated by
|
|
44
|
+
* lowerOnMount after the boxes are computed; empty until then. */
|
|
45
|
+
diagnostics: Diagnostic[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
|
|
49
|
+
type Storage = "progmem" | "flash";
|
|
50
|
+
|
|
51
|
+
export function lowerUIToCpp(
|
|
52
|
+
root: StyledNode,
|
|
53
|
+
boxes: Box[],
|
|
54
|
+
colorFormat: ColorFormat,
|
|
55
|
+
storage: Storage,
|
|
56
|
+
keyboards: KeyboardTemplate[] = [],
|
|
57
|
+
rules: CSSRule[] = [],
|
|
58
|
+
display?: DisplayProfile,
|
|
59
|
+
fontAssets: UIFontAssetModel[] = [],
|
|
60
|
+
allScreens: StyledNode[] = [],
|
|
61
|
+
imageAssetIds: Map<string, number> = new Map(),
|
|
62
|
+
keyframeSets: KeyframeSetModel[] = [],
|
|
63
|
+
): LoweredUI {
|
|
64
|
+
void storage;
|
|
65
|
+
const model = lowerUIToModel(root, boxes, colorFormat, display, fontAssets, allScreens, imageAssetIds, keyframeSets);
|
|
66
|
+
|
|
67
|
+
// Tables are mutable RAM (ui_tick updates bg/dirty/elapsed/active each
|
|
68
|
+
// frame), so no PROGMEM/flash storage keyword — those imply read-only.
|
|
69
|
+
const fontTables = emitFontTables(model);
|
|
70
|
+
const nodeTable = emitNodeTable(model);
|
|
71
|
+
const transitionTable = emitTransitionTable(model);
|
|
72
|
+
const typeDecl = emitTypeDecl(root);
|
|
73
|
+
|
|
74
|
+
// Keyboard loaders + dispatch: collect input nodes in tree order, resolve
|
|
75
|
+
// each to its loader (default by type, or a referenced <keyboard>).
|
|
76
|
+
// Walk ALL screens (inputs may live on any screen, not just the root).
|
|
77
|
+
const inputSpecs: Array<{ type?: string; keyboard?: string }> = [];
|
|
78
|
+
const collectInputs = (n: StyledNode) => {
|
|
79
|
+
if (n.tag === "input") inputSpecs.push({ type: n.type, keyboard: n.keyboard });
|
|
80
|
+
n.children.forEach(collectInputs);
|
|
81
|
+
};
|
|
82
|
+
const inputRoots = allScreens.length > 0 ? allScreens : [root];
|
|
83
|
+
for (const sr of inputRoots) collectInputs(sr);
|
|
84
|
+
|
|
85
|
+
const neededKeyboards: KeyboardTemplate[] = [];
|
|
86
|
+
const addIfNeeded = (kb: KeyboardTemplate) => {
|
|
87
|
+
if (!neededKeyboards.some(k => k.id === kb.id)) neededKeyboards.push(kb);
|
|
88
|
+
};
|
|
89
|
+
for (const spec of inputSpecs) {
|
|
90
|
+
if (spec.keyboard) {
|
|
91
|
+
const match = keyboards.find(k => k.id === spec.keyboard);
|
|
92
|
+
if (match) addIfNeeded(match);
|
|
93
|
+
} else {
|
|
94
|
+
addIfNeeded(spec.type === "number" ? DEFAULT_NUMBER_KEYBOARD : DEFAULT_ALPHA_KEYBOARD);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// The runtime header's keyboard page-swap (the 123/ABC toggle key) calls
|
|
98
|
+
// BOTH default loaders regardless of which input opened the keyboard. Emit
|
|
99
|
+
// both whenever any input exists, or a text-only project fails to link
|
|
100
|
+
// (undefined __ui_kb_load_default_number on non-Arduino linkers).
|
|
101
|
+
if (inputSpecs.length > 0) {
|
|
102
|
+
addIfNeeded(DEFAULT_ALPHA_KEYBOARD);
|
|
103
|
+
addIfNeeded(DEFAULT_NUMBER_KEYBOARD);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const keyboardLoaders = neededKeyboards
|
|
107
|
+
.map(kb => emitKeyboardLoader(loaderNameForId(kb.id), kb, rules, colorFormat))
|
|
108
|
+
.join("\n\n");
|
|
109
|
+
|
|
110
|
+
const dispatchEntries = inputSpecs.map(spec => loaderNameForInput(spec, keyboards));
|
|
111
|
+
const keyboardDispatch = dispatchEntries.length > 0
|
|
112
|
+
? `void (*__ui_kb_loaders[])() = { ${dispatchEntries.join(", ")} };\nconst uint16_t __ui_kb_loader_count = ${dispatchEntries.length};`
|
|
113
|
+
: `void (*__ui_kb_loaders[])() = {};\nconst uint16_t __ui_kb_loader_count = 0;`;
|
|
114
|
+
|
|
115
|
+
const screenCount = model.nodes.length > 0 ? Math.max(...model.nodes.map(n => n.screenId)) + 1 : 1;
|
|
116
|
+
const imageTables = "const UIImage __ui_images[] = {};\nconst uint16_t __ui_image_count = 0;";
|
|
117
|
+
const keyframeTables = emitKeyframeTables(model);
|
|
118
|
+
return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, diagnostics: [] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sanitizedId(id: string): string {
|
|
122
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function loaderNameForId(id: string): string {
|
|
126
|
+
return `__ui_kb_load_${sanitizedId(id)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function loaderNameForInput(input: { type?: string; keyboard?: string }, keyboards: KeyboardTemplate[]): string {
|
|
130
|
+
if (input.keyboard) {
|
|
131
|
+
const match = keyboards.find(k => k.id === input.keyboard);
|
|
132
|
+
if (match) return loaderNameForId(match.id);
|
|
133
|
+
}
|
|
134
|
+
return input.type === "number" ? "__ui_kb_load_default_number" : "__ui_kb_load_default_alpha";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Default key colors (fallback when no CSS matches). Used for all keys.
|
|
138
|
+
const DEFAULT_KEY_BG = 0x4208; // dark gray
|
|
139
|
+
const DEFAULT_KEY_FG = 0xFFFF; // white
|
|
140
|
+
const DEFAULT_KEY_BORDER = 0xFFFF; // white
|
|
141
|
+
const DEFAULT_KB_BG = 0x0000; // black
|
|
142
|
+
|
|
143
|
+
/** Resolve a key's CSS classes into a merged CSSProperty (cascade: last wins). */
|
|
144
|
+
/** Check if a CSS rule's selector matches any of the given class names.
|
|
145
|
+
* Only matches single-compound class selectors (no descendant for keys). */
|
|
146
|
+
function ruleMatchesClass(rule: CSSRule, classes: string[]): boolean {
|
|
147
|
+
// Must be a single compound (no descendant combinator).
|
|
148
|
+
if (rule.selector.compounds.length !== 1) return false;
|
|
149
|
+
const compound = rule.selector.compounds[0];
|
|
150
|
+
// Every simple in the compound must be a class that's in the list.
|
|
151
|
+
for (const s of compound) {
|
|
152
|
+
if (s.kind !== "class" || !classes.includes(s.name)) return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveKeyStyle(keyClasses: string[] | undefined, kbClasses: string[] | undefined, rules: CSSRule[]): CSSProperty {
|
|
158
|
+
const merged: CSSProperty = {};
|
|
159
|
+
const allClasses = [...(kbClasses ?? []), ...(keyClasses ?? [])];
|
|
160
|
+
for (const rule of rules) {
|
|
161
|
+
if (ruleMatchesClass(rule, allClasses)) {
|
|
162
|
+
Object.assign(merged, rule.properties);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return merged;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Resolve the keyboard-level background from CSS (keyboard classes). */
|
|
169
|
+
function resolveKbBg(kbClasses: string[] | undefined, rules: CSSRule[], colorFormat: ColorFormat): number {
|
|
170
|
+
const merged: CSSProperty = {};
|
|
171
|
+
const classes = kbClasses ?? [];
|
|
172
|
+
for (const rule of rules) {
|
|
173
|
+
if (ruleMatchesClass(rule, classes)) {
|
|
174
|
+
Object.assign(merged, rule.properties);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return merged.background ? resolveColor(merged.background, colorFormat) : DEFAULT_KB_BG;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function emitKeyboardLoader(name: string, kb: KeyboardTemplate, rules: CSSRule[], colorFormat: ColorFormat): string {
|
|
181
|
+
const rows = kb.rows;
|
|
182
|
+
const cols = rows.length > 0 ? Math.max(...rows.map(r => r.length)) : 0;
|
|
183
|
+
const kbBg = resolveKbBg(kb.classes, rules, colorFormat);
|
|
184
|
+
const lines: string[] = [];
|
|
185
|
+
lines.push(`void ${name}() {`);
|
|
186
|
+
lines.push(` __ui_kb_rows = ${rows.length};`);
|
|
187
|
+
lines.push(` __ui_kb_cols = ${cols};`);
|
|
188
|
+
lines.push(` __ui_kb_keyCount = 0;`);
|
|
189
|
+
lines.push(` __ui_kb_bg = ${hex(kbBg)};`);
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
for (const key of row) {
|
|
192
|
+
lines.push(` ${emitKeyLine(key, kb.classes, rules, colorFormat)}`);
|
|
193
|
+
}
|
|
194
|
+
// Pad short rows so ui_kb_key_rect's idx/cols math stays aligned. Padded
|
|
195
|
+
// cells use special=255 (skipped in draw + hit-test) with a space char.
|
|
196
|
+
for (let p = row.length; p < cols; p++) {
|
|
197
|
+
lines.push(` ui_kb_add_key(' ', 255, ${hex(DEFAULT_KEY_BG)}, ${hex(DEFAULT_KEY_FG)}, ${hex(DEFAULT_KEY_BORDER)});`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
lines.push(`}`);
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Emit one key's keys[] + styles[] lines, resolving CSS classes to colors. */
|
|
205
|
+
function emitKeyLine(key: UIKeyTemplate, kbClasses: string[] | undefined, rules: CSSRule[], colorFormat: ColorFormat): string {
|
|
206
|
+
// The C++ UIKey.ch field is a single `char`. Special keys (shift=1, bs=2,
|
|
207
|
+
// ok=3, page=4) use the `special` field for their behavior and label — the
|
|
208
|
+
// `ch` value is never read. Their label strings ("OK", "123", "⇧", "⌫") are
|
|
209
|
+
// multi-char or multi-byte and would overflow `char` if emitted as literals.
|
|
210
|
+
// Emit a space placeholder for any key whose ch isn't a single ASCII byte.
|
|
211
|
+
const isSingleAscii = key.special === 0 && key.ch.length === 1 && key.ch.charCodeAt(0) < 128;
|
|
212
|
+
if (key.special === 0 && !isSingleAscii) {
|
|
213
|
+
console.warn(
|
|
214
|
+
`[cuttlefish] keyboard key ch="${key.ch}" is non-ASCII and will be emitted as ` +
|
|
215
|
+
`a space placeholder. The native C++ UIKey.ch field is a single char and ` +
|
|
216
|
+
`cannot hold multi-byte codepoints. Use a custom keyboard layout or the ` +
|
|
217
|
+
`Adafruit path for full Unicode key labels.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const chRaw = isSingleAscii ? key.ch : ' ';
|
|
221
|
+
const chEsc = chRaw === "\\" ? "\\\\" : chRaw === "'" ? "\\'" : chRaw;
|
|
222
|
+
const style = resolveKeyStyle(key.classes, kbClasses, rules);
|
|
223
|
+
const bg = style.background ? resolveColor(style.background, colorFormat) : DEFAULT_KEY_BG;
|
|
224
|
+
const fg = style.color ? resolveColor(style.color, colorFormat) : DEFAULT_KEY_FG;
|
|
225
|
+
const border = style.borderColor ? resolveColor(style.borderColor, colorFormat) : DEFAULT_KEY_BORDER;
|
|
226
|
+
return `ui_kb_add_key('${chEsc}', ${key.special}, ${hex(bg)}, ${hex(fg)}, ${hex(border)});`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function cppKind(kind: UINodeModel["kind"]): string {
|
|
230
|
+
switch (kind) {
|
|
231
|
+
case "fill": return "NODE_FILL";
|
|
232
|
+
case "button": return "NODE_BUTTON";
|
|
233
|
+
case "check": return "NODE_CHECK";
|
|
234
|
+
case "radio": return "NODE_RADIO";
|
|
235
|
+
case "progress": return "NODE_PROGRESS";
|
|
236
|
+
case "range": return "NODE_RANGE";
|
|
237
|
+
case "input": return "NODE_INPUT";
|
|
238
|
+
case "img": return "NODE_IMG";
|
|
239
|
+
case "list": return "NODE_LIST";
|
|
240
|
+
case "canvas": return "NODE_CANVAS";
|
|
241
|
+
case "select": return "NODE_SELECT";
|
|
242
|
+
case "text": return "NODE_TEXT";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hex(c: number): string {
|
|
247
|
+
return `0x${c.toString(16).padStart(4, "0")}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function cppString(value: string | undefined): string {
|
|
251
|
+
return value ? JSON.stringify(value) : "nullptr";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function byteArray(values: number[]): string {
|
|
255
|
+
if (values.length === 0) return "";
|
|
256
|
+
const chunks: string[] = [];
|
|
257
|
+
for (let i = 0; i < values.length; i += 16) {
|
|
258
|
+
chunks.push(" " + values.slice(i, i + 16).map((v) => `0x${(v & 0xff).toString(16).padStart(2, "0")}`).join(", "));
|
|
259
|
+
}
|
|
260
|
+
return chunks.join(",\n");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function emitFontTables(model: UIProgram): string {
|
|
264
|
+
const assets = model.fontAssets ?? [];
|
|
265
|
+
if (assets.length === 0) {
|
|
266
|
+
return [
|
|
267
|
+
`const UIFontFace __ui_font_faces[] = {};`,
|
|
268
|
+
`const uint16_t __ui_font_face_count = 0;`,
|
|
269
|
+
].join("\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const lines: string[] = [];
|
|
273
|
+
for (const asset of assets) {
|
|
274
|
+
lines.push(`// Font ${asset.id}: ${asset.family} ${asset.px}px ${asset.fontWeight} ${asset.fontStyle} ${asset.subset} (source: ${asset.sourcePath.split(/[\\/]/).pop()})`);
|
|
275
|
+
// Bundled DejaVu faces carry their license attribution into the binary
|
|
276
|
+
// (the Bitstream Vera License requires the notice accompany distributions).
|
|
277
|
+
const attribution = defaultFontAttribution(asset.sourcePath);
|
|
278
|
+
if (attribution) lines.push(`// ${attribution}`);
|
|
279
|
+
// Alpha data goes in PROGMEM (constants in flash, not RAM) - accessed via pgm_read_byte on AVR
|
|
280
|
+
lines.push(`static const uint8_t __ui_font_${asset.id}_alpha[] PROGMEM = {`);
|
|
281
|
+
lines.push(byteArray(asset.alpha));
|
|
282
|
+
lines.push(`};`);
|
|
283
|
+
lines.push(`static const UIFontGlyph __ui_font_${asset.id}_glyphs[] = {`);
|
|
284
|
+
for (const glyph of asset.glyphs) {
|
|
285
|
+
lines.push(
|
|
286
|
+
` { ${glyph.codepoint}, ${glyph.xOffset}, ${glyph.yOffset}, ${glyph.width}, ${glyph.height}, ${glyph.advance}, ${glyph.dataOffset} },`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
lines.push(`};`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Font faces and glyphs stay in regular memory for direct struct access
|
|
293
|
+
// (AVR optimized builds can move the whole table to PROGMEM + accessor functions)
|
|
294
|
+
lines.push(`const UIFontFace __ui_font_faces[] = {`);
|
|
295
|
+
for (const asset of assets) {
|
|
296
|
+
lines.push(
|
|
297
|
+
` { ${asset.id}, ${asset.glyphs.length}, ${asset.lineHeight}, ${asset.baseline}, __ui_font_${asset.id}_glyphs, __ui_font_${asset.id}_alpha },`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
lines.push(`};`);
|
|
301
|
+
lines.push(`const uint16_t __ui_font_face_count = ${assets.length};`);
|
|
302
|
+
return lines.join("\n");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function emitNodeTable(model: UIProgram): string {
|
|
306
|
+
// Map node index → list binding so virtualized nodes carry their count/item/
|
|
307
|
+
// tap function pointers on-node (no UIListBinding side table at runtime).
|
|
308
|
+
const listBindingByNode = new Map<number, ReturnType<typeof getListBindings>[number]>();
|
|
309
|
+
for (const lb of getListBindings()) listBindingByNode.set(lb.nodeIndex, lb);
|
|
310
|
+
|
|
311
|
+
// <select> option tables: one fn per select feeding the node's optionTextFn
|
|
312
|
+
// (the modal option list reads options through it). Emitted before the node
|
|
313
|
+
// table so the initializers can reference the names.
|
|
314
|
+
const optionFnByNode = new Map<number, string>();
|
|
315
|
+
const optionFns: string[] = [];
|
|
316
|
+
for (const n of model.nodes) {
|
|
317
|
+
if (n.kind !== "select" || !n.options || n.options.length === 0) continue;
|
|
318
|
+
const fnName = `__ui_select_opts_${n.index}`;
|
|
319
|
+
optionFnByNode.set(n.index, fnName);
|
|
320
|
+
const cases = n.options.map((o, i) =>
|
|
321
|
+
` if (idx == ${i}) { snprintf(buf, static_cast<size_t>(size), "%s", ${JSON.stringify(o.text)}); return; }`).join("\n");
|
|
322
|
+
optionFns.push(`static void ${fnName}(uint8_t idx, char* buf, uint8_t size) {\n${cases}\n buf[0] = 0;\n}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Accumulate rich-text run / segment / line parallel arrays across all
|
|
326
|
+
// run-bearing nodes, assigning each a contiguous slice (runStart/richSegStart/
|
|
327
|
+
// richLineStart + counts). Emitted as global tables after the node table.
|
|
328
|
+
const runRows: string[] = [];
|
|
329
|
+
const segRows: string[] = [];
|
|
330
|
+
const lineRows: string[] = [];
|
|
331
|
+
const richRanges = new Map<number, { runCount: number; runStart: number; richLineCount: number; richSegStart: number; richSegCount: number; richLineStart: number }>();
|
|
332
|
+
for (const n of model.nodes) {
|
|
333
|
+
if (!n.runs || !n.runLines || n.runs.length === 0) continue;
|
|
334
|
+
const runStart = runRows.length;
|
|
335
|
+
for (const r of n.runs) {
|
|
336
|
+
runRows.push(` { .text=${cppString(r.text)}, .fg=${hex(r.fg)}, .textSize=${r.textSize}, .fontFace=${r.fontFace}, .underline=${r.underline}, .letterSpacing=${r.letterSpacing}, .linkTarget=${r.linkTarget} },`);
|
|
337
|
+
}
|
|
338
|
+
const richSegStart = segRows.length;
|
|
339
|
+
for (let s = 0; s < n.runLines.segRun.length; s++) {
|
|
340
|
+
segRows.push(` { .runIndex=${n.runLines.segRun[s]}, .text=${cppString(n.runLines.segText[s])}, .x=${n.runLines.segX[s]}, .w=${n.runLines.segW[s]}, .line=${n.runLines.segLine[s]} },`);
|
|
341
|
+
}
|
|
342
|
+
const richLineStart = lineRows.length;
|
|
343
|
+
for (let l = 0; l < n.runLines.lineY.length; l++) {
|
|
344
|
+
lineRows.push(` { .y=${n.runLines.lineY[l]}, .h=${n.runLines.lineH[l]}, .baseline=${n.runLines.lineBaseline[l]}, .w=${n.runLines.lineW[l]} },`);
|
|
345
|
+
}
|
|
346
|
+
richRanges.set(n.index, {
|
|
347
|
+
runCount: n.runs.length, runStart,
|
|
348
|
+
richLineCount: n.runLines.lineY.length, richSegStart,
|
|
349
|
+
richSegCount: n.runLines.segRun.length, richLineStart,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const lines = model.nodes.map((n) => {
|
|
354
|
+
const text = cppString(n.text);
|
|
355
|
+
const font = "nullptr";
|
|
356
|
+
// Input nodes store the placeholder in .text (static literal) so the draw
|
|
357
|
+
// can show it grayed when textBuffer is empty. textBuffer stays {0} so
|
|
358
|
+
// ui_kb_open starts with a clean edit buffer (no placeholder to delete).
|
|
359
|
+
const inputText = n.kind === "input" && n.textBuffer ? cppString(n.textBuffer) : text;
|
|
360
|
+
const box = `{${n.box.x},${n.box.y},${n.box.w},${n.box.h}}`;
|
|
361
|
+
const parent = n.parentIndex >= 0 ? n.parentIndex : 0xFFFF; // UI_NO_PARENT
|
|
362
|
+
// Progress/range use lastTextWidth as a "previous fill width" for
|
|
363
|
+
// incremental redraw. -1 = never drawn, because fill width 0 is valid.
|
|
364
|
+
const lastTextWidth = n.kind === "progress" || n.kind === "range" ? -1 : 0;
|
|
365
|
+
const shArr = (vals: number[], n = 4) => `{${[...vals.slice(0, n), ...Array(n - Math.min(vals.length, n)).fill(0)].join(",")}}`;
|
|
366
|
+
// Virtualized list: resolve on-node function pointers from the binding.
|
|
367
|
+
const lb = listBindingByNode.get(n.index);
|
|
368
|
+
const virtualized = n.virtualized ? 1 : 0;
|
|
369
|
+
const listCountFn = lb ? lb.countFnName : "nullptr";
|
|
370
|
+
const listItemFn = lb ? lb.itemFnName : "nullptr";
|
|
371
|
+
const listTapFn = lb && lb.tapFnName ? lb.tapFnName : "nullptr";
|
|
372
|
+
const rr = richRanges.get(n.index);
|
|
373
|
+
const runCount = rr ? rr.runCount : 0;
|
|
374
|
+
const runStart = rr ? rr.runStart : 0;
|
|
375
|
+
const richLineCount = rr ? rr.richLineCount : 0;
|
|
376
|
+
const richSegStart = rr ? rr.richSegStart : 0;
|
|
377
|
+
const richSegCount = rr ? rr.richSegCount : 0;
|
|
378
|
+
const richLineStart = rr ? rr.richLineStart : 0;
|
|
379
|
+
return ` { .box=${box}, .bg=${hex(n.bg)}, .fg=${hex(n.fg)}, .kind=${cppKind(n.kind)}, .text=${inputText}, .textBuffer={0}, .hasTextBinding=0, .font=${font}, .hasBg=${n.hasBg ? 1 : 0}, .textAlign=${n.textAlign}, .textSize=${n.textSize}, .lineHeight=${n.lineHeight}, .letterSpacing=${n.letterSpacing}, .fontAntialias=${n.fontAntialias ? 1 : 0}, .fontFace=${n.fontFace}, .borderColor=${hex(n.borderColor)}, .borderStyle=${n.borderStyle}, .borderWidth=${n.borderWidth}, .borderTopWidth=${(n as any).borderTopWidth ?? n.borderWidth}, .borderRightWidth=${(n as any).borderRightWidth ?? n.borderWidth}, .borderBottomWidth=${(n as any).borderBottomWidth ?? n.borderWidth}, .borderLeftWidth=${(n as any).borderLeftWidth ?? n.borderWidth}, .hasPerSideBorder=${(((n as any).borderTopWidth ?? n.borderWidth) !== n.borderWidth || ((n as any).borderRightWidth ?? n.borderWidth) !== n.borderWidth || ((n as any).borderBottomWidth ?? n.borderWidth) !== n.borderWidth || ((n as any).borderLeftWidth ?? n.borderWidth) !== n.borderWidth) ? 1 : 0}, .borderRadius=${Math.min(255, n.borderRadius)}, .paddingTop=${n.paddingTop ?? 0}, .paddingRight=${n.paddingRight ?? 0}, .paddingBottom=${n.paddingBottom ?? 0}, .paddingLeft=${n.paddingLeft ?? 0}, .gradientEnabled=${n.gradientEnabled}, .gradientColor1=${hex(n.gradientColor1)}, .gradientColor2=${hex(n.gradientColor2)}, .outlineColor=${hex(n.outlineColor)}, .outlineStyle=${n.outlineStyle}, .outlineWidth=${n.outlineWidth}, .zIndex=${n.zIndex}, .transformOffsetX=${n.transformOffsetX}, .transformOffsetY=${n.transformOffsetY}, .rotateDeg=${n.rotateDeg}, .pressedOffsetX=${n.pressedOffsetX}, .pressedOffsetY=${n.pressedOffsetY}, .shadowCount=${n.shadowCount}, .shadowOffsetX=${shArr(n.shadowOffsetX)}, .shadowOffsetY=${shArr(n.shadowOffsetY)}, .shadowBlur=${shArr(n.shadowBlur)}, .shadowColor={${n.shadowColor.slice(0, 4).map(hex).join(",")}}, .shadowAlpha=${shArr(n.shadowAlpha)}, .shadowInset=${shArr(n.shadowInset.map(v => v ? 1 : 0))}, .textShadowCount=${n.textShadowCount}, .textShadowOffsetX=${n.textShadowOffsetX}, .textShadowOffsetY=${n.textShadowOffsetY}, .textShadowBlur=${n.textShadowBlur}, .textShadowColor=${hex(n.textShadowColor)}, .textShadowAlpha=${n.textShadowAlpha}, .underline=${n.underline}, .textOverflow=${n.textOverflow ? 1 : 0}, .nowrap=${n.nowrap ? 1 : 0}, .whiteSpaceMode=${n.whiteSpaceMode}, .visible=${n.visible ? 1 : 0}, .opacity=${n.opacity}, .clearColor=${hex(n.clearColor)}, .lastTextWidth=${lastTextWidth}, .lastTextHeight=0, .layoutCacheKey=0, .layoutMetricsW=0, .layoutMetricsH=0, .scrollable=${n.scrollable ? 1 : 0}, .virtualized=${virtualized}, .scrollY=0, .contentHeight=${n.contentHeight}, .overscrollPx=0, .settling=0, .lastPaintedScrollY=0, .listCount=0, .listCountFn=${listCountFn}, .listItemFn=${listItemFn}, .listTapFn=${listTapFn}, .parent=${parent}, .subtreeEnd=${n.subtreeEnd}, .screenId=${n.screenId}, .imgDataId=${n.imgDataId ?? 255}, .objectFit=${n.objectFit ?? 1}, .listItemHeight=${(n as any).listItemHeight ?? 0}, .rangeMin=${n.rangeMin}, .rangeMax=${n.rangeMax}, .maxlen=${n.maxlen}, .canvasW=${n.canvasW ?? 0}, .canvasH=${n.canvasH ?? 0}, .runCount=${runCount}, .richLineCount=${richLineCount}, .runStart=${runStart}, .richSegStart=${richSegStart}, .richSegCount=${richSegCount}, .richLineStart=${richLineStart}, .dirty=0, .value=${n.value}, .disabled=${n.disabled ? 1 : 0}, .optionCount=${n.kind === "select" && n.options ? n.options.length : 0}, .optionTextFn=${optionFnByNode.get(n.index) ?? "nullptr"}, .drawerSide=${(n as any).drawerSide ?? -1}, .toastDuration=${n.toastDuration ?? 0}, .checkedBg=${(n as any).checkedBg !== undefined && (n as any).checkedBg >= 0 ? hex((n as any).checkedBg) : "0x0"}, .checkedFg=${(n as any).checkedFg !== undefined && (n as any).checkedFg >= 0 ? hex((n as any).checkedFg) : "0x0"}, .hasCheckedBg=${(n as any).checkedBg !== undefined && (n as any).checkedBg >= 0 ? 1 : 0}, .hasCheckedFg=${(n as any).checkedFg !== undefined && (n as any).checkedFg >= 0 ? 1 : 0}, .flowAxis=${n.flowAxis ?? 0}, .flowGap=${n.flowGap ?? 0}, .flowFlags=${n.flowFlags ?? 0} },`;
|
|
380
|
+
});
|
|
381
|
+
return [
|
|
382
|
+
// <select> option tables (must precede __ui_nodes[]: initializers
|
|
383
|
+
// reference the function names).
|
|
384
|
+
...optionFns,
|
|
385
|
+
// Mutable (not const) so ui_tick can update bg/dirty during transitions.
|
|
386
|
+
// AVR would want PROGMEM + a shadow copy; ESP32-class has RAM to spare.
|
|
387
|
+
`UINode __ui_nodes[] = {`,
|
|
388
|
+
...lines,
|
|
389
|
+
`};`,
|
|
390
|
+
// Rich-text parallel arrays. Mutable is unnecessary (static content) but
|
|
391
|
+
// matches __ui_nodes[] linkage; nodes reference slices via runStart etc.
|
|
392
|
+
runRows.length > 0
|
|
393
|
+
? [`UIRichRun __ui_runs[] = {`, ...runRows, `};`].join("\n")
|
|
394
|
+
: `UIRichRun __ui_runs[1];`,
|
|
395
|
+
segRows.length > 0
|
|
396
|
+
? [`UIRichSeg __ui_rich_segs[] = {`, ...segRows, `};`].join("\n")
|
|
397
|
+
: `UIRichSeg __ui_rich_segs[1];`,
|
|
398
|
+
lineRows.length > 0
|
|
399
|
+
? [`UIRichLine __ui_rich_lines[] = {`, ...lineRows, `};`].join("\n")
|
|
400
|
+
: `UIRichLine __ui_rich_lines[1];`,
|
|
401
|
+
`const uint16_t __ui_run_count = ${runRows.length};`,
|
|
402
|
+
`const uint16_t __ui_rich_seg_count = ${segRows.length};`,
|
|
403
|
+
`const uint16_t __ui_rich_line_count = ${lineRows.length};`,
|
|
404
|
+
].join("\n");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function emitTransitionTable(model: UIProgram): string {
|
|
408
|
+
// Skip no-op transitions (pressed target equals base) — they'd never change
|
|
409
|
+
// the node's appearance and only bloat the per-tick scan.
|
|
410
|
+
const entries = model.transitions
|
|
411
|
+
.filter((t) => t.pressedTarget !== t.baseTarget)
|
|
412
|
+
.map((t) => {
|
|
413
|
+
const prop = t.prop === "background" ? "PROP_BG" : "PROP_FG";
|
|
414
|
+
return ` { .node=${t.node}, .prop=${prop}, .durationMs=${t.durationMs}, .pressedTarget=${hex(t.pressedTarget)}, .baseTarget=${hex(t.baseTarget)} },`;
|
|
415
|
+
});
|
|
416
|
+
if (entries.length === 0) {
|
|
417
|
+
return `UITransition __ui_trans[] = {};`;
|
|
418
|
+
}
|
|
419
|
+
return [
|
|
420
|
+
`UITransition __ui_trans[] = {`,
|
|
421
|
+
...entries,
|
|
422
|
+
`};`,
|
|
423
|
+
].join("\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Emit keyframe stop arrays + keyframe set index + animation table. */
|
|
427
|
+
function emitKeyframeTables(model: UIProgram): string {
|
|
428
|
+
if (model.keyframeSets.length === 0 && model.animations.length === 0) {
|
|
429
|
+
return [
|
|
430
|
+
`const UIKeyframeSet __ui_keyframe_sets[] = {};`,
|
|
431
|
+
`const uint16_t __ui_keyframe_set_count = 0;`,
|
|
432
|
+
`UIAnimation __ui_anims[] = {};`,
|
|
433
|
+
`const uint16_t __ui_anim_count = 0;`,
|
|
434
|
+
].join("\n");
|
|
435
|
+
}
|
|
436
|
+
const lines: string[] = [];
|
|
437
|
+
// Emit one stop array per keyframe set.
|
|
438
|
+
for (const ks of model.keyframeSets) {
|
|
439
|
+
const safeName = ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
440
|
+
lines.push(`static const UIKeyframeStop __ui_kf_${safeName}_stops[] = {`);
|
|
441
|
+
for (const s of ks.stops) {
|
|
442
|
+
lines.push(` { .percent=${s.percent}, .props=${s.props}, .bg=${hex(s.bg)}, .fg=${hex(s.fg)}, .opacity=${s.opacity}, .transformOffsetX=${s.transformOffsetX}, .transformOffsetY=${s.transformOffsetY}, .translatePctX=${s.translatePctX}, .translatePctY=${s.translatePctY}, .scaleX=${s.scaleX}, .scaleY=${s.scaleY}, .rotateDeg=${s.rotateDeg}, .width=${s.width}, .height=${s.height} },`);
|
|
443
|
+
}
|
|
444
|
+
lines.push(`};`);
|
|
445
|
+
}
|
|
446
|
+
// Emit keyframe set index table.
|
|
447
|
+
lines.push(`const UIKeyframeSet __ui_keyframe_sets[] = {`);
|
|
448
|
+
model.keyframeSets.forEach((ks) => {
|
|
449
|
+
const safeName = ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
450
|
+
lines.push(` { .stopCount=${ks.stops.length}, .stops=__ui_kf_${safeName}_stops },`);
|
|
451
|
+
});
|
|
452
|
+
lines.push(`};`);
|
|
453
|
+
lines.push(`const uint16_t __ui_keyframe_set_count = ${model.keyframeSets.length};`);
|
|
454
|
+
// Emit animation table (mutable — runtime advances elapsed/active).
|
|
455
|
+
lines.push(`UIAnimation __ui_anims[] = {`);
|
|
456
|
+
for (const a of model.animations) {
|
|
457
|
+
lines.push(` { .node=${a.node}, .keyframeSet=${a.keyframeSet}, .durationMs=${a.durationMs}, .delayMs=${a.delayMs}, .iterations=${a.iterations}, .baseWidth=${a.baseWidth}, .baseHeight=${a.baseHeight}, .originX=${a.originX}, .originY=${a.originY}, .timingFunction=${a.timingFunction}, .elapsed=0, .active=1, .lastUpdateMs=0 },`);
|
|
458
|
+
}
|
|
459
|
+
lines.push(`};`);
|
|
460
|
+
lines.push(`const uint16_t __ui_anim_count = ${model.animations.length};`);
|
|
461
|
+
return lines.join("\n");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function emitTypeDecl(root: StyledNode): string {
|
|
465
|
+
// Collect id → tag pairs by walking the tree.
|
|
466
|
+
const ids: Array<{ id: string; tag: string }> = [];
|
|
467
|
+
const collect = (n: StyledNode) => {
|
|
468
|
+
if (n.id) ids.push({ id: n.id, tag: n.tag });
|
|
469
|
+
n.children.forEach(collect);
|
|
470
|
+
};
|
|
471
|
+
collect(root);
|
|
472
|
+
|
|
473
|
+
const fields = ids.map(({ id, tag }) => {
|
|
474
|
+
const typeName = tag.charAt(0).toUpperCase() + tag.slice(1);
|
|
475
|
+
return ` ${id}: ${typeName}Element;`;
|
|
476
|
+
}).join("\n");
|
|
477
|
+
|
|
478
|
+
return [
|
|
479
|
+
`// Auto-generated by cuttlefish UI lowering. Do not edit.`,
|
|
480
|
+
`export interface ScreenTree {`,
|
|
481
|
+
fields,
|
|
482
|
+
`}`,
|
|
483
|
+
``,
|
|
484
|
+
`export const screen: ScreenTree;`,
|
|
485
|
+
].join("\n");
|
|
486
|
+
}
|