@typecad/ui 1.0.0-alpha.11 → 1.0.0-alpha.13
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.
- package/README.md +21 -1
- package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
- package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
- package/assets/fonts/dejavu/LICENSE +187 -0
- package/assets/fonts/dejavu/README.md +49 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +74 -0
- package/dist/engine-index.js +2 -0
- package/dist/preview/build-program.d.ts +7 -1
- package/dist/preview/build-program.js +94 -15
- package/dist/preview/host-gfx.d.ts +3 -0
- package/dist/preview/host-gfx.js +30 -3
- package/dist/preview/host-ui-runtime.d.ts +115 -0
- package/dist/preview/host-ui-runtime.js +928 -46
- package/dist/ui-engine/block-layout.js +25 -11
- package/dist/ui-engine/color.d.ts +6 -0
- package/dist/ui-engine/color.js +80 -2
- package/dist/ui-engine/compat-report.d.ts +8 -0
- package/dist/ui-engine/compat-report.js +124 -0
- package/dist/ui-engine/css-imports.d.ts +4 -0
- package/dist/ui-engine/css-imports.js +70 -0
- package/dist/ui-engine/css-parser.js +58 -14
- package/dist/ui-engine/default-font.d.ts +25 -0
- package/dist/ui-engine/default-font.js +80 -0
- package/dist/ui-engine/font-assets.d.ts +2 -2
- package/dist/ui-engine/font-assets.js +22 -5
- package/dist/ui-engine/html-parser.d.ts +4 -0
- package/dist/ui-engine/html-parser.js +241 -26
- package/dist/ui-engine/image-assets.d.ts +8 -1
- package/dist/ui-engine/image-assets.js +35 -2
- package/dist/ui-engine/image-decode.d.ts +22 -0
- package/dist/ui-engine/image-decode.js +194 -0
- package/dist/ui-engine/inline-parser.js +1 -1
- package/dist/ui-engine/layout-engine.d.ts +19 -0
- package/dist/ui-engine/layout-engine.js +59 -3
- package/dist/ui-engine/model.d.ts +18 -0
- package/dist/ui-engine/model.js +76 -8
- package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
- package/dist/ui-engine/runtime-header/canvas-helpers.js +5 -2
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
- package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
- package/dist/ui-engine/runtime-header/keyboard.js +682 -380
- package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1377
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
- package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
- package/dist/ui-engine/runtime-header/structs.js +228 -216
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +79 -23
- package/dist/ui-engine/runtime-header/types-defines.js +110 -93
- package/dist/ui-engine/shadcn-kit.d.ts +1 -0
- package/dist/ui-engine/shadcn-kit.js +502 -0
- package/dist/ui-engine/style-resolver.d.ts +4 -0
- package/dist/ui-engine/style-resolver.js +58 -6
- package/dist/ui-engine/transpile-ui.js +12 -5
- package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
- package/dist/ui-engine/ua-stylesheet.js +119 -32
- package/dist/ui-engine/ui-lowering.js +37 -3
- package/dist/ui-engine/ui-registry.js +32 -4
- package/dist/ui-engine/yoga-layout.js +57 -52
- package/dist/wizard/config-writer.d.ts +49 -0
- package/dist/wizard/config-writer.js +366 -0
- package/dist/wizard/display-catalog.d.ts +75 -0
- package/dist/wizard/display-catalog.js +207 -0
- package/dist/wizard/index.d.ts +7 -0
- package/dist/wizard/index.js +10 -0
- package/dist/wizard/integration-wizard.d.ts +15 -0
- package/dist/wizard/integration-wizard.js +486 -0
- package/dist/wizard/prompts.d.ts +32 -0
- package/dist/wizard/prompts.js +95 -0
- package/dist/wizard/starter-ui.d.ts +1 -0
- package/dist/wizard/starter-ui.js +56 -0
- package/package.json +18 -5
- package/src/cli.ts +87 -0
- package/src/engine-index.ts +53 -51
- package/src/preview/build-program.ts +810 -734
- package/src/preview/host-gfx.ts +30 -3
- package/src/preview/host-ui-runtime.ts +4133 -3289
- package/src/ui-engine/block-layout.ts +24 -11
- package/src/ui-engine/color.ts +77 -2
- package/src/ui-engine/compat-report.ts +139 -0
- package/src/ui-engine/css-imports.ts +69 -0
- package/src/ui-engine/css-parser.ts +64 -15
- package/src/ui-engine/default-font.ts +95 -0
- package/src/ui-engine/font-assets.ts +545 -525
- package/src/ui-engine/html-parser.ts +615 -397
- package/src/ui-engine/image-assets.ts +37 -2
- package/src/ui-engine/image-decode.ts +244 -0
- package/src/ui-engine/inline-parser.ts +1 -1
- package/src/ui-engine/layout-engine.ts +61 -3
- package/src/ui-engine/model.ts +1317 -1230
- package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
- package/src/ui-engine/runtime-header/canvas-helpers.ts +5 -2
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
- package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
- package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
- package/src/ui-engine/runtime-header/keyboard.ts +689 -386
- package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1402
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
- package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
- package/src/ui-engine/runtime-header/structs.ts +234 -222
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +79 -23
- package/src/ui-engine/runtime-header/types-defines.ts +116 -99
- package/src/ui-engine/shadcn-kit.ts +503 -0
- package/src/ui-engine/style-resolver.ts +471 -416
- package/src/ui-engine/transpile-ui.ts +12 -5
- package/src/ui-engine/ua-stylesheet.ts +137 -31
- package/src/ui-engine/ui-lowering.ts +486 -452
- package/src/ui-engine/ui-registry.ts +556 -524
- package/src/ui-engine/yoga-layout.ts +307 -309
- package/src/wizard/config-writer.ts +404 -0
- package/src/wizard/display-catalog.ts +273 -0
- package/src/wizard/index.ts +38 -0
- package/src/wizard/integration-wizard.ts +619 -0
- package/src/wizard/prompts.ts +145 -0
- package/src/wizard/starter-ui.ts +58 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Automatic image conversion: decode any common image format (PNG, JPEG,
|
|
3
|
+
// GIF, BMP, WebP, AVIF/HEIF, TIFF, SVG, ICO) into the raw RGB565 asset form
|
|
4
|
+
// the runtime consumes, so `<img src="photo.png">` just works — no manual
|
|
5
|
+
// conversion to raw .img dumps.
|
|
6
|
+
//
|
|
7
|
+
// Decoding is asynchronous (sharp); the UI module loader is synchronous. The
|
|
8
|
+
// bridge is a warm-up pass: the transpile graph collector and the preview
|
|
9
|
+
// snapshot builder await `warmUpImageDecoding()` on each UI source BEFORE
|
|
10
|
+
// the module loads, priming a cache keyed by absolute path + mtime. The
|
|
11
|
+
// synchronous image-assets reader then consults `getCachedDecodedImage()`.
|
|
12
|
+
// Anything undecodable (including the legacy raw RGB565 .img files) falls
|
|
13
|
+
// back to the raw reader unchanged.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
/** Identify a buffer by magic bytes. null = unknown (raw RGB565 dump). */
|
|
18
|
+
export function detectImageFormat(buf) {
|
|
19
|
+
if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47)
|
|
20
|
+
return "png";
|
|
21
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
22
|
+
return "jpeg";
|
|
23
|
+
if (buf.length >= 6 && buf.toString("ascii", 0, 3) === "GIF")
|
|
24
|
+
return "gif";
|
|
25
|
+
if (buf.length >= 2 && buf[0] === 0x42 && buf[1] === 0x4d)
|
|
26
|
+
return "bmp";
|
|
27
|
+
if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP")
|
|
28
|
+
return "webp";
|
|
29
|
+
// ISO-BMFF family (AVIF/HEIF): bytes 4..8 are "ftyp".
|
|
30
|
+
if (buf.length >= 8 && buf.toString("ascii", 4, 8) === "ftyp")
|
|
31
|
+
return "avif";
|
|
32
|
+
if (buf.length >= 4 && buf.toString("ascii", 0, 4) === "II*\0")
|
|
33
|
+
return "tiff";
|
|
34
|
+
if (buf.length >= 4 && buf.toString("ascii", 0, 4) === "MM\0*")
|
|
35
|
+
return "tiff";
|
|
36
|
+
if (buf.length >= 4 && buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x01 && buf[3] === 0x00)
|
|
37
|
+
return "ico";
|
|
38
|
+
// SVG is XML text: after an optional BOM, the first non-space char is '<'.
|
|
39
|
+
for (let i = 0; i < Math.min(buf.length, 8); i++) {
|
|
40
|
+
const b = buf[i];
|
|
41
|
+
if (b === 0xef || b === 0xbb || b === 0xbf)
|
|
42
|
+
continue; // UTF-8 BOM bytes
|
|
43
|
+
if (b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d)
|
|
44
|
+
continue;
|
|
45
|
+
if (b === 0x3c)
|
|
46
|
+
return "svg";
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
let sharpLoader = null;
|
|
52
|
+
function loadSharp() {
|
|
53
|
+
if (!sharpLoader) {
|
|
54
|
+
sharpLoader = import("sharp").then((m) => m.default).catch((err) => {
|
|
55
|
+
sharpLoader = null; // a later call can retry (e.g. after an install fix)
|
|
56
|
+
throw new Error(`the sharp image decoder could not be loaded (${err instanceof Error ? err.message : String(err)}) — ` +
|
|
57
|
+
`reinstall dependencies, or convert the image to a raw RGB565 .img file manually`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return sharpLoader;
|
|
61
|
+
}
|
|
62
|
+
/** Run a sharp pipeline to raw pixels: flatten alpha onto black, optionally
|
|
63
|
+
* downscale to fit the display (never enlarge). */
|
|
64
|
+
async function runSharp(pipeline, naturalW, naturalH, opts) {
|
|
65
|
+
if (opts.maxW && opts.maxH && (naturalW > opts.maxW || naturalH > opts.maxH)) {
|
|
66
|
+
pipeline = pipeline.resize({ width: opts.maxW, height: opts.maxH, fit: "inside", withoutEnlargement: true });
|
|
67
|
+
}
|
|
68
|
+
const { data, info } = await pipeline
|
|
69
|
+
.flatten({ background: "#000000" })
|
|
70
|
+
.raw()
|
|
71
|
+
.toBuffer({ resolveWithObject: true });
|
|
72
|
+
return { pixels: data, channels: info.channels, width: info.width, height: info.height };
|
|
73
|
+
}
|
|
74
|
+
/** Decode an .ico: decode-ico returns every frame as RGBA (PNG and BMP
|
|
75
|
+
* frames alike); pick the largest so the asset carries the best detail. */
|
|
76
|
+
async function decodeIco(buf) {
|
|
77
|
+
const decodeIcoMod = await import("decode-ico");
|
|
78
|
+
const decodeIco = decodeIcoMod.default;
|
|
79
|
+
const u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
80
|
+
const frames = decodeIco(u8);
|
|
81
|
+
if (!frames || frames.length === 0)
|
|
82
|
+
throw new Error("no frames in .ico");
|
|
83
|
+
let best = frames[0];
|
|
84
|
+
for (const f of frames) {
|
|
85
|
+
if (f.width * f.height > best.width * best.height)
|
|
86
|
+
best = f;
|
|
87
|
+
}
|
|
88
|
+
return { pixels: best.data, channels: 4, width: best.width, height: best.height };
|
|
89
|
+
}
|
|
90
|
+
function rgbaToRgb565(px, channels) {
|
|
91
|
+
const count = Math.floor(px.length / channels);
|
|
92
|
+
const out = new Array(count);
|
|
93
|
+
for (let i = 0; i < count; i++) {
|
|
94
|
+
const r = px[i * channels] >> 3;
|
|
95
|
+
const g = px[i * channels + 1] >> 2;
|
|
96
|
+
const b = px[i * channels + 2] >> 3;
|
|
97
|
+
out[i] = (r << 11) | (g << 5) | b;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
async function decodeImageBuffer(buf, format, opts) {
|
|
102
|
+
const sharp = await loadSharp();
|
|
103
|
+
let raw;
|
|
104
|
+
if (format === "ico") {
|
|
105
|
+
const ico = await decodeIco(buf);
|
|
106
|
+
if (opts.maxW && opts.maxH && (ico.width > opts.maxW || ico.height > opts.maxH)) {
|
|
107
|
+
// The largest frame still exceeds the display; route its RGBA through
|
|
108
|
+
// sharp for the same fit-inside downscale every other format gets.
|
|
109
|
+
const rgbaBuf = Buffer.from(ico.pixels.buffer, ico.pixels.byteOffset, ico.pixels.byteLength);
|
|
110
|
+
raw = await runSharp(sharp(rgbaBuf, { raw: { width: ico.width, height: ico.height, channels: 4 } }), ico.width, ico.height, opts);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
raw = ico;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// File bytes: auto-orient by EXIF first, then the shared flatten/fit path.
|
|
118
|
+
// metadata() reports the STORED dimensions — an EXIF orientation of 5-8
|
|
119
|
+
// means the pipeline's rotate() transposes the output, so the fit decision
|
|
120
|
+
// must compare against the swapped natural size or portrait photos can
|
|
121
|
+
// skip the downscale they need (and landscape ones downscale needlessly).
|
|
122
|
+
const oriented = sharp(buf, { failOn: "none" }).rotate();
|
|
123
|
+
const meta = await oriented.metadata();
|
|
124
|
+
const exifSwap = (meta.orientation ?? 1) >= 5 && (meta.orientation ?? 1) <= 8;
|
|
125
|
+
const naturalW = exifSwap ? meta.height : meta.width;
|
|
126
|
+
const naturalH = exifSwap ? meta.width : meta.height;
|
|
127
|
+
raw = await runSharp(oriented, naturalW ?? 0, naturalH ?? 0, opts);
|
|
128
|
+
}
|
|
129
|
+
return { width: raw.width, height: raw.height, data: rgbaToRgb565(raw.pixels, raw.channels) };
|
|
130
|
+
}
|
|
131
|
+
const decodedCache = new Map();
|
|
132
|
+
function cacheKey(abs, mtimeMs) {
|
|
133
|
+
return `${abs}\0${mtimeMs}`;
|
|
134
|
+
}
|
|
135
|
+
/** The decoded image for a file, or undefined when it was never warmed up or
|
|
136
|
+
* its decode failed (the caller falls back to the raw RGB565 reader). */
|
|
137
|
+
export function getCachedDecodedImage(absPath) {
|
|
138
|
+
let stat;
|
|
139
|
+
try {
|
|
140
|
+
stat = fs.statSync(absPath);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const entry = decodedCache.get(cacheKey(absPath, stat.mtimeMs));
|
|
146
|
+
return entry ? (entry.image ?? undefined) : undefined;
|
|
147
|
+
}
|
|
148
|
+
const SRC_ATTR = /\bsrc\s*=\s*["']([^"']+)["']/g;
|
|
149
|
+
/** Scan a UI source for src="…" references and pre-decode every referenced
|
|
150
|
+
* image file. Called with `await` BEFORE the UI module loads (the module
|
|
151
|
+
* loader and asset reader are synchronous); failures warn and fall back. */
|
|
152
|
+
export async function warmUpImageDecoding(sourceText, baseDir, opts = {}) {
|
|
153
|
+
const pending = new Set();
|
|
154
|
+
let match;
|
|
155
|
+
SRC_ATTR.lastIndex = 0;
|
|
156
|
+
while ((match = SRC_ATTR.exec(sourceText)) !== null) {
|
|
157
|
+
const src = match[1];
|
|
158
|
+
if (!src || /^(data:|https?:|#)/.test(src))
|
|
159
|
+
continue;
|
|
160
|
+
const abs = path.isAbsolute(src) ? src : path.resolve(baseDir, src);
|
|
161
|
+
pending.add(abs);
|
|
162
|
+
}
|
|
163
|
+
await Promise.all([...pending].map(async (abs) => {
|
|
164
|
+
// Track the mtime up front so a failed decode is memoized under the key
|
|
165
|
+
// getCachedDecodedImage() looks up — the next warm-up of an unchanged
|
|
166
|
+
// file hits the null entry instead of re-attempting (and re-warning).
|
|
167
|
+
let mtimeMs = 0;
|
|
168
|
+
try {
|
|
169
|
+
if (!fs.existsSync(abs))
|
|
170
|
+
return;
|
|
171
|
+
const stat = fs.statSync(abs);
|
|
172
|
+
mtimeMs = stat.mtimeMs;
|
|
173
|
+
const key = cacheKey(abs, mtimeMs);
|
|
174
|
+
if (decodedCache.has(key))
|
|
175
|
+
return;
|
|
176
|
+
const buf = fs.readFileSync(abs);
|
|
177
|
+
const format = detectImageFormat(buf);
|
|
178
|
+
if (!format)
|
|
179
|
+
return; // legacy raw RGB565 dump — the raw reader handles it
|
|
180
|
+
const image = await decodeImageBuffer(buf, format, opts);
|
|
181
|
+
decodedCache.set(key, { mtimeMs, image });
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
if (mtimeMs > 0) {
|
|
185
|
+
decodedCache.set(cacheKey(abs, mtimeMs), { mtimeMs, image: null });
|
|
186
|
+
}
|
|
187
|
+
console.warn(`[img] ${path.basename(abs)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
188
|
+
}
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
/** Test hook: drop cached decodes. */
|
|
192
|
+
export function resetImageDecodeCache() {
|
|
193
|
+
decodedCache.clear();
|
|
194
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Tags whose content participates in inline flow inside a text container.
|
|
2
2
|
* Bare text nodes between them flow too. Recognized as inline children of a
|
|
3
3
|
* text-effective node; absorbed into the parent's run list by the resolver. */
|
|
4
|
-
export const INLINE_TAGS = new Set(["span", "a", "b", "strong", "i", "em", "u"]);
|
|
4
|
+
export const INLINE_TAGS = new Set(["span", "a", "b", "strong", "i", "em", "u", "code", "kbd", "samp"]);
|
|
5
5
|
/** Walk childNodes in document order, building an inline sequence.
|
|
6
6
|
*
|
|
7
7
|
* Returns `undefined` when the element is NOT inline-bearing:
|
|
@@ -21,6 +21,25 @@ export interface LayoutEngine {
|
|
|
21
21
|
}
|
|
22
22
|
/** Static display:none predicate shared by layout engines and lowering. */
|
|
23
23
|
export declare function isDisplayNone(node: StyledNode): boolean;
|
|
24
|
+
/** A resolved CSS length: device pixels, a percentage of the parent
|
|
25
|
+
* (resolved by the layout engine), or the auto keyword (margins). */
|
|
26
|
+
export type CssDim = number | "auto" | `${number}%`;
|
|
27
|
+
/** Parse a single CSS length to a layout dimension. px/bare numbers are device
|
|
28
|
+
* pixels, rem/em resolve × 16 (root font size), percentages are preserved as
|
|
29
|
+
* `${n}%` strings so flexbox can resolve them against the parent, and "auto"
|
|
30
|
+
* passes through (auto margins absorb free space — `margin: 0 auto` centers). */
|
|
31
|
+
export declare function cssDimValue(val: string | undefined): CssDim;
|
|
32
|
+
/** Is this a set (non-default) dimension? Falsy 0 values are skipped when
|
|
33
|
+
* applying to layout; percentages and "auto" always apply. */
|
|
34
|
+
export declare function dimSet(d: CssDim): boolean;
|
|
35
|
+
/** Size- and inset-safe variant of cssDimValue(): Yoga's typings exclude the
|
|
36
|
+
* "auto" keyword for sizes/paddings/insets (only margins accept it), so fold
|
|
37
|
+
* it to undefined — auto sizing is the engine default anyway. */
|
|
38
|
+
export declare function cssSizeValue(val: string | undefined): number | `${number}%` | undefined;
|
|
39
|
+
/** Expand a 1-4 value CSS box shorthand (padding/margin) to
|
|
40
|
+
* [top, right, bottom, left] following the CSS TRBL rotation:
|
|
41
|
+
* 1 = all, 2 = V H, 3 = T H B, 4 = T R B L. */
|
|
42
|
+
export declare function expandBoxShorthand(val: string | undefined): [CssDim, CssDim, CssDim, CssDim];
|
|
24
43
|
/** Parse CSS aspect-ratio values: "16 / 9", "1/1", or "1.777". */
|
|
25
44
|
export declare function parseAspectRatio(value: string | undefined): number | undefined;
|
|
26
45
|
/** Measure a node's intrinsic size. Accounts for the Adafruit GFX font metrics
|
|
@@ -17,6 +17,56 @@ import { assetTextWidth, assetLineHeight } from "./font-assets.js";
|
|
|
17
17
|
export function isDisplayNone(node) {
|
|
18
18
|
return node.style.display?.trim().toLowerCase() === "none";
|
|
19
19
|
}
|
|
20
|
+
/** Parse a single CSS length to a layout dimension. px/bare numbers are device
|
|
21
|
+
* pixels, rem/em resolve × 16 (root font size), percentages are preserved as
|
|
22
|
+
* `${n}%` strings so flexbox can resolve them against the parent, and "auto"
|
|
23
|
+
* passes through (auto margins absorb free space — `margin: 0 auto` centers). */
|
|
24
|
+
export function cssDimValue(val) {
|
|
25
|
+
const v = (val ?? "").trim();
|
|
26
|
+
if (!v)
|
|
27
|
+
return 0;
|
|
28
|
+
if (/^auto$/i.test(v))
|
|
29
|
+
return "auto";
|
|
30
|
+
const remM = /^(-?[\d.]+)rem$/i.exec(v);
|
|
31
|
+
if (remM)
|
|
32
|
+
return Math.round(parseFloat(remM[1]) * 16);
|
|
33
|
+
const emM = /^(-?[\d.]+)em$/i.exec(v);
|
|
34
|
+
if (emM)
|
|
35
|
+
return Math.round(parseFloat(emM[1]) * 16);
|
|
36
|
+
const pctM = /^(-?[\d.]+)%$/.exec(v);
|
|
37
|
+
if (pctM)
|
|
38
|
+
return `${parseFloat(pctM[1])}%`;
|
|
39
|
+
const m = /^(-?[\d.]+)(?:px)?$/i.exec(v);
|
|
40
|
+
return m ? parseFloat(m[1]) : 0;
|
|
41
|
+
}
|
|
42
|
+
/** Is this a set (non-default) dimension? Falsy 0 values are skipped when
|
|
43
|
+
* applying to layout; percentages and "auto" always apply. */
|
|
44
|
+
export function dimSet(d) {
|
|
45
|
+
return d !== 0;
|
|
46
|
+
}
|
|
47
|
+
/** Size- and inset-safe variant of cssDimValue(): Yoga's typings exclude the
|
|
48
|
+
* "auto" keyword for sizes/paddings/insets (only margins accept it), so fold
|
|
49
|
+
* it to undefined — auto sizing is the engine default anyway. */
|
|
50
|
+
export function cssSizeValue(val) {
|
|
51
|
+
const d = cssDimValue(val);
|
|
52
|
+
return d === "auto" ? undefined : d;
|
|
53
|
+
}
|
|
54
|
+
/** Expand a 1-4 value CSS box shorthand (padding/margin) to
|
|
55
|
+
* [top, right, bottom, left] following the CSS TRBL rotation:
|
|
56
|
+
* 1 = all, 2 = V H, 3 = T H B, 4 = T R B L. */
|
|
57
|
+
export function expandBoxShorthand(val) {
|
|
58
|
+
const parts = (val ?? "").trim().split(/\s+/).filter(Boolean);
|
|
59
|
+
const d = parts.map((p) => cssDimValue(p));
|
|
60
|
+
if (d.length <= 1) {
|
|
61
|
+
const v = d[0] ?? 0;
|
|
62
|
+
return [v, v, v, v];
|
|
63
|
+
}
|
|
64
|
+
if (d.length === 2)
|
|
65
|
+
return [d[0], d[1], d[0], d[1]];
|
|
66
|
+
if (d.length === 3)
|
|
67
|
+
return [d[0], d[1], d[2], d[1]];
|
|
68
|
+
return [d[0], d[1], d[2], d[3]];
|
|
69
|
+
}
|
|
20
70
|
/** Parse CSS aspect-ratio values: "16 / 9", "1/1", or "1.777". */
|
|
21
71
|
export function parseAspectRatio(value) {
|
|
22
72
|
const raw = value?.trim().toLowerCase();
|
|
@@ -148,12 +198,18 @@ export function measure(node, availableWidth, fontAssets = []) {
|
|
|
148
198
|
const widthOf = (value) => assetTextWidth(stripInterp(value), node.style, fontAssets) ?? textWidthOf(stripInterp(value), advance);
|
|
149
199
|
if (node.tag === "text" || node.tag === "button" || node.tag === "select") {
|
|
150
200
|
if (node.tag === "select") {
|
|
151
|
-
// Size to the
|
|
201
|
+
// Size to the widest option by RENDERED width, not the full
|
|
202
|
+
// comma-separated text and not character count — with a proportional
|
|
203
|
+
// font, equal-length options can differ in width ("Alpha" vs "Gamma"),
|
|
204
|
+
// and the wider one must fit without wrapping.
|
|
152
205
|
const options = node.options && node.options.length > 0
|
|
153
206
|
? node.options.map((option) => option.text)
|
|
154
207
|
: (node.text ?? "").split(",").map(s => s.trim()).filter(Boolean);
|
|
155
|
-
const
|
|
156
|
-
|
|
208
|
+
const widest = options.length > 0
|
|
209
|
+
? options.reduce((a, b) => widthOf(applyTextTransform(a, node)) >= widthOf(applyTextTransform(b, node)) ? a : b)
|
|
210
|
+
: "";
|
|
211
|
+
// +14: the dropdown chevron the runtimes draw at the right end.
|
|
212
|
+
return { w: widthOf(applyTextTransform(widest, node)) + 14, h: lineHeightOf(node, charH) };
|
|
157
213
|
}
|
|
158
214
|
const text = applyTextTransform(stripInterp(node.text), node);
|
|
159
215
|
const layout = layoutText(text, {
|
|
@@ -14,6 +14,12 @@ export interface UINodeModel {
|
|
|
14
14
|
box: Box;
|
|
15
15
|
bg: number;
|
|
16
16
|
fg: number;
|
|
17
|
+
/** Checked-state color pair baked from :checked rules (RGB565; -1 unset).
|
|
18
|
+
* Check/radio/select draw paths swap to it when the value flips — the
|
|
19
|
+
* switch track, checkbox face, radio dot, and the select list's selected
|
|
20
|
+
* row (shadcn data-[state=checked] theming). */
|
|
21
|
+
checkedBg: number;
|
|
22
|
+
checkedFg: number;
|
|
17
23
|
kind: UINodeKindModel;
|
|
18
24
|
text?: string;
|
|
19
25
|
placeholder?: string;
|
|
@@ -73,6 +79,18 @@ export interface UINodeModel {
|
|
|
73
79
|
nowrap: boolean;
|
|
74
80
|
whiteSpaceMode: 0 | 1 | 2 | 3;
|
|
75
81
|
visible: boolean;
|
|
82
|
+
/** HTML disabled attribute: blocks taps/keyboard and dims the control. */
|
|
83
|
+
disabled: boolean;
|
|
84
|
+
/** <drawer side>: 0=bottom 1=top 2=left 3=right; -1 = not a drawer. */
|
|
85
|
+
drawerSide: number;
|
|
86
|
+
/** <toast duration>: ms before auto-close (0 = manual close only). */
|
|
87
|
+
toastDuration: number;
|
|
88
|
+
/** Visibility-reflow axis (device): 0 none, 1 column, 2 row. */
|
|
89
|
+
flowAxis: number;
|
|
90
|
+
/** Visibility-reflow: main-axis gap between in-flow children (px, 0-255). */
|
|
91
|
+
flowGap: number;
|
|
92
|
+
/** Visibility-reflow flags: bit0 auto height, bit1 auto width, bit2 out-of-flow. */
|
|
93
|
+
flowFlags: number;
|
|
76
94
|
opacity: number;
|
|
77
95
|
clearColor: number;
|
|
78
96
|
lastTextWidth: number;
|
package/dist/ui-engine/model.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { effectiveDisplaySize } from "@typecad/cuttlefish/api/shared";
|
|
2
2
|
import { deriveCapabilities } from "@typecad/cuttlefish/api/shared";
|
|
3
|
-
import { resolveColorInternal } from "./color.js";
|
|
3
|
+
import { resolveColorInternal, isTransparentColor } from "./color.js";
|
|
4
4
|
import { parseAnimation } from "./css-parser.js";
|
|
5
5
|
import { selectFontAssetForStyle, assetTextWidth, assetLineHeight } from "./font-assets.js";
|
|
6
6
|
import { isDisplayNone } from "./layout-engine.js";
|
|
@@ -14,7 +14,7 @@ export const KEYFRAME_PROP_OPACITY = 4;
|
|
|
14
14
|
export const KEYFRAME_PROP_TRANSFORM = 8;
|
|
15
15
|
export const KEYFRAME_PROP_SIZE = 16;
|
|
16
16
|
function nodeKind(tag) {
|
|
17
|
-
if (tag === "screen" || tag === "view")
|
|
17
|
+
if (tag === "screen" || tag === "view" || tag === "drawer" || tag === "dialog" || tag === "toast")
|
|
18
18
|
return "fill";
|
|
19
19
|
if (tag === "button")
|
|
20
20
|
return "button";
|
|
@@ -121,7 +121,12 @@ function borderRadiusOf(style) {
|
|
|
121
121
|
if (!style.borderRadius)
|
|
122
122
|
return 0;
|
|
123
123
|
const px = parseInt(style.borderRadius, 10);
|
|
124
|
-
|
|
124
|
+
// Clamp at 0: kit recipes like `calc(var(--radius) - 2px)` go negative for
|
|
125
|
+
// 0px-radius themes (tweakcn's sharper exports) — a negative radius is both
|
|
126
|
+
// meaningless and a uint8 narrowing error in the emitted node table.
|
|
127
|
+
if (isNaN(px) || px < 0)
|
|
128
|
+
return 0;
|
|
129
|
+
return Math.min(255, px);
|
|
125
130
|
}
|
|
126
131
|
function paddingOf(style) {
|
|
127
132
|
if (!style.padding)
|
|
@@ -449,7 +454,7 @@ function parseBoxShadow(style, format) {
|
|
|
449
454
|
// Split into a numbers zone (offset/blur) and a color zone. The color
|
|
450
455
|
// zone starts at the first # hex, rgb(, rgba(, or color name. css-tree
|
|
451
456
|
// may strip spaces between values, so we can't rely on whitespace split.
|
|
452
|
-
const colorStart = part.search(/#|rgba?\(|\b(?:black|white|red|green|blue|gray|grey|yellow|orange|purple|pink|cyan|magenta|silver|gold|brown|tan|navy|teal|maroon|lime|olive|aqua|fuchsia|transparent)\b/i);
|
|
457
|
+
const colorStart = part.search(/#|rgba?\(|hsla?\(|\b(?:black|white|red|green|blue|gray|grey|yellow|orange|purple|pink|cyan|magenta|silver|gold|brown|tan|navy|teal|maroon|lime|olive|aqua|fuchsia|transparent)\b/i);
|
|
453
458
|
const numZone = colorStart >= 0 ? part.slice(0, colorStart) : part;
|
|
454
459
|
const colorZone = colorStart >= 0 ? part.slice(colorStart) : "";
|
|
455
460
|
// Extract numbers from the numeric zone only (safe — no hex digits here).
|
|
@@ -640,11 +645,13 @@ function animationDeclOf(style) {
|
|
|
640
645
|
function flatten(node, boxes, out, cursor, parentBg, parentIndex = -1, screenId = 0, parentZIndex = 0, parentOpacity = 100) {
|
|
641
646
|
const index = cursor.i++;
|
|
642
647
|
const box = boxes[index] ?? { x: 0, y: 0, w: 0, h: 0 };
|
|
643
|
-
|
|
648
|
+
// `background: transparent` (or alpha-0) means NO fill — the parent's
|
|
649
|
+
// background shows through, matching browsers. (It used to paint black.)
|
|
650
|
+
const bgStr = node.style.background;
|
|
651
|
+
const bgIsGradient = bgStr !== undefined && bgStr.includes("linear-gradient");
|
|
652
|
+
const hasBg = bgStr !== undefined && !bgIsGradient ? !isTransparentColor(bgStr) : !!bgStr;
|
|
644
653
|
// If background is a gradient, extract the first color stop as the base
|
|
645
654
|
// clearColor string (avoids resolveColor choking on "linear-gradient(...)").
|
|
646
|
-
const bgStr = node.style.background;
|
|
647
|
-
const bgIsGradient = bgStr && bgStr.includes("linear-gradient");
|
|
648
655
|
const bgBaseColor = bgIsGradient ? extractFirstGradientColor(bgStr) : bgStr;
|
|
649
656
|
const clearColor = hasBg ? bgBaseColor : parentBg;
|
|
650
657
|
const zIndex = Math.max(-32768, Math.min(32767, parentZIndex + zIndexOf(node.style)));
|
|
@@ -692,6 +699,10 @@ export function lowerUIToModel(root, boxes, colorFormat, display, fontAssets = [
|
|
|
692
699
|
if (s.id)
|
|
693
700
|
screenIdToIndex.set(s.id, i);
|
|
694
701
|
});
|
|
702
|
+
// Raw flex direction per node index (pre-order, parents first) — the
|
|
703
|
+
// visibility-reflow flags use the parent's direction to decide cross-axis
|
|
704
|
+
// stretch vs content sizing (see the flowAxis/flowFlags IIFE below).
|
|
705
|
+
const flowDirByIndex = [];
|
|
695
706
|
const nodes = flat.map(({ index, node, box, hasBg, clearColor, parentIndex, subtreeEnd, screenId, zIndex, effectiveOpacity }) => {
|
|
696
707
|
// If background is a gradient, use the first stop as the base bg color
|
|
697
708
|
// (the runtime draws the actual gradient per-row on top of this).
|
|
@@ -700,7 +711,19 @@ export function lowerUIToModel(root, boxes, colorFormat, display, fontAssets = [
|
|
|
700
711
|
? (grad ? grad.color1 : resolveColorInternal(node.style.background, colorFormat))
|
|
701
712
|
: 0;
|
|
702
713
|
const fg = node.style.color ? resolveColorInternal(node.style.color, colorFormat) : 0xffff;
|
|
703
|
-
|
|
714
|
+
// Checked-state pair from :checked rules (e.g. the kit's switch/checkbox/
|
|
715
|
+
// radio primary pair, the select list's accent pair). -1 = unset.
|
|
716
|
+
const checkedStyle = node.style.checked;
|
|
717
|
+
const checkedBg = checkedStyle && checkedStyle.background
|
|
718
|
+
? resolveColorInternal(checkedStyle.background, colorFormat)
|
|
719
|
+
: -1;
|
|
720
|
+
const checkedFg = checkedStyle && checkedStyle.color
|
|
721
|
+
? resolveColorInternal(checkedStyle.color, colorFormat)
|
|
722
|
+
: -1;
|
|
723
|
+
// Border color defaults to currentColor (the node's resolved text color),
|
|
724
|
+
// matching browsers — `border: 1px solid` on a white-on-dark theme gets a
|
|
725
|
+
// white border, not a hardcoded black one.
|
|
726
|
+
const bColor = node.style.borderColor ? resolveColorInternal(node.style.borderColor, colorFormat) : fg;
|
|
704
727
|
const clear = clearColor ? resolveColorInternal(clearColor, colorFormat) : 0;
|
|
705
728
|
const outline = outlineOf(node.style);
|
|
706
729
|
const outlineColor = outline.color ? resolveColorInternal(outline.color, colorFormat) : fg;
|
|
@@ -792,6 +815,8 @@ export function lowerUIToModel(root, boxes, colorFormat, display, fontAssets = [
|
|
|
792
815
|
box: baseTransformOffset.box,
|
|
793
816
|
bg,
|
|
794
817
|
fg,
|
|
818
|
+
checkedBg,
|
|
819
|
+
checkedFg,
|
|
795
820
|
kind,
|
|
796
821
|
text: applyTextTransform(node.text, node.style),
|
|
797
822
|
placeholder: applyTextTransform(node.placeholder, node.style),
|
|
@@ -871,6 +896,49 @@ export function lowerUIToModel(root, boxes, colorFormat, display, fontAssets = [
|
|
|
871
896
|
nowrap: wsMode === 1 || wsMode === 2,
|
|
872
897
|
whiteSpaceMode: wsMode,
|
|
873
898
|
visible: node.style.visibility !== "hidden" && node.style.visibility !== "collapse" && !isDisplayNone(node),
|
|
899
|
+
disabled: node.disabled === true,
|
|
900
|
+
drawerSide: (node.tag === "drawer" || node.tag === "dialog" || node.tag === "toast")
|
|
901
|
+
? { bottom: 0, top: 1, left: 2, right: 3, center: 4 }[node.tag === "dialog" ? "center" : (node.drawerSide ?? "bottom")] ?? 0
|
|
902
|
+
: -1,
|
|
903
|
+
toastDuration: node.tag === "toast" ? (node.toastDuration ?? 2500) : 0,
|
|
904
|
+
// Visibility-reflow metadata: how this node's in-flow children stack, so
|
|
905
|
+
// the device runtime can re-stack them when a child's visibility changes
|
|
906
|
+
// (the preview re-layouts with yoga every frame; the device's boxes are
|
|
907
|
+
// baked at build time). The flex mapping mirrors yoga-layout.ts exactly;
|
|
908
|
+
// wrapped, reversed, or non-flex-start-justified containers opt out
|
|
909
|
+
// (axis 0) because a linear re-stack can't reproduce their layout.
|
|
910
|
+
...(() => {
|
|
911
|
+
const s = node.style;
|
|
912
|
+
const fd = s.display === "flex" ? s.flexDirection : "column";
|
|
913
|
+
const outOfFlow = s.position === "absolute" || s.position === "fixed";
|
|
914
|
+
const gapPx = (v) => {
|
|
915
|
+
const n = v === undefined ? 0 : parseFloat(v);
|
|
916
|
+
return Number.isFinite(n) && n > 0 ? Math.min(255, Math.round(n)) : 0;
|
|
917
|
+
};
|
|
918
|
+
const dirAxis = (fd === "row" || fd === "row-reverse") ? 2 : 1;
|
|
919
|
+
flowDirByIndex[index] = dirAxis;
|
|
920
|
+
let axis = 0;
|
|
921
|
+
if ((node.children?.length ?? 0) > 0 && !outOfFlow &&
|
|
922
|
+
fd !== "row-reverse" && fd !== "column-reverse" &&
|
|
923
|
+
s.flexWrap !== "wrap" && s.flexWrap !== "wrap-reverse" &&
|
|
924
|
+
(!s.justifyContent || s.justifyContent === "flex-start" || s.justifyContent === "normal")) {
|
|
925
|
+
axis = dirAxis;
|
|
926
|
+
}
|
|
927
|
+
const gap = axis === 2 ? gapPx(s.rowGap) : gapPx(s.columnGap);
|
|
928
|
+
// Content-sized only when the size truly comes from the content: an
|
|
929
|
+
// explicit size, flex-grow space, a flex-basis, or cross-axis
|
|
930
|
+
// stretch (a child of a row parent stretches its height; of a column
|
|
931
|
+
// parent its width) all derive the dimension from elsewhere. Getting
|
|
932
|
+
// this wrong let the reflow resize a flex-grown scroll body to its
|
|
933
|
+
// collapsed content — contentHeight == box.h, nothing scrollable.
|
|
934
|
+
const grown = s.flexGrow !== undefined && parseFloat(String(s.flexGrow)) > 0;
|
|
935
|
+
const basisSet = s.flexBasis !== undefined && s.flexBasis !== "auto";
|
|
936
|
+
const parentDir = parentIndex >= 0 ? (flowDirByIndex[parentIndex] ?? 1) : 0;
|
|
937
|
+
const parentDerived = grown || basisSet || parentDir === 0;
|
|
938
|
+
const autoH = !parentDerived && parentDir !== 2 && (s.height === undefined || s.height === "auto");
|
|
939
|
+
const autoW = !parentDerived && parentDir !== 1 && (s.width === undefined || s.width === "auto");
|
|
940
|
+
return { flowAxis: axis, flowGap: gap, flowFlags: (autoH ? 1 : 0) | (autoW ? 2 : 0) | (outOfFlow ? 4 : 0) };
|
|
941
|
+
})(),
|
|
874
942
|
opacity: effectiveOpacity,
|
|
875
943
|
clearColor: clear,
|
|
876
944
|
lastTextWidth: 0,
|
|
@@ -3,21 +3,40 @@
|
|
|
3
3
|
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
4
|
export function emitBlendBodies() {
|
|
5
5
|
return `
|
|
6
|
+
// Blend LUTs (LVGL draw-unit trick, memory-frugal variant): two 256-entry
|
|
7
|
+
// per-channel tables rebuilt only when the opacity level CHANGES. Blends
|
|
8
|
+
// typically run per-pixel at a fixed opacity (skeleton pulse frame, one
|
|
9
|
+
// shadow pass, a glyph's coverage step), so the table amortizes to one
|
|
10
|
+
// rebuild per pass and every pixel becomes three array lookups instead of
|
|
11
|
+
// three multiply-divide rounds. Total cost: 513 bytes of static RAM.
|
|
12
|
+
static uint8_t __ui_blend_lut_fg[256];
|
|
13
|
+
static uint8_t __ui_blend_lut_bg[256];
|
|
14
|
+
static uint8_t __ui_blend_lut_opacity = 255; // 255 = never built (valid range 0..100)
|
|
15
|
+
static inline void ui_blend_lut_build(uint8_t opacity) {
|
|
16
|
+
if (__ui_blend_lut_opacity == opacity) return;
|
|
17
|
+
for (int16_t v = 0; v < 256; v++) {
|
|
18
|
+
__ui_blend_lut_fg[v] = static_cast<uint8_t>((v * opacity) / 100);
|
|
19
|
+
__ui_blend_lut_bg[v] = static_cast<uint8_t>((v * (100 - opacity)) / 100);
|
|
20
|
+
}
|
|
21
|
+
__ui_blend_lut_opacity = opacity;
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
static inline uint16_t ui_blend565(uint16_t fg, uint16_t bg, uint8_t opacity) {
|
|
7
25
|
if (opacity >= 100) return fg;
|
|
8
26
|
if (opacity == 0) return bg;
|
|
9
|
-
|
|
27
|
+
ui_blend_lut_build(opacity);
|
|
28
|
+
// Blend in 888 internally for higher precision: unpack 565->888 (replicating
|
|
10
29
|
// high bits), blend at 8-bit, then re-quantize to 565. This produces smoother
|
|
11
|
-
// intermediate values for AA text edges, opacity, shadows, and gradients
|
|
30
|
+
// intermediate values for AA text edges, opacity, shadows, and gradients --
|
|
12
31
|
// the 565-channel blend (32 red levels) was too coarse and showed banding.
|
|
13
32
|
uint8_t fr = (fg >> 11) & 0x1F, fg5 = (fg >> 5) & 0x3F, fb = fg & 0x1F;
|
|
14
33
|
uint8_t br = (bg >> 11) & 0x1F, bg5 = (bg >> 5) & 0x3F, bb = bg & 0x1F;
|
|
15
|
-
// Unpack to 8-bit (5-bit
|
|
34
|
+
// Unpack to 8-bit (5-bit -> 8-bit: (v << 3) | (v >> 2)).
|
|
16
35
|
uint16_t fr8 = (fr << 3) | (fr >> 2), fg8 = (fg5 << 2) | (fg5 >> 4), fb8 = (fb << 3) | (fb >> 2);
|
|
17
36
|
uint16_t br8 = (br << 3) | (br >> 2), bg8 = (bg5 << 2) | (bg5 >> 4), bb8 = (bb << 3) | (bb >> 2);
|
|
18
|
-
uint16_t r = static_cast<uint16_t>(
|
|
19
|
-
uint16_t g = static_cast<uint16_t>(
|
|
20
|
-
uint16_t b = static_cast<uint16_t>(
|
|
37
|
+
uint16_t r = static_cast<uint16_t>(__ui_blend_lut_fg[fr8] + __ui_blend_lut_bg[br8]);
|
|
38
|
+
uint16_t g = static_cast<uint16_t>(__ui_blend_lut_fg[fg8] + __ui_blend_lut_bg[bg8]);
|
|
39
|
+
uint16_t b = static_cast<uint16_t>(__ui_blend_lut_fg[fb8] + __ui_blend_lut_bg[bb8]);
|
|
21
40
|
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
|
|
22
41
|
}
|
|
23
42
|
|
|
@@ -28,11 +47,12 @@ static inline uint16_t ui_blend565(uint16_t fg, uint16_t bg, uint8_t opacity) {
|
|
|
28
47
|
static inline uint32_t ui_blend888(uint32_t fg, uint32_t bg, uint8_t opacity) {
|
|
29
48
|
if (opacity >= 100) return fg;
|
|
30
49
|
if (opacity == 0) return bg;
|
|
50
|
+
ui_blend_lut_build(opacity);
|
|
31
51
|
uint8_t fr = (fg >> 16) & 0xff, fg8 = (fg >> 8) & 0xff, fb = fg & 0xff;
|
|
32
52
|
uint8_t br = (bg >> 16) & 0xff, bg8 = (bg >> 8) & 0xff, bb = bg & 0xff;
|
|
33
|
-
uint8_t r = (fr
|
|
34
|
-
uint8_t g = (fg8
|
|
35
|
-
uint8_t b = (fb
|
|
53
|
+
uint8_t r = static_cast<uint8_t>(__ui_blend_lut_fg[fr] + __ui_blend_lut_bg[br]);
|
|
54
|
+
uint8_t g = static_cast<uint8_t>(__ui_blend_lut_fg[fg8] + __ui_blend_lut_bg[bg8]);
|
|
55
|
+
uint8_t b = static_cast<uint8_t>(__ui_blend_lut_fg[fb] + __ui_blend_lut_bg[bb]);
|
|
36
56
|
return (static_cast<uint32_t>(r) << 16) | (static_cast<uint32_t>(g) << 8) | b;
|
|
37
57
|
}
|
|
38
58
|
`;
|
|
@@ -29,13 +29,16 @@ static inline CuttlefishCanvas16* ui_create_canvas_best(int16_t w, int16_t h) {
|
|
|
29
29
|
if (!psram_logged) {
|
|
30
30
|
psram_logged = 1;
|
|
31
31
|
#if defined(ESP32) && defined(ARDUINO)
|
|
32
|
-
Serial.printf("[psram] canvas %dx%d allocated in PSRAM (free=%u)
|
|
32
|
+
Serial.printf("[psram] canvas %dx%d allocated in PSRAM (free=%u)\\n", w, h, ESP.getFreePsram());
|
|
33
33
|
#else
|
|
34
|
-
printf("[psram] canvas %dx%d allocated in PSRAM
|
|
34
|
+
printf("[psram] canvas %dx%d allocated in PSRAM\\n", w, h);
|
|
35
35
|
#endif
|
|
36
36
|
}
|
|
37
37
|
return c;
|
|
38
38
|
}
|
|
39
|
+
// Object allocated but its pixel buffer failed — free the object before
|
|
40
|
+
// falling through to SRAM so it isn't leaked.
|
|
41
|
+
display_deleteCanvas(c);
|
|
39
42
|
// PSRAM allocation failed (rare — fragmented PSRAM) → fall through to SRAM.
|
|
40
43
|
}
|
|
41
44
|
#endif
|