@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,39 @@
|
|
|
1
|
+
import { resolveColor } from "./color.js";
|
|
2
|
+
import { clampInt16, cssOpacityToPercent, cssPx, KEYFRAME_PROP_BG, KEYFRAME_PROP_FG, KEYFRAME_PROP_OPACITY, KEYFRAME_PROP_SIZE, KEYFRAME_PROP_TRANSFORM, parseTransform, } from "./model.js";
|
|
3
|
+
export function buildKeyframeSets(rawKeyframes, colorFormat) {
|
|
4
|
+
return rawKeyframes.map((ks) => ({
|
|
5
|
+
name: ks.name,
|
|
6
|
+
stops: ks.stops.map((stop) => {
|
|
7
|
+
let props = 0;
|
|
8
|
+
if (stop.background)
|
|
9
|
+
props |= KEYFRAME_PROP_BG;
|
|
10
|
+
if (stop.color)
|
|
11
|
+
props |= KEYFRAME_PROP_FG;
|
|
12
|
+
if (stop.opacity)
|
|
13
|
+
props |= KEYFRAME_PROP_OPACITY;
|
|
14
|
+
if (stop.transform || stop.left || stop.top)
|
|
15
|
+
props |= KEYFRAME_PROP_TRANSFORM;
|
|
16
|
+
if (stop.width || stop.height)
|
|
17
|
+
props |= KEYFRAME_PROP_SIZE;
|
|
18
|
+
const transform = parseTransform(stop.transform);
|
|
19
|
+
const x = transform.x + cssPx(stop.left);
|
|
20
|
+
const y = transform.y + cssPx(stop.top);
|
|
21
|
+
return {
|
|
22
|
+
percent: stop.percent,
|
|
23
|
+
props,
|
|
24
|
+
bg: stop.background ? resolveColor(stop.background, colorFormat) : 0,
|
|
25
|
+
fg: stop.color ? resolveColor(stop.color, colorFormat) : 0,
|
|
26
|
+
opacity: stop.opacity ? cssOpacityToPercent(stop.opacity) : 100,
|
|
27
|
+
transformOffsetX: clampInt16(x),
|
|
28
|
+
transformOffsetY: clampInt16(y),
|
|
29
|
+
translatePctX: clampInt16(transform.pctX),
|
|
30
|
+
translatePctY: clampInt16(transform.pctY),
|
|
31
|
+
scaleX: transform.scaleX,
|
|
32
|
+
scaleY: transform.scaleY,
|
|
33
|
+
rotateDeg: clampInt16(transform.rotateDeg),
|
|
34
|
+
width: stop.width ? Math.max(0, Math.min(32767, cssPx(stop.width))) : 0,
|
|
35
|
+
height: stop.height ? Math.max(0, Math.min(32767, cssPx(stop.height))) : 0,
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { StyledNode } from "./style-resolver.js";
|
|
2
|
+
import type { UIFontAssetModel } from "./font-assets.js";
|
|
3
|
+
export interface Box {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
w: number;
|
|
7
|
+
h: number;
|
|
8
|
+
}
|
|
9
|
+
export interface IntrinsicSize {
|
|
10
|
+
w: number;
|
|
11
|
+
h: number;
|
|
12
|
+
}
|
|
13
|
+
export interface LayoutEngine {
|
|
14
|
+
/** Engine id, used to select on the `display` style property. */
|
|
15
|
+
readonly id: "block" | "flex" | "grid";
|
|
16
|
+
/**
|
|
17
|
+
* Arrange a styled tree into a flat list of boxes (pre-order, root first).
|
|
18
|
+
* measureFn provides intrinsic sizes (e.g. text dimensions from the font).
|
|
19
|
+
*/
|
|
20
|
+
arrange(root: StyledNode, viewport: Box, measureFn: (node: StyledNode, availableWidth?: number) => IntrinsicSize): Box[];
|
|
21
|
+
}
|
|
22
|
+
/** Static display:none predicate shared by layout engines and lowering. */
|
|
23
|
+
export declare function isDisplayNone(node: StyledNode): boolean;
|
|
24
|
+
/** Parse CSS aspect-ratio values: "16 / 9", "1/1", or "1.777". */
|
|
25
|
+
export declare function parseAspectRatio(value: string | undefined): number | undefined;
|
|
26
|
+
/** Measure a node's intrinsic size. Accounts for the Adafruit GFX font metrics
|
|
27
|
+
* and the text size the draw dispatch will use. When `fontAssets` is provided,
|
|
28
|
+
* text widths use the real per-glyph advances of any matching custom
|
|
29
|
+
* @font-face (the runtime draws at those advances); otherwise they fall back
|
|
30
|
+
* to the 6*ts default-font advance. */
|
|
31
|
+
export declare function measure(node: StyledNode, availableWidth?: number, fontAssets?: UIFontAssetModel[]): IntrinsicSize;
|
|
32
|
+
/** Bind a set of font assets to measure(), returning a measureFn the layout
|
|
33
|
+
* engines accept. Text nodes using a custom @font-face then measure at the
|
|
34
|
+
* font's real glyph advances; nodes on the default font are unaffected. */
|
|
35
|
+
export declare function measureWithFonts(fontAssets: UIFontAssetModel[]): (node: StyledNode, availableWidth?: number) => IntrinsicSize;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Layout engine — pluggable, staged layout.
|
|
3
|
+
//
|
|
4
|
+
// style tree → measure pass → arrange pass (engine) → computed boxes
|
|
5
|
+
//
|
|
6
|
+
// v1 ships BlockLayoutEngine only. The LayoutEngine interface + measure()
|
|
7
|
+
// are built so that FlexLayoutEngine (v2) is a pure addition: no rewrite of
|
|
8
|
+
// measure, the style tree, the node representation, or the draw layer.
|
|
9
|
+
//
|
|
10
|
+
// measure() is general (returns intrinsic {w,h}) even though block layout
|
|
11
|
+
// only needs it for text — flex reuses it for flex-basis:content.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
import { layoutText } from "./text-layout.js";
|
|
14
|
+
import { layoutRuns } from "./rich-layout.js";
|
|
15
|
+
import { assetTextWidth } from "./font-assets.js";
|
|
16
|
+
/** Static display:none predicate shared by layout engines and lowering. */
|
|
17
|
+
export function isDisplayNone(node) {
|
|
18
|
+
return node.style.display?.trim().toLowerCase() === "none";
|
|
19
|
+
}
|
|
20
|
+
/** Parse CSS aspect-ratio values: "16 / 9", "1/1", or "1.777". */
|
|
21
|
+
export function parseAspectRatio(value) {
|
|
22
|
+
const raw = value?.trim().toLowerCase();
|
|
23
|
+
if (!raw || raw === "auto")
|
|
24
|
+
return undefined;
|
|
25
|
+
const parts = raw.split("/").map((part) => part.trim()).filter(Boolean);
|
|
26
|
+
if (parts.length === 1) {
|
|
27
|
+
const ratio = parseFloat(parts[0]);
|
|
28
|
+
return Number.isFinite(ratio) && ratio > 0 ? ratio : undefined;
|
|
29
|
+
}
|
|
30
|
+
if (parts.length === 2) {
|
|
31
|
+
const width = parseFloat(parts[0]);
|
|
32
|
+
const height = parseFloat(parts[1]);
|
|
33
|
+
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
|
34
|
+
return width / height;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
// The Adafruit GFX default font: 5×7 pixel glyphs, 6px advance width per char.
|
|
40
|
+
// At setTextSize(N), advance = 6×N, height = 8×N (7 glyph + 1 descender line).
|
|
41
|
+
// The draw dispatch in the runtime header uses setTextSize(2).
|
|
42
|
+
const GFX_TEXT_SIZE = 2;
|
|
43
|
+
const GFX_ADVANCE_PER_CHAR = 6 * GFX_TEXT_SIZE; // 12px at size 2
|
|
44
|
+
const GFX_CHAR_HEIGHT = 8 * GFX_TEXT_SIZE; // 16px at size 2
|
|
45
|
+
/** Compute the GFX text size from a node's CSS font-size.
|
|
46
|
+
* Mirrors the logic in model.ts textSizeOf(). font-weight does NOT inflate
|
|
47
|
+
* the bucket (matching web behavior); see model.ts:textSizeOf for rationale. */
|
|
48
|
+
function gfxTextSizeOf(node) {
|
|
49
|
+
let size = GFX_TEXT_SIZE;
|
|
50
|
+
if (node.style.fontSize) {
|
|
51
|
+
const px = parseInt(node.style.fontSize, 10);
|
|
52
|
+
if (!isNaN(px)) {
|
|
53
|
+
if (px <= 12)
|
|
54
|
+
size = 1;
|
|
55
|
+
else if (px <= 20)
|
|
56
|
+
size = 2;
|
|
57
|
+
else if (px <= 28)
|
|
58
|
+
size = 3;
|
|
59
|
+
else
|
|
60
|
+
size = 4;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return size;
|
|
64
|
+
}
|
|
65
|
+
function letterSpacingOf(node) {
|
|
66
|
+
if (!node.style.letterSpacing)
|
|
67
|
+
return 0;
|
|
68
|
+
const px = parseInt(node.style.letterSpacing, 10);
|
|
69
|
+
return Number.isFinite(px) ? px : 0;
|
|
70
|
+
}
|
|
71
|
+
function lineHeightOf(node, charH) {
|
|
72
|
+
const raw = node.style.lineHeight?.trim();
|
|
73
|
+
if (!raw || raw === "normal")
|
|
74
|
+
return charH;
|
|
75
|
+
if (raw.endsWith("%")) {
|
|
76
|
+
const pct = parseFloat(raw);
|
|
77
|
+
return Number.isFinite(pct) ? Math.max(1, Math.round(charH * pct / 100)) : charH;
|
|
78
|
+
}
|
|
79
|
+
if (/^-?\d*\.?\d+$/.test(raw)) {
|
|
80
|
+
const n = parseFloat(raw);
|
|
81
|
+
return Number.isFinite(n) ? Math.max(1, Math.round(charH * n)) : charH;
|
|
82
|
+
}
|
|
83
|
+
const px = parseInt(raw, 10);
|
|
84
|
+
return Number.isFinite(px) && px > 0 ? px : charH;
|
|
85
|
+
}
|
|
86
|
+
function applyTextTransform(text, node) {
|
|
87
|
+
const value = text ?? "";
|
|
88
|
+
switch (node.style.textTransform) {
|
|
89
|
+
case "uppercase": return value.toUpperCase();
|
|
90
|
+
case "lowercase": return value.toLowerCase();
|
|
91
|
+
case "capitalize": return value.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
92
|
+
default: return value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function textWidthOf(text, advance) {
|
|
96
|
+
let w = 0;
|
|
97
|
+
for (const _ch of text)
|
|
98
|
+
w += advance;
|
|
99
|
+
return w;
|
|
100
|
+
}
|
|
101
|
+
/** Measure a node's intrinsic size. Accounts for the Adafruit GFX font metrics
|
|
102
|
+
* and the text size the draw dispatch will use. When `fontAssets` is provided,
|
|
103
|
+
* text widths use the real per-glyph advances of any matching custom
|
|
104
|
+
* @font-face (the runtime draws at those advances); otherwise they fall back
|
|
105
|
+
* to the 6*ts default-font advance. */
|
|
106
|
+
export function measure(node, availableWidth, fontAssets = []) {
|
|
107
|
+
// Rich-text (inline runs): size the node from its multi-run layout. Each run
|
|
108
|
+
// is measured at its own font/size advance (per-run style), and the laid-out
|
|
109
|
+
// width/height drive the node's box. This keeps flex sizing in sync with the
|
|
110
|
+
// precomputed runLines the lowering bakes (and the runtime draws).
|
|
111
|
+
if (node.runs && node.runs.length > 0) {
|
|
112
|
+
const layoutInput = node.runs.map(r => {
|
|
113
|
+
const rs = { ...node.style, ...r.style };
|
|
114
|
+
const rTs = gfxTextSizeOf({ ...node, style: rs });
|
|
115
|
+
const rAdvance = 6 * rTs + letterSpacingOf({ ...node, style: rs });
|
|
116
|
+
return {
|
|
117
|
+
text: applyTextTransform(r.text, { ...node, style: rs }),
|
|
118
|
+
hardBreak: r.hardBreak,
|
|
119
|
+
measureText: (s) => assetTextWidth(s, rs, fontAssets) ?? textWidthOf(s, rAdvance),
|
|
120
|
+
height: 8 * rTs,
|
|
121
|
+
ascent: 7 * rTs,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
const layout = layoutRuns(layoutInput, { maxWidth: availableWidth, whiteSpace: node.style.whiteSpace });
|
|
125
|
+
return { w: layout.width, h: layout.height };
|
|
126
|
+
}
|
|
127
|
+
// Per-node text size (from font-size + font-weight CSS).
|
|
128
|
+
const ts = gfxTextSizeOf(node);
|
|
129
|
+
const advance = 6 * ts + letterSpacingOf(node);
|
|
130
|
+
const charH = 8 * ts;
|
|
131
|
+
// For interpolation text (e.g. "taps: {count}"), strip the braces so layout
|
|
132
|
+
// measures "taps: count" — closer to the resolved runtime width than the
|
|
133
|
+
// literal "{count}". The braces are an authoring delimiter, not rendered.
|
|
134
|
+
const stripInterp = (value) => node.hasInterpolation ? (value ?? "").replace(/[{}]/g, "") : (value ?? "");
|
|
135
|
+
// Width of a string using the asset font's real advances when the node has
|
|
136
|
+
// one, else the 6*ts default-font advance. This keeps the measured box width
|
|
137
|
+
// in sync with what the runtime actually draws (avoids overflow/clipping).
|
|
138
|
+
const widthOf = (value) => assetTextWidth(stripInterp(value), node.style, fontAssets) ?? textWidthOf(stripInterp(value), advance);
|
|
139
|
+
if (node.tag === "text" || node.tag === "button" || node.tag === "select") {
|
|
140
|
+
if (node.tag === "select") {
|
|
141
|
+
// Size to the longest option, not the full comma-separated text
|
|
142
|
+
const options = node.options && node.options.length > 0
|
|
143
|
+
? node.options.map((option) => option.text)
|
|
144
|
+
: (node.text ?? "").split(",").map(s => s.trim()).filter(Boolean);
|
|
145
|
+
const longest = options.length > 0 ? options.reduce((a, b) => a.length >= b.length ? a : b) : "";
|
|
146
|
+
return { w: widthOf(applyTextTransform(longest, node)), h: lineHeightOf(node, charH) };
|
|
147
|
+
}
|
|
148
|
+
const text = applyTextTransform(stripInterp(node.text), node);
|
|
149
|
+
const layout = layoutText(text, {
|
|
150
|
+
maxWidth: availableWidth,
|
|
151
|
+
whiteSpace: node.style.whiteSpace,
|
|
152
|
+
lineHeight: lineHeightOf(node, charH),
|
|
153
|
+
measureText: widthOf,
|
|
154
|
+
});
|
|
155
|
+
return { w: layout.width, h: layout.height };
|
|
156
|
+
}
|
|
157
|
+
if (node.tag === "check" || node.tag === "radio") {
|
|
158
|
+
// Checkbox/radio: 16px indicator + 6px gap + label text
|
|
159
|
+
const text = applyTextTransform(stripInterp(node.text), node);
|
|
160
|
+
const labelMaxWidth = availableWidth !== undefined ? Math.max(0, availableWidth - 22) : undefined;
|
|
161
|
+
const layout = layoutText(text, {
|
|
162
|
+
maxWidth: labelMaxWidth,
|
|
163
|
+
whiteSpace: node.style.whiteSpace,
|
|
164
|
+
lineHeight: lineHeightOf(node, charH),
|
|
165
|
+
measureText: widthOf,
|
|
166
|
+
});
|
|
167
|
+
return { w: 16 + 6 + layout.width, h: Math.max(layout.height, 16) };
|
|
168
|
+
}
|
|
169
|
+
if (node.tag === "progress") {
|
|
170
|
+
// Progress bar: default 200px wide, 12px tall
|
|
171
|
+
return { w: 200, h: 12 };
|
|
172
|
+
}
|
|
173
|
+
if (node.tag === "range") {
|
|
174
|
+
// Slider: default 200px wide, 20px tall (roomy track for touch)
|
|
175
|
+
return { w: 200, h: 20 };
|
|
176
|
+
}
|
|
177
|
+
if (node.tag === "input") {
|
|
178
|
+
// Input field: sized to the placeholder or a default width, 20px tall.
|
|
179
|
+
const text = node.placeholder ?? "";
|
|
180
|
+
const textW = text.length > 0 ? text.length * GFX_ADVANCE_PER_CHAR : 120;
|
|
181
|
+
return { w: Math.max(textW + 16, 120), h: 20 };
|
|
182
|
+
}
|
|
183
|
+
if (node.tag === "img") {
|
|
184
|
+
return { w: node.imgWidth ?? 32, h: node.imgHeight ?? 32 };
|
|
185
|
+
}
|
|
186
|
+
if (node.tag === "canvas") {
|
|
187
|
+
// Canvas intrinsic size = its width/height attributes. Without this the
|
|
188
|
+
// layout collapsed the node to 0x0 (no intrinsic size), so ui.drawCanvas
|
|
189
|
+
// content drew into a 2x2 sliver and was invisible.
|
|
190
|
+
return { w: node.canvasW ?? 0, h: node.canvasH ?? 0 };
|
|
191
|
+
}
|
|
192
|
+
if (node.tag === "list") {
|
|
193
|
+
return { w: 0, h: 100 }; // lists default to 100px tall, expand via flex
|
|
194
|
+
}
|
|
195
|
+
// Containers have no intrinsic size in block layout — they fill available.
|
|
196
|
+
return { w: 0, h: 0 };
|
|
197
|
+
}
|
|
198
|
+
/** Bind a set of font assets to measure(), returning a measureFn the layout
|
|
199
|
+
* engines accept. Text nodes using a custom @font-face then measure at the
|
|
200
|
+
* font's real glyph advances; nodes on the default font are unaffected. */
|
|
201
|
+
export function measureWithFonts(fontAssets) {
|
|
202
|
+
return (node, availableWidth) => measure(node, availableWidth, fontAssets);
|
|
203
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { DisplayProfile } from "@typecad/cuttlefish/api/shared";
|
|
2
|
+
import type { UIFontAssetModel } from "./font-assets.js";
|
|
3
|
+
import type { UIImageAsset } from "./image-assets.js";
|
|
4
|
+
import { type Box } from "./layout-engine.js";
|
|
5
|
+
import type { StyledNode } from "./style-resolver.js";
|
|
6
|
+
export { easeCurveLerpK, TIMING_EASE, TIMING_EASE_IN, TIMING_EASE_IN_OUT, TIMING_EASE_OUT, TIMING_LINEAR, timingFunctionCode, } from "./easing.js";
|
|
7
|
+
export type UINodeKindModel = "fill" | "text" | "button" | "check" | "radio" | "progress" | "range" | "input" | "img" | "list" | "canvas";
|
|
8
|
+
export type UIPropertyModel = "background" | "color" | "text" | "visible" | "borderColor";
|
|
9
|
+
export interface UINodeModel {
|
|
10
|
+
index: number;
|
|
11
|
+
tag: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
classes: string[];
|
|
14
|
+
box: Box;
|
|
15
|
+
bg: number;
|
|
16
|
+
fg: number;
|
|
17
|
+
kind: UINodeKindModel;
|
|
18
|
+
text?: string;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
valueAttr?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
checked?: boolean;
|
|
23
|
+
textBuffer: string;
|
|
24
|
+
hasTextBinding: boolean;
|
|
25
|
+
hasBg: boolean;
|
|
26
|
+
textAlign: 0 | 1 | 2;
|
|
27
|
+
textSize: number;
|
|
28
|
+
lineHeight: number;
|
|
29
|
+
letterSpacing: number;
|
|
30
|
+
fontAntialias: boolean;
|
|
31
|
+
fontFace: number;
|
|
32
|
+
borderColor: number;
|
|
33
|
+
borderStyle: 0 | 1 | 2;
|
|
34
|
+
borderWidth: number;
|
|
35
|
+
/** Per-side border widths. When all four equal borderWidth, the runtime draws
|
|
36
|
+
* the uniform rect outline; when any differs, it draws per-side lines. */
|
|
37
|
+
borderTopWidth: number;
|
|
38
|
+
borderRightWidth: number;
|
|
39
|
+
borderBottomWidth: number;
|
|
40
|
+
borderLeftWidth: number;
|
|
41
|
+
borderRadius: number;
|
|
42
|
+
paddingTop?: number;
|
|
43
|
+
paddingRight?: number;
|
|
44
|
+
paddingBottom?: number;
|
|
45
|
+
paddingLeft?: number;
|
|
46
|
+
gradientEnabled: number;
|
|
47
|
+
gradientColor1: number;
|
|
48
|
+
gradientColor2: number;
|
|
49
|
+
outlineColor: number;
|
|
50
|
+
outlineStyle: 0 | 1 | 2;
|
|
51
|
+
outlineWidth: number;
|
|
52
|
+
zIndex: number;
|
|
53
|
+
transformOffsetX: number;
|
|
54
|
+
transformOffsetY: number;
|
|
55
|
+
rotateDeg: number;
|
|
56
|
+
pressedOffsetX: number;
|
|
57
|
+
pressedOffsetY: number;
|
|
58
|
+
shadowCount: number;
|
|
59
|
+
shadowOffsetX: number[];
|
|
60
|
+
shadowOffsetY: number[];
|
|
61
|
+
shadowBlur: number[];
|
|
62
|
+
shadowColor: number[];
|
|
63
|
+
shadowAlpha: number[];
|
|
64
|
+
shadowInset: boolean[];
|
|
65
|
+
textShadowCount: number;
|
|
66
|
+
textShadowOffsetX: number;
|
|
67
|
+
textShadowOffsetY: number;
|
|
68
|
+
textShadowBlur: number;
|
|
69
|
+
textShadowColor: number;
|
|
70
|
+
textShadowAlpha: number;
|
|
71
|
+
underline: number;
|
|
72
|
+
textOverflow: boolean;
|
|
73
|
+
nowrap: boolean;
|
|
74
|
+
whiteSpaceMode: 0 | 1 | 2 | 3;
|
|
75
|
+
visible: boolean;
|
|
76
|
+
opacity: number;
|
|
77
|
+
clearColor: number;
|
|
78
|
+
lastTextWidth: number;
|
|
79
|
+
lastTextHeight: number;
|
|
80
|
+
dirty: boolean;
|
|
81
|
+
value: number;
|
|
82
|
+
options?: Array<{
|
|
83
|
+
value: string;
|
|
84
|
+
text: string;
|
|
85
|
+
}>;
|
|
86
|
+
scrollable: boolean;
|
|
87
|
+
scrollY: number;
|
|
88
|
+
contentHeight: number;
|
|
89
|
+
/** Elastic excursion past a scroll boundary (0 in-bounds; +top, -bottom). */
|
|
90
|
+
overscrollPx: number;
|
|
91
|
+
/** True while a bounce-back / edge-snap settle animation is running. */
|
|
92
|
+
settling: boolean;
|
|
93
|
+
/** scrollY captured at the last container repaint (Mode B shift delta). */
|
|
94
|
+
lastPaintedScrollY: number;
|
|
95
|
+
rangeMin: number;
|
|
96
|
+
rangeMax: number;
|
|
97
|
+
/** For <input>: max character length (0 = use UI_TEXT_BUF). */
|
|
98
|
+
maxlen: number;
|
|
99
|
+
/** For <input>: text or number keyboard. */
|
|
100
|
+
inputType?: "text" | "number";
|
|
101
|
+
/** For <input>: custom keyboard template id. */
|
|
102
|
+
keyboard?: string;
|
|
103
|
+
parentIndex: number;
|
|
104
|
+
subtreeEnd: number;
|
|
105
|
+
screenId: number;
|
|
106
|
+
imgDataId: number;
|
|
107
|
+
objectFit: 0 | 1 | 2 | 3 | 4;
|
|
108
|
+
listItemHeight: number;
|
|
109
|
+
/** Children are produced by callbacks (virtualized), not static nodes. Set for <list>. */
|
|
110
|
+
virtualized: boolean;
|
|
111
|
+
/** Canvas buffer width in pixels (for kind "canvas"). */
|
|
112
|
+
canvasW: number;
|
|
113
|
+
/** Canvas buffer height in pixels (for kind "canvas"). */
|
|
114
|
+
canvasH: number;
|
|
115
|
+
/** Rich-text runs. Present only for text nodes with mixed inline content;
|
|
116
|
+
* absent for plain single-string text nodes. When present, the runs are the
|
|
117
|
+
* node's content and `text` is empty. */
|
|
118
|
+
runs?: UITextRunModel[];
|
|
119
|
+
/** Precomputed wrapped-line geometry for rich-text runs (struct-of-arrays).
|
|
120
|
+
* The parallel arrays are indexed together; segment i is
|
|
121
|
+
* (segRun[i], segText[i], segX[i], segW[i], segLine[i]). Baked at lower time
|
|
122
|
+
* because runs are static-content only (no runtime re-flow). */
|
|
123
|
+
runLines?: RunLines;
|
|
124
|
+
}
|
|
125
|
+
/** One rich-text run in lowered form: a piece of styled inline text with all
|
|
126
|
+
* style fields pre-resolved to their runtime representations. */
|
|
127
|
+
export interface UITextRunModel {
|
|
128
|
+
text: string;
|
|
129
|
+
/** Resolved RGB565/mono foreground color. */
|
|
130
|
+
fg: number;
|
|
131
|
+
/** GFX text size 1-4 (from run font-size + font-weight). */
|
|
132
|
+
textSize: number;
|
|
133
|
+
/** Font asset id (0 = classic GFX bitmap font). */
|
|
134
|
+
fontFace: number;
|
|
135
|
+
/** text-decoration: 0=none, 1=underline, 2=line-through, 3=both. */
|
|
136
|
+
underline: number;
|
|
137
|
+
/** Letter spacing in px. */
|
|
138
|
+
letterSpacing: number;
|
|
139
|
+
/** Resolved screen index for an <a href> run, or -1 if not a link. */
|
|
140
|
+
linkTarget: number;
|
|
141
|
+
}
|
|
142
|
+
/** Precomputed wrapped-line geometry for a rich-text node, as parallel arrays
|
|
143
|
+
* (mirrors how the C++ runtime emits them). Segment arrays are indexed
|
|
144
|
+
* together; each segment belongs to the line given by its segLine entry. */
|
|
145
|
+
export interface RunLines {
|
|
146
|
+
segRun: number[];
|
|
147
|
+
segText: string[];
|
|
148
|
+
segX: number[];
|
|
149
|
+
segW: number[];
|
|
150
|
+
segLine: number[];
|
|
151
|
+
lineY: number[];
|
|
152
|
+
lineH: number[];
|
|
153
|
+
lineBaseline: number[];
|
|
154
|
+
lineW: number[];
|
|
155
|
+
}
|
|
156
|
+
export interface UITransitionModel {
|
|
157
|
+
node: number;
|
|
158
|
+
prop: "background" | "color";
|
|
159
|
+
durationMs: number;
|
|
160
|
+
pressedTarget: number;
|
|
161
|
+
baseTarget: number;
|
|
162
|
+
elapsed: number;
|
|
163
|
+
prevValue: number;
|
|
164
|
+
targetValue: number;
|
|
165
|
+
active: boolean;
|
|
166
|
+
}
|
|
167
|
+
export interface KeyframeStopModel {
|
|
168
|
+
percent: number;
|
|
169
|
+
props: number;
|
|
170
|
+
bg: number;
|
|
171
|
+
fg: number;
|
|
172
|
+
opacity: number;
|
|
173
|
+
transformOffsetX: number;
|
|
174
|
+
transformOffsetY: number;
|
|
175
|
+
translatePctX: number;
|
|
176
|
+
translatePctY: number;
|
|
177
|
+
scaleX: number;
|
|
178
|
+
scaleY: number;
|
|
179
|
+
rotateDeg: number;
|
|
180
|
+
width: number;
|
|
181
|
+
height: number;
|
|
182
|
+
}
|
|
183
|
+
export declare const KEYFRAME_PROP_BG = 1;
|
|
184
|
+
export declare const KEYFRAME_PROP_FG = 2;
|
|
185
|
+
export declare const KEYFRAME_PROP_OPACITY = 4;
|
|
186
|
+
export declare const KEYFRAME_PROP_TRANSFORM = 8;
|
|
187
|
+
export declare const KEYFRAME_PROP_SIZE = 16;
|
|
188
|
+
export interface KeyframeSetModel {
|
|
189
|
+
name: string;
|
|
190
|
+
stops: KeyframeStopModel[];
|
|
191
|
+
}
|
|
192
|
+
export interface AnimationModel {
|
|
193
|
+
node: number;
|
|
194
|
+
keyframeSet: number;
|
|
195
|
+
durationMs: number;
|
|
196
|
+
delayMs: number;
|
|
197
|
+
iterations: number;
|
|
198
|
+
baseWidth: number;
|
|
199
|
+
baseHeight: number;
|
|
200
|
+
originX: number;
|
|
201
|
+
originY: number;
|
|
202
|
+
timingFunction: number;
|
|
203
|
+
}
|
|
204
|
+
export interface UIProgram {
|
|
205
|
+
width: number;
|
|
206
|
+
height: number;
|
|
207
|
+
colorFormat: "rgb565" | "rgb666" | "rgb888" | "mono";
|
|
208
|
+
display?: DisplayProfile;
|
|
209
|
+
fontAssets: UIFontAssetModel[];
|
|
210
|
+
imageAssets: UIImageAsset[];
|
|
211
|
+
nodes: UINodeModel[];
|
|
212
|
+
transitions: UITransitionModel[];
|
|
213
|
+
keyframeSets: KeyframeSetModel[];
|
|
214
|
+
animations: AnimationModel[];
|
|
215
|
+
}
|
|
216
|
+
type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
|
|
217
|
+
export declare function cssPx(value: string | undefined): number;
|
|
218
|
+
export declare function clampInt8(n: number): number;
|
|
219
|
+
export declare function clampInt16(n: number): number;
|
|
220
|
+
export interface ParsedTransform {
|
|
221
|
+
x: number;
|
|
222
|
+
y: number;
|
|
223
|
+
pctX: number;
|
|
224
|
+
pctY: number;
|
|
225
|
+
scaleX: number;
|
|
226
|
+
scaleY: number;
|
|
227
|
+
rotateDeg: number;
|
|
228
|
+
}
|
|
229
|
+
export declare function parseTransform(transform: string | undefined): ParsedTransform;
|
|
230
|
+
export declare function transformOffset(transform: string | undefined): {
|
|
231
|
+
x: number;
|
|
232
|
+
y: number;
|
|
233
|
+
};
|
|
234
|
+
export declare function transformOriginPercent(value: string | undefined): {
|
|
235
|
+
x: number;
|
|
236
|
+
y: number;
|
|
237
|
+
};
|
|
238
|
+
export declare function cssOpacityToPercent(value: string | undefined): number;
|
|
239
|
+
export declare function lowerUIToModel(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[], imageAssets?: UIImageAsset[]): UIProgram;
|