@typecad/ui 0.1.0-alpha.2 → 1.0.0-alpha.10

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 (86) hide show
  1. package/README.md +7 -1
  2. package/dist/engine-index.js +2 -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 +42 -69
  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.d.ts +1 -0
  16. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +563 -0
  17. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +26 -10
  18. package/dist/ui-engine/runtime-header/display-shim.js +8 -8
  19. package/dist/ui-engine/runtime-header/forward-decls.js +52 -10
  20. package/dist/ui-engine/runtime-header/image-drawing.js +154 -84
  21. package/dist/ui-engine/runtime-header/init-press-input.js +20 -5
  22. package/dist/ui-engine/runtime-header/keyboard.js +62 -18
  23. package/dist/ui-engine/runtime-header/node-decoration.js +8 -9
  24. package/dist/ui-engine/runtime-header/node-draw-body.d.ts +1 -0
  25. package/dist/ui-engine/runtime-header/node-draw-body.js +1402 -0
  26. package/dist/ui-engine/runtime-header/paint-order-coords.js +48 -18
  27. package/dist/ui-engine/runtime-header/paint-rects-repair.js +38 -17
  28. package/dist/ui-engine/runtime-header/scroll-physics.js +15 -18
  29. package/dist/ui-engine/runtime-header/state-bindings-nav.js +23 -13
  30. package/dist/ui-engine/runtime-header/structs.js +12 -10
  31. package/dist/ui-engine/runtime-header/text-rendering.js +132 -71
  32. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +29 -10
  33. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +336 -749
  34. package/dist/ui-engine/runtime-header/tick/flush-phase.js +7 -3
  35. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +19 -3
  36. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +31 -24
  37. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +46 -41
  38. package/dist/ui-engine/runtime-header/types-defines.js +23 -3
  39. package/dist/ui-engine/runtime-header.d.ts +10 -1
  40. package/dist/ui-engine/runtime-header.js +7 -1
  41. package/dist/ui-engine/transpile-ui.js +2 -2
  42. package/dist/ui-engine/ua-stylesheet.js +46 -3
  43. package/dist/ui-engine/ui-lowering.d.ts +1 -3
  44. package/dist/ui-engine/ui-lowering.js +18 -24
  45. package/dist/ui-engine/ui-registry.js +1 -6
  46. package/package.json +2 -2
  47. package/src/engine-index.ts +2 -2
  48. package/src/preview/host-ui-runtime.ts +56 -12
  49. package/src/ui-engine/font-assets.ts +13 -0
  50. package/src/ui-engine/layout-engine.ts +19 -6
  51. package/src/ui-engine/model.ts +10 -5
  52. package/src/ui-engine/runtime-header/antialiasing.ts +52 -32
  53. package/src/ui-engine/runtime-header/blend-bodies.ts +4 -4
  54. package/src/ui-engine/runtime-header/canvas-helpers.ts +42 -69
  55. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +22 -10
  56. package/src/ui-engine/runtime-header/color-mono-refresh.ts +10 -10
  57. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +566 -0
  58. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +26 -10
  59. package/src/ui-engine/runtime-header/display-shim.ts +8 -8
  60. package/src/ui-engine/runtime-header/forward-decls.ts +52 -10
  61. package/src/ui-engine/runtime-header/image-drawing.ts +154 -84
  62. package/src/ui-engine/runtime-header/init-press-input.ts +20 -5
  63. package/src/ui-engine/runtime-header/keyboard.ts +62 -18
  64. package/src/ui-engine/runtime-header/node-decoration.ts +8 -9
  65. package/src/ui-engine/runtime-header/node-draw-body.ts +1402 -0
  66. package/src/ui-engine/runtime-header/paint-order-coords.ts +48 -18
  67. package/src/ui-engine/runtime-header/paint-rects-repair.ts +38 -17
  68. package/src/ui-engine/runtime-header/scroll-physics.ts +15 -18
  69. package/src/ui-engine/runtime-header/state-bindings-nav.ts +23 -13
  70. package/src/ui-engine/runtime-header/structs.ts +12 -10
  71. package/src/ui-engine/runtime-header/text-rendering.ts +132 -71
  72. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +29 -10
  73. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +336 -749
  74. package/src/ui-engine/runtime-header/tick/flush-phase.ts +7 -3
  75. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +19 -3
  76. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +31 -24
  77. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +46 -41
  78. package/src/ui-engine/runtime-header/types-defines.ts +23 -3
  79. package/src/ui-engine/runtime-header.ts +17 -1
  80. package/src/ui-engine/transpile-ui.ts +2 -2
  81. package/src/ui-engine/ua-stylesheet.ts +46 -3
  82. package/src/ui-engine/ui-lowering.ts +19 -26
  83. package/src/ui-engine/ui-registry.ts +1 -7
  84. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +0 -16
  85. package/dist/ui-engine/scroll-memory-diagnostics.js +0 -58
  86. package/src/ui-engine/scroll-memory-diagnostics.ts +0 -79
@@ -14,16 +14,14 @@
14
14
  import { lowerUIToModel } from "./model.js";
15
15
  import { resolveColor } from "./color.js";
16
16
  import { DEFAULT_ALPHA_KEYBOARD, DEFAULT_NUMBER_KEYBOARD } from "./default-keyboards.js";
17
- import { analyzeScrollMemory } from "./scroll-memory-diagnostics.js";
18
17
  import { getListBindings } from "@typecad/cuttlefish/ir/transformers/ui-reactive";
19
- export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = [], scrollCanvasBudgetBytes) {
18
+ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = []) {
20
19
  void storage;
21
20
  const model = lowerUIToModel(root, boxes, colorFormat, display, fontAssets, allScreens, imageAssetIds, keyframeSets);
22
- const scrollMemoryDiagnostics = analyzeScrollMemory(model.nodes, scrollCanvasBudgetBytes);
23
21
  // Tables are mutable RAM (ui_tick updates bg/dirty/elapsed/active each
24
22
  // frame), so no PROGMEM/flash storage keyword — those imply read-only.
25
23
  const fontTables = emitFontTables(model);
26
- const nodeTable = emitNodeTable(model) + "\n" + emitScrollNodeIdLookup(model);
24
+ const nodeTable = emitNodeTable(model);
27
25
  const transitionTable = emitTransitionTable(model);
28
26
  const typeDecl = emitTypeDecl(root);
29
27
  // Keyboard loaders + dispatch: collect input nodes in tree order, resolve
@@ -63,7 +61,7 @@ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [],
63
61
  const screenCount = model.nodes.length > 0 ? Math.max(...model.nodes.map(n => n.screenId)) + 1 : 1;
64
62
  const imageTables = "const UIImage __ui_images[] = {};\nconst uint16_t __ui_image_count = 0;";
65
63
  const keyframeTables = emitKeyframeTables(model);
66
- return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, scrollMemoryDiagnostics, diagnostics: [] };
64
+ return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, diagnostics: [] };
67
65
  }
68
66
  function sanitizedId(id) {
69
67
  return id.replace(/[^a-zA-Z0-9_]/g, "_");
@@ -145,7 +143,20 @@ function emitKeyboardLoader(name, kb, rules, colorFormat) {
145
143
  }
146
144
  /** Emit one key's keys[] + styles[] lines, resolving CSS classes to colors. */
147
145
  function emitKeyLine(key, kbClasses, rules, colorFormat) {
148
- const chEsc = key.ch === "\\" ? "\\\\" : key.ch === "'" ? "\\'" : key.ch;
146
+ // The C++ UIKey.ch field is a single `char`. Special keys (shift=1, bs=2,
147
+ // ok=3, page=4) use the `special` field for their behavior and label — the
148
+ // `ch` value is never read. Their label strings ("OK", "123", "⇧", "⌫") are
149
+ // multi-char or multi-byte and would overflow `char` if emitted as literals.
150
+ // Emit a space placeholder for any key whose ch isn't a single ASCII byte.
151
+ const isSingleAscii = key.special === 0 && key.ch.length === 1 && key.ch.charCodeAt(0) < 128;
152
+ if (key.special === 0 && !isSingleAscii) {
153
+ console.warn(`[cuttlefish] keyboard key ch="${key.ch}" is non-ASCII and will be emitted as ` +
154
+ `a space placeholder. The native C++ UIKey.ch field is a single char and ` +
155
+ `cannot hold multi-byte codepoints. Use a custom keyboard layout or the ` +
156
+ `Adafruit path for full Unicode key labels.`);
157
+ }
158
+ const chRaw = isSingleAscii ? key.ch : ' ';
159
+ const chEsc = chRaw === "\\" ? "\\\\" : chRaw === "'" ? "\\'" : chRaw;
149
160
  const style = resolveKeyStyle(key.classes, kbClasses, rules);
150
161
  const bg = style.background ? resolveColor(style.background, colorFormat) : DEFAULT_KEY_BG;
151
162
  const fg = style.color ? resolveColor(style.color, colorFormat) : DEFAULT_KEY_FG;
@@ -164,6 +175,7 @@ function cppKind(kind) {
164
175
  case "img": return "NODE_IMG";
165
176
  case "list": return "NODE_LIST";
166
177
  case "canvas": return "NODE_CANVAS";
178
+ case "select": return "NODE_SELECT";
167
179
  case "text": return "NODE_TEXT";
168
180
  }
169
181
  }
@@ -213,24 +225,6 @@ function emitFontTables(model) {
213
225
  lines.push(`const uint16_t __ui_font_face_count = ${assets.length};`);
214
226
  return lines.join("\n");
215
227
  }
216
- function emitScrollNodeIdLookup(model) {
217
- const cases = [];
218
- for (const n of model.nodes) {
219
- if (!n.scrollable || n.contentHeight <= n.box.h || !n.id)
220
- continue;
221
- const id = n.id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
222
- cases.push(` if (idx == ${n.index}) return "${id}";`);
223
- }
224
- if (cases.length === 0) {
225
- return "static inline const char* __ui_scroll_node_id(uint16_t idx) { (void)idx; return nullptr; }";
226
- }
227
- return [
228
- "static inline const char* __ui_scroll_node_id(uint16_t idx) {",
229
- ...cases,
230
- " return nullptr;",
231
- "}",
232
- ].join("\n");
233
- }
234
228
  function emitNodeTable(model) {
235
229
  // Map node index → list binding so virtualized nodes carry their count/item/
236
230
  // tap function pointers on-node (no UIListBinding side table at runtime).
@@ -25,7 +25,6 @@ import { selectEngine } from "./select-engine.js";
25
25
  import { measureWithFonts } from "./layout-engine.js";
26
26
  import { lowerUIToCpp } from "./ui-lowering.js";
27
27
  import { getDisplayProfile } from "@typecad/cuttlefish/stores/display-profile-store";
28
- import { resolveScrollConfig } from "@typecad/cuttlefish/api/shared";
29
28
  import { buildUIFontAssets } from "./font-assets.js";
30
29
  import { emitImageTables, loadImageAssets } from "./image-assets.js";
31
30
  import { splitUiFile } from "./ui-file-splitter.js";
@@ -145,11 +144,7 @@ export function lowerOnMount(htmlPath, opts) {
145
144
  const keyframeSets = buildKeyframeSets(mod.rawKeyframes || [], opts.colorFormat);
146
145
  // Load image assets before lowering so imgDataId can be set.
147
146
  const imageAssets = loadImageAssets(allStyled.length > 0 ? allStyled : [mod.styled], path.dirname(abs));
148
- const scrollBudget = resolveScrollConfig(getDisplayProfile(), { buildTarget: getDisplayProfile()._buildTarget }).scrollCanvasBudgetBytes;
149
- const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets, scrollBudget);
150
- for (const d of result.scrollMemoryDiagnostics) {
151
- mod.mountDiagnostics.push({ ...d, source: d.source ?? path.basename(mod.htmlPath) });
152
- }
147
+ const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets);
153
148
  // Layout diagnostics: viewport-overflow (node bottom past the viewport) and
154
149
  // text-overflow (node right edge past its parent). Both reference the node by
155
150
  // a human-label (#id / <tag "text"> / <tag>) so the author can find it in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/ui",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "1.0.0-alpha.10",
4
4
  "description": "TypeCAD UI authoring library — HTML/CSS-driven graphics for microcontrollers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "src"
34
34
  ],
35
35
  "peerDependencies": {
36
- "@typecad/cuttlefish": "0.1.0-alpha.2"
36
+ "@typecad/cuttlefish": "1.0.0-alpha.10"
37
37
  },
38
38
  "dependencies": {
39
39
  "css-tree": "^3.2.1",
@@ -19,8 +19,8 @@ import {
19
19
  } from "./ui-engine/ui-registry.js";
20
20
  import { resolveColor, resolveColorInternal } from "./ui-engine/color.js";
21
21
  import { emitRuntimeHeader } from "./ui-engine/runtime-header.js";
22
+ import { emitCuttlefishGfx } from "./ui-engine/runtime-header/cuttlefish-gfx.js";
22
23
  import { splitUiFile } from "./ui-engine/ui-file-splitter.js";
23
- import { analyzeScrollMemory } from "./ui-engine/scroll-memory-diagnostics.js";
24
24
 
25
25
  export type { TranspilerUIHook };
26
26
 
@@ -44,8 +44,8 @@ export function registerTranspilerUI(): TranspilerUIHook {
44
44
  resolveColor,
45
45
  resolveColorInternal,
46
46
  emitRuntimeHeader,
47
+ emitCuttlefishGfx,
47
48
  splitUiFile,
48
49
  generateProjectUITypeDeclarations,
49
- analyzeScrollMemory,
50
50
  };
51
51
  }
@@ -1121,7 +1121,7 @@ export class PreviewUIRuntime {
1121
1121
  const shadow = this.shadowExtents(node);
1122
1122
  let faceW = node.box.w;
1123
1123
  let faceH = node.box.h;
1124
- if (node.kind === "text" || node.kind === "check" || node.kind === "radio") {
1124
+ if (node.kind === "text" || node.kind === "check" || node.kind === "radio" || node.kind === "select") {
1125
1125
  if (node.lastTextWidth > faceW) faceW = node.lastTextWidth;
1126
1126
  if ((node.lastTextHeight ?? 0) > faceH) faceH = node.lastTextHeight;
1127
1127
  if (textW > faceW) faceW = textW;
@@ -1147,7 +1147,7 @@ export class PreviewUIRuntime {
1147
1147
  const displayText = node.hasTextBinding ? node.textBuffer : node.text;
1148
1148
  const ts = this.nodeTextSize(node);
1149
1149
  let textMaxW = node.box.w;
1150
- if (node.kind === "text" || node.kind === "button") {
1150
+ if (node.kind === "text" || node.kind === "button" || node.kind === "select") {
1151
1151
  const insets = this.textInsets(node);
1152
1152
  textMaxW = Math.max(0, node.box.w - insets.left - insets.right);
1153
1153
  }
@@ -1157,7 +1157,7 @@ export class PreviewUIRuntime {
1157
1157
  const metrics = this.textLayout(node, displayText, textMaxW, ts);
1158
1158
  let paintTextW = metrics.width;
1159
1159
  let paintTextH = metrics.height;
1160
- if (node.kind === "text") {
1160
+ if (node.kind === "text" || node.kind === "select") {
1161
1161
  const insets = this.textInsets(node);
1162
1162
  paintTextW += insets.left + insets.right;
1163
1163
  paintTextH += insets.top + insets.bottom;
@@ -1277,20 +1277,26 @@ export class PreviewUIRuntime {
1277
1277
  const displayText = node.hasTextBinding ? node.textBuffer : node.text;
1278
1278
  const ts = this.nodeTextSize(node);
1279
1279
  let textMaxW = node.box.w;
1280
- if (node.kind === "text" || node.kind === "button") {
1280
+ if (node.kind === "text" || node.kind === "button" || node.kind === "select") {
1281
1281
  const insets = this.textInsets(node);
1282
1282
  textMaxW = Math.max(0, node.box.w - insets.left - insets.right);
1283
1283
  }
1284
1284
  const metrics = this.textLayout(node, displayText, textMaxW, ts);
1285
- const insets = node.kind === "text" ? this.textInsets(node) : { left: 0, right: 0, top: 0, bottom: 0 };
1285
+ const insets = (node.kind === "text" || node.kind === "select") ? this.textInsets(node) : { left: 0, right: 0, top: 0, bottom: 0 };
1286
+ let clearW = metrics.width + insets.left + insets.right;
1287
+ let clearH = metrics.height + insets.top + insets.bottom;
1288
+ if (node.kind === "check" || node.kind === "radio") {
1289
+ clearW += 22;
1290
+ if (clearH < 16) clearH = 16;
1291
+ }
1286
1292
  const rect = this.nodePaintRect(
1287
1293
  node,
1288
1294
  this.baseDrawXForNode(node.index),
1289
1295
  this.baseDrawYForNode(node.index),
1290
1296
  this.drawXForNode(node.index),
1291
1297
  this.drawYForNode(node.index),
1292
- metrics.width + insets.left + insets.right,
1293
- metrics.height + insets.top + insets.bottom,
1298
+ clearW,
1299
+ clearH,
1294
1300
  );
1295
1301
  this.clearNodePaintRect(node, rect);
1296
1302
  }
@@ -1879,6 +1885,9 @@ export class PreviewUIRuntime {
1879
1885
  case "button":
1880
1886
  this.drawButtonNode(node, displayText, bColor, fillBg, drawY, ts);
1881
1887
  break;
1888
+ case "select":
1889
+ this.drawSelectNode(node, displayText, bColor, fillBg, drawY, ts);
1890
+ break;
1882
1891
  case "check":
1883
1892
  this.drawCheckNode(node, displayText, drawY, ts);
1884
1893
  break;
@@ -2343,6 +2352,35 @@ export class PreviewUIRuntime {
2343
2352
  this.drawTextLines(node, displayText, textX, top, textW, ts, node.fg, glyphBg, node.textAlign);
2344
2353
  }
2345
2354
 
2355
+ // <select>: bordered box with its current option text VERTICALLY CENTERED
2356
+ // (unlike drawTextNode, which top-aligns). Mirrors the C++ NODE_SELECT case
2357
+ // so preview and device agree. The label is dynamic (auto-bound to the
2358
+ // selected option), so clear the previous text rect before redrawing.
2359
+ private drawSelectNode(node: MutableNode, displayText: string | undefined, bColor: number, fillBg: number, drawY: number, ts: number): void {
2360
+ const insets = this.textInsets(node);
2361
+ const contentW = Math.max(1, node.box.w - insets.left - insets.right);
2362
+ const layout = this.textLayout(node, displayText, contentW, ts);
2363
+ const clearW = Math.max(node.box.w, node.lastTextWidth ?? 0, layout.width + insets.left + insets.right);
2364
+ const clearH = Math.max(node.box.h, node.lastTextHeight ?? 0, layout.height + insets.top + insets.bottom);
2365
+ const clearCol = node.hasBg ? node.bg : node.clearColor;
2366
+ this.gfx.fillRect(node.box.x, drawY, clearW, clearH, clearCol);
2367
+ node.lastTextWidth = layout.width;
2368
+ node.lastTextHeight = layout.height;
2369
+
2370
+ if (node.borderRadius > 0 && node.hasBg) this.gfx.fillRoundRect(node.box.x, drawY, node.box.w, node.box.h, node.borderRadius, fillBg);
2371
+ else if (node.hasBg) this.gfx.fillRect(node.box.x, drawY, node.box.w, node.box.h, fillBg);
2372
+ this.drawNodeShadow(node, drawY, true);
2373
+ if (node.borderStyle) this.drawNodeBorder(node, node.box.x, drawY, bColor);
2374
+
2375
+ const textX = node.box.x + insets.left;
2376
+ const textY = drawY + insets.top;
2377
+ const textW = Math.max(1, node.box.w - insets.left - insets.right);
2378
+ const textH = Math.max(1, node.box.h - insets.top - insets.bottom);
2379
+ const top = textY + Math.trunc((textH - layout.height) / 2);
2380
+ const glyphBg = node.opacity < 100 ? blendRuntime(node.bg, this.parentClearColor(node), node.opacity) : (node.hasBg ? node.bg : node.clearColor);
2381
+ this.drawTextLines(node, displayText, textX, top, textW, ts, node.fg, glyphBg, node.textAlign);
2382
+ }
2383
+
2346
2384
  private drawCheckNode(node: MutableNode, displayText: string | undefined, drawY: number, ts: number): void {
2347
2385
  const layout = this.textLayout(node, displayText, Math.max(0, node.box.w - 22), ts);
2348
2386
  const clearW = Math.max(node.box.w, node.lastTextWidth);
@@ -2352,7 +2390,11 @@ export class PreviewUIRuntime {
2352
2390
  node.lastTextHeight = Math.max(layout.height, 16);
2353
2391
 
2354
2392
  const cbX = node.box.x;
2355
- const cbY = drawY;
2393
+ // Vertically center the 16px indicator within the box so a tall
2394
+ // (touch-friendly) checkbox doesn't pin the indicator to the top.
2395
+ // (box.h - 16) / 2 is 0 for the default 16px-tall box.
2396
+ const checkOff = Math.max(0, ((node.box.h - 16) / 2) | 0);
2397
+ const cbY = drawY + checkOff;
2356
2398
  if (node.value) {
2357
2399
  this.gfx.fillRect(cbX, cbY, 16, 16, node.fg);
2358
2400
  const inv = node.hasBg ? node.bg : node.clearColor;
@@ -2365,7 +2407,7 @@ export class PreviewUIRuntime {
2365
2407
  } else {
2366
2408
  this.gfx.drawRect(cbX, cbY, 16, 16, node.fg);
2367
2409
  }
2368
- 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);
2410
+ 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);
2369
2411
  }
2370
2412
 
2371
2413
  private drawRadioNode(node: MutableNode, displayText: string | undefined, drawY: number, ts: number): void {
@@ -2377,14 +2419,16 @@ export class PreviewUIRuntime {
2377
2419
  node.lastTextHeight = Math.max(layout.height, 16);
2378
2420
 
2379
2421
  const cbX = node.box.x;
2380
- const cbY = drawY;
2422
+ // Vertically center the 16px indicator within the box (see drawCheckNode).
2423
+ const radioOff = Math.max(0, ((node.box.h - 16) / 2) | 0);
2424
+ const cbY = drawY + radioOff;
2381
2425
  if (node.value) {
2382
2426
  this.gfx.fillCircle(cbX + 8, cbY + 8, 7, node.fg);
2383
2427
  this.gfx.fillCircle(cbX + 8, cbY + 8, 3, node.hasBg ? node.bg : node.clearColor);
2384
2428
  } else {
2385
2429
  this.gfx.drawCircle(cbX + 8, cbY + 8, 7, node.fg);
2386
2430
  }
2387
- 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);
2431
+ 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);
2388
2432
  }
2389
2433
 
2390
2434
  private drawProgressNode(node: MutableNode, drawY: number): void {
@@ -2790,7 +2834,7 @@ export class PreviewUIRuntime {
2790
2834
  node.value = node.value > 0 ? 0 : 1;
2791
2835
  this.markDirty(nodeIndex);
2792
2836
  } else if (node.tag === "select") {
2793
- const count = Math.max(node.options?.length ?? 0, 2);
2837
+ const count = Math.max(node.options?.length ?? 0, 1);
2794
2838
  node.value = (node.value + 1) % count;
2795
2839
  this.markDirty(nodeIndex);
2796
2840
  } else if (node.tag === "radio") {
@@ -158,6 +158,19 @@ export function assetTextWidth(text: string, style: CSSProperty, fontAssets: UIF
158
158
  return w;
159
159
  }
160
160
 
161
+ /** The line height the runtime will render a custom-font node at
162
+ * (ui_asset_text_height → face->lineHeight), or undefined when the node uses
163
+ * the default font. Layout must use this for the glyph-cell height instead of
164
+ * the 8*ts GFX bitmap default, otherwise the laid-out box is shorter than the
165
+ * drawn glyphs and the text overflows its container (e.g. a bold @font-face
166
+ * title spilling past its header's padded box). Mirrors the preview's
167
+ * textHeight(size, fontFace) = asset.lineHeight ?? gfx.textHeight(size). */
168
+ export function assetLineHeight(style: CSSProperty, fontAssets: UIFontAssetModel[]): number | undefined {
169
+ const asset = selectFontAssetForStyle(fontAssets, style);
170
+ if (!asset || asset.lineHeight <= 0) return undefined;
171
+ return asset.lineHeight;
172
+ }
173
+
161
174
  export function buildUIFontAssets(
162
175
  root: StyledNode,
163
176
  fontFaces: CSSFontFace[],
@@ -14,7 +14,7 @@
14
14
  import { StyledNode } from "./style-resolver.js";
15
15
  import { layoutText } from "./text-layout.js";
16
16
  import { layoutRuns } from "./rich-layout.js";
17
- import { assetTextWidth } from "./font-assets.js";
17
+ import { assetTextWidth, assetLineHeight } from "./font-assets.js";
18
18
  import type { UIFontAssetModel } from "./font-assets.js";
19
19
 
20
20
  export interface Box { x: number; y: number; w: number; h: number; }
@@ -135,12 +135,16 @@ export function measure(node: StyledNode, availableWidth?: number, fontAssets: U
135
135
  const rs = { ...node.style, ...r.style } as typeof node.style;
136
136
  const rTs = gfxTextSizeOf({ ...node, style: rs });
137
137
  const rAdvance = 6 * rTs + letterSpacingOf({ ...node, style: rs });
138
+ // Run glyph height: prefer the asset's lineHeight for custom fonts (see
139
+ // charH below); fall back to the 8*rTs GFX bitmap default.
140
+ const rAssetH = assetLineHeight(rs, fontAssets);
141
+ const rHeight = rAssetH ?? (8 * rTs);
138
142
  return {
139
143
  text: applyTextTransform(r.text, { ...node, style: rs }),
140
144
  hardBreak: r.hardBreak,
141
145
  measureText: (s: string) => assetTextWidth(s, rs, fontAssets) ?? textWidthOf(s, rAdvance),
142
- height: 8 * rTs,
143
- ascent: 7 * rTs,
146
+ height: rHeight,
147
+ ascent: rAssetH ?? (7 * rTs),
144
148
  };
145
149
  });
146
150
  const layout = layoutRuns(layoutInput, { maxWidth: availableWidth, whiteSpace: node.style.whiteSpace });
@@ -149,7 +153,13 @@ export function measure(node: StyledNode, availableWidth?: number, fontAssets: U
149
153
  // Per-node text size (from font-size + font-weight CSS).
150
154
  const ts = gfxTextSizeOf(node);
151
155
  const advance = 6 * ts + letterSpacingOf(node);
152
- const charH = 8 * ts;
156
+ // Glyph-cell height: the runtime draws a custom @font-face at its real
157
+ // lineHeight (ui_asset_text_height → face->lineHeight), NOT the 8*ts GFX
158
+ // bitmap default. Use the asset's lineHeight when one matches so the box
159
+ // matches the drawn height (otherwise bold @font-face titles overflow their
160
+ // header). Falls back to 8*ts for the default font. Mirrors the preview.
161
+ const assetH = assetLineHeight(node.style, fontAssets);
162
+ const charH = assetH ?? (8 * ts);
153
163
  // For interpolation text (e.g. "taps: {count}"), strip the braces so layout
154
164
  // measures "taps: count" — closer to the resolved runtime width than the
155
165
  // literal "{count}". The braces are an authoring delimiter, not rendered.
@@ -199,10 +209,13 @@ export function measure(node: StyledNode, availableWidth?: number, fontAssets: U
199
209
  return { w: 200, h: 20 };
200
210
  }
201
211
  if (node.tag === "input") {
202
- // Input field: sized to the placeholder or a default width, 20px tall.
212
+ // Input field: sized to the placeholder or a default width. Height defaults
213
+ // to 20px but an explicit `height` style overrides it (the UA input rule no
214
+ // longer forces a min-height, so the author's height wins).
203
215
  const text = node.placeholder ?? "";
204
216
  const textW = text.length > 0 ? text.length * GFX_ADVANCE_PER_CHAR : 120;
205
- return { w: Math.max(textW + 16, 120), h: 20 };
217
+ const explicitH = node.style.height ? parseInt(String(node.style.height), 10) : NaN;
218
+ return { w: Math.max(textW + 16, 120), h: Number.isNaN(explicitH) ? 20 : explicitH };
206
219
  }
207
220
  if (node.tag === "img") {
208
221
  return { w: (node as any).imgWidth ?? 32, h: (node as any).imgHeight ?? 32 };
@@ -4,7 +4,7 @@ import { deriveCapabilities } from "@typecad/cuttlefish/api/shared";
4
4
  import { resolveColorInternal } from "./color.js";
5
5
  import { parseAnimation, type CSSProperty } from "./css-parser.js";
6
6
  import type { UIFontAssetModel } from "./font-assets.js";
7
- import { selectFontAssetForStyle, assetTextWidth } from "./font-assets.js";
7
+ import { selectFontAssetForStyle, assetTextWidth, assetLineHeight } from "./font-assets.js";
8
8
  import type { UIImageAsset } from "./image-assets.js";
9
9
  import { isDisplayNone, type Box } from "./layout-engine.js";
10
10
  import type { StyledNode } from "./style-resolver.js";
@@ -21,7 +21,7 @@ export {
21
21
  timingFunctionCode,
22
22
  } from "./easing.js";
23
23
 
24
- export type UINodeKindModel = "fill" | "text" | "button" | "check" | "radio" | "progress" | "range" | "input" | "img" | "list" | "canvas";
24
+ export type UINodeKindModel = "fill" | "text" | "button" | "check" | "radio" | "progress" | "range" | "input" | "img" | "list" | "canvas" | "select";
25
25
  export type UIPropertyModel = "background" | "color" | "text" | "visible" | "borderColor";
26
26
 
27
27
  export interface UINodeModel {
@@ -264,6 +264,7 @@ function nodeKind(tag: string): UINodeKindModel {
264
264
  if (tag === "img") return "img";
265
265
  if (tag === "list") return "list";
266
266
  if (tag === "canvas") return "canvas";
267
+ if (tag === "select") return "select";
267
268
  return "text";
268
269
  }
269
270
 
@@ -953,6 +954,7 @@ export function lowerUIToModel(
953
954
  const rs = runStyleOf(r);
954
955
  const rTextSize = textSizeOf(rs);
955
956
  const rLetterSpacing = letterSpacingOf(rs);
957
+ const rAssetH = assetLineHeight(rs, fontAssets);
956
958
  return {
957
959
  text: applyTextTransform(r.text, rs) ?? "",
958
960
  hardBreak: r.hardBreak,
@@ -960,8 +962,11 @@ export function lowerUIToModel(
960
962
  // flat 6*textSize + letterSpacing per-char default-font advance.
961
963
  measureText: (s: string) =>
962
964
  assetTextWidth(s, rs, fontAssets) ?? textWidthOfDefault(s, rTextSize, rLetterSpacing),
963
- height: 8 * rTextSize,
964
- ascent: 7 * rTextSize,
965
+ // Asset lineHeight when the run has a custom font, else 8*textSize.
966
+ // Must match layout-engine.ts's rich-run height (so the baked
967
+ // runLines geometry matches the measured box).
968
+ height: rAssetH ?? (8 * rTextSize),
969
+ ascent: rAssetH ?? (7 * rTextSize),
965
970
  };
966
971
  });
967
972
  const layout = layoutRuns(layoutInput, { maxWidth: box.w, whiteSpace: node.style.whiteSpace });
@@ -1029,7 +1034,7 @@ export function lowerUIToModel(
1029
1034
  hasBg,
1030
1035
  textAlign: textAlign(node.style),
1031
1036
  textSize,
1032
- lineHeight: lineHeightOf(node.style, textSize),
1037
+ lineHeight: assetLineHeight(node.style, fontAssets) ?? lineHeightOf(node.style, textSize),
1033
1038
  letterSpacing: letterSpacingOf(node.style),
1034
1039
  fontAntialias: fontAntialiasOf(node.style, display),
1035
1040
  fontFace: fontFaceOf(node.style, fontAssets),
@@ -12,10 +12,18 @@ export function emitAntialiasing(): string {
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
  }