@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
@@ -0,0 +1,536 @@
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
+
49
+ import { renderGlcdfontArray } from "@typecad/cuttlefish/api/shared";
50
+
51
+ export function emitCuttlefishGfx(active: boolean): string {
52
+ if (!active) return "";
53
+
54
+ // The font array (shared with the SDL adapter).
55
+ const fontArray = renderGlcdfontArray("cuttlefish_glcdfont", "unsigned char");
56
+
57
+ return `
58
+ // ── CuttlefishGFX (native display GFX base) ────────────────────────────────
59
+ // Emitted only when strategy.providesDisplayAdapter() is true.
60
+ #ifndef CUTTLEFISH_GFX_DEFINED
61
+ #define CUTTLEFISH_GFX_DEFINED
62
+
63
+ #include <stdint.h>
64
+ #include <stdlib.h>
65
+ #include <string.h>
66
+
67
+ // ── Panel-ops interface ────────────────────────────────────────────────────
68
+ // Each native adapter fills a CuttlefishPanelOps with function pointers that
69
+ // drive its specific bus + panel. CuttlefishGFX calls through these; it never
70
+ // touches hardware directly. Pointers marked "may be nullptr" must be null-
71
+ // checked by callers.
72
+ struct CuttlefishPanelOps {
73
+ void (*startWrite)(void* ctx); // assert CS, take bus lock (may be nullptr)
74
+ void (*endWrite)(void* ctx); // release CS / bus lock (may be nullptr)
75
+ void (*setAddrWindow)(void* ctx, int16_t x, int16_t y, int16_t w, int16_t h);
76
+ void (*writePixels)(void* ctx, const uint16_t* px, uint32_t n);
77
+ void (*writePixel)(void* ctx, int16_t x, int16_t y, uint16_t c); // direct mode
78
+ void (*fillRect)(void* ctx, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c);
79
+ int16_t (*width)(void* ctx);
80
+ int16_t (*height)(void* ctx);
81
+ // For SSD1309/SSD1680 only — flush a backing-store buffer to the panel.
82
+ // May be nullptr on direct-mode panels.
83
+ void (*flush)(void* ctx, int16_t x, int16_t y, int16_t w, int16_t h);
84
+ };
85
+
86
+ // ── Color snap (mono displays) ─────────────────────────────────────────────
87
+ // Same logic as the Adafruit SSD1309 adapter: any nonzero 565 → white.
88
+ static inline uint16_t ssd_mono(uint16_t c) { return c ? 0xFFFFu : 0x0000u; }
89
+
90
+ // ── 5x7 font (Adafruit glcdfont, BSD-3-Clause — see file header) ──────────
91
+ // 256 glyphs × 5 bytes per glyph. Indexed as cuttlefish_glcdfont[ch * 5 + col].
92
+ ${fontArray}
93
+
94
+ // ── CuttlefishGFX base class ───────────────────────────────────────────────
95
+ class CuttlefishGFX {
96
+ public:
97
+ CuttlefishGFX(const CuttlefishPanelOps* ops, void* ctx)
98
+ : ops_(ops), ctx_(ctx),
99
+ cursor_x_(0), cursor_y_(0),
100
+ textcolor_(0xFFFFu), textbgcolor_(0x0000u),
101
+ textsize_(1), wrap_(true) {}
102
+ virtual ~CuttlefishGFX() {}
103
+
104
+ // Geometry — match Adafruit_GFX signatures exactly.
105
+ virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
106
+ virtual void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color);
107
+ virtual void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color);
108
+ virtual void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color);
109
+ virtual void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
110
+ virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
111
+ virtual void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color);
112
+ virtual void fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color);
113
+ virtual void drawCircle(int16_t x, int16_t y, int16_t r, uint16_t color);
114
+ virtual void fillCircle(int16_t x, int16_t y, int16_t r, uint16_t color);
115
+ virtual void drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color);
116
+ virtual void fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color);
117
+
118
+ // Bitmaps — match Adafruit_GFX signatures.
119
+ virtual void drawRGBBitmap(int16_t x, int16_t y, const uint16_t* bitmap, int16_t w, int16_t h);
120
+
121
+ // Text
122
+ virtual void drawChar(int16_t x, int16_t y, unsigned char c, uint16_t color, uint16_t bg, uint8_t size);
123
+ virtual void write(uint8_t c);
124
+ void print(const char* s) { while (*s) write((uint8_t)*s++); }
125
+ void setCursor(int16_t x, int16_t y) { cursor_x_ = x; cursor_y_ = y; }
126
+ void setTextColor(uint16_t c) { textcolor_ = c; }
127
+ void setTextColor(uint16_t c, uint16_t bg) { textcolor_ = c; textbgcolor_ = bg; }
128
+ void setTextSize(uint8_t s) { textsize_ = (s > 0) ? s : 1; }
129
+ void setTextWrap(bool w) { wrap_ = w; }
130
+
131
+ // virtual so CuttlefishCanvas16/Mono overrides route through their stored
132
+ // canvas dimensions instead of trying to deref the null ops_.
133
+ virtual int16_t width() const { return (ops_ && ops_->width) ? ops_->width(ctx_) : 0; }
134
+ virtual int16_t height() const { return (ops_ && ops_->height) ? ops_->height(ctx_) : 0; }
135
+ int16_t getCursorX() const { return cursor_x_; }
136
+ int16_t getCursorY() const { return cursor_y_; }
137
+
138
+ // Adafruit circle helpers — public because drawRoundRect/fillRoundRect call them.
139
+ void drawCircleHelper(int16_t x, int16_t y, int16_t r, uint8_t cornername, uint16_t color);
140
+ void fillCircleHelper(int16_t x, int16_t y, int16_t r, uint8_t cornername, int16_t delta, uint16_t color);
141
+
142
+ protected:
143
+ const CuttlefishPanelOps* ops_;
144
+ void* ctx_;
145
+ int16_t cursor_x_, cursor_y_;
146
+ uint16_t textcolor_, textbgcolor_;
147
+ uint8_t textsize_;
148
+ bool wrap_;
149
+ };
150
+
151
+ // ── RGB565 canvas (offscreen; for AA text + scroll compositing) ─────────────
152
+ // Mirrors Adafruit_GFXcanvas16. Allocates w*h*2 bytes; consumer must free via
153
+ // display_deleteCanvas (the runtime reuses persistent canvas slots — see
154
+ // AGENTS.md: __ui_container_canvas, __ui_repair_canvas, __ui_list_canvas,
155
+ // __ui_node_canvas).
156
+ class CuttlefishCanvas16 : public CuttlefishGFX {
157
+ public:
158
+ CuttlefishCanvas16(int16_t w, int16_t h);
159
+ virtual ~CuttlefishCanvas16();
160
+ virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
161
+ virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
162
+ // Override width()/height() — the base implementation reads ops_->width,
163
+ // which is null for canvases (constructed with CuttlefishGFX(nullptr, nullptr)).
164
+ // Without these overrides, any code path that calls width()/height() on a
165
+ // canvas target (text wrapping, scroll clipping, ui_display_target_bounds)
166
+ // dereferences null.
167
+ virtual int16_t width() const { return canvas_w_; }
168
+ virtual int16_t height() const { return canvas_h_; }
169
+ uint16_t* getBuffer() const { return buffer_; }
170
+ uint16_t getPixel(int16_t x, int16_t y) const;
171
+ void fillScreen(uint16_t color) { fillRect(0, 0, canvas_w_, canvas_h_, color); }
172
+ int16_t canvasWidth() const { return canvas_w_; }
173
+ int16_t canvasHeight() const { return canvas_h_; }
174
+ private:
175
+ uint16_t* buffer_;
176
+ int16_t canvas_w_, canvas_h_;
177
+ };
178
+
179
+ // ── 1-bit mono canvas (SSD1309/SSD1680 backing stores) ──────────────────────
180
+ // Mirrors Adafruit_GFXcanvasMono. (w+7)/8 bytes per row. The buffer is the
181
+ // panel backing store on SSD1309 — flush via display_partial_refresh.
182
+ class CuttlefishCanvasMono : public CuttlefishGFX {
183
+ public:
184
+ CuttlefishCanvasMono(int16_t w, int16_t h);
185
+ virtual ~CuttlefishCanvasMono();
186
+ virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
187
+ virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color);
188
+ // Override width()/height() — see CuttlefishCanvas16 for rationale.
189
+ virtual int16_t width() const { return canvas_w_; }
190
+ virtual int16_t height() const { return canvas_h_; }
191
+ uint8_t* getBuffer() const { return buffer_; }
192
+ uint16_t getPixel(int16_t x, int16_t y) const;
193
+ void fillScreen(uint16_t color) { fillRect(0, 0, canvas_w_, canvas_h_, color); }
194
+ int16_t canvasWidth() const { return canvas_w_; }
195
+ int16_t canvasHeight() const { return canvas_h_; }
196
+ private:
197
+ uint8_t* buffer_; // (w+7)/8 bytes per row × h rows
198
+ int16_t canvas_w_, canvas_h_;
199
+ };
200
+
201
+ // ───────────────────────────────────────────────────────────────────────────
202
+ // Implementations. Ported verbatim from Adafruit_GFX.cpp.
203
+ // ───────────────────────────────────────────────────────────────────────────
204
+
205
+ void CuttlefishGFX::drawPixel(int16_t x, int16_t y, uint16_t color) {
206
+ // Dispatch through ops_->writePixel when present (live panel); otherwise
207
+ // rely on subclasses (CuttlefishCanvas16/Mono) overriding drawPixel to
208
+ // write into their buffers. The null check on ops_ is required because
209
+ // canvases construct their base with CuttlefishGFX(nullptr, nullptr).
210
+ if (ops_ && ops_->writePixel) ops_->writePixel(ctx_, x, y, color);
211
+ }
212
+
213
+ void CuttlefishGFX::drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) {
214
+ // Dispatch through the virtual fillRect so canvases (which override it)
215
+ // route into their buffer. Do NOT call the panel op directly here —
216
+ // canvases construct their base with a null panel-ops struct.
217
+ fillRect(x, y, 1, h, color);
218
+ }
219
+
220
+ void CuttlefishGFX::drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) {
221
+ fillRect(x, y, w, 1, color);
222
+ }
223
+
224
+ void CuttlefishGFX::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
225
+ drawFastHLine(x, y, w, color);
226
+ drawFastHLine(x, y + h - 1, w, color);
227
+ drawFastVLine(x, y, h, color);
228
+ drawFastVLine(x + w - 1, y, h, color);
229
+ }
230
+
231
+ void CuttlefishGFX::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
232
+ // Live panel fast-path: bypass the per-pixel virtual call and ask the
233
+ // panel to fill directly. Canvases override this method to write into
234
+ // their buffer (so they never reach this base path).
235
+ if (ops_ && ops_->fillRect) ops_->fillRect(ctx_, x, y, w, h, color);
236
+ }
237
+
238
+ // Bresenham line — Adafruit_GFX.cpp drawLine, unchanged.
239
+ void CuttlefishGFX::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
240
+ int16_t steep = abs(y1 - y0) > abs(x1 - x0);
241
+ if (steep) {
242
+ int16_t t = x0; x0 = y0; y0 = t;
243
+ t = x1; x1 = y1; y1 = t;
244
+ }
245
+ if (x0 > x1) {
246
+ int16_t t = x0; x0 = x1; x1 = t;
247
+ t = y0; y0 = y1; y1 = t;
248
+ }
249
+ int16_t dx = x1 - x0, dy = abs(y1 - y0);
250
+ int16_t err = dx / 2;
251
+ int16_t ystep = (y0 < y1) ? 1 : -1;
252
+ for (; x0 <= x1; x0++) {
253
+ if (steep) drawPixel(y0, x0, color);
254
+ else drawPixel(x0, y0, color);
255
+ err -= dy;
256
+ if (err < 0) { y0 += ystep; err += dx; }
257
+ }
258
+ }
259
+
260
+ // Midpoint circle — Adafruit_GFX.cpp drawCircle, unchanged.
261
+ void CuttlefishGFX::drawCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color) {
262
+ if (r <= 0) { drawPixel(x0, y0, color); return; }
263
+ int16_t f = 1 - r, ddF_x = 1, ddF_y = -2 * r;
264
+ int16_t x = 0, y = r;
265
+ drawPixel(x0, y0 + r, color);
266
+ drawPixel(x0, y0 - r, color);
267
+ drawPixel(x0 + r, y0, color);
268
+ drawPixel(x0 - r, y0, color);
269
+ while (x < y) {
270
+ if (f >= 0) { y--; ddF_y += 2; f += ddF_y; }
271
+ x++; ddF_x += 2; f += ddF_x;
272
+ drawPixel(x0 + x, y0 + y, color);
273
+ drawPixel(x0 - x, y0 + y, color);
274
+ drawPixel(x0 + x, y0 - y, color);
275
+ drawPixel(x0 - x, y0 - y, color);
276
+ drawPixel(x0 + y, y0 + x, color);
277
+ drawPixel(x0 - y, y0 + x, color);
278
+ drawPixel(x0 + y, y0 - x, color);
279
+ drawPixel(x0 - y, y0 - x, color);
280
+ }
281
+ }
282
+
283
+ void CuttlefishGFX::drawCircleHelper(int16_t x0, int16_t y0, int16_t r, uint8_t cornername, uint16_t color) {
284
+ if (r <= 0) return;
285
+ int16_t f = 1 - r, ddF_x = 1, ddF_y = -2 * r;
286
+ int16_t x = 0, y = r;
287
+ while (x < y) {
288
+ if (f >= 0) { y--; ddF_y += 2; f += ddF_y; }
289
+ x++; ddF_x += 2; f += ddF_x;
290
+ if (cornername & 0x4) {
291
+ drawPixel(x0 + x, y0 + y, color);
292
+ drawPixel(x0 + y, y0 + x, color);
293
+ }
294
+ if (cornername & 0x2) {
295
+ drawPixel(x0 + x, y0 - y, color);
296
+ drawPixel(x0 + y, y0 - x, color);
297
+ }
298
+ if (cornername & 0x8) {
299
+ drawPixel(x0 - y, y0 + x, color);
300
+ drawPixel(x0 - x, y0 + y, color);
301
+ }
302
+ if (cornername & 0x1) {
303
+ drawPixel(x0 - y, y0 - x, color);
304
+ drawPixel(x0 - x, y0 - y, color);
305
+ }
306
+ }
307
+ }
308
+
309
+ void CuttlefishGFX::fillCircle(int16_t x0, int16_t y0, int16_t r, uint16_t color) {
310
+ if (r <= 0) { drawPixel(x0, y0, color); return; }
311
+ drawFastVLine(x0, y0 - r, 2 * r + 1, color);
312
+ fillCircleHelper(x0, y0, r, 3, 0, color);
313
+ }
314
+
315
+ void CuttlefishGFX::fillCircleHelper(int16_t x0, int16_t y0, int16_t r, uint8_t cornername, int16_t delta, uint16_t color) {
316
+ if (r <= 0) return;
317
+ int16_t f = 1 - r, ddF_x = 1, ddF_y = -2 * r;
318
+ int16_t x = 0, y = r;
319
+ while (x < y) {
320
+ if (f >= 0) { y--; ddF_y += 2; f += ddF_y; }
321
+ x++; ddF_x += 2; f += ddF_x;
322
+ if (cornername & 0x1) {
323
+ drawFastVLine(x0 + x, y0 - y, 2 * y + 1 + delta, color);
324
+ drawFastVLine(x0 + y, y0 - x, 2 * x + 1 + delta, color);
325
+ }
326
+ if (cornername & 0x2) {
327
+ drawFastVLine(x0 - x, y0 - y, 2 * y + 1 + delta, color);
328
+ drawFastVLine(x0 - y, y0 - x, 2 * x + 1 + delta, color);
329
+ }
330
+ }
331
+ }
332
+
333
+ void CuttlefishGFX::drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
334
+ drawFastHLine(x + r, y, w - 2 * r, color); // Top
335
+ drawFastHLine(x + r, y + h - 1, w - 2 * r, color); // Bottom
336
+ drawFastVLine(x, y + r, h - 2 * r, color); // Left
337
+ drawFastVLine(x + w - 1, y + r, h - 2 * r, color); // Right
338
+ drawCircleHelper(x + r, y + r, r, 1, color);
339
+ drawCircleHelper(x + w - r - 1, y + r, r, 2, color);
340
+ drawCircleHelper(x + w - r - 1, y + h - r - 1, r, 4, color);
341
+ drawCircleHelper(x + r, y + h - r - 1, r, 8, color);
342
+ }
343
+
344
+ void CuttlefishGFX::fillRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, uint16_t color) {
345
+ fillRect(x + r, y, w - 2 * r, h, color);
346
+ fillCircleHelper(x + w - r - 1, y + r, r, 1, h - 2 * r - 1, color);
347
+ fillCircleHelper(x + r, y + r, r, 2, h - 2 * r - 1, color);
348
+ }
349
+
350
+ void CuttlefishGFX::drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
351
+ drawLine(x0, y0, x1, y1, color);
352
+ drawLine(x1, y1, x2, y2, color);
353
+ drawLine(x2, y2, x0, y0, color);
354
+ }
355
+
356
+ void CuttlefishGFX::fillTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
357
+ int16_t a, b, y, last;
358
+ // Sort coordinates by Y order ascending.
359
+ if (y0 > y1) { int16_t t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
360
+ if (y1 > y2) { int16_t t = y1; y1 = y2; y2 = t; t = x1; x1 = x2; x2 = t; }
361
+ if (y0 > y1) { int16_t t = y0; y0 = y1; y1 = t; t = x0; x0 = x1; x1 = t; }
362
+ if (y0 == y2) { drawFastHLine(x0, y0, x1 - x0, color); return; }
363
+ int16_t dx01 = x1 - x0, dy01 = y1 - y0;
364
+ int16_t dx02 = x2 - x0, dy02 = y2 - y0;
365
+ int16_t dx12 = x2 - x1, dy12 = y2 - y1;
366
+ int32_t sa = 0, sb = 0;
367
+ last = (y1 == y2) ? y1 : y1 - 1;
368
+ for (y = y0; y <= last; y++) {
369
+ a = x0 + sa / dy01;
370
+ b = x0 + sb / dy02;
371
+ sa += dx01;
372
+ sb += dx02;
373
+ if (a > b) { int16_t t = a; a = b; b = t; }
374
+ drawFastHLine(a, y, b - a + 1, color);
375
+ }
376
+ sa = (int32_t)dx12 * (last - y1);
377
+ sb = (int32_t)dx02 * (last - y0);
378
+ for (; y <= y2; y++) {
379
+ a = x1 + sa / dy12;
380
+ b = x0 + sb / dy02;
381
+ sa += dx12;
382
+ sb += dx02;
383
+ if (a > b) { int16_t t = a; a = b; b = t; }
384
+ drawFastHLine(a, y, b - a + 1, color);
385
+ }
386
+ }
387
+
388
+ // RGB565 bitmap — Adafruit_GFX.cpp drawRGBBitmap, adapted to call writePixel
389
+ // through the panel ops (no memory-backed variant; the Adafruit mem-version
390
+ // is unused by the cuttlefish runtime).
391
+ void CuttlefishGFX::drawRGBBitmap(int16_t x, int16_t y, const uint16_t* bitmap, int16_t w, int16_t h) {
392
+ if (!bitmap || w <= 0 || h <= 0) return;
393
+ for (int16_t j = 0; j < h; j++) {
394
+ for (int16_t i = 0; i < w; i++) {
395
+ drawPixel(x + i, y + j, bitmap[(int32_t)j * w + i]);
396
+ }
397
+ }
398
+ }
399
+
400
+ // Glyph rendering — Adafruit_GFX.cpp drawChar, unchanged algorithm.
401
+ void CuttlefishGFX::drawChar(int16_t x, int16_t y, unsigned char c, uint16_t color, uint16_t bg, uint8_t size) {
402
+ if ((x >= width()) || // Clip right
403
+ (y >= height()) || // Clip bottom
404
+ ((x + 5 * size - 1) < 0) || // Clip left
405
+ ((y + 8 * size - 1) < 0)) // Clip top
406
+ return;
407
+ if (c >= 176) c++; // Adafruit '±' fixup
408
+ for (int8_t i = 0; i < 5; i++) {
409
+ uint8_t line = cuttlefish_glcdfont[(size_t)c * 5 + i];
410
+ for (int8_t j = 0; j < 8; j++, line >>= 1) {
411
+ if (line & 1) {
412
+ if (size == 1) drawPixel(x + i, y + j, color);
413
+ else fillRect(x + i * size, y + j * size, size, size, color);
414
+ } else if (bg != color) {
415
+ if (size == 1) drawPixel(x + i, y + j, bg);
416
+ else fillRect(x + i * size, y + j * size, size, size, bg);
417
+ }
418
+ }
419
+ }
420
+ if (bg != color) {
421
+ if (size == 1) drawFastVLine(x + 5, y, 8, bg);
422
+ else fillRect(x + 5 * size, y, size, 8 * size, bg);
423
+ }
424
+ }
425
+
426
+ void CuttlefishGFX::write(uint8_t c) {
427
+ if (c == '\\n') {
428
+ cursor_x_ = 0;
429
+ cursor_y_ += textsize_ * 8;
430
+ } else if (c != '\\r') {
431
+ int16_t w = 6 * textsize_;
432
+ if (wrap_ && ((cursor_x_ + w) > width())) {
433
+ cursor_x_ = 0;
434
+ cursor_y_ += textsize_ * 8;
435
+ }
436
+ drawChar(cursor_x_, cursor_y_, c, textcolor_, textbgcolor_, textsize_);
437
+ cursor_x_ += w;
438
+ }
439
+ }
440
+
441
+ // ── Canvas implementations ─────────────────────────────────────────────────
442
+
443
+ CuttlefishCanvas16::CuttlefishCanvas16(int16_t w, int16_t h)
444
+ : CuttlefishGFX(nullptr, nullptr),
445
+ buffer_(nullptr), canvas_w_(w), canvas_h_(h) {
446
+ if ((w > 0) && (h > 0)) {
447
+ size_t bytes = (size_t)w * (size_t)h * sizeof(uint16_t);
448
+ buffer_ = (uint16_t*)malloc(bytes);
449
+ if (buffer_) memset(buffer_, 0, bytes);
450
+ }
451
+ }
452
+
453
+ CuttlefishCanvas16::~CuttlefishCanvas16() {
454
+ if (buffer_) free(buffer_);
455
+ }
456
+
457
+ void CuttlefishCanvas16::drawPixel(int16_t x, int16_t y, uint16_t color) {
458
+ if (!buffer_) return;
459
+ if ((x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return;
460
+ buffer_[(size_t)y * (size_t)canvas_w_ + (size_t)x] = color;
461
+ }
462
+
463
+ void CuttlefishCanvas16::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
464
+ if (!buffer_) return;
465
+ int16_t x1 = (x < 0) ? 0 : x;
466
+ int16_t y1 = (y < 0) ? 0 : y;
467
+ int16_t x2 = x + w; if (x2 > canvas_w_) x2 = canvas_w_;
468
+ int16_t y2 = y + h; if (y2 > canvas_h_) y2 = canvas_h_;
469
+ // Cull before the inner loop (AGENTS.md: clip before expensive work).
470
+ if (x2 <= x1 || y2 <= y1) return;
471
+ for (int16_t j = y1; j < y2; j++) {
472
+ uint16_t* row = buffer_ + (size_t)j * (size_t)canvas_w_;
473
+ for (int16_t i = x1; i < x2; i++) row[i] = color;
474
+ }
475
+ }
476
+
477
+ uint16_t CuttlefishCanvas16::getPixel(int16_t x, int16_t y) const {
478
+ if (!buffer_ || (x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return 0;
479
+ return buffer_[(size_t)y * (size_t)canvas_w_ + (size_t)x];
480
+ }
481
+
482
+ CuttlefishCanvasMono::CuttlefishCanvasMono(int16_t w, int16_t h)
483
+ : CuttlefishGFX(nullptr, nullptr),
484
+ buffer_(nullptr), canvas_w_(w), canvas_h_(h) {
485
+ if ((w > 0) && (h > 0)) {
486
+ size_t row_bytes = ((size_t)w + 7u) / 8u;
487
+ size_t bytes = row_bytes * (size_t)h;
488
+ buffer_ = (uint8_t*)malloc(bytes);
489
+ if (buffer_) memset(buffer_, 0, bytes);
490
+ }
491
+ }
492
+
493
+ CuttlefishCanvasMono::~CuttlefishCanvasMono() {
494
+ if (buffer_) free(buffer_);
495
+ }
496
+
497
+ void CuttlefishCanvasMono::drawPixel(int16_t x, int16_t y, uint16_t color) {
498
+ if (!buffer_) return;
499
+ if ((x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return;
500
+ size_t row_bytes = ((size_t)canvas_w_ + 7u) / 8u;
501
+ uint8_t* row = buffer_ + (size_t)y * row_bytes;
502
+ uint8_t mask = 0x80u >> (x & 7);
503
+ if (color) row[x >> 3] |= mask;
504
+ else row[x >> 3] &= ~mask;
505
+ }
506
+
507
+ void CuttlefishCanvasMono::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
508
+ if (!buffer_) return;
509
+ int16_t x1 = (x < 0) ? 0 : x;
510
+ int16_t y1 = (y < 0) ? 0 : y;
511
+ int16_t x2 = x + w; if (x2 > canvas_w_) x2 = canvas_w_;
512
+ int16_t y2 = y + h; if (y2 > canvas_h_) y2 = canvas_h_;
513
+ if (x2 <= x1 || y2 <= y1) return;
514
+ uint8_t fill = color ? 0xFFu : 0x00u;
515
+ for (int16_t j = y1; j < y2; j++) {
516
+ for (int16_t i = x1; i < x2; i++) {
517
+ size_t row_bytes = ((size_t)canvas_w_ + 7u) / 8u;
518
+ uint8_t* row = buffer_ + (size_t)j * row_bytes;
519
+ uint8_t mask = 0x80u >> (i & 7);
520
+ if (color) row[i >> 3] |= mask;
521
+ else row[i >> 3] &= ~mask;
522
+ }
523
+ }
524
+ (void)fill;
525
+ }
526
+
527
+ uint16_t CuttlefishCanvasMono::getPixel(int16_t x, int16_t y) const {
528
+ if (!buffer_ || (x < 0) || (y < 0) || (x >= canvas_w_) || (y >= canvas_h_)) return 0;
529
+ size_t row_bytes = ((size_t)canvas_w_ + 7u) / 8u;
530
+ const uint8_t* row = buffer_ + (size_t)y * row_bytes;
531
+ return (row[x >> 3] & (0x80u >> (x & 7))) ? 0xFFFFu : 0x0000u;
532
+ }
533
+
534
+ #endif // CUTTLEFISH_GFX_DEFINED
535
+ `;
536
+ }
@@ -3,10 +3,6 @@
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitForwardDecls(): string {
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
@@ -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(): string {
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;