react-pebble 0.1.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/dist/lib/compiler.cjs +3 -0
- package/dist/lib/compiler.cjs.map +1 -0
- package/dist/lib/compiler.js +54 -0
- package/dist/lib/compiler.js.map +1 -0
- package/dist/lib/components.cjs +2 -0
- package/dist/lib/components.cjs.map +1 -0
- package/dist/lib/components.js +80 -0
- package/dist/lib/components.js.map +1 -0
- package/dist/lib/hooks.cjs +2 -0
- package/dist/lib/hooks.cjs.map +1 -0
- package/dist/lib/hooks.js +99 -0
- package/dist/lib/hooks.js.map +1 -0
- package/dist/lib/index.cjs +2 -0
- package/dist/lib/index.cjs.map +1 -0
- package/dist/lib/index.js +585 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/platform.cjs +2 -0
- package/dist/lib/platform.cjs.map +1 -0
- package/dist/lib/platform.js +52 -0
- package/dist/lib/platform.js.map +1 -0
- package/dist/lib/plugin.cjs +60 -0
- package/dist/lib/plugin.cjs.map +1 -0
- package/dist/lib/plugin.js +102 -0
- package/dist/lib/plugin.js.map +1 -0
- package/dist/lib/src/compiler/index.d.ts +40 -0
- package/dist/lib/src/components/index.d.ts +129 -0
- package/dist/lib/src/hooks/index.d.ts +75 -0
- package/dist/lib/src/index.d.ts +36 -0
- package/dist/lib/src/pebble-dom-shim.d.ts +45 -0
- package/dist/lib/src/pebble-dom.d.ts +59 -0
- package/dist/lib/src/pebble-output.d.ts +44 -0
- package/dist/lib/src/pebble-reconciler.d.ts +16 -0
- package/dist/lib/src/pebble-render.d.ts +31 -0
- package/dist/lib/src/platform.d.ts +30 -0
- package/dist/lib/src/plugin/index.d.ts +20 -0
- package/package.json +90 -0
- package/scripts/compile-to-piu.ts +1794 -0
- package/scripts/deploy.sh +46 -0
- package/src/compiler/index.ts +114 -0
- package/src/components/index.tsx +280 -0
- package/src/hooks/index.ts +311 -0
- package/src/index.ts +126 -0
- package/src/pebble-dom-shim.ts +266 -0
- package/src/pebble-dom.ts +190 -0
- package/src/pebble-output.ts +310 -0
- package/src/pebble-reconciler.ts +54 -0
- package/src/pebble-render.ts +311 -0
- package/src/platform.ts +50 -0
- package/src/plugin/index.ts +274 -0
- package/src/types/moddable.d.ts +156 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pebble-dom.ts — Virtual DOM layer for React Pebble
|
|
3
|
+
*
|
|
4
|
+
* Modeled after Ink's dom.ts but stripped of Yoga layout.
|
|
5
|
+
* Pebble uses absolute positioning (x, y, w, h) rather than flexbox,
|
|
6
|
+
* so nodes are just plain JS objects with type, props, and children.
|
|
7
|
+
*
|
|
8
|
+
* Each node type maps to a Pebble drawing primitive:
|
|
9
|
+
* pbl-root → Container root (the Window)
|
|
10
|
+
* pbl-rect → fillRect / drawRect
|
|
11
|
+
* pbl-circle → fillCircle / drawCircle
|
|
12
|
+
* pbl-text → drawText
|
|
13
|
+
* pbl-line → drawLine
|
|
14
|
+
* pbl-image → drawImage (bitmap)
|
|
15
|
+
* pbl-group → Logical grouping with offset (no draw call)
|
|
16
|
+
* #text → Raw text content (only valid inside pbl-text)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Element types
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export type ElementType =
|
|
24
|
+
| 'pbl-root'
|
|
25
|
+
| 'pbl-rect'
|
|
26
|
+
| 'pbl-circle'
|
|
27
|
+
| 'pbl-text'
|
|
28
|
+
| 'pbl-line'
|
|
29
|
+
| 'pbl-image'
|
|
30
|
+
| 'pbl-group'
|
|
31
|
+
| 'pbl-statusbar'
|
|
32
|
+
| 'pbl-actionbar';
|
|
33
|
+
|
|
34
|
+
export const ELEMENT_TYPES: ReadonlySet<ElementType> = new Set<ElementType>([
|
|
35
|
+
'pbl-root',
|
|
36
|
+
'pbl-rect',
|
|
37
|
+
'pbl-circle',
|
|
38
|
+
'pbl-text',
|
|
39
|
+
'pbl-line',
|
|
40
|
+
'pbl-image',
|
|
41
|
+
'pbl-group',
|
|
42
|
+
'pbl-statusbar',
|
|
43
|
+
'pbl-actionbar',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Node shapes
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Loose prop bag — element-specific shapes are documented on each component
|
|
52
|
+
* wrapper, but the reconciler treats them uniformly.
|
|
53
|
+
*/
|
|
54
|
+
export type NodeProps = Record<string, unknown> & {
|
|
55
|
+
_hidden?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface DOMElement {
|
|
59
|
+
id: number;
|
|
60
|
+
type: ElementType;
|
|
61
|
+
props: NodeProps;
|
|
62
|
+
children: Array<DOMElement | TextNode>;
|
|
63
|
+
parent: DOMElement | null;
|
|
64
|
+
/** Called by the reconciler after each commit; wired up by `render()`. */
|
|
65
|
+
onRender: (() => void) | null;
|
|
66
|
+
/** Optional layout pass; we currently don't use this. */
|
|
67
|
+
onComputeLayout: (() => void) | null;
|
|
68
|
+
/** Internal dirty flag (currently informational only). */
|
|
69
|
+
_dirty: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TextNode {
|
|
73
|
+
id: number;
|
|
74
|
+
type: '#text';
|
|
75
|
+
value: string;
|
|
76
|
+
parent: DOMElement | null;
|
|
77
|
+
/** Saved value while the node is hidden by Suspense. */
|
|
78
|
+
_hiddenValue?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type AnyNode = DOMElement | TextNode;
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Node creation
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
let nextNodeId = 1;
|
|
88
|
+
|
|
89
|
+
export function createNode(type: ElementType): DOMElement {
|
|
90
|
+
return {
|
|
91
|
+
id: nextNodeId++,
|
|
92
|
+
type,
|
|
93
|
+
props: {},
|
|
94
|
+
children: [],
|
|
95
|
+
parent: null,
|
|
96
|
+
onRender: null,
|
|
97
|
+
onComputeLayout: null,
|
|
98
|
+
_dirty: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createTextNode(text: string): TextNode {
|
|
103
|
+
return {
|
|
104
|
+
id: nextNodeId++,
|
|
105
|
+
type: '#text',
|
|
106
|
+
value: text,
|
|
107
|
+
parent: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Tree manipulation
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export function appendChildNode(parent: DOMElement, child: AnyNode): void {
|
|
116
|
+
if (child.parent) {
|
|
117
|
+
removeChildNode(child.parent, child);
|
|
118
|
+
}
|
|
119
|
+
child.parent = parent;
|
|
120
|
+
parent.children.push(child);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function insertBeforeNode(
|
|
124
|
+
parent: DOMElement,
|
|
125
|
+
child: AnyNode,
|
|
126
|
+
beforeChild: AnyNode,
|
|
127
|
+
): void {
|
|
128
|
+
if (child.parent) {
|
|
129
|
+
removeChildNode(child.parent, child);
|
|
130
|
+
}
|
|
131
|
+
child.parent = parent;
|
|
132
|
+
const idx = parent.children.indexOf(beforeChild);
|
|
133
|
+
if (idx >= 0) {
|
|
134
|
+
parent.children.splice(idx, 0, child);
|
|
135
|
+
} else {
|
|
136
|
+
parent.children.push(child);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function removeChildNode(parent: DOMElement, child: AnyNode): void {
|
|
141
|
+
parent.children = parent.children.filter((c) => c !== child);
|
|
142
|
+
child.parent = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Property / attribute helpers
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
export function setAttribute(node: DOMElement, key: string, value: unknown): void {
|
|
150
|
+
if (value === undefined) {
|
|
151
|
+
delete node.props[key];
|
|
152
|
+
} else {
|
|
153
|
+
node.props[key] = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function setTextNodeValue(node: TextNode, text: string): void {
|
|
158
|
+
node.value = text;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Tree traversal helpers
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
export function getTextContent(node: AnyNode): string {
|
|
166
|
+
if (node.type === '#text') {
|
|
167
|
+
return node.value;
|
|
168
|
+
}
|
|
169
|
+
return node.children.map(getTextContent).join('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type Visitor = (node: AnyNode, depth: number) => void;
|
|
173
|
+
|
|
174
|
+
export function walkTree(root: AnyNode, visitor: Visitor, depth = 0): void {
|
|
175
|
+
visitor(root, depth);
|
|
176
|
+
if (root.type !== '#text') {
|
|
177
|
+
for (const child of root.children) {
|
|
178
|
+
walkTree(child, visitor, depth + 1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function findRoot(node: AnyNode): DOMElement {
|
|
184
|
+
let current: AnyNode = node;
|
|
185
|
+
while (current.parent) {
|
|
186
|
+
current = current.parent;
|
|
187
|
+
}
|
|
188
|
+
// The root is always a DOMElement (created via createNode).
|
|
189
|
+
return current as DOMElement;
|
|
190
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pebble-output.ts — Poco output layer for react-pebble on Pebble Alloy.
|
|
3
|
+
*
|
|
4
|
+
* Walks the virtual DOM tree (pebble-dom) and issues draw calls against
|
|
5
|
+
* a `commodetto/Poco` renderer, which writes into the watch framebuffer.
|
|
6
|
+
*
|
|
7
|
+
* Key differences from a canvas-style API:
|
|
8
|
+
*
|
|
9
|
+
* - **No stateful color or font.** Every draw call takes the color (an int
|
|
10
|
+
* produced by `poco.makeColor(r, g, b)`) and font (a `new poco.Font(...)`
|
|
11
|
+
* object) as arguments. We maintain per-Poco caches so we resolve each
|
|
12
|
+
* named color / font exactly once.
|
|
13
|
+
* - **No native line, circle, or stroked rectangle.** Poco only has
|
|
14
|
+
* `fillRectangle`, `drawText`, and bitmap draws. Outlines (stroke) are
|
|
15
|
+
* emulated as four thin fillRectangles. Axis-aligned lines likewise.
|
|
16
|
+
* Circles and diagonal lines would need the `commodetto/outline` extension
|
|
17
|
+
* — they're currently stubbed (TODO for a later pass).
|
|
18
|
+
* - **Text alignment is manual.** `drawText(text, font, color, x, y)` only
|
|
19
|
+
* draws at a point. For center/right alignment we measure with
|
|
20
|
+
* `getTextWidth` and compute the origin ourselves.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type Poco from 'commodetto/Poco';
|
|
24
|
+
import type { PocoColor, PocoFont } from 'commodetto/Poco';
|
|
25
|
+
import type { DOMElement, NodeProps } from './pebble-dom.js';
|
|
26
|
+
import { getTextContent } from './pebble-dom.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Named palette — mapped to RGB, then resolved to PocoColor via a cache.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export interface RGB {
|
|
33
|
+
r: number;
|
|
34
|
+
g: number;
|
|
35
|
+
b: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const COLOR_PALETTE: Readonly<Record<string, RGB>> = {
|
|
39
|
+
black: { r: 0, g: 0, b: 0 },
|
|
40
|
+
white: { r: 255, g: 255, b: 255 },
|
|
41
|
+
red: { r: 255, g: 0, b: 0 },
|
|
42
|
+
green: { r: 0, g: 255, b: 0 },
|
|
43
|
+
blue: { r: 0, g: 0, b: 255 },
|
|
44
|
+
yellow: { r: 255, g: 255, b: 0 },
|
|
45
|
+
orange: { r: 255, g: 128, b: 0 },
|
|
46
|
+
cyan: { r: 0, g: 255, b: 255 },
|
|
47
|
+
magenta: { r: 255, g: 0, b: 255 },
|
|
48
|
+
clear: { r: 0, g: 0, b: 0 },
|
|
49
|
+
lightGray: { r: 192, g: 192, b: 192 },
|
|
50
|
+
darkGray: { r: 64, g: 64, b: 64 },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Named font shortcuts — mapped to (family, size) pairs.
|
|
55
|
+
//
|
|
56
|
+
// The family names here need to match fonts available in the Moddable
|
|
57
|
+
// manifest. The Alloy scaffold ships "Bitham-Black" as a default — we use
|
|
58
|
+
// it for every logical font for now. Revisit once we add custom font
|
|
59
|
+
// resources to the library manifest.
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export interface FontSpec {
|
|
63
|
+
family: string;
|
|
64
|
+
size: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const FONT_PALETTE: Readonly<Record<string, FontSpec>> = {
|
|
68
|
+
gothic14: { family: 'Bitham-Black', size: 14 },
|
|
69
|
+
gothic14Bold: { family: 'Bitham-Black', size: 14 },
|
|
70
|
+
gothic18: { family: 'Bitham-Black', size: 18 },
|
|
71
|
+
gothic18Bold: { family: 'Bitham-Black', size: 18 },
|
|
72
|
+
gothic24: { family: 'Bitham-Black', size: 24 },
|
|
73
|
+
gothic24Bold: { family: 'Bitham-Black', size: 24 },
|
|
74
|
+
gothic28: { family: 'Bitham-Black', size: 28 },
|
|
75
|
+
gothic28Bold: { family: 'Bitham-Black', size: 28 },
|
|
76
|
+
bitham30Black: { family: 'Bitham-Black', size: 30 },
|
|
77
|
+
bitham42Bold: { family: 'Bitham-Black', size: 42 },
|
|
78
|
+
bitham42Light: { family: 'Bitham-Black', size: 42 },
|
|
79
|
+
bitham34MediumNumbers: { family: 'Bitham-Black', size: 34 },
|
|
80
|
+
bitham42MediumNumbers: { family: 'Bitham-Black', size: 42 },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const DEFAULT_FONT_KEY = 'gothic18';
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Prop accessors — the DOM is loosely typed so we coerce here.
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function num(p: NodeProps, key: string): number {
|
|
90
|
+
const v = p[key];
|
|
91
|
+
return typeof v === 'number' ? v : 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function str(p: NodeProps, key: string): string | undefined {
|
|
95
|
+
const v = p[key];
|
|
96
|
+
return typeof v === 'string' ? v : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Renderer options
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
export interface RenderOptions {
|
|
104
|
+
backgroundColor?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Incremental update region. If provided, `poco.begin(x, y, w, h)` is used
|
|
107
|
+
* to clip drawing to just that region. Otherwise the full frame is redrawn.
|
|
108
|
+
*/
|
|
109
|
+
dirty?: { x: number; y: number; w: number; h: number };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// PocoRenderer — owns a Poco instance plus color/font caches
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
export class PocoRenderer {
|
|
117
|
+
readonly poco: Poco;
|
|
118
|
+
private readonly colorCache = new Map<string, PocoColor>();
|
|
119
|
+
private readonly fontCache = new Map<string, PocoFont>();
|
|
120
|
+
|
|
121
|
+
constructor(poco: Poco) {
|
|
122
|
+
this.poco = poco;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Render the full tree into a fresh frame.
|
|
127
|
+
*/
|
|
128
|
+
render(rootNode: DOMElement, options: RenderOptions = {}): void {
|
|
129
|
+
const { poco } = this;
|
|
130
|
+
const dirty = options.dirty;
|
|
131
|
+
|
|
132
|
+
if (dirty) {
|
|
133
|
+
poco.begin(dirty.x, dirty.y, dirty.w, dirty.h);
|
|
134
|
+
} else {
|
|
135
|
+
poco.begin();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Clear to background
|
|
139
|
+
const bg = this.getColor(options.backgroundColor ?? 'black');
|
|
140
|
+
poco.fillRectangle(bg, 0, 0, poco.width, poco.height);
|
|
141
|
+
|
|
142
|
+
// Walk the tree
|
|
143
|
+
this.renderChildren(rootNode, 0, 0);
|
|
144
|
+
|
|
145
|
+
poco.end();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Resolve a color name (or pass-through int) to a PocoColor. */
|
|
149
|
+
getColor(name: string | undefined): PocoColor {
|
|
150
|
+
const key = name ?? 'black';
|
|
151
|
+
const cached = this.colorCache.get(key);
|
|
152
|
+
if (cached !== undefined) return cached;
|
|
153
|
+
|
|
154
|
+
const rgb = COLOR_PALETTE[key] ?? COLOR_PALETTE.white!;
|
|
155
|
+
const color = this.poco.makeColor(rgb.r, rgb.g, rgb.b);
|
|
156
|
+
this.colorCache.set(key, color);
|
|
157
|
+
return color;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Resolve a font name to a PocoFont (cached). */
|
|
161
|
+
getFont(name: string | undefined): PocoFont {
|
|
162
|
+
const key = name ?? DEFAULT_FONT_KEY;
|
|
163
|
+
const cached = this.fontCache.get(key);
|
|
164
|
+
if (cached !== undefined) return cached;
|
|
165
|
+
|
|
166
|
+
const spec = FONT_PALETTE[key] ?? FONT_PALETTE[DEFAULT_FONT_KEY]!;
|
|
167
|
+
// `poco.Font` is a constructor hanging off the Poco instance.
|
|
168
|
+
const FontCtor = this.poco.Font;
|
|
169
|
+
const font = new FontCtor(spec.family, spec.size);
|
|
170
|
+
this.fontCache.set(key, font);
|
|
171
|
+
return font;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// -------------------------------------------------------------------------
|
|
175
|
+
// Private: node renderers
|
|
176
|
+
// -------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
private renderChildren(node: DOMElement, ox: number, oy: number): void {
|
|
179
|
+
for (const child of node.children) {
|
|
180
|
+
if (child.type === '#text') continue;
|
|
181
|
+
this.renderNode(child, ox, oy);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private renderNode(node: DOMElement, ox: number, oy: number): void {
|
|
186
|
+
const p = node.props;
|
|
187
|
+
if (p._hidden) return;
|
|
188
|
+
|
|
189
|
+
const x = num(p, 'x') + ox;
|
|
190
|
+
const y = num(p, 'y') + oy;
|
|
191
|
+
|
|
192
|
+
switch (node.type) {
|
|
193
|
+
case 'pbl-rect': {
|
|
194
|
+
const w = num(p, 'w') || num(p, 'width');
|
|
195
|
+
const h = num(p, 'h') || num(p, 'height');
|
|
196
|
+
const fill = str(p, 'fill');
|
|
197
|
+
const stroke = str(p, 'stroke');
|
|
198
|
+
|
|
199
|
+
if (fill) {
|
|
200
|
+
this.poco.fillRectangle(this.getColor(fill), x, y, w, h);
|
|
201
|
+
}
|
|
202
|
+
if (stroke) {
|
|
203
|
+
// Emulate outline with four thin fill rects.
|
|
204
|
+
const sw = num(p, 'strokeWidth') || 1;
|
|
205
|
+
const c = this.getColor(stroke);
|
|
206
|
+
this.poco.fillRectangle(c, x, y, w, sw); // top
|
|
207
|
+
this.poco.fillRectangle(c, x, y + h - sw, w, sw); // bottom
|
|
208
|
+
this.poco.fillRectangle(c, x, y, sw, h); // left
|
|
209
|
+
this.poco.fillRectangle(c, x + w - sw, y, sw, h); // right
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.renderChildren(node, x, y);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'pbl-text': {
|
|
217
|
+
const text = getTextContent(node);
|
|
218
|
+
if (!text) break;
|
|
219
|
+
|
|
220
|
+
const boxW = num(p, 'w') || num(p, 'width') || this.poco.width - x;
|
|
221
|
+
const font = this.getFont(str(p, 'font'));
|
|
222
|
+
const color = this.getColor(str(p, 'color') ?? 'white');
|
|
223
|
+
const align = str(p, 'align') ?? 'left';
|
|
224
|
+
|
|
225
|
+
let tx = x;
|
|
226
|
+
if (align === 'center' || align === 'right') {
|
|
227
|
+
const tw = this.poco.getTextWidth(text, font);
|
|
228
|
+
if (align === 'center') {
|
|
229
|
+
tx = x + Math.floor((boxW - tw) / 2);
|
|
230
|
+
} else {
|
|
231
|
+
tx = x + boxW - tw;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
this.poco.drawText(text, font, color, tx, y);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case 'pbl-line': {
|
|
240
|
+
// Only axis-aligned lines are supported natively. Diagonals would
|
|
241
|
+
// need the commodetto/outline extension — TODO.
|
|
242
|
+
const x2 = num(p, 'x2') + ox;
|
|
243
|
+
const y2 = num(p, 'y2') + oy;
|
|
244
|
+
const c = this.getColor(str(p, 'color') ?? str(p, 'stroke') ?? 'white');
|
|
245
|
+
const sw = num(p, 'strokeWidth') || 1;
|
|
246
|
+
|
|
247
|
+
if (x === x2) {
|
|
248
|
+
// Vertical
|
|
249
|
+
const top = Math.min(y, y2);
|
|
250
|
+
const h = Math.abs(y2 - y) || 1;
|
|
251
|
+
this.poco.fillRectangle(c, x, top, sw, h);
|
|
252
|
+
} else if (y === y2) {
|
|
253
|
+
// Horizontal
|
|
254
|
+
const left = Math.min(x, x2);
|
|
255
|
+
const w = Math.abs(x2 - x) || 1;
|
|
256
|
+
this.poco.fillRectangle(c, left, y, w, sw);
|
|
257
|
+
}
|
|
258
|
+
// else: diagonal line — silently skipped for now
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
case 'pbl-circle': {
|
|
263
|
+
// TODO: implement via commodetto/outline extension (blendOutline)
|
|
264
|
+
// or a Bresenham-style approximation. Stubbed for now.
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'pbl-image': {
|
|
269
|
+
const bitmap = p.bitmap;
|
|
270
|
+
if (bitmap) {
|
|
271
|
+
this.poco.drawBitmap(bitmap as never, x, y);
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case 'pbl-group': {
|
|
277
|
+
this.renderChildren(node, x, y);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
case 'pbl-statusbar':
|
|
282
|
+
case 'pbl-actionbar': {
|
|
283
|
+
// Alloy has no built-in status/action bar UI; these are no-ops for now.
|
|
284
|
+
// An app that wants a status bar should draw its own.
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
case 'pbl-root': {
|
|
289
|
+
this.renderChildren(node, ox, oy);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Compatibility shims — still useful for mock-mode tests that want to
|
|
298
|
+
// resolve a color name without constructing a Poco. These return *names*
|
|
299
|
+
// rather than native handles.
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
export function resolveColorName(color: string | undefined): string {
|
|
303
|
+
if (!color) return 'black';
|
|
304
|
+
return color in COLOR_PALETTE ? color : 'black';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function resolveFontName(font: string | undefined): string {
|
|
308
|
+
if (!font) return DEFAULT_FONT_KEY;
|
|
309
|
+
return font in FONT_PALETTE ? font : DEFAULT_FONT_KEY;
|
|
310
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pebble-reconciler.ts — Preact-backed reconciler for react-pebble.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old react-reconciler host config. We don't need a custom
|
|
5
|
+
* reconciler at all with Preact — Preact's `render(vnode, parentDom)` does
|
|
6
|
+
* the diffing, and we provide a DOM-shaped container via `pebble-dom-shim`
|
|
7
|
+
* so Preact writes into our pebble-dom tree instead of a real DOM.
|
|
8
|
+
*
|
|
9
|
+
* The public surface (for pebble-render.ts) is:
|
|
10
|
+
* - createReconcilerContainer() → a pair of root shim + pebble-dom root
|
|
11
|
+
* - updateContainer(vnode, container) → runs preact.render()
|
|
12
|
+
* - unmountContainer(container) → runs preact.render(null, ...)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { render as preactRender } from 'preact';
|
|
16
|
+
import type { ComponentChild } from 'preact';
|
|
17
|
+
import type { DOMElement } from './pebble-dom.js';
|
|
18
|
+
import { createShimRoot, shimDocument } from './pebble-dom-shim.js';
|
|
19
|
+
import type { ShimElement } from './pebble-dom-shim.js';
|
|
20
|
+
|
|
21
|
+
// Preact's render() references `document` internally. In non-browser
|
|
22
|
+
// environments (Node mock mode, Alloy XS) we shim it with our pebble-dom
|
|
23
|
+
// adapter so Preact doesn't crash.
|
|
24
|
+
if (typeof document === 'undefined') {
|
|
25
|
+
(globalThis as unknown as { document: unknown }).document = shimDocument;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PebbleContainer {
|
|
29
|
+
shimRoot: ShimElement;
|
|
30
|
+
pblRoot: DOMElement;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createContainer(): PebbleContainer {
|
|
34
|
+
const shimRoot = createShimRoot();
|
|
35
|
+
return {
|
|
36
|
+
shimRoot,
|
|
37
|
+
pblRoot: shimRoot._pbl,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function updateContainer(vnode: ComponentChild, container: PebbleContainer): void {
|
|
42
|
+
preactRender(vnode, container.shimRoot as unknown as Element);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function unmountContainer(container: PebbleContainer): void {
|
|
46
|
+
preactRender(null, container.shimRoot as unknown as Element);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// For backwards-compat with the old default export pattern.
|
|
50
|
+
export default {
|
|
51
|
+
createContainer,
|
|
52
|
+
updateContainer,
|
|
53
|
+
unmountContainer,
|
|
54
|
+
};
|