@typecad/ui 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1443 -0
- package/dist/engine-index.d.ts +7 -0
- package/dist/engine-index.js +41 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +42 -0
- package/dist/preview/build-program.d.ts +7 -0
- package/dist/preview/build-program.js +665 -0
- package/dist/preview/host-gfx.d.ts +90 -0
- package/dist/preview/host-gfx.js +552 -0
- package/dist/preview/host-ui-runtime.d.ts +241 -0
- package/dist/preview/host-ui-runtime.js +3236 -0
- package/dist/preview/types.d.ts +83 -0
- package/dist/preview/types.js +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.js +10 -0
- package/dist/ui-engine/block-layout.d.ts +8 -0
- package/dist/ui-engine/block-layout.js +95 -0
- package/dist/ui-engine/color.d.ts +49 -0
- package/dist/ui-engine/color.js +273 -0
- package/dist/ui-engine/css-parser.d.ts +155 -0
- package/dist/ui-engine/css-parser.js +1058 -0
- package/dist/ui-engine/default-keyboards.d.ts +3 -0
- package/dist/ui-engine/default-keyboards.js +53 -0
- package/dist/ui-engine/easing.d.ts +12 -0
- package/dist/ui-engine/easing.js +76 -0
- package/dist/ui-engine/font-assets.d.ts +50 -0
- package/dist/ui-engine/font-assets.js +431 -0
- package/dist/ui-engine/html-parser.d.ts +116 -0
- package/dist/ui-engine/html-parser.js +284 -0
- package/dist/ui-engine/image-assets.d.ts +21 -0
- package/dist/ui-engine/image-assets.js +125 -0
- package/dist/ui-engine/inline-parser.d.ts +30 -0
- package/dist/ui-engine/inline-parser.js +66 -0
- package/dist/ui-engine/keyframes.d.ts +3 -0
- package/dist/ui-engine/keyframes.js +39 -0
- package/dist/ui-engine/layout-engine.d.ts +35 -0
- package/dist/ui-engine/layout-engine.js +203 -0
- package/dist/ui-engine/model.d.ts +239 -0
- package/dist/ui-engine/model.js +987 -0
- package/dist/ui-engine/rich-layout.d.ts +51 -0
- package/dist/ui-engine/rich-layout.js +220 -0
- package/dist/ui-engine/run-types.d.ts +13 -0
- package/dist/ui-engine/run-types.js +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
- package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
- package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
- package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
- package/dist/ui-engine/runtime-header/display-shim.js +104 -0
- package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
- package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
- package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
- package/dist/ui-engine/runtime-header/guard-close.js +8 -0
- package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
- package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
- package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
- package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
- package/dist/ui-engine/runtime-header/keyboard.js +342 -0
- package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
- package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
- package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
- package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
- package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
- package/dist/ui-engine/runtime-header/structs.js +220 -0
- package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
- package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
- package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick.js +17 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
- package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
- package/dist/ui-engine/runtime-header/types-defines.js +79 -0
- package/dist/ui-engine/runtime-header.d.ts +1 -0
- package/dist/ui-engine/runtime-header.js +69 -0
- package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
- package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
- package/dist/ui-engine/select-engine.d.ts +4 -0
- package/dist/ui-engine/select-engine.js +19 -0
- package/dist/ui-engine/style-resolver.d.ts +77 -0
- package/dist/ui-engine/style-resolver.js +341 -0
- package/dist/ui-engine/text-layout.d.ts +19 -0
- package/dist/ui-engine/text-layout.js +92 -0
- package/dist/ui-engine/transpile-ui.d.ts +11 -0
- package/dist/ui-engine/transpile-ui.js +33 -0
- package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
- package/dist/ui-engine/ua-stylesheet.js +56 -0
- package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
- package/dist/ui-engine/ui-file-splitter.js +47 -0
- package/dist/ui-engine/ui-lowering.d.ts +33 -0
- package/dist/ui-engine/ui-lowering.js +390 -0
- package/dist/ui-engine/ui-registry.d.ts +81 -0
- package/dist/ui-engine/ui-registry.js +429 -0
- package/dist/ui-engine/yoga-layout.d.ts +12 -0
- package/dist/ui-engine/yoga-layout.js +314 -0
- package/package.json +86 -0
- package/src/engine-index.ts +51 -0
- package/src/index.ts +232 -0
- package/src/preview/build-program.ts +734 -0
- package/src/preview/host-gfx.ts +580 -0
- package/src/preview/host-ui-runtime.ts +3245 -0
- package/src/preview/types.ts +96 -0
- package/src/types.ts +127 -0
- package/src/ui-engine/block-layout.ts +104 -0
- package/src/ui-engine/color.ts +263 -0
- package/src/ui-engine/css-parser.ts +1003 -0
- package/src/ui-engine/default-keyboards.ts +58 -0
- package/src/ui-engine/easing.ts +53 -0
- package/src/ui-engine/font-assets.ts +512 -0
- package/src/ui-engine/html-parser.ts +397 -0
- package/src/ui-engine/image-assets.ts +170 -0
- package/src/ui-engine/inline-parser.ts +78 -0
- package/src/ui-engine/keyframes.ts +47 -0
- package/src/ui-engine/layout-engine.ts +228 -0
- package/src/ui-engine/model.ts +1225 -0
- package/src/ui-engine/rich-layout.ts +257 -0
- package/src/ui-engine/run-types.ts +14 -0
- package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
- package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
- package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
- package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
- package/src/ui-engine/runtime-header/display-shim.ts +104 -0
- package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
- package/src/ui-engine/runtime-header/guard-close.ts +8 -0
- package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
- package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
- package/src/ui-engine/runtime-header/keyboard.ts +342 -0
- package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
- package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
- package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
- package/src/ui-engine/runtime-header/structs.ts +220 -0
- package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
- package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
- package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
- package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
- package/src/ui-engine/runtime-header/tick.ts +18 -0
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
- package/src/ui-engine/runtime-header/types-defines.ts +79 -0
- package/src/ui-engine/runtime-header.ts +71 -0
- package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
- package/src/ui-engine/select-engine.ts +21 -0
- package/src/ui-engine/style-resolver.ts +416 -0
- package/src/ui-engine/text-layout.ts +114 -0
- package/src/ui-engine/transpile-ui.ts +45 -0
- package/src/ui-engine/ua-stylesheet.ts +60 -0
- package/src/ui-engine/ui-file-splitter.ts +59 -0
- package/src/ui-engine/ui-lowering.ts +459 -0
- package/src/ui-engine/ui-registry.ts +530 -0
- package/src/ui-engine/yoga-layout.ts +309 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// .ui single-file component splitter.
|
|
3
|
+
//
|
|
4
|
+
// A .ui file combines <script>, <style>, and an HTML template in one file:
|
|
5
|
+
//
|
|
6
|
+
// <script>
|
|
7
|
+
// export const count = ui.signal(0);
|
|
8
|
+
// </script>
|
|
9
|
+
// <style>
|
|
10
|
+
// #btn { background: #3399ff; }
|
|
11
|
+
// </style>
|
|
12
|
+
// <screen id="home">
|
|
13
|
+
// <button id="btn" on:click="increment">Tap</button>
|
|
14
|
+
// </screen>
|
|
15
|
+
//
|
|
16
|
+
// splitUiFile extracts the three streams so the transpiler can feed them to the
|
|
17
|
+
// existing pipelines: script → TS, style → CSS, template → HTML parser. The
|
|
18
|
+
// blocks may appear in any order; multiple <style> blocks are concatenated.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/**
|
|
21
|
+
* Split a .ui single-file component into its script, style, and template parts.
|
|
22
|
+
* Extracts <script> and <style> blocks (case-insensitive tags) and returns the
|
|
23
|
+
* remainder as the HTML template. Multiple <style> blocks are concatenated.
|
|
24
|
+
*/
|
|
25
|
+
export function splitUiFile(src) {
|
|
26
|
+
let script = "";
|
|
27
|
+
const styles = [];
|
|
28
|
+
// Remove script and style blocks from the source, collecting their contents.
|
|
29
|
+
// The remainder is the HTML template.
|
|
30
|
+
let html = src;
|
|
31
|
+
// Extract <script>...</script> (first one wins; v1 supports a single block).
|
|
32
|
+
html = html.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, (_m, content) => {
|
|
33
|
+
if (!script)
|
|
34
|
+
script = content.trim();
|
|
35
|
+
return "";
|
|
36
|
+
});
|
|
37
|
+
// Extract all <style>...</style> blocks.
|
|
38
|
+
html = html.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gi, (_m, content) => {
|
|
39
|
+
styles.push(content.trim());
|
|
40
|
+
return "";
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
script,
|
|
44
|
+
style: styles.join("\n"),
|
|
45
|
+
html: html.trim(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { StyledNode } from "./style-resolver.js";
|
|
2
|
+
import { Box } from "./layout-engine.js";
|
|
3
|
+
import { type KeyframeSetModel } from "./model.js";
|
|
4
|
+
import type { KeyboardTemplate } from "./html-parser.js";
|
|
5
|
+
import type { CSSRule } from "./css-parser.js";
|
|
6
|
+
import type { DisplayProfile } from "@typecad/cuttlefish/api/shared";
|
|
7
|
+
import type { UIFontAssetModel } from "./font-assets.js";
|
|
8
|
+
import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
|
|
9
|
+
export interface LoweredUI {
|
|
10
|
+
fontTables: string;
|
|
11
|
+
nodeTable: string;
|
|
12
|
+
transitionTable: string;
|
|
13
|
+
typeDecl: string;
|
|
14
|
+
/** C++ keyboard loader function bodies (one per keyboard in use). */
|
|
15
|
+
keyboardLoaders: string;
|
|
16
|
+
/** C++ dispatch table mapping input node index → loader function. */
|
|
17
|
+
keyboardDispatch: string;
|
|
18
|
+
/** Number of distinct screens (for multi-screen navigation). */
|
|
19
|
+
screenCount: number;
|
|
20
|
+
/** C++ image data arrays + index table (for <img> support). */
|
|
21
|
+
imageTables: string;
|
|
22
|
+
/** C++ keyframe data arrays + animation table. */
|
|
23
|
+
keyframeTables: string;
|
|
24
|
+
/** Compile-time warnings for scroll viewports that exceed the canvas budget. */
|
|
25
|
+
scrollMemoryDiagnostics: Diagnostic[];
|
|
26
|
+
/** Layout-time diagnostics (viewport overflow, text overflow). Populated by
|
|
27
|
+
* lowerOnMount after the boxes are computed; empty until then. */
|
|
28
|
+
diagnostics: Diagnostic[];
|
|
29
|
+
}
|
|
30
|
+
type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
|
|
31
|
+
type Storage = "progmem" | "flash";
|
|
32
|
+
export declare function lowerUIToCpp(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, storage: Storage, keyboards?: KeyboardTemplate[], rules?: CSSRule[], display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[], scrollCanvasBudgetBytes?: number): LoweredUI;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,390 @@
|
|
|
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
|
+
import { lowerUIToModel } from "./model.js";
|
|
15
|
+
import { resolveColor } from "./color.js";
|
|
16
|
+
import { DEFAULT_ALPHA_KEYBOARD, DEFAULT_NUMBER_KEYBOARD } from "./default-keyboards.js";
|
|
17
|
+
import { analyzeScrollMemory } from "./scroll-memory-diagnostics.js";
|
|
18
|
+
import { getListBindings } from "@typecad/cuttlefish/ir/transformers/ui-reactive";
|
|
19
|
+
export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = [], scrollCanvasBudgetBytes) {
|
|
20
|
+
void storage;
|
|
21
|
+
const model = lowerUIToModel(root, boxes, colorFormat, display, fontAssets, allScreens, imageAssetIds, keyframeSets);
|
|
22
|
+
const scrollMemoryDiagnostics = analyzeScrollMemory(model.nodes, scrollCanvasBudgetBytes);
|
|
23
|
+
// Tables are mutable RAM (ui_tick updates bg/dirty/elapsed/active each
|
|
24
|
+
// frame), so no PROGMEM/flash storage keyword — those imply read-only.
|
|
25
|
+
const fontTables = emitFontTables(model);
|
|
26
|
+
const nodeTable = emitNodeTable(model) + "\n" + emitScrollNodeIdLookup(model);
|
|
27
|
+
const transitionTable = emitTransitionTable(model);
|
|
28
|
+
const typeDecl = emitTypeDecl(root);
|
|
29
|
+
// Keyboard loaders + dispatch: collect input nodes in tree order, resolve
|
|
30
|
+
// each to its loader (default by type, or a referenced <keyboard>).
|
|
31
|
+
// Walk ALL screens (inputs may live on any screen, not just the root).
|
|
32
|
+
const inputSpecs = [];
|
|
33
|
+
const collectInputs = (n) => {
|
|
34
|
+
if (n.tag === "input")
|
|
35
|
+
inputSpecs.push({ type: n.type, keyboard: n.keyboard });
|
|
36
|
+
n.children.forEach(collectInputs);
|
|
37
|
+
};
|
|
38
|
+
const inputRoots = allScreens.length > 0 ? allScreens : [root];
|
|
39
|
+
for (const sr of inputRoots)
|
|
40
|
+
collectInputs(sr);
|
|
41
|
+
const neededKeyboards = [];
|
|
42
|
+
const addIfNeeded = (kb) => {
|
|
43
|
+
if (!neededKeyboards.some(k => k.id === kb.id))
|
|
44
|
+
neededKeyboards.push(kb);
|
|
45
|
+
};
|
|
46
|
+
for (const spec of inputSpecs) {
|
|
47
|
+
if (spec.keyboard) {
|
|
48
|
+
const match = keyboards.find(k => k.id === spec.keyboard);
|
|
49
|
+
if (match)
|
|
50
|
+
addIfNeeded(match);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
addIfNeeded(spec.type === "number" ? DEFAULT_NUMBER_KEYBOARD : DEFAULT_ALPHA_KEYBOARD);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const keyboardLoaders = neededKeyboards
|
|
57
|
+
.map(kb => emitKeyboardLoader(loaderNameForId(kb.id), kb, rules, colorFormat))
|
|
58
|
+
.join("\n\n");
|
|
59
|
+
const dispatchEntries = inputSpecs.map(spec => loaderNameForInput(spec, keyboards));
|
|
60
|
+
const keyboardDispatch = dispatchEntries.length > 0
|
|
61
|
+
? `void (*__ui_kb_loaders[])() = { ${dispatchEntries.join(", ")} };\nconst uint16_t __ui_kb_loader_count = ${dispatchEntries.length};`
|
|
62
|
+
: `void (*__ui_kb_loaders[])() = {};\nconst uint16_t __ui_kb_loader_count = 0;`;
|
|
63
|
+
const screenCount = model.nodes.length > 0 ? Math.max(...model.nodes.map(n => n.screenId)) + 1 : 1;
|
|
64
|
+
const imageTables = "const UIImage __ui_images[] = {};\nconst uint16_t __ui_image_count = 0;";
|
|
65
|
+
const keyframeTables = emitKeyframeTables(model);
|
|
66
|
+
return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, scrollMemoryDiagnostics, diagnostics: [] };
|
|
67
|
+
}
|
|
68
|
+
function sanitizedId(id) {
|
|
69
|
+
return id.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
70
|
+
}
|
|
71
|
+
function loaderNameForId(id) {
|
|
72
|
+
return `__ui_kb_load_${sanitizedId(id)}`;
|
|
73
|
+
}
|
|
74
|
+
function loaderNameForInput(input, keyboards) {
|
|
75
|
+
if (input.keyboard) {
|
|
76
|
+
const match = keyboards.find(k => k.id === input.keyboard);
|
|
77
|
+
if (match)
|
|
78
|
+
return loaderNameForId(match.id);
|
|
79
|
+
}
|
|
80
|
+
return input.type === "number" ? "__ui_kb_load_default_number" : "__ui_kb_load_default_alpha";
|
|
81
|
+
}
|
|
82
|
+
// Default key colors (fallback when no CSS matches). Used for all keys.
|
|
83
|
+
const DEFAULT_KEY_BG = 0x4208; // dark gray
|
|
84
|
+
const DEFAULT_KEY_FG = 0xFFFF; // white
|
|
85
|
+
const DEFAULT_KEY_BORDER = 0xFFFF; // white
|
|
86
|
+
const DEFAULT_KB_BG = 0x0000; // black
|
|
87
|
+
/** Resolve a key's CSS classes into a merged CSSProperty (cascade: last wins). */
|
|
88
|
+
/** Check if a CSS rule's selector matches any of the given class names.
|
|
89
|
+
* Only matches single-compound class selectors (no descendant for keys). */
|
|
90
|
+
function ruleMatchesClass(rule, classes) {
|
|
91
|
+
// Must be a single compound (no descendant combinator).
|
|
92
|
+
if (rule.selector.compounds.length !== 1)
|
|
93
|
+
return false;
|
|
94
|
+
const compound = rule.selector.compounds[0];
|
|
95
|
+
// Every simple in the compound must be a class that's in the list.
|
|
96
|
+
for (const s of compound) {
|
|
97
|
+
if (s.kind !== "class" || !classes.includes(s.name))
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function resolveKeyStyle(keyClasses, kbClasses, rules) {
|
|
103
|
+
const merged = {};
|
|
104
|
+
const allClasses = [...(kbClasses ?? []), ...(keyClasses ?? [])];
|
|
105
|
+
for (const rule of rules) {
|
|
106
|
+
if (ruleMatchesClass(rule, allClasses)) {
|
|
107
|
+
Object.assign(merged, rule.properties);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return merged;
|
|
111
|
+
}
|
|
112
|
+
/** Resolve the keyboard-level background from CSS (keyboard classes). */
|
|
113
|
+
function resolveKbBg(kbClasses, rules, colorFormat) {
|
|
114
|
+
const merged = {};
|
|
115
|
+
const classes = kbClasses ?? [];
|
|
116
|
+
for (const rule of rules) {
|
|
117
|
+
if (ruleMatchesClass(rule, classes)) {
|
|
118
|
+
Object.assign(merged, rule.properties);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return merged.background ? resolveColor(merged.background, colorFormat) : DEFAULT_KB_BG;
|
|
122
|
+
}
|
|
123
|
+
function emitKeyboardLoader(name, kb, rules, colorFormat) {
|
|
124
|
+
const rows = kb.rows;
|
|
125
|
+
const cols = rows.length > 0 ? Math.max(...rows.map(r => r.length)) : 0;
|
|
126
|
+
const kbBg = resolveKbBg(kb.classes, rules, colorFormat);
|
|
127
|
+
const lines = [];
|
|
128
|
+
lines.push(`void ${name}() {`);
|
|
129
|
+
lines.push(` __ui_kb_rows = ${rows.length};`);
|
|
130
|
+
lines.push(` __ui_kb_cols = ${cols};`);
|
|
131
|
+
lines.push(` __ui_kb_keyCount = 0;`);
|
|
132
|
+
lines.push(` __ui_kb_bg = ${hex(kbBg)};`);
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
for (const key of row) {
|
|
135
|
+
lines.push(` ${emitKeyLine(key, kb.classes, rules, colorFormat)}`);
|
|
136
|
+
}
|
|
137
|
+
// Pad short rows so ui_kb_key_rect's idx/cols math stays aligned. Padded
|
|
138
|
+
// cells use special=255 (skipped in draw + hit-test) with a space char.
|
|
139
|
+
for (let p = row.length; p < cols; p++) {
|
|
140
|
+
lines.push(` ui_kb_add_key(' ', 255, ${hex(DEFAULT_KEY_BG)}, ${hex(DEFAULT_KEY_FG)}, ${hex(DEFAULT_KEY_BORDER)});`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push(`}`);
|
|
144
|
+
return lines.join("\n");
|
|
145
|
+
}
|
|
146
|
+
/** Emit one key's keys[] + styles[] lines, resolving CSS classes to colors. */
|
|
147
|
+
function emitKeyLine(key, kbClasses, rules, colorFormat) {
|
|
148
|
+
const chEsc = key.ch === "\\" ? "\\\\" : key.ch === "'" ? "\\'" : key.ch;
|
|
149
|
+
const style = resolveKeyStyle(key.classes, kbClasses, rules);
|
|
150
|
+
const bg = style.background ? resolveColor(style.background, colorFormat) : DEFAULT_KEY_BG;
|
|
151
|
+
const fg = style.color ? resolveColor(style.color, colorFormat) : DEFAULT_KEY_FG;
|
|
152
|
+
const border = style.borderColor ? resolveColor(style.borderColor, colorFormat) : DEFAULT_KEY_BORDER;
|
|
153
|
+
return `ui_kb_add_key('${chEsc}', ${key.special}, ${hex(bg)}, ${hex(fg)}, ${hex(border)});`;
|
|
154
|
+
}
|
|
155
|
+
function cppKind(kind) {
|
|
156
|
+
switch (kind) {
|
|
157
|
+
case "fill": return "NODE_FILL";
|
|
158
|
+
case "button": return "NODE_BUTTON";
|
|
159
|
+
case "check": return "NODE_CHECK";
|
|
160
|
+
case "radio": return "NODE_RADIO";
|
|
161
|
+
case "progress": return "NODE_PROGRESS";
|
|
162
|
+
case "range": return "NODE_RANGE";
|
|
163
|
+
case "input": return "NODE_INPUT";
|
|
164
|
+
case "img": return "NODE_IMG";
|
|
165
|
+
case "list": return "NODE_LIST";
|
|
166
|
+
case "canvas": return "NODE_CANVAS";
|
|
167
|
+
case "text": return "NODE_TEXT";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function hex(c) {
|
|
171
|
+
return `0x${c.toString(16).padStart(4, "0")}`;
|
|
172
|
+
}
|
|
173
|
+
function cppString(value) {
|
|
174
|
+
return value ? JSON.stringify(value) : "nullptr";
|
|
175
|
+
}
|
|
176
|
+
function byteArray(values) {
|
|
177
|
+
if (values.length === 0)
|
|
178
|
+
return "";
|
|
179
|
+
const chunks = [];
|
|
180
|
+
for (let i = 0; i < values.length; i += 16) {
|
|
181
|
+
chunks.push(" " + values.slice(i, i + 16).map((v) => `0x${(v & 0xff).toString(16).padStart(2, "0")}`).join(", "));
|
|
182
|
+
}
|
|
183
|
+
return chunks.join(",\n");
|
|
184
|
+
}
|
|
185
|
+
function emitFontTables(model) {
|
|
186
|
+
const assets = model.fontAssets ?? [];
|
|
187
|
+
if (assets.length === 0) {
|
|
188
|
+
return [
|
|
189
|
+
`const UIFontFace __ui_font_faces[] = {};`,
|
|
190
|
+
`const uint16_t __ui_font_face_count = 0;`,
|
|
191
|
+
].join("\n");
|
|
192
|
+
}
|
|
193
|
+
const lines = [];
|
|
194
|
+
for (const asset of assets) {
|
|
195
|
+
lines.push(`// Font ${asset.id}: ${asset.family} ${asset.px}px ${asset.fontWeight} ${asset.fontStyle} ${asset.subset}`);
|
|
196
|
+
// Alpha data goes in PROGMEM (constants in flash, not RAM) - accessed via pgm_read_byte on AVR
|
|
197
|
+
lines.push(`static const uint8_t __ui_font_${asset.id}_alpha[] PROGMEM = {`);
|
|
198
|
+
lines.push(byteArray(asset.alpha));
|
|
199
|
+
lines.push(`};`);
|
|
200
|
+
lines.push(`static const UIFontGlyph __ui_font_${asset.id}_glyphs[] = {`);
|
|
201
|
+
for (const glyph of asset.glyphs) {
|
|
202
|
+
lines.push(` { ${glyph.codepoint}, ${glyph.xOffset}, ${glyph.yOffset}, ${glyph.width}, ${glyph.height}, ${glyph.advance}, ${glyph.dataOffset} },`);
|
|
203
|
+
}
|
|
204
|
+
lines.push(`};`);
|
|
205
|
+
}
|
|
206
|
+
// Font faces and glyphs stay in regular memory for direct struct access
|
|
207
|
+
// (AVR optimized builds can move the whole table to PROGMEM + accessor functions)
|
|
208
|
+
lines.push(`const UIFontFace __ui_font_faces[] = {`);
|
|
209
|
+
for (const asset of assets) {
|
|
210
|
+
lines.push(` { ${asset.id}, ${asset.glyphs.length}, ${asset.lineHeight}, ${asset.baseline}, __ui_font_${asset.id}_glyphs, __ui_font_${asset.id}_alpha },`);
|
|
211
|
+
}
|
|
212
|
+
lines.push(`};`);
|
|
213
|
+
lines.push(`const uint16_t __ui_font_face_count = ${assets.length};`);
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
function emitScrollNodeIdLookup(model) {
|
|
217
|
+
const cases = [];
|
|
218
|
+
for (const n of model.nodes) {
|
|
219
|
+
if (!n.scrollable || n.contentHeight <= n.box.h || !n.id)
|
|
220
|
+
continue;
|
|
221
|
+
const id = n.id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
222
|
+
cases.push(` if (idx == ${n.index}) return "${id}";`);
|
|
223
|
+
}
|
|
224
|
+
if (cases.length === 0) {
|
|
225
|
+
return "static inline const char* __ui_scroll_node_id(uint16_t idx) { (void)idx; return nullptr; }";
|
|
226
|
+
}
|
|
227
|
+
return [
|
|
228
|
+
"static inline const char* __ui_scroll_node_id(uint16_t idx) {",
|
|
229
|
+
...cases,
|
|
230
|
+
" return nullptr;",
|
|
231
|
+
"}",
|
|
232
|
+
].join("\n");
|
|
233
|
+
}
|
|
234
|
+
function emitNodeTable(model) {
|
|
235
|
+
// Map node index → list binding so virtualized nodes carry their count/item/
|
|
236
|
+
// tap function pointers on-node (no UIListBinding side table at runtime).
|
|
237
|
+
const listBindingByNode = new Map();
|
|
238
|
+
for (const lb of getListBindings())
|
|
239
|
+
listBindingByNode.set(lb.nodeIndex, lb);
|
|
240
|
+
// Accumulate rich-text run / segment / line parallel arrays across all
|
|
241
|
+
// run-bearing nodes, assigning each a contiguous slice (runStart/richSegStart/
|
|
242
|
+
// richLineStart + counts). Emitted as global tables after the node table.
|
|
243
|
+
const runRows = [];
|
|
244
|
+
const segRows = [];
|
|
245
|
+
const lineRows = [];
|
|
246
|
+
const richRanges = new Map();
|
|
247
|
+
for (const n of model.nodes) {
|
|
248
|
+
if (!n.runs || !n.runLines || n.runs.length === 0)
|
|
249
|
+
continue;
|
|
250
|
+
const runStart = runRows.length;
|
|
251
|
+
for (const r of n.runs) {
|
|
252
|
+
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} },`);
|
|
253
|
+
}
|
|
254
|
+
const richSegStart = segRows.length;
|
|
255
|
+
for (let s = 0; s < n.runLines.segRun.length; s++) {
|
|
256
|
+
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]} },`);
|
|
257
|
+
}
|
|
258
|
+
const richLineStart = lineRows.length;
|
|
259
|
+
for (let l = 0; l < n.runLines.lineY.length; l++) {
|
|
260
|
+
lineRows.push(` { .y=${n.runLines.lineY[l]}, .h=${n.runLines.lineH[l]}, .baseline=${n.runLines.lineBaseline[l]}, .w=${n.runLines.lineW[l]} },`);
|
|
261
|
+
}
|
|
262
|
+
richRanges.set(n.index, {
|
|
263
|
+
runCount: n.runs.length, runStart,
|
|
264
|
+
richLineCount: n.runLines.lineY.length, richSegStart,
|
|
265
|
+
richSegCount: n.runLines.segRun.length, richLineStart,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
const lines = model.nodes.map((n) => {
|
|
269
|
+
const text = cppString(n.text);
|
|
270
|
+
const font = "nullptr";
|
|
271
|
+
// Input nodes store the placeholder in .text (static literal) so the draw
|
|
272
|
+
// can show it grayed when textBuffer is empty. textBuffer stays {0} so
|
|
273
|
+
// ui_kb_open starts with a clean edit buffer (no placeholder to delete).
|
|
274
|
+
const inputText = n.kind === "input" && n.textBuffer ? cppString(n.textBuffer) : text;
|
|
275
|
+
const box = `{${n.box.x},${n.box.y},${n.box.w},${n.box.h}}`;
|
|
276
|
+
const parent = n.parentIndex >= 0 ? n.parentIndex : 0xFFFF; // UI_NO_PARENT
|
|
277
|
+
// Progress/range use lastTextWidth as a "previous fill width" for
|
|
278
|
+
// incremental redraw. -1 = never drawn, because fill width 0 is valid.
|
|
279
|
+
const lastTextWidth = n.kind === "progress" || n.kind === "range" ? -1 : 0;
|
|
280
|
+
const shArr = (vals, n = 4) => `{${[...vals.slice(0, n), ...Array(n - Math.min(vals.length, n)).fill(0)].join(",")}}`;
|
|
281
|
+
// Virtualized list: resolve on-node function pointers from the binding.
|
|
282
|
+
const lb = listBindingByNode.get(n.index);
|
|
283
|
+
const virtualized = n.virtualized ? 1 : 0;
|
|
284
|
+
const listCountFn = lb ? lb.countFnName : "nullptr";
|
|
285
|
+
const listItemFn = lb ? lb.itemFnName : "nullptr";
|
|
286
|
+
const listTapFn = lb && lb.tapFnName ? lb.tapFnName : "nullptr";
|
|
287
|
+
const rr = richRanges.get(n.index);
|
|
288
|
+
const runCount = rr ? rr.runCount : 0;
|
|
289
|
+
const runStart = rr ? rr.runStart : 0;
|
|
290
|
+
const richLineCount = rr ? rr.richLineCount : 0;
|
|
291
|
+
const richSegStart = rr ? rr.richSegStart : 0;
|
|
292
|
+
const richSegCount = rr ? rr.richSegCount : 0;
|
|
293
|
+
const richLineStart = rr ? rr.richLineStart : 0;
|
|
294
|
+
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.borderTopWidth ?? n.borderWidth}, .borderRightWidth=${n.borderRightWidth ?? n.borderWidth}, .borderBottomWidth=${n.borderBottomWidth ?? n.borderWidth}, .borderLeftWidth=${n.borderLeftWidth ?? n.borderWidth}, .hasPerSideBorder=${((n.borderTopWidth ?? n.borderWidth) !== n.borderWidth || (n.borderRightWidth ?? n.borderWidth) !== n.borderWidth || (n.borderBottomWidth ?? n.borderWidth) !== n.borderWidth || (n.borderLeftWidth ?? n.borderWidth) !== n.borderWidth) ? 1 : 0}, .borderRadius=${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.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} },`;
|
|
295
|
+
});
|
|
296
|
+
return [
|
|
297
|
+
// Mutable (not const) so ui_tick can update bg/dirty during transitions.
|
|
298
|
+
// AVR would want PROGMEM + a shadow copy; ESP32-class has RAM to spare.
|
|
299
|
+
`UINode __ui_nodes[] = {`,
|
|
300
|
+
...lines,
|
|
301
|
+
`};`,
|
|
302
|
+
// Rich-text parallel arrays. Mutable is unnecessary (static content) but
|
|
303
|
+
// matches __ui_nodes[] linkage; nodes reference slices via runStart etc.
|
|
304
|
+
runRows.length > 0
|
|
305
|
+
? [`UIRichRun __ui_runs[] = {`, ...runRows, `};`].join("\n")
|
|
306
|
+
: `UIRichRun __ui_runs[1];`,
|
|
307
|
+
segRows.length > 0
|
|
308
|
+
? [`UIRichSeg __ui_rich_segs[] = {`, ...segRows, `};`].join("\n")
|
|
309
|
+
: `UIRichSeg __ui_rich_segs[1];`,
|
|
310
|
+
lineRows.length > 0
|
|
311
|
+
? [`UIRichLine __ui_rich_lines[] = {`, ...lineRows, `};`].join("\n")
|
|
312
|
+
: `UIRichLine __ui_rich_lines[1];`,
|
|
313
|
+
`const uint16_t __ui_run_count = ${runRows.length};`,
|
|
314
|
+
`const uint16_t __ui_rich_seg_count = ${segRows.length};`,
|
|
315
|
+
`const uint16_t __ui_rich_line_count = ${lineRows.length};`,
|
|
316
|
+
].join("\n");
|
|
317
|
+
}
|
|
318
|
+
function emitTransitionTable(model) {
|
|
319
|
+
const entries = model.transitions.map((t) => {
|
|
320
|
+
const prop = t.prop === "background" ? "PROP_BG" : "PROP_FG";
|
|
321
|
+
return ` { .node=${t.node}, .prop=${prop}, .durationMs=${t.durationMs}, .pressedTarget=${hex(t.pressedTarget)}, .baseTarget=${hex(t.baseTarget)} },`;
|
|
322
|
+
});
|
|
323
|
+
if (entries.length === 0) {
|
|
324
|
+
return `UITransition __ui_trans[] = {};`;
|
|
325
|
+
}
|
|
326
|
+
return [
|
|
327
|
+
`UITransition __ui_trans[] = {`,
|
|
328
|
+
...entries,
|
|
329
|
+
`};`,
|
|
330
|
+
].join("\n");
|
|
331
|
+
}
|
|
332
|
+
/** Emit keyframe stop arrays + keyframe set index + animation table. */
|
|
333
|
+
function emitKeyframeTables(model) {
|
|
334
|
+
if (model.keyframeSets.length === 0 && model.animations.length === 0) {
|
|
335
|
+
return [
|
|
336
|
+
`const UIKeyframeSet __ui_keyframe_sets[] = {};`,
|
|
337
|
+
`const uint16_t __ui_keyframe_set_count = 0;`,
|
|
338
|
+
`UIAnimation __ui_anims[] = {};`,
|
|
339
|
+
`const uint16_t __ui_anim_count = 0;`,
|
|
340
|
+
].join("\n");
|
|
341
|
+
}
|
|
342
|
+
const lines = [];
|
|
343
|
+
// Emit one stop array per keyframe set.
|
|
344
|
+
for (const ks of model.keyframeSets) {
|
|
345
|
+
const safeName = ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
346
|
+
lines.push(`static const UIKeyframeStop __ui_kf_${safeName}_stops[] = {`);
|
|
347
|
+
for (const s of ks.stops) {
|
|
348
|
+
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} },`);
|
|
349
|
+
}
|
|
350
|
+
lines.push(`};`);
|
|
351
|
+
}
|
|
352
|
+
// Emit keyframe set index table.
|
|
353
|
+
lines.push(`const UIKeyframeSet __ui_keyframe_sets[] = {`);
|
|
354
|
+
model.keyframeSets.forEach((ks) => {
|
|
355
|
+
const safeName = ks.name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
356
|
+
lines.push(` { .stopCount=${ks.stops.length}, .stops=__ui_kf_${safeName}_stops },`);
|
|
357
|
+
});
|
|
358
|
+
lines.push(`};`);
|
|
359
|
+
lines.push(`const uint16_t __ui_keyframe_set_count = ${model.keyframeSets.length};`);
|
|
360
|
+
// Emit animation table (mutable — runtime advances elapsed/active).
|
|
361
|
+
lines.push(`UIAnimation __ui_anims[] = {`);
|
|
362
|
+
for (const a of model.animations) {
|
|
363
|
+
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 },`);
|
|
364
|
+
}
|
|
365
|
+
lines.push(`};`);
|
|
366
|
+
lines.push(`const uint16_t __ui_anim_count = ${model.animations.length};`);
|
|
367
|
+
return lines.join("\n");
|
|
368
|
+
}
|
|
369
|
+
function emitTypeDecl(root) {
|
|
370
|
+
// Collect id → tag pairs by walking the tree.
|
|
371
|
+
const ids = [];
|
|
372
|
+
const collect = (n) => {
|
|
373
|
+
if (n.id)
|
|
374
|
+
ids.push({ id: n.id, tag: n.tag });
|
|
375
|
+
n.children.forEach(collect);
|
|
376
|
+
};
|
|
377
|
+
collect(root);
|
|
378
|
+
const fields = ids.map(({ id, tag }) => {
|
|
379
|
+
const typeName = tag.charAt(0).toUpperCase() + tag.slice(1);
|
|
380
|
+
return ` ${id}: ${typeName}Element;`;
|
|
381
|
+
}).join("\n");
|
|
382
|
+
return [
|
|
383
|
+
`// Auto-generated by cuttlefish UI lowering. Do not edit.`,
|
|
384
|
+
`export interface ScreenTree {`,
|
|
385
|
+
fields,
|
|
386
|
+
`}`,
|
|
387
|
+
``,
|
|
388
|
+
`export const screen: ScreenTree;`,
|
|
389
|
+
].join("\n");
|
|
390
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { KeyboardTemplate } from "./html-parser.js";
|
|
2
|
+
import type { CSSFontFace, CSSRule, KeyframeSet } from "./css-parser.js";
|
|
3
|
+
import { StyledNode } from "./style-resolver.js";
|
|
4
|
+
import { LoweredUI } from "./ui-lowering.js";
|
|
5
|
+
import type { UIFontAssetModel } from "./font-assets.js";
|
|
6
|
+
import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
|
|
7
|
+
export interface UIModule {
|
|
8
|
+
/** Absolute path of the .ui.html source. */
|
|
9
|
+
htmlPath: string;
|
|
10
|
+
/** Absolute path of the generated type declaration for this UI module. */
|
|
11
|
+
typeDeclPath: string;
|
|
12
|
+
/** Source root overlaid with typeDeclRoot for .ui.html TypeScript imports. */
|
|
13
|
+
typeDeclSourceRoot: string;
|
|
14
|
+
/** Generated type declaration root used with TypeScript rootDirs. */
|
|
15
|
+
typeDeclRoot: string;
|
|
16
|
+
/** Resolved-style tree (HTML + CSS merged). Layout deferred to mount. */
|
|
17
|
+
styled: StyledNode;
|
|
18
|
+
/** All resolved <screen> trees (for multi-screen navigation). */
|
|
19
|
+
allStyledScreens: StyledNode[];
|
|
20
|
+
/** <keyboard> templates parsed from the same .ui.html (sibling declarations). */
|
|
21
|
+
keyboards: KeyboardTemplate[];
|
|
22
|
+
/** CSS rules from the sibling .ui.css (used for keyboard key styling). */
|
|
23
|
+
rules: CSSRule[];
|
|
24
|
+
/** @font-face rules from CSS, resolved into build-time font assets. */
|
|
25
|
+
fontFaces: CSSFontFace[];
|
|
26
|
+
fontAssets: UIFontAssetModel[];
|
|
27
|
+
/** Raw @keyframes blocks parsed from CSS. */
|
|
28
|
+
rawKeyframes: KeyframeSet[];
|
|
29
|
+
/** Parser-level warnings (unknown CSS properties / HTML tags). */
|
|
30
|
+
diagnostics: Diagnostic[];
|
|
31
|
+
/** Layout-time warnings (scroll memory budget, etc.) from ui.mount lowering. */
|
|
32
|
+
mountDiagnostics: Diagnostic[];
|
|
33
|
+
}
|
|
34
|
+
export interface LowerOptions {
|
|
35
|
+
colorFormat: "rgb565" | "rgb666" | "rgb888" | "mono";
|
|
36
|
+
storage: "progmem" | "flash";
|
|
37
|
+
viewport: {
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface UITypeDeclInfo {
|
|
43
|
+
dtsPath: string;
|
|
44
|
+
sourceRoot: string;
|
|
45
|
+
typesRoot: string;
|
|
46
|
+
}
|
|
47
|
+
export interface UITypeDeclarationResult {
|
|
48
|
+
written: string[];
|
|
49
|
+
errors: Array<{
|
|
50
|
+
filePath: string;
|
|
51
|
+
error: Error;
|
|
52
|
+
}>;
|
|
53
|
+
}
|
|
54
|
+
/** Reset the registry. Called at the start of each transpile run. */
|
|
55
|
+
export declare function resetUIRegistry(): void;
|
|
56
|
+
/** Load a `.ui.html` module: parse HTML + sibling `.ui.css`, resolve styles, cache. */
|
|
57
|
+
export declare function loadUIModule(htmlPath: string): UIModule;
|
|
58
|
+
/**
|
|
59
|
+
* Load a UI module from already-read HTML + CSS text (used by the .ui single-file
|
|
60
|
+
* component path, which splits the file and passes the parts directly). The
|
|
61
|
+
* disk-reading `loadUIModule` delegates here after reading the files.
|
|
62
|
+
*/
|
|
63
|
+
export declare function loadUIModuleFromText(htmlPath: string, htmlText: string, cssText: string, cssPathForFonts?: string): UIModule;
|
|
64
|
+
/** Produce (or return cached) the lowered C++ tables for a module, using the mount viewport. */
|
|
65
|
+
export declare function lowerOnMount(htmlPath: string, opts: LowerOptions): LoweredUI;
|
|
66
|
+
export declare function getUIModule(htmlPath: string): UIModule | undefined;
|
|
67
|
+
export declare function getLoweredUIModule(htmlPath: string): LoweredUI | undefined;
|
|
68
|
+
export declare function hasUIModule(htmlPath: string): boolean;
|
|
69
|
+
/** All loaded UI modules (used by the emitter to inject every mounted tree). */
|
|
70
|
+
export declare function allUIModules(): UIModule[];
|
|
71
|
+
/** All lowered UI modules (those whose ui.mount has been processed). */
|
|
72
|
+
export declare function allLoweredUIModules(): Array<{
|
|
73
|
+
htmlPath: string;
|
|
74
|
+
lowered: LoweredUI;
|
|
75
|
+
}>;
|
|
76
|
+
export declare function markEntryHasUI(): void;
|
|
77
|
+
export declare function entryHasUI(): boolean;
|
|
78
|
+
export declare function clearEntryHasUI(): void;
|
|
79
|
+
export declare function uiTypeDeclInfoForHtmlPath(htmlPath: string): UITypeDeclInfo;
|
|
80
|
+
export declare function writeUITypeDeclarationFromHtmlText(htmlPath: string, htmlText: string): string;
|
|
81
|
+
export declare function generateProjectUITypeDeclarations(projectRoot: string): UITypeDeclarationResult;
|