lecodes-web-canvas 2.0.0
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/package.json +26 -0
- package/src/contract.d.ts +16 -0
- package/src/core/SceneNode.ts +109 -0
- package/src/core/hitTest.ts +30 -0
- package/src/core/text.ts +259 -0
- package/src/core/util/colors.ts +30 -0
- package/src/core/util/genericFamilies.ts +30 -0
- package/src/core/util/geometry.ts +121 -0
- package/src/core/util/svgSrc.ts +141 -0
- package/src/host/BrowserHost.ts +132 -0
- package/src/host/HeadlessHost.ts +64 -0
- package/src/host/Host.ts +85 -0
- package/src/layout/CuiPaint.gen.ts +121 -0
- package/src/layout/LayoutEngine.ts +78 -0
- package/src/layout/creatorUi/loadCreatorUi.ts +87 -0
- package/src/layout/creatorUi/paint-read.ts +131 -0
- package/src/layout/creatorUi/readGradient.ts +62 -0
- package/src/paint/Canvas2DPainter.ts +258 -0
- package/src/paint/Painter.ts +62 -0
- package/src/paint/RecordingPainter.ts +101 -0
- package/src/paint/paintTree.ts +314 -0
- package/src/runtime/TreeUI.ts +822 -0
- package/src/serialize/serializeRenderTree.ts +150 -0
- package/src/types.ts +299 -0
- package/src/vite-env.d.ts +1 -0
- package/src/web/BrowserUI.ts +312 -0
- package/src/web/chrome.ts +12 -0
- package/src/web/renderer.ts +192 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { BorderRadius, RenderNode, ResolvedStyle } from "../types"
|
|
2
|
+
import { rgbaToHex } from "../core/util/colors"
|
|
3
|
+
|
|
4
|
+
export interface SerializeOptions {
|
|
5
|
+
/** Decimal places for rects (default 2). */
|
|
6
|
+
precision?: number
|
|
7
|
+
/** Include the `id` (creator-ui node id) — off by default; ids are unstable across runs. */
|
|
8
|
+
includeIds?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Serialize a render tree into a plain, JSON-friendly object — the primary text output. Colors
|
|
13
|
+
* become "#rrggbbaa", rects are rounded, and only meaningful fields appear, so the result is
|
|
14
|
+
* readable for AI feedback and stable for snapshot assertions.
|
|
15
|
+
*/
|
|
16
|
+
export function serializeRenderTree(tree: RenderNode | null, opts: SerializeOptions = {}): unknown {
|
|
17
|
+
if (!tree) return null
|
|
18
|
+
return serializeNode(tree, opts.precision ?? 2, opts.includeIds ?? false)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Convenience: the serialized tree as pretty JSON text. */
|
|
22
|
+
export function renderTreeToText(tree: RenderNode | null, opts?: SerializeOptions): string {
|
|
23
|
+
return JSON.stringify(serializeRenderTree(tree, opts), null, 2)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Serialize a whole frame — the main screen plus any widget overlays — so the JSON describes
|
|
28
|
+
* everything painted (PNG/JSON parity). Widget trees are attached as an `overlays` array on the main
|
|
29
|
+
* root (they composite above it). With no widgets this returns the bare main tree, unchanged; with no
|
|
30
|
+
* main screen but live widgets it returns `{ type: "frame", overlays }`.
|
|
31
|
+
*/
|
|
32
|
+
export function serializeFrame(main: RenderNode | null, widgets: RenderNode[] = [], opts: SerializeOptions = {}): unknown {
|
|
33
|
+
const precision = opts.precision ?? 2
|
|
34
|
+
const includeIds = opts.includeIds ?? false
|
|
35
|
+
const root = main ? serializeNode(main, precision, includeIds) : null
|
|
36
|
+
if (widgets.length === 0) return root
|
|
37
|
+
const overlays = widgets.map((w) => serializeNode(w, precision, includeIds))
|
|
38
|
+
if (root) {
|
|
39
|
+
root.overlays = overlays
|
|
40
|
+
return root
|
|
41
|
+
}
|
|
42
|
+
return { type: "frame", overlays }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Convenience: the serialized frame (screen + overlays) as pretty JSON text. */
|
|
46
|
+
export function frameToText(main: RenderNode | null, widgets: RenderNode[] = [], opts?: SerializeOptions): string {
|
|
47
|
+
return JSON.stringify(serializeFrame(main, widgets, opts), null, 2)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function round(n: number, p: number): number {
|
|
51
|
+
const f = 10 ** p
|
|
52
|
+
return Math.round(n * f) / f
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function serializeNode(node: RenderNode, precision: number, includeIds: boolean): any {
|
|
56
|
+
const r = node.rect
|
|
57
|
+
const out: any = { type: node.type }
|
|
58
|
+
if (node.name) out.name = node.name
|
|
59
|
+
out.rect = [round(r.x, precision), round(r.y, precision), round(r.width, precision), round(r.height, precision)]
|
|
60
|
+
if (includeIds) out.id = node.id
|
|
61
|
+
|
|
62
|
+
const style = serializeStyle(node.style)
|
|
63
|
+
if (style) out.style = style
|
|
64
|
+
|
|
65
|
+
if (node.text !== undefined) out.text = node.text
|
|
66
|
+
if (node.lines && (node.lines.length !== 1 || node.lines[0] !== node.text)) out.lines = node.lines
|
|
67
|
+
// Editables: show what the field says — its typed value, or the placeholder an empty field
|
|
68
|
+
// renders (also the AI-facing selector for label-less inputs).
|
|
69
|
+
if (node.value !== undefined && node.value !== "") out.value = node.value
|
|
70
|
+
if (node.style.placeholder !== undefined) out.placeholder = node.style.placeholder
|
|
71
|
+
if (node.image) {
|
|
72
|
+
out.image = { src: node.image.src, natural: [node.image.naturalWidth, node.image.naturalHeight] }
|
|
73
|
+
}
|
|
74
|
+
if (node.sourceRect) out.sourceRect = node.sourceRect
|
|
75
|
+
if (node.state && (node.state.pressed || node.state.classes?.length)) {
|
|
76
|
+
out.state = {
|
|
77
|
+
...(node.state.pressed ? { pressed: true } : {}),
|
|
78
|
+
...(node.state.classes?.length ? { classes: node.state.classes } : {}),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (node.scroll) {
|
|
82
|
+
out.scroll = { offset: round(node.scroll.offset, precision), content: round(node.scroll.content, precision) }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (node.children.length > 0) {
|
|
86
|
+
out.children = node.children.map((c) => serializeNode(c, precision, includeIds))
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function radiusObj(radius: BorderRadius) {
|
|
92
|
+
if (radius.tl === radius.tr && radius.tr === radius.br && radius.br === radius.bl) return radius.tl
|
|
93
|
+
return [radius.tl, radius.tr, radius.br, radius.bl]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function serializeStyle(s: ResolvedStyle): Record<string, unknown> | undefined {
|
|
97
|
+
const out: Record<string, unknown> = {}
|
|
98
|
+
if (s.backgroundColor !== undefined) out.backgroundColor = rgbaToHex(s.backgroundColor)
|
|
99
|
+
if (s.opacity !== undefined) out.opacity = s.opacity
|
|
100
|
+
if (s.color !== undefined) out.color = rgbaToHex(s.color)
|
|
101
|
+
if (s.fontSize !== undefined) out.fontSize = s.fontSize
|
|
102
|
+
if (s.fontFamily !== undefined) out.fontFamily = s.fontFamily
|
|
103
|
+
if (s.fontWeight !== undefined) out.fontWeight = s.fontWeight
|
|
104
|
+
if (s.fontStyle !== undefined && s.fontStyle !== "normal") out.fontStyle = s.fontStyle
|
|
105
|
+
if (s.lineHeight !== undefined) out.lineHeight = s.lineHeight
|
|
106
|
+
if (s.letterSpacing) out.letterSpacing = s.letterSpacing
|
|
107
|
+
if (s.textAlign !== undefined && s.textAlign !== "start") out.textAlign = s.textAlign
|
|
108
|
+
if (s.textDecoration !== undefined && s.textDecoration !== "none") out.textDecoration = s.textDecoration
|
|
109
|
+
// Only an ACTIVE clamp is worth showing: `lines` already carries the visible result (with its "…"),
|
|
110
|
+
// and `textOverflow` means nothing without a clamp.
|
|
111
|
+
if (s.lineClamp) {
|
|
112
|
+
out.lineClamp = s.lineClamp
|
|
113
|
+
if (s.textOverflow === "clip") out.textOverflow = s.textOverflow
|
|
114
|
+
}
|
|
115
|
+
if (s.objectFit !== undefined) out.objectFit = s.objectFit
|
|
116
|
+
if (s.tintColor !== undefined) out.tintColor = rgbaToHex(s.tintColor)
|
|
117
|
+
if (s.overlayColor !== undefined) out.overlayColor = rgbaToHex(s.overlayColor)
|
|
118
|
+
if (s.backgroundImage !== undefined) out.backgroundImage = s.backgroundImage
|
|
119
|
+
if (s.backgroundSize !== undefined) out.backgroundSize = s.backgroundSize
|
|
120
|
+
if (s.backgroundGradients !== undefined && s.backgroundGradients.length > 0) {
|
|
121
|
+
out.backgroundGradients = s.backgroundGradients.map((g) => {
|
|
122
|
+
const stops = g.stops.map((st) => ({ color: rgbaToHex(st.color), position: st.position }))
|
|
123
|
+
if (g.type === "radial") {
|
|
124
|
+
const rad: Record<string, unknown> = { type: "radial", shape: g.shape, extent: g.extent, cx: g.cx, cy: g.cy, stops }
|
|
125
|
+
if (g.extent === "explicit") {
|
|
126
|
+
rad.rx = g.rx
|
|
127
|
+
rad.ry = g.ry
|
|
128
|
+
rad.rxUnit = g.rxUnit
|
|
129
|
+
rad.ryUnit = g.ryUnit
|
|
130
|
+
}
|
|
131
|
+
return rad
|
|
132
|
+
}
|
|
133
|
+
return { angle: g.angle, stops } // linear stays shape-compatible with the pre-radial output
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (s.borderRadius && (s.borderRadius.tl || s.borderRadius.tr || s.borderRadius.br || s.borderRadius.bl)) {
|
|
138
|
+
out.borderRadius = radiusObj(s.borderRadius)
|
|
139
|
+
}
|
|
140
|
+
if (s.border) {
|
|
141
|
+
const b: Record<string, unknown> = {}
|
|
142
|
+
for (const side of ["top", "right", "bottom", "left"] as const) {
|
|
143
|
+
const sd = s.border[side]
|
|
144
|
+
if (sd && sd.width > 0) b[side] = { width: sd.width, color: rgbaToHex(sd.color) }
|
|
145
|
+
}
|
|
146
|
+
if (Object.keys(b).length > 0) out.border = b
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Object.keys(out).length > 0 ? out : undefined
|
|
150
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Shared, platform-agnostic types for the renderer.
|
|
2
|
+
|
|
3
|
+
/** An axis-aligned rectangle in CSS pixels. */
|
|
4
|
+
export interface Rect {
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
width: number
|
|
8
|
+
height: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A color packed as 0xRRGGBBAA (the same layout creator-ui hands back to the uint color handlers,
|
|
13
|
+
* and the same RGBA packing the desktop tgfx renderer uses). `null`/absent = no color.
|
|
14
|
+
*/
|
|
15
|
+
export type RGBA = number
|
|
16
|
+
|
|
17
|
+
export interface BorderSide {
|
|
18
|
+
width: number
|
|
19
|
+
color: RGBA
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface BorderRadius {
|
|
23
|
+
tl: number
|
|
24
|
+
tr: number
|
|
25
|
+
br: number
|
|
26
|
+
bl: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GradientStop {
|
|
30
|
+
color: RGBA
|
|
31
|
+
/** 0..1 */
|
|
32
|
+
position: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A linear gradient. `angle` is CSS degrees (0 = to top, 90 = to right, clockwise). */
|
|
36
|
+
export interface LinearGradient {
|
|
37
|
+
type: "linear"
|
|
38
|
+
angle: number
|
|
39
|
+
stops: GradientStop[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** CSS `<extent-keyword>` for a radial gradient (default `farthest-corner`). */
|
|
43
|
+
export type RadialExtent = "closest-side" | "closest-corner" | "farthest-side" | "farthest-corner"
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A radial gradient — the frequently-used subset creator-ui resolves into geometry: shape, size, and
|
|
47
|
+
* a keyword/percent center (`cx`/`cy` as a fraction of the box, 0..1). Size is either a CSS extent
|
|
48
|
+
* keyword or `"explicit"` — the latter carries `rx`/`ry` (CSS px, or a box-axis fraction when the
|
|
49
|
+
* matching unit is `"fraction"`, from a `%` radius).
|
|
50
|
+
*/
|
|
51
|
+
export interface RadialGradient {
|
|
52
|
+
type: "radial"
|
|
53
|
+
shape: "circle" | "ellipse"
|
|
54
|
+
extent: RadialExtent | "explicit"
|
|
55
|
+
cx: number
|
|
56
|
+
cy: number
|
|
57
|
+
rx?: number
|
|
58
|
+
ry?: number
|
|
59
|
+
rxUnit?: "px" | "fraction"
|
|
60
|
+
ryUnit?: "px" | "fraction"
|
|
61
|
+
stops: GradientStop[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type Gradient = LinearGradient | RadialGradient
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Paint-ready style for one node. Filled by the layout engine's per-prop style handlers, plus
|
|
68
|
+
* text-property inheritance resolved while building the render tree. Only paint-relevant props
|
|
69
|
+
* live here — layout props (flex, padding, width…) are consumed inside creator-ui.
|
|
70
|
+
*/
|
|
71
|
+
export interface ResolvedStyle {
|
|
72
|
+
backgroundColor?: RGBA
|
|
73
|
+
opacity?: number
|
|
74
|
+
/** A screen: black over its content and children, 0..1 (a transition's pose). */
|
|
75
|
+
dim?: number
|
|
76
|
+
|
|
77
|
+
border?: Partial<Record<"top" | "right" | "bottom" | "left", BorderSide>>
|
|
78
|
+
borderRadius?: BorderRadius
|
|
79
|
+
overflow?: "visible" | "hidden"
|
|
80
|
+
|
|
81
|
+
/** `"none"` removes the node (and its subtree) from the render tree; anything else shows it. */
|
|
82
|
+
display?: string
|
|
83
|
+
|
|
84
|
+
// text (resolved with inheritance)
|
|
85
|
+
color?: RGBA
|
|
86
|
+
fontSize?: number
|
|
87
|
+
fontFamily?: string
|
|
88
|
+
fontWeight?: number
|
|
89
|
+
fontStyle?: "normal" | "italic"
|
|
90
|
+
/** Always set by the record: the core resolves `normal` (fontSize × 1.2) and every explicit form. */
|
|
91
|
+
lineHeight: number
|
|
92
|
+
letterSpacing?: number
|
|
93
|
+
textAlign?: "start" | "center" | "end"
|
|
94
|
+
textDecoration?: "underline" | "line-through" | "none"
|
|
95
|
+
/** Cap the text at N lines (0 / undefined = unbounded). NOT inherited, unlike the props above —
|
|
96
|
+
* it belongs to the text node that carries it (registry `nodes: ["text"]`). */
|
|
97
|
+
lineClamp?: number
|
|
98
|
+
/** How a clamped line ends. Only meaningful together with `lineClamp`; default "ellipsis". */
|
|
99
|
+
textOverflow?: "ellipsis" | "clip"
|
|
100
|
+
|
|
101
|
+
// image
|
|
102
|
+
objectFit?: "cover" | "contain" | "fill"
|
|
103
|
+
tintColor?: RGBA
|
|
104
|
+
|
|
105
|
+
// input / textarea
|
|
106
|
+
placeholder?: string
|
|
107
|
+
placeholderColor?: RGBA
|
|
108
|
+
inputType?: string
|
|
109
|
+
|
|
110
|
+
// background image (string url) painted behind content
|
|
111
|
+
backgroundImage?: string
|
|
112
|
+
backgroundSize?: "cover" | "contain" | "tile"
|
|
113
|
+
|
|
114
|
+
// background gradient layer stack (parsed from the CSS string the wasm forwards). `[0]` is the
|
|
115
|
+
// topmost layer — CSS paints the first-listed background-image layer on top. Usually one entry.
|
|
116
|
+
backgroundGradients?: Gradient[]
|
|
117
|
+
|
|
118
|
+
/** Scroll axis for a scrollable node ("x" = horizontal, "y" = vertical, the default). */
|
|
119
|
+
scrollAxis?: "x" | "y"
|
|
120
|
+
|
|
121
|
+
// widget: a full-screen backdrop behind the widget that intercepts taps (onOverlayTap)
|
|
122
|
+
overlayColor?: RGBA
|
|
123
|
+
|
|
124
|
+
/** 2D affine transform [a,b,c,d,e,f] (creator-ui parses the `transform` string → matrix). Applied
|
|
125
|
+
* about the node's centre (CSS default), to the node + its subtree. Undefined = identity. */
|
|
126
|
+
transform?: [number, number, number, number, number, number]
|
|
127
|
+
/** Pivot of `transform`, per axis a % of the box (`percent`) or px (creator-ui parses the
|
|
128
|
+
* `transformOrigin` string and dispatches it typed on its origin channel). Undefined = the centre. */
|
|
129
|
+
transformOrigin?: { x: number, y: number, xPercent: boolean, yPercent: boolean }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type NodeType =
|
|
133
|
+
| "screen"
|
|
134
|
+
| "box"
|
|
135
|
+
| "row"
|
|
136
|
+
| "column"
|
|
137
|
+
| "text"
|
|
138
|
+
| "image"
|
|
139
|
+
| "button"
|
|
140
|
+
| string
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The authoring-layer (worker) node the renderer consumes — the other end of the `_creatorUI`
|
|
144
|
+
* contract. The renderer never mutates it for layout (it mirrors it into a `SceneNode`); it only reads
|
|
145
|
+
* these fields and, once, writes back `_id`. The single-letter listener arrays and `_`-prefixed fields
|
|
146
|
+
* are the stable wire names the worker emits (chisel's method-DCE keeps `_*` members) — documented once
|
|
147
|
+
* here so Renderer / Screen / SceneNode / TouchSystem don't each re-discover them. The index signature
|
|
148
|
+
* keeps forward-compatibility with fields not yet enumerated.
|
|
149
|
+
*/
|
|
150
|
+
export interface WorkerNode {
|
|
151
|
+
type: string
|
|
152
|
+
/** The creator-ui (Yoga) node id — assigned by the renderer at mount, 0 once released. */
|
|
153
|
+
_id: number
|
|
154
|
+
/** The raw authoring style object (primitives + `$`-class state blocks like `$pressed`). */
|
|
155
|
+
_style?: Record<string, any>
|
|
156
|
+
/** Active user style classes (names without the leading `$`), set via `setClass`. Kept on the
|
|
157
|
+
* node so a re-mounted node (screens re-assign `_id` on every open) comes back with them active. */
|
|
158
|
+
_classes?: Set<string>
|
|
159
|
+
children?: (WorkerNode | null | undefined)[]
|
|
160
|
+
/** Author-given semantic name (LeCodes `name`). */
|
|
161
|
+
name?: string
|
|
162
|
+
|
|
163
|
+
// ----- content (read the `_`-backing first; the public getter is DCE'd outside the bundle) -----
|
|
164
|
+
text?: string
|
|
165
|
+
_text?: string
|
|
166
|
+
src?: unknown
|
|
167
|
+
_src?: unknown
|
|
168
|
+
/** Image source crop (atlas frame), texture px, stashed before mount. */
|
|
169
|
+
_sourceRect?: [number, number, number, number]
|
|
170
|
+
/** Current input/textarea value (the worker's `input.value`). */
|
|
171
|
+
value?: unknown
|
|
172
|
+
/** Input/textarea value as last set by code, stashed before mount (pre-filled form). */
|
|
173
|
+
_value?: string
|
|
174
|
+
|
|
175
|
+
// ----- flags -----
|
|
176
|
+
scrollable?: boolean
|
|
177
|
+
/** vlist: an inverted (chat) list — starts scrolled to the end; appends stick to the bottom. */
|
|
178
|
+
_inverted?: boolean
|
|
179
|
+
|
|
180
|
+
// ----- vlist windowing (the renderer drives `_syncWindow`; the worker re-renders items) -----
|
|
181
|
+
_keys?: string[]
|
|
182
|
+
_estimates?: number[]
|
|
183
|
+
_syncWindow?: (removed: string[], keys: string[]) => void
|
|
184
|
+
_endThreshold?: number
|
|
185
|
+
_startThreshold?: number
|
|
186
|
+
|
|
187
|
+
// ----- interaction (node-level dispatch, like the Android views) -----
|
|
188
|
+
_emitClick?: (fingerId: number, x: number, y: number) => any
|
|
189
|
+
_emitTouchStart?: (fingerId: number, x: number, y: number) => any
|
|
190
|
+
touchStartListeners?: any[]
|
|
191
|
+
|
|
192
|
+
// ----- listeners (worker wire names) -----
|
|
193
|
+
ol?: (() => void)[] // onOpen (screen)
|
|
194
|
+
cl?: ((...args: any[]) => void)[] // onClose (screen) OR onChange (input) — overloaded by type
|
|
195
|
+
ll?: ((layout: { left: number; top: number; width: number; height: number }) => void)[] // onLayout
|
|
196
|
+
fl?: (() => void)[] // onFocus (input)
|
|
197
|
+
bl?: (() => void)[] // onBlur (input)
|
|
198
|
+
sl?: ((offset: number) => void)[] // onScroll
|
|
199
|
+
osl?: ((over: number) => void)[] // onOverscroll
|
|
200
|
+
srl?: (() => void)[] // onScrollRelease
|
|
201
|
+
otl?: (() => void)[] // onOverlayTap (widget)
|
|
202
|
+
erl?: (() => void)[] // onEndReached (vlist)
|
|
203
|
+
strl?: (() => void)[] // onStartReached (vlist)
|
|
204
|
+
|
|
205
|
+
/** Escape hatch for worker fields not yet enumerated here. */
|
|
206
|
+
[key: string]: any
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Decoded image info, produced by the host. */
|
|
210
|
+
export interface ImageInfo {
|
|
211
|
+
src: string
|
|
212
|
+
naturalWidth: number
|
|
213
|
+
naturalHeight: number
|
|
214
|
+
/** Opaque backend handle (e.g. an HTMLImageElement / ImageBitmap) for the canvas painter. */
|
|
215
|
+
handle?: unknown
|
|
216
|
+
/** A vector source (SVG) the host can re-rasterize: the painter asks for the handle at the DEVICE
|
|
217
|
+
* size it is about to draw (w × h px), so an icon is never a stretched bitmap. Absent when the
|
|
218
|
+
* backend already draws the source at the destination scale (a browser's SVG `<img>`). */
|
|
219
|
+
rasterize?(w: number, h: number): unknown
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The render tree: the resolved, absolute-positioned snapshot of one screen. It is BOTH the
|
|
224
|
+
* semantic description (serialized to JSON for AI feedback / tests) AND the paint source
|
|
225
|
+
* (walked by paintTree into a Painter). Fully resolved — no inheritance or wrapping left to do.
|
|
226
|
+
*/
|
|
227
|
+
export interface RenderNode {
|
|
228
|
+
/** The creator-ui (Yoga) node id. */
|
|
229
|
+
id: number
|
|
230
|
+
type: NodeType
|
|
231
|
+
/** Author-given semantic name (LeCodes `name`): a stable selector for tests + AI review feedback. */
|
|
232
|
+
name?: string
|
|
233
|
+
/** Absolute rectangle, in CSS pixels. */
|
|
234
|
+
rect: Rect
|
|
235
|
+
style: ResolvedStyle
|
|
236
|
+
|
|
237
|
+
text?: string
|
|
238
|
+
/** Text after wrapping to the node's content width (its width minus padding). */
|
|
239
|
+
lines?: string[]
|
|
240
|
+
|
|
241
|
+
/** Resolved padding `[left, top, right, bottom]` (px), present on text/input nodes that have it.
|
|
242
|
+
* The rect is the border box; self-rendered content (text / input value) is inset by this. */
|
|
243
|
+
padding?: [number, number, number, number]
|
|
244
|
+
|
|
245
|
+
/** Current text of an input/textarea node ("" when empty → the placeholder is shown). */
|
|
246
|
+
value?: string
|
|
247
|
+
|
|
248
|
+
image?: ImageInfo
|
|
249
|
+
/** Per-node source crop (atlas/spritesheet), in texture px — overrides object-fit. */
|
|
250
|
+
sourceRect?: [number, number, number, number]
|
|
251
|
+
|
|
252
|
+
state?: { pressed?: boolean; focused?: boolean; classes?: string[] }
|
|
253
|
+
|
|
254
|
+
/** Set when the node clips its children (scrollable, or overflow:hidden). */
|
|
255
|
+
clip?: boolean
|
|
256
|
+
/** Present for scrollable nodes: scroll axis + current offset + total content size along it. */
|
|
257
|
+
scroll?: { offset: number; content: number; axis: "x" | "y" }
|
|
258
|
+
|
|
259
|
+
children: RenderNode[]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One compositing layer: the main screen, the screen that LEFT and is still shown (the runtime
|
|
264
|
+
* moves both through their own transform / opacity / dim until it drops it), or a widget overlay.
|
|
265
|
+
* A widget with `overlayColor` carries a full-screen `backdrop` painted behind it (and which
|
|
266
|
+
* intercepts taps → onOverlayTap).
|
|
267
|
+
*/
|
|
268
|
+
export interface Layer {
|
|
269
|
+
tree: RenderNode
|
|
270
|
+
/** True for the screen that left: painted, never the target of input or of a semantic read. */
|
|
271
|
+
leaving?: boolean
|
|
272
|
+
/** True for a widget overlay (composited above the main screen). */
|
|
273
|
+
widget?: boolean
|
|
274
|
+
backdrop?: { color: RGBA; jsNode: WorkerNode }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @deprecated A uniform transform that used to scale the whole UI into the band above the keyboard.
|
|
279
|
+
* The UI is now re-laid-out into the reduced height instead, so this is no longer produced.
|
|
280
|
+
*/
|
|
281
|
+
export interface UITransform {
|
|
282
|
+
scale: number
|
|
283
|
+
tx: number
|
|
284
|
+
ty: number
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Simulated on-screen keyboard state. The keyboard *visual* is the host's (DOM on web); Core owns
|
|
289
|
+
* this state. When an input is focused the UI is RE-LAID-OUT into the area above `rect` (like
|
|
290
|
+
* iOS/Android), not scaled. Emitted to the host via `onKeyboardChange`.
|
|
291
|
+
*/
|
|
292
|
+
export interface KeyboardState {
|
|
293
|
+
/** The bottom band the keyboard occupies (host draws its keyboard here); the UI lays out above it. */
|
|
294
|
+
rect: Rect
|
|
295
|
+
/** @deprecated No longer set — the UI is re-laid-out above the keyboard, not scaled. */
|
|
296
|
+
transform?: UITransform
|
|
297
|
+
/** The focused input: its absolute rect, current value, and type (for the host's capture field). */
|
|
298
|
+
input: { nodeId: number; rect: Rect; value: string; type: string }
|
|
299
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|