@typecad/ui 1.0.0-alpha.6 → 1.0.0-alpha.8
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 +6 -0
- package/dist/engine-index.js +2 -2
- package/dist/preview/host-ui-runtime.d.ts +1 -0
- package/dist/preview/host-ui-runtime.js +56 -11
- package/dist/ui-engine/font-assets.d.ts +8 -0
- package/dist/ui-engine/font-assets.js +13 -0
- package/dist/ui-engine/layout-engine.js +19 -6
- package/dist/ui-engine/model.d.ts +1 -1
- package/dist/ui-engine/model.js +10 -4
- package/dist/ui-engine/runtime-header/antialiasing.js +52 -32
- package/dist/ui-engine/runtime-header/blend-bodies.js +4 -4
- package/dist/ui-engine/runtime-header/canvas-helpers.js +42 -69
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +22 -10
- package/dist/ui-engine/runtime-header/color-mono-refresh.js +10 -10
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.d.ts +1 -0
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +563 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +26 -10
- package/dist/ui-engine/runtime-header/display-shim.js +8 -8
- package/dist/ui-engine/runtime-header/forward-decls.js +52 -10
- package/dist/ui-engine/runtime-header/image-drawing.js +154 -84
- package/dist/ui-engine/runtime-header/init-press-input.js +20 -5
- package/dist/ui-engine/runtime-header/keyboard.js +62 -18
- package/dist/ui-engine/runtime-header/node-decoration.js +8 -9
- package/dist/ui-engine/runtime-header/node-draw-body.d.ts +1 -0
- package/dist/ui-engine/runtime-header/node-draw-body.js +1402 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.js +48 -18
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +38 -17
- package/dist/ui-engine/runtime-header/scroll-physics.js +15 -18
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +23 -13
- package/dist/ui-engine/runtime-header/structs.js +12 -10
- package/dist/ui-engine/runtime-header/text-rendering.js +132 -71
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +29 -10
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +336 -749
- package/dist/ui-engine/runtime-header/tick/flush-phase.js +7 -3
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +19 -3
- package/dist/ui-engine/runtime-header/tick/transitions-phase.js +31 -24
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +46 -41
- package/dist/ui-engine/runtime-header/types-defines.js +23 -3
- package/dist/ui-engine/runtime-header.d.ts +10 -1
- package/dist/ui-engine/runtime-header.js +7 -1
- package/dist/ui-engine/transpile-ui.js +2 -2
- package/dist/ui-engine/ua-stylesheet.js +46 -3
- package/dist/ui-engine/ui-lowering.d.ts +1 -3
- package/dist/ui-engine/ui-lowering.js +18 -24
- package/dist/ui-engine/ui-registry.js +1 -6
- package/package.json +2 -2
- package/src/engine-index.ts +2 -2
- package/src/preview/host-ui-runtime.ts +56 -12
- package/src/ui-engine/font-assets.ts +13 -0
- package/src/ui-engine/layout-engine.ts +19 -6
- package/src/ui-engine/model.ts +10 -5
- package/src/ui-engine/runtime-header/antialiasing.ts +52 -32
- package/src/ui-engine/runtime-header/blend-bodies.ts +4 -4
- package/src/ui-engine/runtime-header/canvas-helpers.ts +42 -69
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +22 -10
- package/src/ui-engine/runtime-header/color-mono-refresh.ts +10 -10
- package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +566 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +26 -10
- package/src/ui-engine/runtime-header/display-shim.ts +8 -8
- package/src/ui-engine/runtime-header/forward-decls.ts +52 -10
- package/src/ui-engine/runtime-header/image-drawing.ts +154 -84
- package/src/ui-engine/runtime-header/init-press-input.ts +20 -5
- package/src/ui-engine/runtime-header/keyboard.ts +62 -18
- package/src/ui-engine/runtime-header/node-decoration.ts +8 -9
- package/src/ui-engine/runtime-header/node-draw-body.ts +1402 -0
- package/src/ui-engine/runtime-header/paint-order-coords.ts +48 -18
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +38 -17
- package/src/ui-engine/runtime-header/scroll-physics.ts +15 -18
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +23 -13
- package/src/ui-engine/runtime-header/structs.ts +12 -10
- package/src/ui-engine/runtime-header/text-rendering.ts +132 -71
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +29 -10
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +336 -749
- package/src/ui-engine/runtime-header/tick/flush-phase.ts +7 -3
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +19 -3
- package/src/ui-engine/runtime-header/tick/transitions-phase.ts +31 -24
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +46 -41
- package/src/ui-engine/runtime-header/types-defines.ts +23 -3
- package/src/ui-engine/runtime-header.ts +17 -1
- package/src/ui-engine/transpile-ui.ts +2 -2
- package/src/ui-engine/ua-stylesheet.ts +46 -3
- package/src/ui-engine/ui-lowering.ts +19 -26
- package/src/ui-engine/ui-registry.ts +1 -7
- package/dist/ui-engine/scroll-memory-diagnostics.d.ts +0 -16
- package/dist/ui-engine/scroll-memory-diagnostics.js +0 -58
- package/src/ui-engine/scroll-memory-diagnostics.ts +0 -79
package/README.md
CHANGED
|
@@ -1123,6 +1123,12 @@ display: {
|
|
|
1123
1123
|
rst: 22,
|
|
1124
1124
|
backlight: 17, // optional — pin number for backlight
|
|
1125
1125
|
|
|
1126
|
+
// Experimental and opt-in: use only after verifying the controller's
|
|
1127
|
+
// GET_SCANLINE (0x45) readback and wiring SDO/MISO in spiPins. Some ST7796S
|
|
1128
|
+
// modules stop scanning when this command is read, so the default is off.
|
|
1129
|
+
// scanlineSync: true,
|
|
1130
|
+
// spiPins: { mosi: 11, sck: 12, miso: 13 },
|
|
1131
|
+
|
|
1126
1132
|
// Antialiasing (optional)
|
|
1127
1133
|
antialias: true, // smooths shapes and text; text can opt out with font-smoothing:none
|
|
1128
1134
|
|
package/dist/engine-index.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
import { resetUIRegistry, loadUIModule, loadUIModuleFromText, getUIModule, hasUIModule, allUIModules, allLoweredUIModules, markEntryHasUI, entryHasUI, clearEntryHasUI, lowerOnMount, generateProjectUITypeDeclarations, } from "./ui-engine/ui-registry.js";
|
|
13
13
|
import { resolveColor, resolveColorInternal } from "./ui-engine/color.js";
|
|
14
14
|
import { emitRuntimeHeader } from "./ui-engine/runtime-header.js";
|
|
15
|
+
import { emitCuttlefishGfx } from "./ui-engine/runtime-header/cuttlefish-gfx.js";
|
|
15
16
|
import { splitUiFile } from "./ui-engine/ui-file-splitter.js";
|
|
16
|
-
import { analyzeScrollMemory } from "./ui-engine/scroll-memory-diagnostics.js";
|
|
17
17
|
/**
|
|
18
18
|
* Build and return the TranspilerUIHook — the object cuttlefish core uses
|
|
19
19
|
* to access all UI capabilities (parsing, layout, lowering, emission).
|
|
@@ -34,8 +34,8 @@ export function registerTranspilerUI() {
|
|
|
34
34
|
resolveColor,
|
|
35
35
|
resolveColorInternal,
|
|
36
36
|
emitRuntimeHeader,
|
|
37
|
+
emitCuttlefishGfx,
|
|
37
38
|
splitUiFile,
|
|
38
39
|
generateProjectUITypeDeclarations,
|
|
39
|
-
analyzeScrollMemory,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
@@ -187,6 +187,7 @@ export declare class PreviewUIRuntime {
|
|
|
187
187
|
* sequence is supported — same constraint as bindList item expressions. */
|
|
188
188
|
private runCanvasBody;
|
|
189
189
|
private drawButtonNode;
|
|
190
|
+
private drawSelectNode;
|
|
190
191
|
private drawCheckNode;
|
|
191
192
|
private drawRadioNode;
|
|
192
193
|
private drawProgressNode;
|
|
@@ -1062,7 +1062,7 @@ export class PreviewUIRuntime {
|
|
|
1062
1062
|
const shadow = this.shadowExtents(node);
|
|
1063
1063
|
let faceW = node.box.w;
|
|
1064
1064
|
let faceH = node.box.h;
|
|
1065
|
-
if (node.kind === "text" || node.kind === "check" || node.kind === "radio") {
|
|
1065
|
+
if (node.kind === "text" || node.kind === "check" || node.kind === "radio" || node.kind === "select") {
|
|
1066
1066
|
if (node.lastTextWidth > faceW)
|
|
1067
1067
|
faceW = node.lastTextWidth;
|
|
1068
1068
|
if ((node.lastTextHeight ?? 0) > faceH)
|
|
@@ -1090,7 +1090,7 @@ export class PreviewUIRuntime {
|
|
|
1090
1090
|
const displayText = node.hasTextBinding ? node.textBuffer : node.text;
|
|
1091
1091
|
const ts = this.nodeTextSize(node);
|
|
1092
1092
|
let textMaxW = node.box.w;
|
|
1093
|
-
if (node.kind === "text" || node.kind === "button") {
|
|
1093
|
+
if (node.kind === "text" || node.kind === "button" || node.kind === "select") {
|
|
1094
1094
|
const insets = this.textInsets(node);
|
|
1095
1095
|
textMaxW = Math.max(0, node.box.w - insets.left - insets.right);
|
|
1096
1096
|
}
|
|
@@ -1100,7 +1100,7 @@ export class PreviewUIRuntime {
|
|
|
1100
1100
|
const metrics = this.textLayout(node, displayText, textMaxW, ts);
|
|
1101
1101
|
let paintTextW = metrics.width;
|
|
1102
1102
|
let paintTextH = metrics.height;
|
|
1103
|
-
if (node.kind === "text") {
|
|
1103
|
+
if (node.kind === "text" || node.kind === "select") {
|
|
1104
1104
|
const insets = this.textInsets(node);
|
|
1105
1105
|
paintTextW += insets.left + insets.right;
|
|
1106
1106
|
paintTextH += insets.top + insets.bottom;
|
|
@@ -1226,13 +1226,20 @@ export class PreviewUIRuntime {
|
|
|
1226
1226
|
const displayText = node.hasTextBinding ? node.textBuffer : node.text;
|
|
1227
1227
|
const ts = this.nodeTextSize(node);
|
|
1228
1228
|
let textMaxW = node.box.w;
|
|
1229
|
-
if (node.kind === "text" || node.kind === "button") {
|
|
1229
|
+
if (node.kind === "text" || node.kind === "button" || node.kind === "select") {
|
|
1230
1230
|
const insets = this.textInsets(node);
|
|
1231
1231
|
textMaxW = Math.max(0, node.box.w - insets.left - insets.right);
|
|
1232
1232
|
}
|
|
1233
1233
|
const metrics = this.textLayout(node, displayText, textMaxW, ts);
|
|
1234
|
-
const insets = node.kind === "text" ? this.textInsets(node) : { left: 0, right: 0, top: 0, bottom: 0 };
|
|
1235
|
-
|
|
1234
|
+
const insets = (node.kind === "text" || node.kind === "select") ? this.textInsets(node) : { left: 0, right: 0, top: 0, bottom: 0 };
|
|
1235
|
+
let clearW = metrics.width + insets.left + insets.right;
|
|
1236
|
+
let clearH = metrics.height + insets.top + insets.bottom;
|
|
1237
|
+
if (node.kind === "check" || node.kind === "radio") {
|
|
1238
|
+
clearW += 22;
|
|
1239
|
+
if (clearH < 16)
|
|
1240
|
+
clearH = 16;
|
|
1241
|
+
}
|
|
1242
|
+
const rect = this.nodePaintRect(node, this.baseDrawXForNode(node.index), this.baseDrawYForNode(node.index), this.drawXForNode(node.index), this.drawYForNode(node.index), clearW, clearH);
|
|
1236
1243
|
this.clearNodePaintRect(node, rect);
|
|
1237
1244
|
}
|
|
1238
1245
|
tryRepairGeometryFill(node, oldRect) {
|
|
@@ -1845,6 +1852,9 @@ export class PreviewUIRuntime {
|
|
|
1845
1852
|
case "button":
|
|
1846
1853
|
this.drawButtonNode(node, displayText, bColor, fillBg, drawY, ts);
|
|
1847
1854
|
break;
|
|
1855
|
+
case "select":
|
|
1856
|
+
this.drawSelectNode(node, displayText, bColor, fillBg, drawY, ts);
|
|
1857
|
+
break;
|
|
1848
1858
|
case "check":
|
|
1849
1859
|
this.drawCheckNode(node, displayText, drawY, ts);
|
|
1850
1860
|
break;
|
|
@@ -2318,6 +2328,35 @@ export class PreviewUIRuntime {
|
|
|
2318
2328
|
const glyphBg = node.opacity < 100 ? blendRuntime(node.bg, this.parentClearColor(node), node.opacity) : (node.hasBg ? node.bg : node.clearColor);
|
|
2319
2329
|
this.drawTextLines(node, displayText, textX, top, textW, ts, node.fg, glyphBg, node.textAlign);
|
|
2320
2330
|
}
|
|
2331
|
+
// <select>: bordered box with its current option text VERTICALLY CENTERED
|
|
2332
|
+
// (unlike drawTextNode, which top-aligns). Mirrors the C++ NODE_SELECT case
|
|
2333
|
+
// so preview and device agree. The label is dynamic (auto-bound to the
|
|
2334
|
+
// selected option), so clear the previous text rect before redrawing.
|
|
2335
|
+
drawSelectNode(node, displayText, bColor, fillBg, drawY, ts) {
|
|
2336
|
+
const insets = this.textInsets(node);
|
|
2337
|
+
const contentW = Math.max(1, node.box.w - insets.left - insets.right);
|
|
2338
|
+
const layout = this.textLayout(node, displayText, contentW, ts);
|
|
2339
|
+
const clearW = Math.max(node.box.w, node.lastTextWidth ?? 0, layout.width + insets.left + insets.right);
|
|
2340
|
+
const clearH = Math.max(node.box.h, node.lastTextHeight ?? 0, layout.height + insets.top + insets.bottom);
|
|
2341
|
+
const clearCol = node.hasBg ? node.bg : node.clearColor;
|
|
2342
|
+
this.gfx.fillRect(node.box.x, drawY, clearW, clearH, clearCol);
|
|
2343
|
+
node.lastTextWidth = layout.width;
|
|
2344
|
+
node.lastTextHeight = layout.height;
|
|
2345
|
+
if (node.borderRadius > 0 && node.hasBg)
|
|
2346
|
+
this.gfx.fillRoundRect(node.box.x, drawY, node.box.w, node.box.h, node.borderRadius, fillBg);
|
|
2347
|
+
else if (node.hasBg)
|
|
2348
|
+
this.gfx.fillRect(node.box.x, drawY, node.box.w, node.box.h, fillBg);
|
|
2349
|
+
this.drawNodeShadow(node, drawY, true);
|
|
2350
|
+
if (node.borderStyle)
|
|
2351
|
+
this.drawNodeBorder(node, node.box.x, drawY, bColor);
|
|
2352
|
+
const textX = node.box.x + insets.left;
|
|
2353
|
+
const textY = drawY + insets.top;
|
|
2354
|
+
const textW = Math.max(1, node.box.w - insets.left - insets.right);
|
|
2355
|
+
const textH = Math.max(1, node.box.h - insets.top - insets.bottom);
|
|
2356
|
+
const top = textY + Math.trunc((textH - layout.height) / 2);
|
|
2357
|
+
const glyphBg = node.opacity < 100 ? blendRuntime(node.bg, this.parentClearColor(node), node.opacity) : (node.hasBg ? node.bg : node.clearColor);
|
|
2358
|
+
this.drawTextLines(node, displayText, textX, top, textW, ts, node.fg, glyphBg, node.textAlign);
|
|
2359
|
+
}
|
|
2321
2360
|
drawCheckNode(node, displayText, drawY, ts) {
|
|
2322
2361
|
const layout = this.textLayout(node, displayText, Math.max(0, node.box.w - 22), ts);
|
|
2323
2362
|
const clearW = Math.max(node.box.w, node.lastTextWidth);
|
|
@@ -2326,7 +2365,11 @@ export class PreviewUIRuntime {
|
|
|
2326
2365
|
node.lastTextWidth = 22 + layout.width;
|
|
2327
2366
|
node.lastTextHeight = Math.max(layout.height, 16);
|
|
2328
2367
|
const cbX = node.box.x;
|
|
2329
|
-
|
|
2368
|
+
// Vertically center the 16px indicator within the box so a tall
|
|
2369
|
+
// (touch-friendly) checkbox doesn't pin the indicator to the top.
|
|
2370
|
+
// (box.h - 16) / 2 is 0 for the default 16px-tall box.
|
|
2371
|
+
const checkOff = Math.max(0, ((node.box.h - 16) / 2) | 0);
|
|
2372
|
+
const cbY = drawY + checkOff;
|
|
2330
2373
|
if (node.value) {
|
|
2331
2374
|
this.gfx.fillRect(cbX, cbY, 16, 16, node.fg);
|
|
2332
2375
|
const inv = node.hasBg ? node.bg : node.clearColor;
|
|
@@ -2340,7 +2383,7 @@ export class PreviewUIRuntime {
|
|
|
2340
2383
|
else {
|
|
2341
2384
|
this.gfx.drawRect(cbX, cbY, 16, 16, node.fg);
|
|
2342
2385
|
}
|
|
2343
|
-
this.drawTextLines(node, displayText, node.box.x + 22, drawY, Math.max(0, node.box.w - 22), ts, node.fg, node.hasBg ? node.bg : node.clearColor, 0);
|
|
2386
|
+
this.drawTextLines(node, displayText, node.box.x + 22, drawY + checkOff, Math.max(0, node.box.w - 22), ts, node.fg, node.hasBg ? node.bg : node.clearColor, 0);
|
|
2344
2387
|
}
|
|
2345
2388
|
drawRadioNode(node, displayText, drawY, ts) {
|
|
2346
2389
|
const layout = this.textLayout(node, displayText, Math.max(0, node.box.w - 22), ts);
|
|
@@ -2350,7 +2393,9 @@ export class PreviewUIRuntime {
|
|
|
2350
2393
|
node.lastTextWidth = 22 + layout.width;
|
|
2351
2394
|
node.lastTextHeight = Math.max(layout.height, 16);
|
|
2352
2395
|
const cbX = node.box.x;
|
|
2353
|
-
|
|
2396
|
+
// Vertically center the 16px indicator within the box (see drawCheckNode).
|
|
2397
|
+
const radioOff = Math.max(0, ((node.box.h - 16) / 2) | 0);
|
|
2398
|
+
const cbY = drawY + radioOff;
|
|
2354
2399
|
if (node.value) {
|
|
2355
2400
|
this.gfx.fillCircle(cbX + 8, cbY + 8, 7, node.fg);
|
|
2356
2401
|
this.gfx.fillCircle(cbX + 8, cbY + 8, 3, node.hasBg ? node.bg : node.clearColor);
|
|
@@ -2358,7 +2403,7 @@ export class PreviewUIRuntime {
|
|
|
2358
2403
|
else {
|
|
2359
2404
|
this.gfx.drawCircle(cbX + 8, cbY + 8, 7, node.fg);
|
|
2360
2405
|
}
|
|
2361
|
-
this.drawTextLines(node, displayText, node.box.x + 22, drawY, Math.max(0, node.box.w - 22), ts, node.fg, node.hasBg ? node.bg : node.clearColor, 0);
|
|
2406
|
+
this.drawTextLines(node, displayText, node.box.x + 22, drawY + radioOff, Math.max(0, node.box.w - 22), ts, node.fg, node.hasBg ? node.bg : node.clearColor, 0);
|
|
2362
2407
|
}
|
|
2363
2408
|
drawProgressNode(node, drawY) {
|
|
2364
2409
|
const bx = node.box.x;
|
|
@@ -2788,7 +2833,7 @@ export class PreviewUIRuntime {
|
|
|
2788
2833
|
this.markDirty(nodeIndex);
|
|
2789
2834
|
}
|
|
2790
2835
|
else if (node.tag === "select") {
|
|
2791
|
-
const count = Math.max(node.options?.length ?? 0,
|
|
2836
|
+
const count = Math.max(node.options?.length ?? 0, 1);
|
|
2792
2837
|
node.value = (node.value + 1) % count;
|
|
2793
2838
|
this.markDirty(nodeIndex);
|
|
2794
2839
|
}
|
|
@@ -46,5 +46,13 @@ export declare function selectFontAssetForStyle(fontAssets: UIFontAssetModel[],
|
|
|
46
46
|
* Characters missing from the asset's subset fall back to half the line height
|
|
47
47
|
* (matching the runtime's ui_asset_text_width fallback). */
|
|
48
48
|
export declare function assetTextWidth(text: string, style: CSSProperty, fontAssets: UIFontAssetModel[]): number | undefined;
|
|
49
|
+
/** The line height the runtime will render a custom-font node at
|
|
50
|
+
* (ui_asset_text_height → face->lineHeight), or undefined when the node uses
|
|
51
|
+
* the default font. Layout must use this for the glyph-cell height instead of
|
|
52
|
+
* the 8*ts GFX bitmap default, otherwise the laid-out box is shorter than the
|
|
53
|
+
* drawn glyphs and the text overflows its container (e.g. a bold @font-face
|
|
54
|
+
* title spilling past its header's padded box). Mirrors the preview's
|
|
55
|
+
* textHeight(size, fontFace) = asset.lineHeight ?? gfx.textHeight(size). */
|
|
56
|
+
export declare function assetLineHeight(style: CSSProperty, fontAssets: UIFontAssetModel[]): number | undefined;
|
|
49
57
|
export declare function buildUIFontAssets(root: StyledNode, fontFaces: CSSFontFace[], baseDir: string): UIFontAssetModel[];
|
|
50
58
|
export declare function planUIFontAssets(root: StyledNode, fontFaces: CSSFontFace[], baseDir: string): UIFontAssetPlan[];
|
|
@@ -97,6 +97,19 @@ export function assetTextWidth(text, style, fontAssets) {
|
|
|
97
97
|
}
|
|
98
98
|
return w;
|
|
99
99
|
}
|
|
100
|
+
/** The line height the runtime will render a custom-font node at
|
|
101
|
+
* (ui_asset_text_height → face->lineHeight), or undefined when the node uses
|
|
102
|
+
* the default font. Layout must use this for the glyph-cell height instead of
|
|
103
|
+
* the 8*ts GFX bitmap default, otherwise the laid-out box is shorter than the
|
|
104
|
+
* drawn glyphs and the text overflows its container (e.g. a bold @font-face
|
|
105
|
+
* title spilling past its header's padded box). Mirrors the preview's
|
|
106
|
+
* textHeight(size, fontFace) = asset.lineHeight ?? gfx.textHeight(size). */
|
|
107
|
+
export function assetLineHeight(style, fontAssets) {
|
|
108
|
+
const asset = selectFontAssetForStyle(fontAssets, style);
|
|
109
|
+
if (!asset || asset.lineHeight <= 0)
|
|
110
|
+
return undefined;
|
|
111
|
+
return asset.lineHeight;
|
|
112
|
+
}
|
|
100
113
|
export function buildUIFontAssets(root, fontFaces, baseDir) {
|
|
101
114
|
const plans = planUIFontAssets(root, fontFaces, baseDir);
|
|
102
115
|
const parsedFonts = new Map();
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
import { layoutText } from "./text-layout.js";
|
|
14
14
|
import { layoutRuns } from "./rich-layout.js";
|
|
15
|
-
import { assetTextWidth } from "./font-assets.js";
|
|
15
|
+
import { assetTextWidth, assetLineHeight } from "./font-assets.js";
|
|
16
16
|
/** Static display:none predicate shared by layout engines and lowering. */
|
|
17
17
|
export function isDisplayNone(node) {
|
|
18
18
|
return node.style.display?.trim().toLowerCase() === "none";
|
|
@@ -113,12 +113,16 @@ export function measure(node, availableWidth, fontAssets = []) {
|
|
|
113
113
|
const rs = { ...node.style, ...r.style };
|
|
114
114
|
const rTs = gfxTextSizeOf({ ...node, style: rs });
|
|
115
115
|
const rAdvance = 6 * rTs + letterSpacingOf({ ...node, style: rs });
|
|
116
|
+
// Run glyph height: prefer the asset's lineHeight for custom fonts (see
|
|
117
|
+
// charH below); fall back to the 8*rTs GFX bitmap default.
|
|
118
|
+
const rAssetH = assetLineHeight(rs, fontAssets);
|
|
119
|
+
const rHeight = rAssetH ?? (8 * rTs);
|
|
116
120
|
return {
|
|
117
121
|
text: applyTextTransform(r.text, { ...node, style: rs }),
|
|
118
122
|
hardBreak: r.hardBreak,
|
|
119
123
|
measureText: (s) => assetTextWidth(s, rs, fontAssets) ?? textWidthOf(s, rAdvance),
|
|
120
|
-
height:
|
|
121
|
-
ascent: 7 * rTs,
|
|
124
|
+
height: rHeight,
|
|
125
|
+
ascent: rAssetH ?? (7 * rTs),
|
|
122
126
|
};
|
|
123
127
|
});
|
|
124
128
|
const layout = layoutRuns(layoutInput, { maxWidth: availableWidth, whiteSpace: node.style.whiteSpace });
|
|
@@ -127,7 +131,13 @@ export function measure(node, availableWidth, fontAssets = []) {
|
|
|
127
131
|
// Per-node text size (from font-size + font-weight CSS).
|
|
128
132
|
const ts = gfxTextSizeOf(node);
|
|
129
133
|
const advance = 6 * ts + letterSpacingOf(node);
|
|
130
|
-
|
|
134
|
+
// Glyph-cell height: the runtime draws a custom @font-face at its real
|
|
135
|
+
// lineHeight (ui_asset_text_height → face->lineHeight), NOT the 8*ts GFX
|
|
136
|
+
// bitmap default. Use the asset's lineHeight when one matches so the box
|
|
137
|
+
// matches the drawn height (otherwise bold @font-face titles overflow their
|
|
138
|
+
// header). Falls back to 8*ts for the default font. Mirrors the preview.
|
|
139
|
+
const assetH = assetLineHeight(node.style, fontAssets);
|
|
140
|
+
const charH = assetH ?? (8 * ts);
|
|
131
141
|
// For interpolation text (e.g. "taps: {count}"), strip the braces so layout
|
|
132
142
|
// measures "taps: count" — closer to the resolved runtime width than the
|
|
133
143
|
// literal "{count}". The braces are an authoring delimiter, not rendered.
|
|
@@ -175,10 +185,13 @@ export function measure(node, availableWidth, fontAssets = []) {
|
|
|
175
185
|
return { w: 200, h: 20 };
|
|
176
186
|
}
|
|
177
187
|
if (node.tag === "input") {
|
|
178
|
-
// Input field: sized to the placeholder or a default width
|
|
188
|
+
// Input field: sized to the placeholder or a default width. Height defaults
|
|
189
|
+
// to 20px but an explicit `height` style overrides it (the UA input rule no
|
|
190
|
+
// longer forces a min-height, so the author's height wins).
|
|
179
191
|
const text = node.placeholder ?? "";
|
|
180
192
|
const textW = text.length > 0 ? text.length * GFX_ADVANCE_PER_CHAR : 120;
|
|
181
|
-
|
|
193
|
+
const explicitH = node.style.height ? parseInt(String(node.style.height), 10) : NaN;
|
|
194
|
+
return { w: Math.max(textW + 16, 120), h: Number.isNaN(explicitH) ? 20 : explicitH };
|
|
182
195
|
}
|
|
183
196
|
if (node.tag === "img") {
|
|
184
197
|
return { w: node.imgWidth ?? 32, h: node.imgHeight ?? 32 };
|
|
@@ -4,7 +4,7 @@ import type { UIImageAsset } from "./image-assets.js";
|
|
|
4
4
|
import { type Box } from "./layout-engine.js";
|
|
5
5
|
import type { StyledNode } from "./style-resolver.js";
|
|
6
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";
|
|
7
|
+
export type UINodeKindModel = "fill" | "text" | "button" | "check" | "radio" | "progress" | "range" | "input" | "img" | "list" | "canvas" | "select";
|
|
8
8
|
export type UIPropertyModel = "background" | "color" | "text" | "visible" | "borderColor";
|
|
9
9
|
export interface UINodeModel {
|
|
10
10
|
index: number;
|
package/dist/ui-engine/model.js
CHANGED
|
@@ -2,7 +2,7 @@ import { effectiveDisplaySize } from "@typecad/cuttlefish/api/shared";
|
|
|
2
2
|
import { deriveCapabilities } from "@typecad/cuttlefish/api/shared";
|
|
3
3
|
import { resolveColorInternal } from "./color.js";
|
|
4
4
|
import { parseAnimation } from "./css-parser.js";
|
|
5
|
-
import { selectFontAssetForStyle, assetTextWidth } from "./font-assets.js";
|
|
5
|
+
import { selectFontAssetForStyle, assetTextWidth, assetLineHeight } from "./font-assets.js";
|
|
6
6
|
import { isDisplayNone } from "./layout-engine.js";
|
|
7
7
|
import { whiteSpaceMode } from "./text-layout.js";
|
|
8
8
|
import { layoutRuns } from "./rich-layout.js";
|
|
@@ -34,6 +34,8 @@ function nodeKind(tag) {
|
|
|
34
34
|
return "list";
|
|
35
35
|
if (tag === "canvas")
|
|
36
36
|
return "canvas";
|
|
37
|
+
if (tag === "select")
|
|
38
|
+
return "select";
|
|
37
39
|
return "text";
|
|
38
40
|
}
|
|
39
41
|
function intAttr(value, fallback) {
|
|
@@ -725,14 +727,18 @@ export function lowerUIToModel(root, boxes, colorFormat, display, fontAssets = [
|
|
|
725
727
|
const rs = runStyleOf(r);
|
|
726
728
|
const rTextSize = textSizeOf(rs);
|
|
727
729
|
const rLetterSpacing = letterSpacingOf(rs);
|
|
730
|
+
const rAssetH = assetLineHeight(rs, fontAssets);
|
|
728
731
|
return {
|
|
729
732
|
text: applyTextTransform(r.text, rs) ?? "",
|
|
730
733
|
hardBreak: r.hardBreak,
|
|
731
734
|
// Per-glyph asset advance when the run has a custom font, else the
|
|
732
735
|
// flat 6*textSize + letterSpacing per-char default-font advance.
|
|
733
736
|
measureText: (s) => assetTextWidth(s, rs, fontAssets) ?? textWidthOfDefault(s, rTextSize, rLetterSpacing),
|
|
734
|
-
|
|
735
|
-
|
|
737
|
+
// Asset lineHeight when the run has a custom font, else 8*textSize.
|
|
738
|
+
// Must match layout-engine.ts's rich-run height (so the baked
|
|
739
|
+
// runLines geometry matches the measured box).
|
|
740
|
+
height: rAssetH ?? (8 * rTextSize),
|
|
741
|
+
ascent: rAssetH ?? (7 * rTextSize),
|
|
736
742
|
};
|
|
737
743
|
});
|
|
738
744
|
const layout = layoutRuns(layoutInput, { maxWidth: box.w, whiteSpace: node.style.whiteSpace });
|
|
@@ -797,7 +803,7 @@ export function lowerUIToModel(root, boxes, colorFormat, display, fontAssets = [
|
|
|
797
803
|
hasBg,
|
|
798
804
|
textAlign: textAlign(node.style),
|
|
799
805
|
textSize,
|
|
800
|
-
lineHeight: lineHeightOf(node.style, textSize),
|
|
806
|
+
lineHeight: assetLineHeight(node.style, fontAssets) ?? lineHeightOf(node.style, textSize),
|
|
801
807
|
letterSpacing: letterSpacingOf(node.style),
|
|
802
808
|
fontAntialias: fontAntialiasOf(node.style, display),
|
|
803
809
|
fontFace: fontFaceOf(node.style, fontAssets),
|
|
@@ -12,10 +12,18 @@ export function emitAntialiasing() {
|
|
|
12
12
|
// then pushed to the display. The ILI9341 has no efficient SPI read-back, so
|
|
13
13
|
// all blending happens in RAM.
|
|
14
14
|
#ifdef UI_AA
|
|
15
|
+
#include <math.h> // fabs, floor, ceil, sqrtf for AA coverage math
|
|
16
|
+
|
|
17
|
+
// AA is only useful on RAM-backed targets. Direct SPI targets have no safe
|
|
18
|
+
// read/modify/write surface for coverage blending; callers still get the
|
|
19
|
+
// regular primitive path instead of an allocation plus many tiny pushes.
|
|
20
|
+
static inline uint8_t ui_aa_target_is_ram() {
|
|
21
|
+
return !ui_display_is_default_target();
|
|
22
|
+
}
|
|
15
23
|
|
|
16
24
|
// Get (or allocate) a canvas sized to the element being drawn.
|
|
17
25
|
static inline CuttlefishCanvas16* ui_aa_begin(int16_t w, int16_t h, UI_COLOR_T bg) {
|
|
18
|
-
if (w <= 0 || h <= 0) return nullptr;
|
|
26
|
+
if (w <= 0 || h <= 0 || !ui_aa_target_is_ram()) return nullptr;
|
|
19
27
|
if (!__ui_aa_canvas || display_canvasWidth(__ui_aa_canvas) < w || display_canvasHeight(__ui_aa_canvas) < h) {
|
|
20
28
|
display_deleteCanvas(__ui_aa_canvas);
|
|
21
29
|
__ui_aa_canvas = display_createCanvas(w > 0 ? w : 1, h > 0 ? h : 1);
|
|
@@ -33,7 +41,7 @@ static inline void ui_aa_push(CuttlefishCanvas16* c, int16_t dx, int16_t dy) {
|
|
|
33
41
|
// or the display directly when not. Row-by-row drawRGBBitmap (no transparent
|
|
34
42
|
// alpha — the AA canvas already has the blended pixels).
|
|
35
43
|
for (int16_t row = 0; row < h; row++) {
|
|
36
|
-
ui_display_draw_rgb_bitmap(dx, dy + row, display_canvasBuffer(c) + (
|
|
44
|
+
ui_display_draw_rgb_bitmap(dx, dy + row, display_canvasBuffer(c) + static_cast<int32_t>(row) * w, w, 1);
|
|
37
45
|
}
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -44,16 +52,16 @@ static inline void ui_aa_pixel(CuttlefishCanvas16* c, int16_t x, int16_t y, UI_C
|
|
|
44
52
|
if (x < 0 || y < 0 || x >= display_canvasWidth(c) || y >= display_canvasHeight(c)) return;
|
|
45
53
|
if (cov >= 255) { display_targetDrawPixel((CuttlefishDisplayTarget*)c, x, y, color); return; }
|
|
46
54
|
UI_COLOR_T bg = display_canvasGetPixel(c, x, y);
|
|
47
|
-
uint8_t op =
|
|
55
|
+
uint8_t op = static_cast<uint8_t>(static_cast<uint16_t>(cov) * 100 / 255);
|
|
48
56
|
display_targetDrawPixel((CuttlefishDisplayTarget*)c, x, y, ui_blend(color, bg, op));
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
// Xiaolin Wu antialiased line. Coordinates are in canvas-local space.
|
|
52
60
|
static inline void ui_aa_line(CuttlefishCanvas16* c, float x0, float y0, float x1, float y1, UI_COLOR_T color) {
|
|
53
61
|
if (!c || !display_canvasBuffer(c)) return;
|
|
54
|
-
auto ipart = [](float f) { return (
|
|
55
|
-
auto round_f = [](float f) { return
|
|
56
|
-
auto fpart = [](float f) { return f -
|
|
62
|
+
auto ipart = [](float f) { return static_cast<int16_t>(f); };
|
|
63
|
+
auto round_f = [](float f) { return static_cast<int16_t>(f + 0.5f); };
|
|
64
|
+
auto fpart = [](float f) { return f - static_cast<float>(static_cast<int16_t>(f)); };
|
|
57
65
|
auto rfpart = [&](float f) { return 1.0f - fpart(f); };
|
|
58
66
|
|
|
59
67
|
bool steep = fabs(y1 - y0) > fabs(x1 - x0);
|
|
@@ -63,17 +71,29 @@ static inline void ui_aa_line(CuttlefishCanvas16* c, float x0, float y0, float x
|
|
|
63
71
|
float dx = x1 - x0, dy = y1 - y0;
|
|
64
72
|
float gradient = (dx == 0) ? 1.0f : dy / dx;
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
float xend =
|
|
68
|
-
float
|
|
74
|
+
// Handle first endpoint.
|
|
75
|
+
float xend = static_cast<float>(round_f(x0));
|
|
76
|
+
float yend = y0 + gradient * (xend - x0);
|
|
77
|
+
float xgap = rfpart(x0);
|
|
78
|
+
int16_t xpx1 = static_cast<int16_t>(xend);
|
|
79
|
+
int16_t ypx1 = ipart(yend);
|
|
80
|
+
if (steep) {
|
|
81
|
+
ui_aa_pixel(c, ypx1, xpx1, color, static_cast<uint8_t>(rfpart(yend) * xgap * 255));
|
|
82
|
+
ui_aa_pixel(c, ypx1 + 1, xpx1, color, static_cast<uint8_t>(fpart(yend) * xgap * 255));
|
|
83
|
+
} else {
|
|
84
|
+
ui_aa_pixel(c, xpx1, ypx1, color, static_cast<uint8_t>(rfpart(yend) * xgap * 255));
|
|
85
|
+
ui_aa_pixel(c, xpx1, ypx1 + 1, color, static_cast<uint8_t>(fpart(yend) * xgap * 255));
|
|
86
|
+
}
|
|
87
|
+
float intery = yend + gradient;
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
// Main loop.
|
|
90
|
+
for (int16_t x = xpx1 + 1; x <= static_cast<int16_t>(round_f(x1)) - 1; x++) {
|
|
71
91
|
if (steep) {
|
|
72
|
-
ui_aa_pixel(c, ipart(intery), x, color,
|
|
73
|
-
ui_aa_pixel(c, ipart(intery) + 1, x, color,
|
|
92
|
+
ui_aa_pixel(c, ipart(intery), x, color, static_cast<uint8_t>(rfpart(intery) * 255));
|
|
93
|
+
ui_aa_pixel(c, ipart(intery) + 1, x, color, static_cast<uint8_t>(fpart(intery) * 255));
|
|
74
94
|
} else {
|
|
75
|
-
ui_aa_pixel(c, x, ipart(intery), color,
|
|
76
|
-
ui_aa_pixel(c, x, ipart(intery) + 1, color,
|
|
95
|
+
ui_aa_pixel(c, x, ipart(intery), color, static_cast<uint8_t>(rfpart(intery) * 255));
|
|
96
|
+
ui_aa_pixel(c, x, ipart(intery) + 1, color, static_cast<uint8_t>(fpart(intery) * 255));
|
|
77
97
|
}
|
|
78
98
|
intery += gradient;
|
|
79
99
|
}
|
|
@@ -84,24 +104,24 @@ static inline void ui_aa_circle(CuttlefishCanvas16* c, int16_t cx, int16_t cy, f
|
|
|
84
104
|
if (!c || !display_canvasBuffer(c)) return;
|
|
85
105
|
if (r <= 0) return;
|
|
86
106
|
// Walk each scanline from top to bottom of the bounding box.
|
|
87
|
-
int16_t y0 = (
|
|
88
|
-
int16_t y1 = (
|
|
107
|
+
int16_t y0 = static_cast<int16_t>(floor(cy - r));
|
|
108
|
+
int16_t y1 = static_cast<int16_t>(ceil(cy + r));
|
|
89
109
|
for (int16_t y = y0; y <= y1; y++) {
|
|
90
|
-
float dy = (
|
|
110
|
+
float dy = static_cast<float>(y) - cy;
|
|
91
111
|
float dx = r * r - dy * dy;
|
|
92
112
|
if (dx < 0) continue;
|
|
93
113
|
float halfW = sqrtf(dx);
|
|
94
|
-
float leftX = (
|
|
95
|
-
float rightX = (
|
|
114
|
+
float leftX = static_cast<float>(cx) - halfW;
|
|
115
|
+
float rightX = static_cast<float>(cx) + halfW;
|
|
96
116
|
// Left edge: blend two pixels at the coverage split.
|
|
97
|
-
int16_t lx = (
|
|
117
|
+
int16_t lx = static_cast<int16_t>(floor(leftX));
|
|
98
118
|
float lfrac = leftX - lx;
|
|
99
|
-
ui_aa_pixel(c, lx, y, color,
|
|
119
|
+
ui_aa_pixel(c, lx, y, color, static_cast<uint8_t>((1.0f - lfrac) * 255));
|
|
100
120
|
ui_aa_pixel(c, lx + 1, y, color, 0); // interior starts here (drawn solid below)
|
|
101
121
|
// Right edge.
|
|
102
|
-
int16_t rx = (
|
|
122
|
+
int16_t rx = static_cast<int16_t>(floor(rightX));
|
|
103
123
|
float rfrac = rightX - rx;
|
|
104
|
-
ui_aa_pixel(c, rx, y, color,
|
|
124
|
+
ui_aa_pixel(c, rx, y, color, static_cast<uint8_t>(rfrac * 255));
|
|
105
125
|
ui_aa_pixel(c, rx + 1, y, color, 0);
|
|
106
126
|
// Solid fill between edges (skip the edge pixels already blended).
|
|
107
127
|
for (int16_t x = lx + 1; x < rx; x++) {
|
|
@@ -114,21 +134,21 @@ static inline void ui_aa_circle(CuttlefishCanvas16* c, int16_t cx, int16_t cy, f
|
|
|
114
134
|
static inline void ui_aa_fill_circle(CuttlefishCanvas16* c, int16_t cx, int16_t cy, float r, UI_COLOR_T color) {
|
|
115
135
|
if (!c || !display_canvasBuffer(c)) return;
|
|
116
136
|
if (r <= 0) return;
|
|
117
|
-
int16_t y0 = (
|
|
118
|
-
int16_t y1 = (
|
|
137
|
+
int16_t y0 = static_cast<int16_t>(floor(cy - r));
|
|
138
|
+
int16_t y1 = static_cast<int16_t>(ceil(cy + r));
|
|
119
139
|
for (int16_t y = y0; y <= y1; y++) {
|
|
120
|
-
float dy = (
|
|
140
|
+
float dy = static_cast<float>(y) - cy;
|
|
121
141
|
float dx = r * r - dy * dy;
|
|
122
142
|
if (dx < 0) continue;
|
|
123
143
|
float halfW = sqrtf(dx);
|
|
124
|
-
float leftX = (
|
|
125
|
-
float rightX = (
|
|
126
|
-
int16_t lx = (
|
|
127
|
-
int16_t rx = (
|
|
144
|
+
float leftX = static_cast<float>(cx) - halfW;
|
|
145
|
+
float rightX = static_cast<float>(cx) + halfW;
|
|
146
|
+
int16_t lx = static_cast<int16_t>(floor(leftX));
|
|
147
|
+
int16_t rx = static_cast<int16_t>(ceil(rightX));
|
|
128
148
|
// Blend left edge.
|
|
129
|
-
ui_aa_pixel(c, lx, y, color,
|
|
149
|
+
ui_aa_pixel(c, lx, y, color, static_cast<uint8_t>((1.0f - (leftX - lx)) * 255));
|
|
130
150
|
// Blend right edge.
|
|
131
|
-
ui_aa_pixel(c, rx, y, color,
|
|
151
|
+
ui_aa_pixel(c, rx, y, color, static_cast<uint8_t>((rightX - (rx - 1)) * 255));
|
|
132
152
|
// Solid interior.
|
|
133
153
|
for (int16_t x = lx + 1; x < rx; x++) {
|
|
134
154
|
if (x >= 0 && x < display_canvasWidth(c) && y >= 0 && y < display_canvasHeight(c)) display_targetDrawPixel((CuttlefishDisplayTarget*)c, x, y, color);
|
|
@@ -15,9 +15,9 @@ static inline uint16_t ui_blend565(uint16_t fg, uint16_t bg, uint8_t opacity) {
|
|
|
15
15
|
// Unpack to 8-bit (5-bit → 8-bit: (v << 3) | (v >> 2)).
|
|
16
16
|
uint16_t fr8 = (fr << 3) | (fr >> 2), fg8 = (fg5 << 2) | (fg5 >> 4), fb8 = (fb << 3) | (fb >> 2);
|
|
17
17
|
uint16_t br8 = (br << 3) | (br >> 2), bg8 = (bg5 << 2) | (bg5 >> 4), bb8 = (bb << 3) | (bb >> 2);
|
|
18
|
-
uint16_t r =
|
|
19
|
-
uint16_t g =
|
|
20
|
-
uint16_t b =
|
|
18
|
+
uint16_t r = static_cast<uint16_t>((fr8 * opacity + br8 * (100 - opacity)) / 100);
|
|
19
|
+
uint16_t g = static_cast<uint16_t>((fg8 * opacity + bg8 * (100 - opacity)) / 100);
|
|
20
|
+
uint16_t b = static_cast<uint16_t>((fb8 * opacity + bb8 * (100 - opacity)) / 100);
|
|
21
21
|
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -33,7 +33,7 @@ static inline uint32_t ui_blend888(uint32_t fg, uint32_t bg, uint8_t opacity) {
|
|
|
33
33
|
uint8_t r = (fr * opacity + br * (100 - opacity)) / 100;
|
|
34
34
|
uint8_t g = (fg8 * opacity + bg8 * (100 - opacity)) / 100;
|
|
35
35
|
uint8_t b = (fb * opacity + bb * (100 - opacity)) / 100;
|
|
36
|
-
return ((
|
|
36
|
+
return (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | b;
|
|
37
37
|
}
|
|
38
38
|
`;
|
|
39
39
|
}
|