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

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 (27) hide show
  1. package/dist/engine-index.js +2 -0
  2. package/dist/ui-engine/runtime-header/canvas-helpers.js +12 -6
  3. package/dist/ui-engine/runtime-header/cuttlefish-gfx.d.ts +1 -0
  4. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +533 -0
  5. package/dist/ui-engine/runtime-header/forward-decls.js +5 -5
  6. package/dist/ui-engine/runtime-header/keyboard.js +45 -4
  7. package/dist/ui-engine/runtime-header/node-decoration.js +0 -1
  8. package/dist/ui-engine/runtime-header/scroll-physics.js +0 -4
  9. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +42 -7
  10. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +4 -1
  11. package/dist/ui-engine/runtime-header.d.ts +10 -1
  12. package/dist/ui-engine/runtime-header.js +5 -1
  13. package/dist/ui-engine/ui-lowering.js +14 -1
  14. package/dist/ui-engine/ui-registry.js +2 -1
  15. package/package.json +2 -2
  16. package/src/engine-index.ts +2 -0
  17. package/src/ui-engine/runtime-header/canvas-helpers.ts +12 -6
  18. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +536 -0
  19. package/src/ui-engine/runtime-header/forward-decls.ts +5 -5
  20. package/src/ui-engine/runtime-header/keyboard.ts +45 -4
  21. package/src/ui-engine/runtime-header/node-decoration.ts +0 -1
  22. package/src/ui-engine/runtime-header/scroll-physics.ts +0 -4
  23. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +42 -7
  24. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +4 -1
  25. package/src/ui-engine/runtime-header.ts +15 -1
  26. package/src/ui-engine/ui-lowering.ts +16 -1
  27. package/src/ui-engine/ui-registry.ts +2 -1
@@ -328,15 +328,56 @@ static inline void ui_kb_draw_text_row() {
328
328
  }
329
329
 
330
330
  // Draw the full keyboard overlay (background + text row + all keys).
331
+ // Renders into a dedicated offscreen canvas first, then pushes in a single
332
+ // SPI transaction — avoids the per-key/per-rect display writes that show up
333
+ // as visible row-by-row painting on SPI TFTs.
334
+ // (__ui_kb_canvas is forward-declared in the touch-keyboard-fwd slice.)
331
335
  static inline void ui_kb_draw() {
332
- // Opaque background over the keyboard box.
333
- ui_display_fill_rect(__ui_kb_box.x, __ui_kb_box.y, __ui_kb_box.w, __ui_kb_box.h, __ui_kb_bg);
336
+ int16_t kw = __ui_kb_box.w;
337
+ int16_t kh = __ui_kb_box.h;
338
+ if (kw <= 0 || kh <= 0) return;
339
+ // (Re)allocate the keyboard canvas to exact size (see repair-canvas comment
340
+ // — stride mismatch corrupts the push when reusing a differently-sized canvas).
341
+ if (!__ui_kb_canvas || !display_canvasBuffer(__ui_kb_canvas) ||
342
+ display_canvasWidth(__ui_kb_canvas) != kw ||
343
+ display_canvasHeight(__ui_kb_canvas) != kh) {
344
+ display_deleteCanvas(__ui_kb_canvas);
345
+ __ui_kb_canvas = display_createCanvas(kw, kh);
346
+ }
347
+ if (!__ui_kb_canvas || !display_canvasBuffer(__ui_kb_canvas)) {
348
+ // Canvas alloc failed — fall back to direct draw (slow but correct).
349
+ ui_display_fill_rect(__ui_kb_box.x, __ui_kb_box.y, kw, kh, __ui_kb_bg);
350
+ ui_kb_draw_text_row();
351
+ for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
352
+ if (__ui_kb_keys[i].special == 255) continue;
353
+ ui_kb_draw_key(i);
354
+ }
355
+ return;
356
+ }
357
+ // Redirect drawing into the canvas. All ui_display_* calls below go to RAM.
358
+ CuttlefishDisplayTarget* __kb_prev_target = ui_display_get_target();
359
+ ui_display_set_target(__ui_kb_canvas);
360
+ // Opaque background over the keyboard box (canvas-local 0,0).
361
+ ui_display_fill_rect(0, 0, kw, kh, __ui_kb_bg);
362
+ // Text row + keys: temporarily shift box origin to canvas-local so the
363
+ // existing draw_text_row / draw_key functions compute coords at (0,0).
364
+ int16_t saveBoxX = __ui_kb_box.x;
365
+ int16_t saveBoxY = __ui_kb_box.y;
366
+ __ui_kb_box.x = 0;
367
+ __ui_kb_box.y = 0;
334
368
  ui_kb_draw_text_row();
335
- // Keys: one rect per key, label centered.
336
369
  for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
337
- if (__ui_kb_keys[i].special == 255) continue; // padding cell, skip
370
+ if (__ui_kb_keys[i].special == 255) continue;
338
371
  ui_kb_draw_key(i);
339
372
  }
373
+ __ui_kb_box.x = saveBoxX;
374
+ __ui_kb_box.y = saveBoxY;
375
+ // Restore display target and push the canvas in one transaction.
376
+ ui_display_set_target(__kb_prev_target);
377
+ display_startWrite();
378
+ display_setAddrWindow(saveBoxX, saveBoxY, kw, kh);
379
+ display_writePixels(display_canvasBuffer(__ui_kb_canvas), (uint32_t)kw * kh);
380
+ display_endWrite();
340
381
  }
341
382
  `;
342
383
  }
@@ -176,7 +176,6 @@ static inline void ui_draw_node_border(uint16_t i, int16_t drawX, int16_t drawY,
176
176
  if (__ui_nodes[i].hasPerSideBorder) {
177
177
  int16_t w = __ui_nodes[i].box.w;
178
178
  int16_t h = __ui_nodes[i].box.h;
179
- uint8_t st = __ui_nodes[i].borderStyle;
180
179
  // Top edge
181
180
  if (__ui_nodes[i].borderTopWidth > 0) {
182
181
  ui_display_fill_rect(drawX, drawY, w, __ui_nodes[i].borderTopWidth, color);
@@ -7,7 +7,6 @@ export function emitScrollPhysics() {
7
7
  // raw touch sample → smoothed delta (dy). Capacitive: passthrough 1:1.
8
8
  // Resistive: deadband suppresses sub-N-px jitter (steady drag still 1:1).
9
9
  // 'none' tier compiles drag scroll out entirely.
10
- static int16_t __ui_scroll_prev_dy = 0; // last smoothed delta (low-pass state)
11
10
 
12
11
  static inline int16_t ui_scroll_scale_dy(int16_t dy) {
13
12
  int32_t scaled = (int32_t)dy * (int32_t)UI_SCROLL_DRAG_SCALE_X10;
@@ -16,17 +15,14 @@ static inline int16_t ui_scroll_scale_dy(int16_t dy) {
16
15
 
17
16
  static inline int16_t ui_scroll_smooth_dy(int16_t dy) {
18
17
  #if UI_SCROLL_INPUT_TIER_CAPACITIVE
19
- __ui_scroll_prev_dy = dy;
20
18
  return ui_scroll_scale_dy(dy);
21
19
  #elif UI_SCROLL_INPUT_TIER_RESISTIVE
22
20
  int16_t db = (int16_t)UI_SCROLL_DEADBAND_PX;
23
21
  if (dy >= -db && dy <= db) {
24
22
  // Deadband: kill per-sample jitter around zero. Steady drag (|dy|>db) below
25
23
  // passes through unchanged, so steady-state is 1:1 (spec Q1).
26
- __ui_scroll_prev_dy = 0;
27
24
  return 0;
28
25
  }
29
- __ui_scroll_prev_dy = dy;
30
26
  return ui_scroll_scale_dy(dy);
31
27
  #else
32
28
  (void)dy;
@@ -27,9 +27,48 @@ export function emitTickDirtyDrawPhase() {
27
27
  if (__ui_kb_dirty == 1) {
28
28
  ui_kb_draw();
29
29
  } else if (__ui_kb_dirty == 2) {
30
- ui_kb_draw_text_row();
31
- if (__ui_kb_repaint_key >= 0 && __ui_kb_keys[__ui_kb_repaint_key].special != 255) {
32
- ui_kb_draw_key((uint8_t)__ui_kb_repaint_key);
30
+ // Buffer the targeted update through the keyboard canvas too — drawing
31
+ // the text row directly to the display (fill_rect clear + text redraw)
32
+ // flashes on SPI TFTs because the clear is visible for one frame.
33
+ // Re-render into the canvas and push in one transaction. The canvas
34
+ // already holds the last full-keyboard frame, so we only need to redraw
35
+ // the text row + the single changed key on top of it.
36
+ // (__ui_kb_canvas is declared static in the keyboard slice, earlier in
37
+ // this same translation unit — no extern needed.)
38
+ if (__ui_kb_canvas && display_canvasBuffer(__ui_kb_canvas)) {
39
+ int16_t kw = __ui_kb_box.w;
40
+ int16_t saveBoxX = __ui_kb_box.x;
41
+ int16_t saveBoxY = __ui_kb_box.y;
42
+ CuttlefishDisplayTarget* __kb_prev = ui_display_get_target();
43
+ ui_display_set_target(__ui_kb_canvas);
44
+ __ui_kb_box.x = 0;
45
+ __ui_kb_box.y = 0;
46
+ ui_kb_draw_text_row();
47
+ uint8_t has_key_repaint = (__ui_kb_repaint_key >= 0 && __ui_kb_keys[__ui_kb_repaint_key].special != 255) ? 1 : 0;
48
+ if (has_key_repaint) {
49
+ ui_kb_draw_key((uint8_t)__ui_kb_repaint_key);
50
+ }
51
+ __ui_kb_box.x = saveBoxX;
52
+ __ui_kb_box.y = saveBoxY;
53
+ ui_display_set_target(__kb_prev);
54
+ // Push only the changed region: text row alone (UI_KB_TEXT_H) when no
55
+ // key highlight changed, or the full canvas when a key was repainted
56
+ // (the key could be anywhere in the keyboard grid).
57
+ display_startWrite();
58
+ if (has_key_repaint) {
59
+ display_setAddrWindow(saveBoxX, saveBoxY, kw, __ui_kb_box.h);
60
+ display_writePixels(display_canvasBuffer(__ui_kb_canvas), (uint32_t)kw * __ui_kb_box.h);
61
+ } else {
62
+ display_setAddrWindow(saveBoxX, saveBoxY, kw, UI_KB_TEXT_H);
63
+ display_writePixels(display_canvasBuffer(__ui_kb_canvas), (uint32_t)kw * UI_KB_TEXT_H);
64
+ }
65
+ display_endWrite();
66
+ } else {
67
+ // No canvas — fall back to direct draw (slow but correct).
68
+ ui_kb_draw_text_row();
69
+ if (__ui_kb_repaint_key >= 0 && __ui_kb_keys[__ui_kb_repaint_key].special != 255) {
70
+ ui_kb_draw_key((uint8_t)__ui_kb_repaint_key);
71
+ }
33
72
  }
34
73
  }
35
74
  __ui_kb_dirty = 0;
@@ -272,10 +311,6 @@ export function emitTickDirtyDrawPhase() {
272
311
 
273
312
  UIRect paintRect;
274
313
  ui_node_paint_rect(i, baseDrawX, baseDrawY, drawX, drawY, paintTextW, paintTextH, &paintRect);
275
- int16_t cullX = paintRect.x;
276
- int16_t cullY = paintRect.y;
277
- int16_t cullW = paintRect.w;
278
- int16_t cullH = paintRect.h;
279
314
  if (drawingBufferedScroll) {
280
315
  // Canvas-local clip: skip nodes fully outside the viewport (0..vw, 0..vh).
281
316
  // Use the node's face rect (box.w/h), NOT the paint rect — the paint rect
@@ -51,6 +51,7 @@ static UIKeyStyle __ui_kb_styles[UI_KB_MAX];
51
51
  static UI_COLOR_T __ui_kb_bg = 0x0000; // keyboard background (resolved from CSS)
52
52
  static uint8_t __ui_kb_bs_held = 0;
53
53
  static uint8_t __ui_kb_dirty = 0; // 0=clean, 1=full redraw, 2=text row + single key
54
+ // (__ui_kb_canvas is declared in forward-decls.ts alongside the other persistent canvases.)
54
55
  // Forward-declared here (defined in the keyboard subsystem block below) so the
55
56
  // UI_HIDE_OSK caret/blink paths in ui_tick and the input draw can reference it.
56
57
  static int16_t __ui_kb_target;
@@ -268,7 +269,9 @@ static void ui_touch_up() {
268
269
  // including holds (released above) and taps on empty space (__ui_touch_node
269
270
  // == -1), which is what makes "wake on any touch" work for display-sleep.
270
271
  // Placed AFTER the click/release dispatch so onClick always fires first.
271
- __ui_tap_seq++;
272
+ // Note: explicit '+ 1' instead of '++' — GCC 13+ (IDF v6) deprecates '++'
273
+ // on volatile-qualified types under -Werror=volatile.
274
+ __ui_tap_seq = __ui_tap_seq + 1;
272
275
  __ui_tap_node = __ui_touch_node;
273
276
  // Virtualized list item tap: if the touch was inside a list, compute the item
274
277
  // index from the touch Y. Use total movement (not drag flag) to distinguish
@@ -1 +1,10 @@
1
- export declare function emitRuntimeHeader(): string;
1
+ export interface EmitRuntimeHeaderOptions {
2
+ /**
3
+ * True when a native display adapter (AVR, ESP32) is in use. Kept for
4
+ * backwards compatibility — the CuttlefishGFX slice is now emitted
5
+ * separately via emitCuttlefishGfx() so it can land BEFORE display adapter
6
+ * declarations that instantiate CuttlefishGFX by value.
7
+ */
8
+ nativeDisplayActive?: boolean;
9
+ }
10
+ export declare function emitRuntimeHeader(opts?: EmitRuntimeHeaderOptions): string;
@@ -40,7 +40,11 @@ import { emitTick } from "./runtime-header/tick.js";
40
40
  import { emitAntialiasing } from "./runtime-header/antialiasing.js";
41
41
  import { emitKeyboard } from "./runtime-header/keyboard.js";
42
42
  import { emitGuardClose } from "./runtime-header/guard-close.js";
43
- export function emitRuntimeHeader() {
43
+ export function emitRuntimeHeader(opts) {
44
+ // Note: the CuttlefishGFX slice (emitCuttlefishGfx) is emitted separately
45
+ // by ui-emitter.ts BEFORE the display adapter declaration. It is NOT
46
+ // emitted here — passing nativeDisplayActive has no effect.
47
+ void opts;
44
48
  return [
45
49
  emitTypesDefines(),
46
50
  emitStructs(),
@@ -145,7 +145,20 @@ function emitKeyboardLoader(name, kb, rules, colorFormat) {
145
145
  }
146
146
  /** Emit one key's keys[] + styles[] lines, resolving CSS classes to colors. */
147
147
  function emitKeyLine(key, kbClasses, rules, colorFormat) {
148
- const chEsc = key.ch === "\\" ? "\\\\" : key.ch === "'" ? "\\'" : key.ch;
148
+ // The C++ UIKey.ch field is a single `char`. Special keys (shift=1, bs=2,
149
+ // ok=3, page=4) use the `special` field for their behavior and label — the
150
+ // `ch` value is never read. Their label strings ("OK", "123", "⇧", "⌫") are
151
+ // multi-char or multi-byte and would overflow `char` if emitted as literals.
152
+ // Emit a space placeholder for any key whose ch isn't a single ASCII byte.
153
+ const isSingleAscii = key.special === 0 && key.ch.length === 1 && key.ch.charCodeAt(0) < 128;
154
+ if (key.special === 0 && !isSingleAscii) {
155
+ console.warn(`[cuttlefish] keyboard key ch="${key.ch}" is non-ASCII and will be emitted as ` +
156
+ `a space placeholder. The native C++ UIKey.ch field is a single char and ` +
157
+ `cannot hold multi-byte codepoints. Use a custom keyboard layout or the ` +
158
+ `Adafruit path for full Unicode key labels.`);
159
+ }
160
+ const chRaw = isSingleAscii ? key.ch : ' ';
161
+ const chEsc = chRaw === "\\" ? "\\\\" : chRaw === "'" ? "\\'" : chRaw;
149
162
  const style = resolveKeyStyle(key.classes, kbClasses, rules);
150
163
  const bg = style.background ? resolveColor(style.background, colorFormat) : DEFAULT_KEY_BG;
151
164
  const fg = style.color ? resolveColor(style.color, colorFormat) : DEFAULT_KEY_FG;
@@ -145,7 +145,8 @@ export function lowerOnMount(htmlPath, opts) {
145
145
  const keyframeSets = buildKeyframeSets(mod.rawKeyframes || [], opts.colorFormat);
146
146
  // Load image assets before lowering so imgDataId can be set.
147
147
  const imageAssets = loadImageAssets(allStyled.length > 0 ? allStyled : [mod.styled], path.dirname(abs));
148
- const scrollBudget = resolveScrollConfig(getDisplayProfile(), { buildTarget: getDisplayProfile()._buildTarget }).scrollCanvasBudgetBytes;
148
+ const _p = getDisplayProfile();
149
+ const scrollBudget = resolveScrollConfig(_p, { buildTarget: _p._buildTarget, psram: _p._psram }).scrollCanvasBudgetBytes;
149
150
  const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets, scrollBudget);
150
151
  for (const d of result.scrollMemoryDiagnostics) {
151
152
  mod.mountDiagnostics.push({ ...d, source: d.source ?? path.basename(mod.htmlPath) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/ui",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.7",
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": "1.0.0-alpha.6"
36
+ "@typecad/cuttlefish": "1.0.0-alpha.7"
37
37
  },
38
38
  "dependencies": {
39
39
  "css-tree": "^3.2.1",
@@ -19,6 +19,7 @@ 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
24
  import { analyzeScrollMemory } from "./ui-engine/scroll-memory-diagnostics.js";
24
25
 
@@ -44,6 +45,7 @@ export function registerTranspilerUI(): TranspilerUIHook {
44
45
  resolveColor,
45
46
  resolveColorInternal,
46
47
  emitRuntimeHeader,
48
+ emitCuttlefishGfx,
47
49
  splitUiFile,
48
50
  generateProjectUITypeDeclarations,
49
51
  analyzeScrollMemory,
@@ -10,6 +10,7 @@ static inline void ui_release_canvas_state() {
10
10
  __ui_list_canvas_node = -1;
11
11
  display_deleteCanvas(__ui_node_canvas); __ui_node_canvas = nullptr;
12
12
  display_deleteCanvas(__ui_repair_canvas); __ui_repair_canvas = nullptr;
13
+ display_deleteCanvas(__ui_kb_canvas); __ui_kb_canvas = nullptr;
13
14
  }
14
15
 
15
16
  // Lazily allocate/reuse a viewport-sized canvas for a scroll container. Resizes
@@ -62,7 +63,7 @@ static inline void ui_warn_scroll_memory(uint16_t nodeIdx, uint8_t reason) {
62
63
  if (reason == 1) reasonText = "scroll viewport exceeds compile-time canvas budget";
63
64
  else if (reason == 2) reasonText = "using Mode C strip fallback (smooth scroll canvas unavailable)";
64
65
 
65
- #if defined(ESP32)
66
+ #if defined(ESP32) && defined(ARDUINO)
66
67
  uint32_t freeHeap = ESP.getFreeHeap();
67
68
  uint32_t maxAlloc = ESP.getMaxAllocHeap();
68
69
  Serial.printf(
@@ -71,7 +72,7 @@ static inline void ui_warn_scroll_memory(uint16_t nodeIdx, uint8_t reason) {
71
72
  "Shrink the scroll viewport in CSS, trim fonts/images, or use PSRAM.\\n",
72
73
  label, vw, vh, (unsigned long)need, reasonText,
73
74
  (unsigned long)freeHeap, (unsigned long)maxAlloc, UI_SCROLL_CANVAS_BUDGET_BYTES);
74
- #elif defined(ESP8266)
75
+ #elif defined(ESP8266) && defined(ARDUINO)
75
76
  uint32_t freeHeap = ESP.getFreeHeap();
76
77
  Serial.printf(
77
78
  "[cuttlefish] WARNING: #%s (%dx%d) needs %lu bytes for accurate scroll — %s. "
@@ -139,13 +140,18 @@ static inline void ui_shift_container_canvas(CuttlefishCanvas16* canvas, int16_t
139
140
  }
140
141
 
141
142
  // Repair canvas: parent-seeded background repaints + exposed-strip redraws.
142
- // Grow-only between navigations so small scroll-delta changes do not allocate
143
- // and free a new strip canvas during drag.
143
+ // Reused across navigations when size matches; reallocated on exact-size
144
+ // mismatch (a reused larger canvas would keep its old stride, corrupting the
145
+ // pushed pixels — see the inline comment below).
144
146
  static inline CuttlefishCanvas16* ui_get_repair_canvas(int16_t w, int16_t h) {
145
147
  if (w <= 0 || h <= 0) return nullptr;
148
+ // Reallocate when size differs at all (not just when growing). A reused
149
+ // larger canvas keeps its old stride, and ui_push_canvas_rect uses that
150
+ // stride for row offsets — a mismatch with the caller's expected (w,h)
151
+ // corrupts the pushed pixels. Exact-size reallocation guarantees stride == w.
146
152
  if (!__ui_repair_canvas || !display_canvasBuffer(__ui_repair_canvas) ||
147
- display_canvasWidth(__ui_repair_canvas) < w ||
148
- display_canvasHeight(__ui_repair_canvas) < h) {
153
+ display_canvasWidth(__ui_repair_canvas) != w ||
154
+ display_canvasHeight(__ui_repair_canvas) != h) {
149
155
  display_deleteCanvas(__ui_repair_canvas);
150
156
  __ui_repair_canvas = ui_create_canvas_best(w, h);
151
157
  }