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