@typecad/ui 1.0.0-alpha.7 → 1.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +6 -0
  2. package/dist/engine-index.js +0 -2
  3. package/dist/preview/host-ui-runtime.d.ts +1 -0
  4. package/dist/preview/host-ui-runtime.js +56 -11
  5. package/dist/ui-engine/font-assets.d.ts +8 -0
  6. package/dist/ui-engine/font-assets.js +13 -0
  7. package/dist/ui-engine/layout-engine.js +19 -6
  8. package/dist/ui-engine/model.d.ts +1 -1
  9. package/dist/ui-engine/model.js +10 -4
  10. package/dist/ui-engine/runtime-header/antialiasing.js +52 -32
  11. package/dist/ui-engine/runtime-header/blend-bodies.js +4 -4
  12. package/dist/ui-engine/runtime-header/canvas-helpers.js +32 -65
  13. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +22 -10
  14. package/dist/ui-engine/runtime-header/color-mono-refresh.js +10 -10
  15. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +59 -29
  16. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +26 -10
  17. package/dist/ui-engine/runtime-header/display-shim.js +8 -8
  18. package/dist/ui-engine/runtime-header/forward-decls.js +48 -6
  19. package/dist/ui-engine/runtime-header/image-drawing.js +154 -84
  20. package/dist/ui-engine/runtime-header/init-press-input.js +20 -5
  21. package/dist/ui-engine/runtime-header/keyboard.js +19 -16
  22. package/dist/ui-engine/runtime-header/node-decoration.js +8 -8
  23. package/dist/ui-engine/runtime-header/node-draw-body.d.ts +1 -0
  24. package/dist/ui-engine/runtime-header/node-draw-body.js +1402 -0
  25. package/dist/ui-engine/runtime-header/paint-order-coords.js +48 -18
  26. package/dist/ui-engine/runtime-header/paint-rects-repair.js +38 -17
  27. package/dist/ui-engine/runtime-header/scroll-physics.js +15 -14
  28. package/dist/ui-engine/runtime-header/state-bindings-nav.js +23 -13
  29. package/dist/ui-engine/runtime-header/structs.js +12 -10
  30. package/dist/ui-engine/runtime-header/text-rendering.js +132 -71
  31. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +29 -10
  32. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +299 -747
  33. package/dist/ui-engine/runtime-header/tick/flush-phase.js +7 -3
  34. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +19 -3
  35. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +31 -24
  36. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +42 -40
  37. package/dist/ui-engine/runtime-header/types-defines.js +23 -3
  38. package/dist/ui-engine/runtime-header.js +2 -0
  39. package/dist/ui-engine/transpile-ui.js +2 -2
  40. package/dist/ui-engine/ua-stylesheet.js +46 -3
  41. package/dist/ui-engine/ui-lowering.d.ts +1 -3
  42. package/dist/ui-engine/ui-lowering.js +4 -23
  43. package/dist/ui-engine/ui-registry.js +1 -7
  44. package/package.json +2 -2
  45. package/src/engine-index.ts +0 -2
  46. package/src/preview/host-ui-runtime.ts +56 -12
  47. package/src/ui-engine/font-assets.ts +13 -0
  48. package/src/ui-engine/layout-engine.ts +19 -6
  49. package/src/ui-engine/model.ts +10 -5
  50. package/src/ui-engine/runtime-header/antialiasing.ts +52 -32
  51. package/src/ui-engine/runtime-header/blend-bodies.ts +4 -4
  52. package/src/ui-engine/runtime-header/canvas-helpers.ts +32 -65
  53. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +22 -10
  54. package/src/ui-engine/runtime-header/color-mono-refresh.ts +10 -10
  55. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +59 -29
  56. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +26 -10
  57. package/src/ui-engine/runtime-header/display-shim.ts +8 -8
  58. package/src/ui-engine/runtime-header/forward-decls.ts +48 -6
  59. package/src/ui-engine/runtime-header/image-drawing.ts +154 -84
  60. package/src/ui-engine/runtime-header/init-press-input.ts +20 -5
  61. package/src/ui-engine/runtime-header/keyboard.ts +19 -16
  62. package/src/ui-engine/runtime-header/node-decoration.ts +8 -8
  63. package/src/ui-engine/runtime-header/node-draw-body.ts +1402 -0
  64. package/src/ui-engine/runtime-header/paint-order-coords.ts +48 -18
  65. package/src/ui-engine/runtime-header/paint-rects-repair.ts +38 -17
  66. package/src/ui-engine/runtime-header/scroll-physics.ts +15 -14
  67. package/src/ui-engine/runtime-header/state-bindings-nav.ts +23 -13
  68. package/src/ui-engine/runtime-header/structs.ts +12 -10
  69. package/src/ui-engine/runtime-header/text-rendering.ts +132 -71
  70. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +29 -10
  71. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +299 -747
  72. package/src/ui-engine/runtime-header/tick/flush-phase.ts +7 -3
  73. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +19 -3
  74. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +31 -24
  75. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +42 -40
  76. package/src/ui-engine/runtime-header/types-defines.ts +23 -3
  77. package/src/ui-engine/runtime-header.ts +2 -0
  78. package/src/ui-engine/transpile-ui.ts +2 -2
  79. package/src/ui-engine/ua-stylesheet.ts +46 -3
  80. package/src/ui-engine/ui-lowering.ts +3 -25
  81. package/src/ui-engine/ui-registry.ts +1 -8
  82. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +0 -16
  83. package/dist/ui-engine/scroll-memory-diagnostics.js +0 -58
  84. 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
 
@@ -14,7 +14,6 @@ import { resolveColor, resolveColorInternal } from "./ui-engine/color.js";
14
14
  import { emitRuntimeHeader } from "./ui-engine/runtime-header.js";
15
15
  import { emitCuttlefishGfx } from "./ui-engine/runtime-header/cuttlefish-gfx.js";
16
16
  import { splitUiFile } from "./ui-engine/ui-file-splitter.js";
17
- import { analyzeScrollMemory } from "./ui-engine/scroll-memory-diagnostics.js";
18
17
  /**
19
18
  * Build and return the TranspilerUIHook — the object cuttlefish core uses
20
19
  * to access all UI capabilities (parsing, layout, lowering, emission).
@@ -38,6 +37,5 @@ export function registerTranspilerUI() {
38
37
  emitCuttlefishGfx,
39
38
  splitUiFile,
40
39
  generateProjectUITypeDeclarations,
41
- analyzeScrollMemory,
42
40
  };
43
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
- const rect = this.nodePaintRect(node, this.baseDrawXForNode(node.index), this.baseDrawYForNode(node.index), this.drawXForNode(node.index), this.drawYForNode(node.index), metrics.width + insets.left + insets.right, metrics.height + insets.top + insets.bottom);
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
- const cbY = drawY;
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
- const cbY = drawY;
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, 2);
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: 8 * rTs,
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
- const charH = 8 * ts;
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, 20px tall.
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
- return { w: Math.max(textW + 16, 120), h: 20 };
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;
@@ -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
- height: 8 * rTextSize,
735
- ascent: 7 * rTextSize,
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) + (int32_t)row * w, w, 1);
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 = (uint8_t)((uint16_t)cov * 100 / 255);
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 (int16_t)f; };
55
- auto round_f = [](float f) { return (int16_t)(f + 0.5f); };
56
- auto fpart = [](float f) { return f - (float)(int16_t)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
- int16_t xpx1 = round_f(x0);
67
- float xend = x0 + 0.5f * (xpx1 - x0) * (xpx1 - x0 < 0 ? -1 : 0); // simplified
68
- float intery = y0 + gradient * (xpx1 - x0);
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
- for (int16_t x = xpx1; x <= ipart(x1); x++) {
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, (uint8_t)(rfpart(intery) * 255));
73
- ui_aa_pixel(c, ipart(intery) + 1, x, color, (uint8_t)(fpart(intery) * 255));
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, (uint8_t)(rfpart(intery) * 255));
76
- ui_aa_pixel(c, x, ipart(intery) + 1, color, (uint8_t)(fpart(intery) * 255));
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 = (int16_t)floor(cy - r);
88
- int16_t y1 = (int16_t)ceil(cy + r);
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 = (float)y - cy;
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 = (float)cx - halfW;
95
- float rightX = (float)cx + halfW;
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 = (int16_t)floor(leftX);
117
+ int16_t lx = static_cast<int16_t>(floor(leftX));
98
118
  float lfrac = leftX - lx;
99
- ui_aa_pixel(c, lx, y, color, (uint8_t)((1.0f - lfrac) * 255));
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 = (int16_t)floor(rightX);
122
+ int16_t rx = static_cast<int16_t>(floor(rightX));
103
123
  float rfrac = rightX - rx;
104
- ui_aa_pixel(c, rx, y, color, (uint8_t)(rfrac * 255));
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 = (int16_t)floor(cy - r);
118
- int16_t y1 = (int16_t)ceil(cy + r);
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 = (float)y - cy;
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 = (float)cx - halfW;
125
- float rightX = (float)cx + halfW;
126
- int16_t lx = (int16_t)floor(leftX);
127
- int16_t rx = (int16_t)ceil(rightX);
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, (uint8_t)((1.0f - (leftX - lx)) * 255));
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, (uint8_t)((rightX - (rx - 1)) * 255));
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 = (uint16_t)((fr8 * opacity + br8 * (100 - opacity)) / 100);
19
- uint16_t g = (uint16_t)((fg8 * opacity + bg8 * (100 - opacity)) / 100);
20
- uint16_t b = (uint16_t)((fb8 * opacity + bb8 * (100 - opacity)) / 100);
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 ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
36
+ return (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | b;
37
37
  }
38
38
  `;
39
39
  }