@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
@@ -12,6 +12,7 @@
12
12
  import { resetUIRegistry, loadUIModule, loadUIModuleFromText, getUIModule, hasUIModule, allUIModules, allLoweredUIModules, markEntryHasUI, entryHasUI, clearEntryHasUI, lowerOnMount, generateProjectUITypeDeclarations, } from "./ui-engine/ui-registry.js";
13
13
  import { resolveColor, resolveColorInternal } from "./ui-engine/color.js";
14
14
  import { emitRuntimeHeader } from "./ui-engine/runtime-header.js";
15
+ import { emitCuttlefishGfx } from "./ui-engine/runtime-header/cuttlefish-gfx.js";
15
16
  import { splitUiFile } from "./ui-engine/ui-file-splitter.js";
16
17
  import { analyzeScrollMemory } from "./ui-engine/scroll-memory-diagnostics.js";
17
18
  /**
@@ -34,6 +35,7 @@ export function registerTranspilerUI() {
34
35
  resolveColor,
35
36
  resolveColorInternal,
36
37
  emitRuntimeHeader,
38
+ emitCuttlefishGfx,
37
39
  splitUiFile,
38
40
  generateProjectUITypeDeclarations,
39
41
  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
  }
@@ -0,0 +1 @@
1
+ export declare function emitCuttlefishGfx(active: boolean): string;
@@ -0,0 +1,533 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Slice of the C++ runtime header. Defines the CuttlefishGFX class — the
3
+ // shared geometry/canvas/text base used by native (non-Arduino) display
4
+ // adapters. Emitted ONLY when a native adapter is active; Arduino paths use
5
+ // Adafruit_GFX directly and never see this code.
6
+ //
7
+ // ──── BSD-3-Clause attribution ────────────────────────────────────────────
8
+ // The geometry algorithm implementations emitted by this slice (drawLine,
9
+ // drawCircle, drawCircleHelper, fillCircleHelper, drawRoundRect,
10
+ // fillRoundRect, drawTriangle, fillTriangle, drawChar, and the glyph loop
11
+ // in write()) are ported from Adafruit's Adafruit_GFX library:
12
+ //
13
+ // Adafruit_GFX.cpp / Adafruit_GFX.h
14
+ // Copyright (c) 2013 Adafruit Industries. All rights reserved.
15
+ // Licensed under BSD-3-Clause.
16
+ // Upstream: https://github.com/adafruit/Adafruit-GFX-Library
17
+ //
18
+ // Redistribution and use in source and binary forms, with or without
19
+ // modification, are permitted provided that the following conditions are met:
20
+ //
21
+ // - Redistributions of source code must retain the above copyright notice,
22
+ // this list of conditions and the following disclaimer.
23
+ // - Redistributions in binary form must reproduce the above copyright notice,
24
+ // this list of conditions and the following disclaimer in the documentation
25
+ // and/or other materials provided with the distribution.
26
+ //
27
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
28
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
29
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
30
+ // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
31
+ // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
32
+ // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
33
+ // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
34
+ // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
35
+ // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36
+ // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
37
+ // POSSIBILITY OF SUCH DAMAGE.
38
+ // ──── End BSD-3-Clause attribution ────────────────────────────────────────
39
+ //
40
+ // Pixel output of the ported algorithms is byte-for-byte identical to
41
+ // Adafruit_GFX because the runtime's text layout and the AGENTS.md
42
+ // scroll/canvas invariants depend on it.
43
+ //
44
+ // AGENTS.md rendering guardrails: canvas ops reuse persistent buffers,
45
+ // use memmove-shift repair strips for scroll (handled in scroll slices that
46
+ // consume getBuffer()), never allocate per-frame.
47
+ // ---------------------------------------------------------------------------
48
+ import { renderGlcdfontArray } from "@typecad/cuttlefish/api/shared";
49
+ export function emitCuttlefishGfx(active) {
50
+ if (!active)
51
+ return "";
52
+ // The font array (shared with the SDL adapter).
53
+ const fontArray = renderGlcdfontArray("cuttlefish_glcdfont", "unsigned char");
54
+ return `
55
+ // ── CuttlefishGFX (native display GFX base) ────────────────────────────────
56
+ // Emitted only when strategy.providesDisplayAdapter() is true.
57
+ #ifndef CUTTLEFISH_GFX_DEFINED
58
+ #define CUTTLEFISH_GFX_DEFINED
59
+
60
+ #include <stdint.h>
61
+ #include <stdlib.h>
62
+ #include <string.h>
63
+
64
+ // ── Panel-ops interface ────────────────────────────────────────────────────
65
+ // Each native adapter fills a CuttlefishPanelOps with function pointers that
66
+ // drive its specific bus + panel. CuttlefishGFX calls through these; it never
67
+ // touches hardware directly. Pointers marked "may be nullptr" must be null-
68
+ // checked by callers.
69
+ struct CuttlefishPanelOps {
70
+ void (*startWrite)(void* ctx); // assert CS, take bus lock (may be nullptr)
71
+ void (*endWrite)(void* ctx); // release CS / bus lock (may be nullptr)
72
+ void (*setAddrWindow)(void* ctx, int16_t x, int16_t y, int16_t w, int16_t h);
73
+ void (*writePixels)(void* ctx, const uint16_t* px, uint32_t n);
74
+ void (*writePixel)(void* ctx, int16_t x, int16_t y, uint16_t c); // direct mode
75
+ void (*fillRect)(void* ctx, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c);
76
+ int16_t (*width)(void* ctx);
77
+ int16_t (*height)(void* ctx);
78
+ // For SSD1309/SSD1680 only — flush a backing-store buffer to the panel.
79
+ // May be nullptr on direct-mode panels.
80
+ void (*flush)(void* ctx, int16_t x, int16_t y, int16_t w, int16_t h);
81
+ };
82
+
83
+ // ── Color snap (mono displays) ─────────────────────────────────────────────
84
+ // Same logic as the Adafruit SSD1309 adapter: any nonzero 565 → white.
85
+ static inline uint16_t ssd_mono(uint16_t c) { return c ? 0xFFFFu : 0x0000u; }
86
+
87
+ // ── 5x7 font (Adafruit glcdfont, BSD-3-Clause — see file header) ──────────
88
+ // 256 glyphs × 5 bytes per glyph. Indexed as cuttlefish_glcdfont[ch * 5 + col].
89
+ ${fontArray}
90
+
91
+ // ── CuttlefishGFX base class ───────────────────────────────────────────────
92
+ class CuttlefishGFX {
93
+ public:
94
+ CuttlefishGFX(const CuttlefishPanelOps* ops, void* ctx)
95
+ : ops_(ops), ctx_(ctx),
96
+ cursor_x_(0), cursor_y_(0),
97
+ textcolor_(0xFFFFu), textbgcolor_(0x0000u),
98
+ textsize_(1), wrap_(true) {}
99
+ virtual ~CuttlefishGFX() {}
100
+
101
+ // Geometry — match Adafruit_GFX signatures exactly.
102
+ virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
103
+ virtual void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
104
+ virtual void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color);
105
+ virtual void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color);
106
+ virtual void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
107
+ virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
108
+ virtual void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color);
109
+ virtual void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color);
110
+ virtual void drawCircle(int16_t x, int16_t y, int16_t r, uint16_t color);
111
+ virtual void fillCircle(int16_t x, int16_t y, int16_t r, uint16_t color);
112
+ virtual void drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color);
113
+ virtual void fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color);
114
+
115
+ // Bitmaps — match Adafruit_GFX signatures.
116
+ virtual void drawRGBBitmap(int16_t x, int16_t y, const uint16_t* bitmap, int16_t w, int16_t h);
117
+
118
+ // Text
119
+ virtual void drawChar(int16_t x, int16_t y, unsigned char c, uint16_t color, uint16_t bg, uint8_t size);
120
+ virtual void write(uint8_t c);
121
+ void print(const char* s) { while (*s) write((uint8_t)*s++); }
122
+ void setCursor(int16_t x, int16_t y) { cursor_x_ = x; cursor_y_ = y; }
123
+ void setTextColor(uint16_t c) { textcolor_ = c; }
124
+ void setTextColor(uint16_t c, uint16_t bg) { textcolor_ = c; textbgcolor_ = bg; }
125
+ void setTextSize(uint8_t s) { textsize_ = (s > 0) ? s : 1; }
126
+ void setTextWrap(bool w) { wrap_ = w; }
127
+
128
+ // virtual so CuttlefishCanvas16/Mono overrides route through their stored
129
+ // canvas dimensions instead of trying to deref the null ops_.
130
+ virtual int16_t width() const { return (ops_ && ops_->width) ? ops_->width(ctx_) : 0; }
131
+ virtual int16_t height() const { return (ops_ && ops_->height) ? ops_->height(ctx_) : 0; }
132
+ int16_t getCursorX() const { return cursor_x_; }
133
+ int16_t getCursorY() const { return cursor_y_; }
134
+
135
+ // Adafruit circle helpers — public because drawRoundRect/fillRoundRect call them.
136
+ void drawCircleHelper(int16_t x, int16_t y, int16_t r, uint8_t cornername, uint16_t color);
137
+ void fillCircleHelper(int16_t x, int16_t y, int16_t r, uint8_t cornername, int16_t delta, uint16_t color);
138
+
139
+ protected:
140
+ const CuttlefishPanelOps* ops_;
141
+ void* ctx_;
142
+ int16_t cursor_x_, cursor_y_;
143
+ uint16_t textcolor_, textbgcolor_;
144
+ uint8_t textsize_;
145
+ bool wrap_;
146
+ };
147
+
148
+ // ── RGB565 canvas (offscreen; for AA text + scroll compositing) ─────────────
149
+ // Mirrors Adafruit_GFXcanvas16. Allocates w*h*2 bytes; consumer must free via
150
+ // display_deleteCanvas (the runtime reuses persistent canvas slots — see
151
+ // AGENTS.md: __ui_container_canvas, __ui_repair_canvas, __ui_list_canvas,
152
+ // __ui_node_canvas).
153
+ class CuttlefishCanvas16 : public CuttlefishGFX {
154
+ public:
155
+ CuttlefishCanvas16(int16_t w, int16_t h);
156
+ virtual ~CuttlefishCanvas16();
157
+ virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
158
+ virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
159
+ // Override width()/height() — the base implementation reads ops_->width,
160
+ // which is null for canvases (constructed with CuttlefishGFX(nullptr, nullptr)).
161
+ // Without these overrides, any code path that calls width()/height() on a
162
+ // canvas target (text wrapping, scroll clipping, ui_display_target_bounds)
163
+ // dereferences null.
164
+ virtual int16_t width() const { return canvas_w_; }
165
+ virtual int16_t height() const { return canvas_h_; }
166
+ uint16_t* getBuffer() const { return buffer_; }
167
+ uint16_t getPixel(int16_t x, int16_t y) const;
168
+ void fillScreen(uint16_t color) { fillRect(0, 0, canvas_w_, canvas_h_, color); }
169
+ int16_t canvasWidth() const { return canvas_w_; }
170
+ int16_t canvasHeight() const { return canvas_h_; }
171
+ private:
172
+ uint16_t* buffer_;
173
+ int16_t canvas_w_, canvas_h_;
174
+ };
175
+
176
+ // ── 1-bit mono canvas (SSD1309/SSD1680 backing stores) ──────────────────────
177
+ // Mirrors Adafruit_GFXcanvasMono. (w+7)/8 bytes per row. The buffer is the
178
+ // panel backing store on SSD1309 — flush via display_partial_refresh.
179
+ class CuttlefishCanvasMono : public CuttlefishGFX {
180
+ public:
181
+ CuttlefishCanvasMono(int16_t w, int16_t h);
182
+ virtual ~CuttlefishCanvasMono();
183
+ virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
184
+ virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
185
+ // Override width()/height() — see CuttlefishCanvas16 for rationale.
186
+ virtual int16_t width() const { return canvas_w_; }
187
+ virtual int16_t height() const { return canvas_h_; }
188
+ uint8_t* getBuffer() const { return buffer_; }
189
+ uint16_t getPixel(int16_t x, int16_t y) const;
190
+ void fillScreen(uint16_t color) { fillRect(0, 0, canvas_w_, canvas_h_, color); }
191
+ int16_t canvasWidth() const { return canvas_w_; }
192
+ int16_t canvasHeight() const { return canvas_h_; }
193
+ private:
194
+ uint8_t* buffer_; // (w+7)/8 bytes per row × h rows
195
+ int16_t canvas_w_, canvas_h_;
196
+ };
197
+
198
+ // ───────────────────────────────────────────────────────────────────────────
199
+ // Implementations. Ported verbatim from Adafruit_GFX.cpp.
200
+ // ───────────────────────────────────────────────────────────────────────────
201
+
202
+ void CuttlefishGFX::drawPixel(int16_t x, int16_t y, uint16_t color) {
203
+ // Dispatch through ops_->writePixel when present (live panel); otherwise
204
+ // rely on subclasses (CuttlefishCanvas16/Mono) overriding drawPixel to
205
+ // write into their buffers. The null check on ops_ is required because
206
+ // canvases construct their base with CuttlefishGFX(nullptr, nullptr).
207
+ if (ops_ && ops_->writePixel) ops_->writePixel(ctx_, x, y, color);
208
+ }
209
+
210
+ void CuttlefishGFX::drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) {
211
+ // Dispatch through the virtual fillRect so canvases (which override it)
212
+ // route into their buffer. Do NOT call the panel op directly here —
213
+ // canvases construct their base with a null panel-ops struct.
214
+ fillRect(x, y, 1, h, color);
215
+ }
216
+
217
+ void CuttlefishGFX::drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) {
218
+ fillRect(x, y, w, 1, color);
219
+ }
220
+
221
+ void CuttlefishGFX::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
222
+ drawFastHLine(x, y, w, color);
223
+ drawFastHLine(x, y + h - 1, w, color);
224
+ drawFastVLine(x, y, h, color);
225
+ drawFastVLine(x + w - 1, y, h, color);
226
+ }
227
+
228
+ void CuttlefishGFX::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
229
+ // Live panel fast-path: bypass the per-pixel virtual call and ask the
230
+ // panel to fill directly. Canvases override this method to write into
231
+ // their buffer (so they never reach this base path).
232
+ if (ops_ && ops_->fillRect) ops_->fillRect(ctx_, x, y, w, h, color);
233
+ }
234
+
235
+ // Bresenham line — Adafruit_GFX.cpp drawLine, unchanged.
236
+ void CuttlefishGFX::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
237
+ int16_t steep = abs(y1 - y0) > abs(x1 - x0);
238
+ if (steep) {
239
+ int16_t t = x0; x0 = y0; y0 = t;
240
+ t = x1; x1 = y1; y1 = t;
241
+ }
242
+ if (x0 > x1) {
243
+ int16_t t = x0; x0 = x1; x1 = t;
244
+ t = y0; y0 = y1; y1 = t;
245
+ }
246
+ int16_t dx = x1 - x0, dy = abs(y1 - y0);
247
+ int16_t err = dx / 2;
248
+ int16_t ystep = (y0 < y1) ? 1 : -1;
249
+ for (; x0 <= x1; x0++) {
250
+ if (steep) drawPixel(y0, x0, color);
251
+ else drawPixel(x0, y0, color);
252
+ err -= dy;
253
+ if (err < 0) { y0 += ystep; err += dx; }
254
+ }
255
+ }
256
+
257
+ // Midpoint circle — Adafruit_GFX.cpp drawCircle, unchanged.
258
+ void CuttlefishGFX::drawCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color) {
259
+ if (r <= 0) { drawPixel(x0, y0, color); return; }
260
+ int16_t f = 1 - r, ddF_x = 1, ddF_y = -2 * r;
261
+ int16_t x = 0, y = r;
262
+ drawPixel(x0, y0 + r, color);
263
+ drawPixel(x0, y0 - r, color);
264
+ drawPixel(x0 + r, y0, color);
265
+ drawPixel(x0 - r, y0, color);
266
+ while (x < y) {
267
+ if (f >= 0) { y--; ddF_y += 2; f += ddF_y; }
268
+ x++; ddF_x += 2; f += ddF_x;
269
+ drawPixel(x0 + x, y0 + y, color);
270
+ drawPixel(x0 - x, y0 + y, color);
271
+ drawPixel(x0 + x, y0 - y, color);
272
+ drawPixel(x0 - x, y0 - y, color);
273
+ drawPixel(x0 + y, y0 + x, color);
274
+ drawPixel(x0 - y, y0 + x, color);
275
+ drawPixel(x0 + y, y0 - x, color);
276
+ drawPixel(x0 - y, y0 - x, color);
277
+ }
278
+ }
279
+
280
+ void CuttlefishGFX::drawCircleHelper(int16_t x0, int16_t y0, int16_t r, uint8_t cornername, uint16_t color) {
281
+ if (r <= 0) return;
282
+ int16_t f = 1 - r, ddF_x = 1, ddF_y = -2 * r;
283
+ int16_t x = 0, y = r;
284
+ while (x < y) {
285
+ if (f >= 0) { y--; ddF_y += 2; f += ddF_y; }
286
+ x++; ddF_x += 2; f += ddF_x;
287
+ if (cornername & 0x4) {
288
+ drawPixel(x0 + x, y0 + y, color);
289
+ drawPixel(x0 + y, y0 + x, color);
290
+ }
291
+ if (cornername & 0x2) {
292
+ drawPixel(x0 + x, y0 - y, color);
293
+ drawPixel(x0 + y, y0 - x, color);
294
+ }
295
+ if (cornername & 0x8) {
296
+ drawPixel(x0 - y, y0 + x, color);
297
+ drawPixel(x0 - x, y0 + y, color);
298
+ }
299
+ if (cornername & 0x1) {
300
+ drawPixel(x0 - y, y0 - x, color);
301
+ drawPixel(x0 - x, y0 - y, color);
302
+ }
303
+ }
304
+ }
305
+
306
+ void CuttlefishGFX::fillCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color) {
307
+ if (r <= 0) { drawPixel(x0, y0, color); return; }
308
+ drawFastVLine(x0, y0 - r, 2 * r + 1, color);
309
+ fillCircleHelper(x0, y0, r, 3, 0, color);
310
+ }
311
+
312
+ void CuttlefishGFX::fillCircleHelper(int16_t x0, int16_t y0, int16_t r, uint8_t cornername, int16_t delta, uint16_t color) {
313
+ if (r <= 0) return;
314
+ int16_t f = 1 - r, ddF_x = 1, ddF_y = -2 * r;
315
+ int16_t x = 0, y = r;
316
+ while (x < y) {
317
+ if (f >= 0) { y--; ddF_y += 2; f += ddF_y; }
318
+ x++; ddF_x += 2; f += ddF_x;
319
+ if (cornername & 0x1) {
320
+ drawFastVLine(x0 + x, y0 - y, 2 * y + 1 + delta, color);
321
+ drawFastVLine(x0 + y, y0 - x, 2 * x + 1 + delta, color);
322
+ }
323
+ if (cornername & 0x2) {
324
+ drawFastVLine(x0 - x, y0 - y, 2 * y + 1 + delta, color);
325
+ drawFastVLine(x0 - y, y0 - x, 2 * x + 1 + delta, color);
326
+ }
327
+ }
328
+ }
329
+
330
+ void CuttlefishGFX::drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
331
+ drawFastHLine(x + r, y, w - 2 * r, color); // Top
332
+ drawFastHLine(x + r, y + h - 1, w - 2 * r, color); // Bottom
333
+ drawFastVLine(x, y + r, h - 2 * r, color); // Left
334
+ drawFastVLine(x + w - 1, y + r, h - 2 * r, color); // Right
335
+ drawCircleHelper(x + r, y + r, r, 1, color);
336
+ drawCircleHelper(x + w - r - 1, y + r, r, 2, color);
337
+ drawCircleHelper(x + w - r - 1, y + h - r - 1, r, 4, color);
338
+ drawCircleHelper(x + r, y + h - r - 1, r, 8, color);
339
+ }
340
+
341
+ void CuttlefishGFX::fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
342
+ fillRect(x + r, y, w - 2 * r, h, color);
343
+ fillCircleHelper(x + w - r - 1, y + r, r, 1, h - 2 * r - 1, color);
344
+ fillCircleHelper(x + r, y + r, r, 2, h - 2 * r - 1, color);
345
+ }
346
+
347
+ void CuttlefishGFX::drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
348
+ drawLine(x0, y0, x1, y1, color);
349
+ drawLine(x1, y1, x2, y2, color);
350
+ drawLine(x2, y2, x0, y0, color);
351
+ }
352
+
353
+ void CuttlefishGFX::fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
354
+ int16_t a, b, y, last;
355
+ // Sort coordinates by Y order ascending.
356
+ if (y0 > y1) { int16_t t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
357
+ if (y1 > y2) { int16_t t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
358
+ if (y0 > y1) { int16_t t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
359
+ if (y0 == y2) { drawFastHLine(x0, y0, x1 - x0, color); return; }
360
+ int16_t dx01 = x1 - x0, dy01 = y1 - y0;
361
+ int16_t dx02 = x2 - x0, dy02 = y2 - y0;
362
+ int16_t dx12 = x2 - x1, dy12 = y2 - y1;
363
+ int32_t sa = 0, sb = 0;
364
+ last = (y1 == y2) ? y1 : y1 - 1;
365
+ for (y = y0; y <= last; y++) {
366
+ a = x0 + sa / dy01;
367
+ b = x0 + sb / dy02;
368
+ sa += dx01;
369
+ sb += dx02;
370
+ if (a > b) { int16_t t = a; a = b; b = t; }
371
+ drawFastHLine(a, y, b - a + 1, color);
372
+ }
373
+ sa = (int32_t)dx12 * (last - y1);
374
+ sb = (int32_t)dx02 * (last - y0);
375
+ for (; y <= y2; y++) {
376
+ a = x1 + sa / dy12;
377
+ b = x0 + sb / dy02;
378
+ sa += dx12;
379
+ sb += dx02;
380
+ if (a > b) { int16_t t = a; a = b; b = t; }
381
+ drawFastHLine(a, y, b - a + 1, color);
382
+ }
383
+ }
384
+
385
+ // RGB565 bitmap — Adafruit_GFX.cpp drawRGBBitmap, adapted to call writePixel
386
+ // through the panel ops (no memory-backed variant; the Adafruit mem-version
387
+ // is unused by the cuttlefish runtime).
388
+ void CuttlefishGFX::drawRGBBitmap(int16_t x, int16_t y, const uint16_t* bitmap, int16_t w, int16_t h) {
389
+ if (!bitmap || w <= 0 || h <= 0) return;
390
+ for (int16_t j = 0; j < h; j++) {
391
+ for (int16_t i = 0; i < w; i++) {
392
+ drawPixel(x + i, y + j, bitmap[(int32_t)j * w + i]);
393
+ }
394
+ }
395
+ }
396
+
397
+ // Glyph rendering — Adafruit_GFX.cpp drawChar, unchanged algorithm.
398
+ void CuttlefishGFX::drawChar(int16_t x, int16_t y, unsigned char c, uint16_t color, uint16_t bg, uint8_t size) {
399
+ if ((x >= width()) || // Clip right
400
+ (y >= height()) || // Clip bottom
401
+ ((x + 5 * size - 1) < 0) || // Clip left
402
+ ((y + 8 * size - 1) < 0)) // Clip top
403
+ return;
404
+ if (c >= 176) c++; // Adafruit '±' fixup
405
+ for (int8_t i = 0; i < 5; i++) {
406
+ uint8_t line = cuttlefish_glcdfont[(size_t)c * 5 + i];
407
+ for (int8_t j = 0; j < 8; j++, line >>= 1) {
408
+ if (line & 1) {
409
+ if (size == 1) drawPixel(x + i, y + j, color);
410
+ else fillRect(x + i * size, y + j * size, size, size, color);
411
+ } else if (bg != color) {
412
+ if (size == 1) drawPixel(x + i, y + j, bg);
413
+ else fillRect(x + i * size, y + j * size, size, size, bg);
414
+ }
415
+ }
416
+ }
417
+ if (bg != color) {
418
+ if (size == 1) drawFastVLine(x + 5, y, 8, bg);
419
+ else fillRect(x + 5 * size, y, size, 8 * size, bg);
420
+ }
421
+ }
422
+
423
+ void CuttlefishGFX::write(uint8_t c) {
424
+ if (c == '\\n') {
425
+ cursor_x_ = 0;
426
+ cursor_y_ += textsize_ * 8;
427
+ } else if (c != '\\r') {
428
+ int16_t w = 6 * textsize_;
429
+ if (wrap_ && ((cursor_x_ + w) > width())) {
430
+ cursor_x_ = 0;
431
+ cursor_y_ += textsize_ * 8;
432
+ }
433
+ drawChar(cursor_x_, cursor_y_, c, textcolor_, textbgcolor_, textsize_);
434
+ cursor_x_ += w;
435
+ }
436
+ }
437
+
438
+ // ── Canvas implementations ─────────────────────────────────────────────────
439
+
440
+ CuttlefishCanvas16::CuttlefishCanvas16(int16_t w, int16_t h)
441
+ : CuttlefishGFX(nullptr, nullptr),
442
+ buffer_(nullptr), canvas_w_(w), canvas_h_(h) {
443
+ if ((w > 0) && (h > 0)) {
444
+ size_t bytes = (size_t)w * (size_t)h * sizeof(uint16_t);
445
+ buffer_ = (uint16_t*)malloc(bytes);
446
+ if (buffer_) memset(buffer_, 0, bytes);
447
+ }
448
+ }
449
+
450
+ CuttlefishCanvas16::~CuttlefishCanvas16() {
451
+ if (buffer_) free(buffer_);
452
+ }
453
+
454
+ void CuttlefishCanvas16::drawPixel(int16_t x, int16_t y, uint16_t color) {
455
+ if (!buffer_) return;
456
+ if ((x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return;
457
+ buffer_[(size_t)y * (size_t)canvas_w_ + (size_t)x] = color;
458
+ }
459
+
460
+ void CuttlefishCanvas16::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
461
+ if (!buffer_) return;
462
+ int16_t x1 = (x < 0) ? 0 : x;
463
+ int16_t y1 = (y < 0) ? 0 : y;
464
+ int16_t x2 = x + w; if (x2 > canvas_w_) x2 = canvas_w_;
465
+ int16_t y2 = y + h; if (y2 > canvas_h_) y2 = canvas_h_;
466
+ // Cull before the inner loop (AGENTS.md: clip before expensive work).
467
+ if (x2 <= x1 || y2 <= y1) return;
468
+ for (int16_t j = y1; j < y2; j++) {
469
+ uint16_t* row = buffer_ + (size_t)j * (size_t)canvas_w_;
470
+ for (int16_t i = x1; i < x2; i++) row[i] = color;
471
+ }
472
+ }
473
+
474
+ uint16_t CuttlefishCanvas16::getPixel(int16_t x, int16_t y) const {
475
+ if (!buffer_ || (x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return 0;
476
+ return buffer_[(size_t)y * (size_t)canvas_w_ + (size_t)x];
477
+ }
478
+
479
+ CuttlefishCanvasMono::CuttlefishCanvasMono(int16_t w, int16_t h)
480
+ : CuttlefishGFX(nullptr, nullptr),
481
+ buffer_(nullptr), canvas_w_(w), canvas_h_(h) {
482
+ if ((w > 0) && (h > 0)) {
483
+ size_t row_bytes = ((size_t)w + 7u) / 8u;
484
+ size_t bytes = row_bytes * (size_t)h;
485
+ buffer_ = (uint8_t*)malloc(bytes);
486
+ if (buffer_) memset(buffer_, 0, bytes);
487
+ }
488
+ }
489
+
490
+ CuttlefishCanvasMono::~CuttlefishCanvasMono() {
491
+ if (buffer_) free(buffer_);
492
+ }
493
+
494
+ void CuttlefishCanvasMono::drawPixel(int16_t x, int16_t y, uint16_t color) {
495
+ if (!buffer_) return;
496
+ if ((x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return;
497
+ size_t row_bytes = ((size_t)canvas_w_ + 7u) / 8u;
498
+ uint8_t* row = buffer_ + (size_t)y * row_bytes;
499
+ uint8_t mask = 0x80u >> (x & 7);
500
+ if (color) row[x >> 3] |= mask;
501
+ else row[x >> 3] &= ~mask;
502
+ }
503
+
504
+ void CuttlefishCanvasMono::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
505
+ if (!buffer_) return;
506
+ int16_t x1 = (x < 0) ? 0 : x;
507
+ int16_t y1 = (y < 0) ? 0 : y;
508
+ int16_t x2 = x + w; if (x2 > canvas_w_) x2 = canvas_w_;
509
+ int16_t y2 = y + h; if (y2 > canvas_h_) y2 = canvas_h_;
510
+ if (x2 <= x1 || y2 <= y1) return;
511
+ uint8_t fill = color ? 0xFFu : 0x00u;
512
+ for (int16_t j = y1; j < y2; j++) {
513
+ for (int16_t i = x1; i < x2; i++) {
514
+ size_t row_bytes = ((size_t)canvas_w_ + 7u) / 8u;
515
+ uint8_t* row = buffer_ + (size_t)j * row_bytes;
516
+ uint8_t mask = 0x80u >> (i & 7);
517
+ if (color) row[i >> 3] |= mask;
518
+ else row[i >> 3] &= ~mask;
519
+ }
520
+ }
521
+ (void)fill;
522
+ }
523
+
524
+ uint16_t CuttlefishCanvasMono::getPixel(int16_t x, int16_t y) const {
525
+ if (!buffer_ || (x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return 0;
526
+ size_t row_bytes = ((size_t)canvas_w_ + 7u) / 8u;
527
+ const uint8_t* row = buffer_ + (size_t)y * row_bytes;
528
+ return (row[x >> 3] & (0x80u >> (x & 7))) ? 0xFFFFu : 0x0000u;
529
+ }
530
+
531
+ #endif // CUTTLEFISH_GFX_DEFINED
532
+ `;
533
+ }
@@ -3,10 +3,6 @@
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitForwardDecls() {
5
5
  return `
6
- static uint8_t __ui_fade_opacity = 100; // fade-in animation (0=transparent, 100=full)
7
- static uint16_t __ui_fade_elapsed = 0;
8
- static uint16_t __ui_fade_duration = 200; // ms
9
-
10
6
  // Early forward declaration: ui_navigate (below) calls ui_release_canvas_state
11
7
  // and ui_set_pressed (defined later) during screen changes. Needed on native
12
8
  // (single TU, no Arduino auto-prototyper).
@@ -14,7 +10,8 @@ static inline void ui_release_canvas_state();
14
10
  static inline void ui_set_pressed(uint16_t nodeIdx, uint8_t pressed);
15
11
  static inline void ui_refresh_active_screen_bg_node();
16
12
 
17
- // Navigate to a screen by index. Marks the new screen's nodes dirty + starts fade.
13
+ // Navigate to a screen by index. Marks the new screen's nodes dirty, releases
14
+ // persistent canvas state, and optionally clears the display.
18
15
  static inline void ui_navigate(uint8_t screenIdx) {
19
16
  if (screenIdx >= __ui_screen_count || screenIdx == __ui_active_screen) return;
20
17
  __ui_active_screen = screenIdx;
@@ -93,12 +90,14 @@ static inline void ui_draw_node_decoration_clipped(uint16_t nodeIdx, int16_t dra
93
90
  // single native translation unit (Arduino's auto-prototyper hides this;
94
91
  // native emits one TU so explicit forwards are needed).
95
92
  static inline int8_t ui_rich_link_hit(uint16_t nodeIdx, int16_t px, int16_t py);
93
+ #ifdef UI_AA
96
94
  static inline CuttlefishCanvas16* ui_aa_begin(int16_t w, int16_t h, UI_COLOR_T bg);
97
95
  static inline void ui_aa_end(CuttlefishCanvas16* c);
98
96
  static inline void ui_aa_push(CuttlefishCanvas16* c, int16_t dx, int16_t dy);
99
97
  static inline void ui_aa_line(CuttlefishCanvas16* c, float x0, float y0, float x1, float y1, UI_COLOR_T color);
100
98
  static inline void ui_aa_circle(CuttlefishCanvas16* c, int16_t cx, int16_t cy, float r, UI_COLOR_T color);
101
99
  static inline void ui_aa_fill_circle(CuttlefishCanvas16* c, int16_t cx, int16_t cy, float r, UI_COLOR_T color);
100
+ #endif
102
101
  static inline uint8_t ui_repair_current_node_paint_with_parent(uint16_t nodeIdx, UIRect* r);
103
102
  static inline void ui_clear_node_paint_rect(uint16_t nodeIdx, const UIRect* paintRect);
104
103
  static inline uint8_t ui_try_repair_geometry_fill(uint16_t nodeIdx, const UIRect* oldRect);
@@ -126,6 +125,7 @@ static CuttlefishCanvas16* __ui_list_canvas = nullptr; // virtualized <lis
126
125
  static int16_t __ui_list_canvas_node = -1; // node currently represented by __ui_list_canvas
127
126
  static CuttlefishCanvas16* __ui_node_canvas = nullptr; // <canvas> element offscreen
128
127
  static CuttlefishCanvas16* __ui_repair_canvas = nullptr; // buffered-paint / exposed-strip
128
+ static CuttlefishCanvas16* __ui_kb_canvas = nullptr; // on-screen keyboard overlay
129
129
  static int16_t __ui_canvas_fallback_w = 0; // dimensions for direct <canvas> fallback
130
130
  static int16_t __ui_canvas_fallback_h = 0;
131
131
  // Draw offset: normally zero. The <canvas> allocation fallback sets it so