@hypen-space/web 0.4.35 → 0.4.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/canvas/accessibility.js +174 -174
  2. package/dist/canvas/accessibility.js.map +1 -10
  3. package/dist/canvas/events.js +264 -360
  4. package/dist/canvas/events.js.map +1 -11
  5. package/dist/canvas/index.js +11 -47
  6. package/dist/canvas/index.js.map +1 -9
  7. package/dist/canvas/input.js +178 -289
  8. package/dist/canvas/input.js.map +1 -11
  9. package/dist/canvas/layout.js +349 -547
  10. package/dist/canvas/layout.js.map +1 -12
  11. package/dist/canvas/paint.js +1122 -1146
  12. package/dist/canvas/paint.js.map +1 -12
  13. package/dist/canvas/renderer.js +352 -2259
  14. package/dist/canvas/renderer.js.map +1 -17
  15. package/dist/canvas/text.js +129 -264
  16. package/dist/canvas/text.js.map +1 -11
  17. package/dist/canvas/types.js +7 -2
  18. package/dist/canvas/types.js.map +1 -9
  19. package/dist/canvas/utils.js +158 -161
  20. package/dist/canvas/utils.js.map +1 -10
  21. package/dist/client.js +19 -45
  22. package/dist/client.js.map +1 -9
  23. package/dist/dom/applicators/advanced-layout.js +105 -138
  24. package/dist/dom/applicators/advanced-layout.js.map +1 -10
  25. package/dist/dom/applicators/background.js +36 -76
  26. package/dist/dom/applicators/background.js.map +1 -10
  27. package/dist/dom/applicators/border.js +82 -106
  28. package/dist/dom/applicators/border.js.map +1 -10
  29. package/dist/dom/applicators/color.js +17 -58
  30. package/dist/dom/applicators/color.js.map +1 -10
  31. package/dist/dom/applicators/display.js +39 -79
  32. package/dist/dom/applicators/display.js.map +1 -10
  33. package/dist/dom/applicators/effects.js +121 -147
  34. package/dist/dom/applicators/effects.js.map +1 -10
  35. package/dist/dom/applicators/events.js +413 -450
  36. package/dist/dom/applicators/events.js.map +1 -11
  37. package/dist/dom/applicators/font.js +119 -140
  38. package/dist/dom/applicators/font.js.map +1 -10
  39. package/dist/dom/applicators/index.js +386 -1770
  40. package/dist/dom/applicators/index.js.map +1 -26
  41. package/dist/dom/applicators/layout.js +143 -149
  42. package/dist/dom/applicators/layout.js.map +1 -10
  43. package/dist/dom/applicators/margin.js +64 -105
  44. package/dist/dom/applicators/margin.js.map +1 -10
  45. package/dist/dom/applicators/padding.js +64 -105
  46. package/dist/dom/applicators/padding.js.map +1 -10
  47. package/dist/dom/applicators/size.js +169 -172
  48. package/dist/dom/applicators/size.js.map +1 -10
  49. package/dist/dom/applicators/transform.js +73 -114
  50. package/dist/dom/applicators/transform.js.map +1 -10
  51. package/dist/dom/applicators/transition.js +47 -88
  52. package/dist/dom/applicators/transition.js.map +1 -10
  53. package/dist/dom/applicators/types.js +7 -2
  54. package/dist/dom/applicators/types.js.map +1 -9
  55. package/dist/dom/applicators/typography.js +68 -109
  56. package/dist/dom/applicators/typography.js.map +1 -10
  57. package/dist/dom/canvas/index.js +49 -83
  58. package/dist/dom/canvas/index.js.map +1 -10
  59. package/dist/dom/components/app.js +16 -63
  60. package/dist/dom/components/app.js.map +1 -10
  61. package/dist/dom/components/audio.js +31 -76
  62. package/dist/dom/components/audio.js.map +1 -10
  63. package/dist/dom/components/avatar.js +41 -87
  64. package/dist/dom/components/avatar.js.map +1 -10
  65. package/dist/dom/components/badge.js +35 -83
  66. package/dist/dom/components/badge.js.map +1 -10
  67. package/dist/dom/components/button.js +26 -69
  68. package/dist/dom/components/button.js.map +1 -10
  69. package/dist/dom/components/card.js +13 -63
  70. package/dist/dom/components/card.js.map +1 -10
  71. package/dist/dom/components/center.js +21 -65
  72. package/dist/dom/components/center.js.map +1 -10
  73. package/dist/dom/components/checkbox.js +37 -82
  74. package/dist/dom/components/checkbox.js.map +1 -10
  75. package/dist/dom/components/column.js +16 -62
  76. package/dist/dom/components/column.js.map +1 -10
  77. package/dist/dom/components/container.js +11 -59
  78. package/dist/dom/components/container.js.map +1 -10
  79. package/dist/dom/components/divider.js +27 -73
  80. package/dist/dom/components/divider.js.map +1 -10
  81. package/dist/dom/components/grid.js +41 -80
  82. package/dist/dom/components/grid.js.map +1 -10
  83. package/dist/dom/components/heading.js +28 -74
  84. package/dist/dom/components/heading.js.map +1 -10
  85. package/dist/dom/components/hypenapp.js +291 -288
  86. package/dist/dom/components/hypenapp.js.map +1 -10
  87. package/dist/dom/components/image.js +18 -67
  88. package/dist/dom/components/image.js.map +1 -10
  89. package/dist/dom/components/index.js +109 -1427
  90. package/dist/dom/components/index.js.map +1 -42
  91. package/dist/dom/components/input.js +19 -69
  92. package/dist/dom/components/input.js.map +1 -10
  93. package/dist/dom/components/link.js +23 -70
  94. package/dist/dom/components/link.js.map +1 -10
  95. package/dist/dom/components/list.js +26 -72
  96. package/dist/dom/components/list.js.map +1 -10
  97. package/dist/dom/components/paragraph.js +14 -63
  98. package/dist/dom/components/paragraph.js.map +1 -10
  99. package/dist/dom/components/progressbar.js +38 -85
  100. package/dist/dom/components/progressbar.js.map +1 -10
  101. package/dist/dom/components/route.js +27 -73
  102. package/dist/dom/components/route.js.map +1 -10
  103. package/dist/dom/components/router.js +15 -62
  104. package/dist/dom/components/router.js.map +1 -10
  105. package/dist/dom/components/row.js +25 -70
  106. package/dist/dom/components/row.js.map +1 -10
  107. package/dist/dom/components/select.js +42 -85
  108. package/dist/dom/components/select.js.map +1 -10
  109. package/dist/dom/components/slider.js +31 -76
  110. package/dist/dom/components/slider.js.map +1 -10
  111. package/dist/dom/components/spacer.js +10 -60
  112. package/dist/dom/components/spacer.js.map +1 -10
  113. package/dist/dom/components/spinner.js +41 -88
  114. package/dist/dom/components/spinner.js.map +1 -10
  115. package/dist/dom/components/stack.js +19 -67
  116. package/dist/dom/components/stack.js.map +1 -10
  117. package/dist/dom/components/switch.js +50 -93
  118. package/dist/dom/components/switch.js.map +1 -10
  119. package/dist/dom/components/text.js +24 -69
  120. package/dist/dom/components/text.js.map +1 -10
  121. package/dist/dom/components/textarea.js +35 -79
  122. package/dist/dom/components/textarea.js.map +1 -10
  123. package/dist/dom/components/video.js +35 -79
  124. package/dist/dom/components/video.js.map +1 -10
  125. package/dist/dom/debug.js +159 -160
  126. package/dist/dom/debug.js.map +1 -10
  127. package/dist/dom/element-data.js +181 -143
  128. package/dist/dom/element-data.js.map +1 -10
  129. package/dist/dom/events.js +133 -142
  130. package/dist/dom/events.js.map +1 -10
  131. package/dist/dom/index.js +11 -48
  132. package/dist/dom/index.js.map +1 -9
  133. package/dist/dom/renderer.js +311 -3596
  134. package/dist/dom/renderer.js.map +1 -62
  135. package/dist/hypen.js +385 -3880
  136. package/dist/hypen.js.map +1 -63
  137. package/dist/index.js +36 -53
  138. package/dist/index.js.map +1 -9
  139. package/package.json +2 -2
@@ -1,2272 +1,365 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- function __accessProp(key) {
6
- return this[key];
7
- }
8
- var __toCommonJS = (from) => {
9
- var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
- if (entry)
11
- return entry;
12
- entry = __defProp({}, "__esModule", { value: true });
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (var key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(entry, key))
16
- __defProp(entry, key, {
17
- get: __accessProp.bind(from, key),
18
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
- });
20
- }
21
- __moduleCache.set(from, entry);
22
- return entry;
23
- };
24
- var __moduleCache;
25
- var __returnValue = (v) => v;
26
- function __exportSetter(name, newValue) {
27
- this[name] = __returnValue.bind(null, newValue);
28
- }
29
- var __export = (target, all) => {
30
- for (var name in all)
31
- __defProp(target, name, {
32
- get: all[name],
33
- enumerable: true,
34
- configurable: true,
35
- set: __exportSetter.bind(all, name)
36
- });
1
+ /**
2
+ * Canvas Renderer
3
+ *
4
+ * Main renderer class that orchestrates layout, painting, and events
5
+ */
6
+ import { frameworkLoggers } from "@hypen-space/core/logger";
7
+ const log = frameworkLoggers.canvas;
8
+ import { computeLayout } from "./layout.js";
9
+ import { paintNode, registerPainter } from "./paint.js";
10
+ import { CanvasEventManager } from "./events.js";
11
+ import { InputOverlay } from "./input.js";
12
+ import { AccessibilityLayer } from "./accessibility.js";
13
+ const DEFAULT_OPTIONS = {
14
+ devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
15
+ backgroundColor: "#ffffff",
16
+ enableAccessibility: true,
17
+ enableHitTesting: true,
18
+ enableInputOverlay: true,
19
+ enableDirtyRects: false,
20
+ enableLayerCaching: false,
21
+ maxLayerCacheSize: 10,
22
+ showLayoutBounds: false,
23
+ showDirtyRects: false,
24
+ logPerformance: false,
37
25
  };
38
- var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
39
-
40
- // src/canvas/accessibility.ts
41
- class AccessibilityLayer {
42
- shadowRoot;
43
- nodeMap = new Map;
44
- enabled;
45
- constructor(container, enabled = true) {
46
- this.enabled = enabled && typeof document !== "undefined";
47
- if (this.enabled && typeof document !== "undefined") {
48
- this.shadowRoot = document.createElement("div");
49
- this.shadowRoot.setAttribute("role", "application");
50
- this.shadowRoot.setAttribute("aria-label", "Hypen Canvas Application");
51
- this.shadowRoot.style.position = "absolute";
52
- this.shadowRoot.style.left = "-9999px";
53
- this.shadowRoot.style.width = "1px";
54
- this.shadowRoot.style.height = "1px";
55
- this.shadowRoot.style.overflow = "hidden";
56
- if (container) {
57
- container.appendChild(this.shadowRoot);
58
- }
59
- } else {
60
- this.shadowRoot = {};
61
- }
62
- }
63
- syncTree(root) {
64
- if (!this.enabled)
65
- return;
66
- this.shadowRoot.innerHTML = "";
67
- this.nodeMap.clear();
68
- const shadowNode = this.createShadowNode(root);
69
- if (shadowNode) {
70
- this.shadowRoot.appendChild(shadowNode);
71
- }
72
- }
73
- createShadowNode(node) {
74
- if (!node.visible)
75
- return null;
76
- let element;
77
- switch (node.type.toLowerCase()) {
78
- case "button":
79
- element = document.createElement("button");
80
- element.textContent = this.getNodeText(node);
81
- if (node.props.onclick) {
82
- element.setAttribute("aria-label", "Clickable button");
26
+ /**
27
+ * Canvas Renderer
28
+ */
29
+ export class CanvasRenderer {
30
+ canvas;
31
+ ctx;
32
+ engine;
33
+ options;
34
+ rootNode = null;
35
+ nodes = new Map();
36
+ eventManager;
37
+ inputOverlay;
38
+ accessibilityLayer;
39
+ rafId = null;
40
+ needsRedraw = false;
41
+ frameCount = 0;
42
+ lastFrameTime = 0;
43
+ constructor(canvas, engine, options) {
44
+ this.canvas = canvas;
45
+ this.engine = engine;
46
+ this.options = { ...DEFAULT_OPTIONS, ...options };
47
+ // Get context
48
+ const ctx = canvas.getContext("2d");
49
+ if (!ctx) {
50
+ throw new Error("Failed to get 2D context from canvas");
83
51
  }
84
- break;
85
- case "input":
86
- element = document.createElement("input");
87
- element.type = node.props.type || "text";
88
- element.value = node.props.value || "";
89
- if (node.props.placeholder) {
90
- element.placeholder = node.props.placeholder;
52
+ this.ctx = ctx;
53
+ // Setup HiDPI
54
+ this.setupHiDPI();
55
+ // Initialize subsystems
56
+ this.eventManager = new CanvasEventManager(canvas, engine);
57
+ this.inputOverlay = new InputOverlay(canvas.parentElement || (typeof document !== "undefined" ? document.body : null));
58
+ this.accessibilityLayer = new AccessibilityLayer(canvas.parentElement || (typeof document !== "undefined" ? document.body : null), this.options.enableAccessibility || false);
59
+ // Listen for redraw requests from event manager
60
+ this.canvas.addEventListener("hypen:redraw", () => this.scheduleRedraw());
61
+ // Don't schedule initial render - wait for patches
62
+ }
63
+ /**
64
+ * Setup HiDPI rendering
65
+ */
66
+ setupHiDPI() {
67
+ const dpr = this.options.devicePixelRatio || 1;
68
+ const rect = this.canvas.getBoundingClientRect();
69
+ this.canvas.width = rect.width * dpr;
70
+ this.canvas.height = rect.height * dpr;
71
+ this.ctx.scale(dpr, dpr);
72
+ // Update canvas display size
73
+ this.canvas.style.width = `${rect.width}px`;
74
+ this.canvas.style.height = `${rect.height}px`;
75
+ }
76
+ /**
77
+ * Apply patches from engine
78
+ */
79
+ applyPatches(patches) {
80
+ for (const patch of patches) {
81
+ this.applyPatch(patch);
91
82
  }
92
- break;
93
- case "textarea":
94
- element = document.createElement("textarea");
95
- element.value = node.props.value || "";
96
- if (node.props.placeholder) {
97
- element.placeholder = node.props.placeholder;
83
+ // Update accessibility layer
84
+ if (this.rootNode) {
85
+ this.accessibilityLayer.syncTree(this.rootNode);
98
86
  }
99
- break;
100
- case "image":
101
- element = document.createElement("img");
102
- element.src = node.props.src || "";
103
- element.alt = node.props.alt || "Image";
104
- break;
105
- case "text":
106
- element = document.createElement("span");
107
- element.textContent = String(node.props[0] || node.props.text || "");
108
- break;
109
- case "column":
110
- case "row":
111
- case "container":
112
- case "box":
113
- element = document.createElement("div");
114
- element.setAttribute("role", node.type === "column" ? "group" : "group");
115
- break;
116
- default:
117
- element = document.createElement("div");
118
- }
119
- element.setAttribute("data-hypen-id", node.id);
120
- if (node.props["aria-label"]) {
121
- element.setAttribute("aria-label", node.props["aria-label"]);
122
- }
123
- if (node.focusable) {
124
- element.tabIndex = 0;
125
- }
126
- this.nodeMap.set(node.id, element);
127
- for (const child of node.children) {
128
- const childElement = this.createShadowNode(child);
129
- if (childElement) {
130
- element.appendChild(childElement);
131
- }
132
- }
133
- return element;
134
- }
135
- getNodeText(node) {
136
- if (node.type === "text") {
137
- return String(node.props[0] || node.props.text || "");
138
- }
139
- let text = "";
140
- for (const child of node.children) {
141
- text += this.getNodeText(child);
142
- }
143
- return text;
144
- }
145
- focusNode(nodeId) {
146
- if (!this.enabled)
147
- return;
148
- const element = this.nodeMap.get(nodeId);
149
- if (element) {
150
- element.focus();
151
- }
152
- }
153
- updateNode(node) {
154
- if (!this.enabled)
155
- return;
156
- const element = this.nodeMap.get(node.id);
157
- if (!element)
158
- return;
159
- if (node.type === "text") {
160
- element.textContent = String(node.props[0] || node.props.text || "");
161
- }
162
- if (node.type === "input" || node.type === "textarea") {
163
- element.value = node.props.value || "";
164
- }
165
- element.style.display = node.visible ? "" : "none";
166
- }
167
- getElement(nodeId) {
168
- return this.nodeMap.get(nodeId);
169
- }
170
- setEnabled(enabled) {
171
- this.enabled = enabled;
172
- if (!enabled && this.shadowRoot.parentElement) {
173
- this.shadowRoot.remove();
174
- }
175
- }
176
- destroy() {
177
- if (this.shadowRoot.parentElement) {
178
- this.shadowRoot.remove();
179
- }
180
- this.nodeMap.clear();
181
- }
182
- }
183
-
184
- // src/canvas/utils.ts
185
- function parseSpacing(value) {
186
- if (typeof value === "number") {
187
- return { top: value, right: value, bottom: value, left: value };
188
- }
189
- if (typeof value === "string") {
190
- const parts = value.split(/\s+/).map((v) => parseFloat(v) || 0);
191
- if (parts.length === 1) {
192
- return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
193
- }
194
- if (parts.length === 2) {
195
- return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
196
- }
197
- if (parts.length === 4) {
198
- return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
199
- }
200
- }
201
- if (typeof value === "object" && value !== null) {
202
- return {
203
- top: parseFloat(value.top) || 0,
204
- right: parseFloat(value.right) || 0,
205
- bottom: parseFloat(value.bottom) || 0,
206
- left: parseFloat(value.left) || 0
207
- };
208
- }
209
- return { top: 0, right: 0, bottom: 0, left: 0 };
210
- }
211
- function parseSize(value) {
212
- if (typeof value === "number")
213
- return value;
214
- if (typeof value === "string") {
215
- if (value === "auto")
216
- return null;
217
- const num = parseFloat(value);
218
- return isNaN(num) ? null : num;
219
- }
220
- return null;
221
- }
222
- function isPointInRect(point, rect) {
223
- return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
224
- }
225
- function isPointInRoundedRect(point, rect, radius) {
226
- const { x, y, width, height } = rect;
227
- if (!isPointInRect(point, rect))
228
- return false;
229
- if (radius <= 0)
230
- return true;
231
- const px = point.x;
232
- const py = point.y;
233
- if (px < x + radius && py < y + radius) {
234
- return Math.pow(px - (x + radius), 2) + Math.pow(py - (y + radius), 2) <= Math.pow(radius, 2);
235
- }
236
- if (px > x + width - radius && py < y + radius) {
237
- return Math.pow(px - (x + width - radius), 2) + Math.pow(py - (y + radius), 2) <= Math.pow(radius, 2);
238
- }
239
- if (px < x + radius && py > y + height - radius) {
240
- return Math.pow(px - (x + radius), 2) + Math.pow(py - (y + height - radius), 2) <= Math.pow(radius, 2);
241
- }
242
- if (px > x + width - radius && py > y + height - radius) {
243
- return Math.pow(px - (x + width - radius), 2) + Math.pow(py - (y + height - radius), 2) <= Math.pow(radius, 2);
244
- }
245
- return true;
246
- }
247
- function mergeRects(rects) {
248
- if (rects.length === 0)
249
- return null;
250
- if (rects.length === 1)
251
- return rects[0];
252
- let minX = Infinity;
253
- let minY = Infinity;
254
- let maxX = -Infinity;
255
- let maxY = -Infinity;
256
- for (const rect of rects) {
257
- minX = Math.min(minX, rect.x);
258
- minY = Math.min(minY, rect.y);
259
- maxX = Math.max(maxX, rect.x + rect.width);
260
- maxY = Math.max(maxY, rect.y + rect.height);
261
- }
262
- return {
263
- x: minX,
264
- y: minY,
265
- width: maxX - minX,
266
- height: maxY - minY
267
- };
268
- }
269
- function createFontString(fontSize, fontWeight, fontFamily) {
270
- return `${fontWeight} ${fontSize}px ${fontFamily}`;
271
- }
272
- function walkTree(node, callback) {
273
- callback(node);
274
- for (const child of node.children) {
275
- walkTree(child, callback);
276
- }
277
- }
278
- function findNodeById(root, id) {
279
- if (root.id === id)
280
- return root;
281
- for (const child of root.children) {
282
- const found = findNodeById(child, id);
283
- if (found)
284
- return found;
285
- }
286
- return null;
287
- }
288
- function getAbsoluteBounds(node) {
289
- if (!node.layout)
290
- return null;
291
- let x = node.layout.x;
292
- let y = node.layout.y;
293
- let current = node.parent;
294
- while (current && current.layout) {
295
- x += current.layout.contentX;
296
- y += current.layout.contentY;
297
- current = current.parent;
298
- }
299
- return {
300
- x,
301
- y,
302
- width: node.layout.width,
303
- height: node.layout.height
304
- };
305
- }
306
-
307
- // src/canvas/text.ts
308
- import { frameworkLoggers } from "@hypen-space/core/logger";
309
- var log = frameworkLoggers.canvas;
310
- var textMetricsCache = new Map;
311
- function getCacheKey(text, fontStyle, maxWidth) {
312
- return `${text}|${fontStyle.fontSize}|${fontStyle.fontWeight}|${fontStyle.fontFamily}|${maxWidth || "auto"}`;
313
- }
314
- function measureText(ctx, text, fontStyle, maxWidth) {
315
- const cacheKey = getCacheKey(text, fontStyle, maxWidth);
316
- const cached = textMetricsCache.get(cacheKey);
317
- if (cached)
318
- return cached;
319
- const font = createFontString(fontStyle.fontSize, fontStyle.fontWeight, fontStyle.fontFamily);
320
- ctx.save();
321
- ctx.font = font;
322
- const lineHeight = fontStyle.lineHeight || fontStyle.fontSize * 1.2;
323
- if (!maxWidth) {
324
- const metrics = ctx.measureText(text);
325
- const result2 = {
326
- width: metrics.width,
327
- height: lineHeight,
328
- lines: [text],
329
- lineHeight
330
- };
331
- ctx.restore();
332
- textMetricsCache.set(cacheKey, result2);
333
- return result2;
334
- }
335
- const lines = wrapText(ctx, text, maxWidth);
336
- const width = Math.max(...lines.map((line) => ctx.measureText(line).width));
337
- const height = lines.length * lineHeight;
338
- const result = {
339
- width,
340
- height,
341
- lines,
342
- lineHeight
343
- };
344
- ctx.restore();
345
- textMetricsCache.set(cacheKey, result);
346
- return result;
347
- }
348
- function wrapText(ctx, text, maxWidth) {
349
- const lines = [];
350
- const paragraphs = text.split(`
351
- `);
352
- for (const paragraph of paragraphs) {
353
- const words = paragraph.split(" ");
354
- let currentLine = "";
355
- for (const word of words) {
356
- const testLine = currentLine ? `${currentLine} ${word}` : word;
357
- const metrics = ctx.measureText(testLine);
358
- if (metrics.width > maxWidth && currentLine) {
359
- lines.push(currentLine);
360
- currentLine = word;
361
- } else {
362
- currentLine = testLine;
363
- }
364
- }
365
- if (currentLine) {
366
- lines.push(currentLine);
367
- }
368
- }
369
- return lines.length > 0 ? lines : [""];
370
- }
371
- function renderText(ctx, text, x, y, width, height, style) {
372
- const font = createFontString(style.fontSize, style.fontWeight, style.fontFamily);
373
- ctx.save();
374
- ctx.font = font;
375
- ctx.fillStyle = style.color;
376
- ctx.textBaseline = "top";
377
- const metrics = measureText(ctx, text, style, width);
378
- let startY = y;
379
- if (style.verticalAlign === "middle") {
380
- startY = y + (height - metrics.height) / 2;
381
- } else if (style.verticalAlign === "bottom") {
382
- startY = y + height - metrics.height;
383
- }
384
- for (let i = 0;i < metrics.lines.length; i++) {
385
- const line = metrics.lines[i];
386
- const lineY = startY + i * metrics.lineHeight;
387
- let lineX = x;
388
- if (style.textAlign === "center") {
389
- const lineWidth = ctx.measureText(line).width;
390
- lineX = x + (width - lineWidth) / 2;
391
- } else if (style.textAlign === "right") {
392
- const lineWidth = ctx.measureText(line).width;
393
- lineX = x + width - lineWidth;
394
- }
395
- ctx.fillText(line, lineX, lineY);
396
- }
397
- ctx.restore();
398
- }
399
- function clearTextCache() {
400
- textMetricsCache.clear();
401
- }
402
- async function loadFont(fontFamily, fontWeight = "normal") {
403
- if (!("fonts" in document))
404
- return;
405
- const font = `${fontWeight} 16px ${fontFamily}`;
406
- try {
407
- await document.fonts.load(font);
408
- } catch (error) {
409
- log.warn(`Failed to load font: ${font}`, error);
410
- }
411
- }
412
-
413
- // src/canvas/input.ts
414
- class InputOverlay {
415
- container;
416
- overlay = null;
417
- focusedNode = null;
418
- onChangeCallback = null;
419
- bindPath = null;
420
- engine = null;
421
- constructor(container) {
422
- this.container = container || {};
423
- }
424
- showInput(node, canvasBounds, onChange, engine) {
425
- if (typeof document === "undefined")
426
- return;
427
- this.hideInput();
428
- this.bindPath = node.props.bind || null;
429
- this.engine = engine || null;
430
- const bounds = getAbsoluteBounds(node);
431
- if (!bounds)
432
- return;
433
- const isMultiline = node.type === "textarea";
434
- this.overlay = isMultiline ? document.createElement("textarea") : document.createElement("input");
435
- this.styleOverlay(node, bounds, canvasBounds);
436
- const value = node.props.value || "";
437
- this.overlay.value = value;
438
- this.onChangeCallback = onChange;
439
- this.overlay.addEventListener("input", this.onInput.bind(this));
440
- this.overlay.addEventListener("blur", this.onBlur.bind(this));
441
- this.overlay.addEventListener("keydown", this.onKeyDown.bind(this));
442
- this.container.appendChild(this.overlay);
443
- this.overlay.focus();
444
- this.focusedNode = node;
445
- }
446
- hideInput() {
447
- if (this.overlay) {
448
- this.overlay.remove();
449
- this.overlay = null;
450
- }
451
- this.focusedNode = null;
452
- this.onChangeCallback = null;
453
- this.bindPath = null;
454
- this.engine = null;
455
- }
456
- updatePosition(node, canvasBounds) {
457
- if (!this.overlay || node !== this.focusedNode)
458
- return;
459
- const bounds = getAbsoluteBounds(node);
460
- if (!bounds)
461
- return;
462
- this.positionOverlay(bounds, canvasBounds);
463
- }
464
- styleOverlay(node, bounds, canvasBounds) {
465
- if (!this.overlay)
466
- return;
467
- const props = node.props;
468
- this.positionOverlay(bounds, canvasBounds);
469
- const fontSize = parseFloat(props.fontSize) || 16;
470
- const fontWeight = props.fontWeight || "normal";
471
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
472
- const color = props.color || "#000000";
473
- Object.assign(this.overlay.style, {
474
- fontSize: `${fontSize}px`,
475
- fontWeight,
476
- fontFamily,
477
- color,
478
- border: "none",
479
- outline: "2px solid #007bff",
480
- backgroundColor: props.backgroundColor || "#ffffff",
481
- padding: `${props.padding || 8}px`,
482
- borderRadius: `${props.borderRadius || 4}px`,
483
- boxSizing: "border-box",
484
- resize: "none"
485
- });
486
- if (props.placeholder) {
487
- this.overlay.placeholder = props.placeholder;
488
- }
489
- if (this.overlay instanceof HTMLInputElement && props.type) {
490
- this.overlay.type = props.type;
491
- }
492
- }
493
- positionOverlay(bounds, canvasBounds) {
494
- if (!this.overlay)
495
- return;
496
- const canvas = this.container.querySelector("canvas");
497
- const scaleX = canvasBounds.width / canvas.width;
498
- const scaleY = canvasBounds.height / canvas.height;
499
- const left = bounds.x * scaleX;
500
- const top = bounds.y * scaleY;
501
- const width = bounds.width * scaleX;
502
- const height = bounds.height * scaleY;
503
- Object.assign(this.overlay.style, {
504
- position: "absolute",
505
- left: `${left}px`,
506
- top: `${top}px`,
507
- width: `${width}px`,
508
- height: `${height}px`
509
- });
510
- }
511
- onInput(e) {
512
- if (!this.overlay || !this.onChangeCallback)
513
- return;
514
- const value = this.overlay.value;
515
- this.onChangeCallback(value);
516
- if (this.bindPath && this.engine) {
517
- this.engine.dispatchAction("__hypen_bind", {
518
- path: this.bindPath,
519
- value
520
- });
521
- }
522
- }
523
- onBlur() {
524
- this.hideInput();
525
- }
526
- onKeyDown(e) {
527
- if (!this.overlay)
528
- return;
529
- if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
530
- e.preventDefault();
531
- this.overlay.blur();
532
- }
533
- if (e.key === "Escape") {
534
- e.preventDefault();
535
- this.overlay.blur();
536
- }
537
- }
538
- isShown() {
539
- return this.overlay !== null;
540
- }
541
- getFocusedNode() {
542
- return this.focusedNode;
543
- }
544
- }
545
-
546
- // src/canvas/paint.ts
547
- var customPainters = new Map;
548
- function registerPainter(type, painter) {
549
- customPainters.set(type.toLowerCase(), painter);
550
- }
551
- function paintNode(ctx, node) {
552
- if (!node.visible || !node.layout)
553
- return;
554
- ctx.save();
555
- applyTransforms(ctx, node);
556
- if (node.opacity < 1) {
557
- ctx.globalAlpha = node.opacity;
558
- }
559
- const customPainter = customPainters.get(node.type.toLowerCase());
560
- if (customPainter) {
561
- customPainter(ctx, node);
562
- ctx.restore();
563
- return;
564
- }
565
- switch (node.type.toLowerCase()) {
566
- case "column":
567
- case "row":
568
- case "stack":
569
- paintContainer(ctx, node);
570
- break;
571
- case "text":
572
- paintText(ctx, node);
573
- break;
574
- case "button":
575
- paintButton(ctx, node);
576
- break;
577
- case "input":
578
- paintInput(ctx, node);
579
- break;
580
- case "image":
581
- paintImage(ctx, node);
582
- break;
583
- case "spacer":
584
- break;
585
- case "divider":
586
- case "separator":
587
- paintDivider(ctx, node);
588
- break;
589
- case "checkbox":
590
- paintCheckbox(ctx, node);
591
- break;
592
- case "radio":
593
- paintRadio(ctx, node);
594
- break;
595
- case "switch":
596
- case "toggle":
597
- paintSwitch(ctx, node);
598
- break;
599
- case "slider":
600
- paintSlider(ctx, node);
601
- break;
602
- case "progress":
603
- case "progressbar":
604
- paintProgress(ctx, node);
605
- break;
606
- case "spinner":
607
- case "loading":
608
- paintSpinner(ctx, node);
609
- break;
610
- case "card":
611
- paintCard(ctx, node);
612
- break;
613
- case "badge":
614
- paintBadge(ctx, node);
615
- break;
616
- case "avatar":
617
- paintAvatar(ctx, node);
618
- break;
619
- case "icon":
620
- paintIcon(ctx, node);
621
- break;
622
- case "link":
623
- paintLink(ctx, node);
624
- break;
625
- case "app":
626
- case "container":
627
- case "box":
628
- paintContainer(ctx, node);
629
- break;
630
- default:
631
- paintContainer(ctx, node);
632
- }
633
- ctx.restore();
634
- for (const child of node.children) {
635
- paintNode(ctx, child);
636
- }
637
- if (node._needsRestore) {
638
- ctx.restore();
639
- delete node._needsRestore;
640
- }
641
- }
642
- function paintContainer(ctx, node) {
643
- const layout = node.layout;
644
- const props = node.props;
645
- const x = layout.x;
646
- const y = layout.y;
647
- const width = layout.width;
648
- const height = layout.height;
649
- const radius = layout.border.radius;
650
- const shadow = props.shadow || props.boxShadow;
651
- if (shadow) {
652
- applyShadow(ctx, shadow);
653
- }
654
- const backgroundColor = props.backgroundColor || props.background;
655
- if (backgroundColor) {
656
- if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
657
- ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
658
- } else {
659
- ctx.fillStyle = backgroundColor;
660
- }
661
- if (radius > 0) {
662
- drawRoundedRect(ctx, x, y, width, height, radius);
663
- ctx.fill();
664
- } else {
665
- ctx.fillRect(x, y, width, height);
666
- }
667
- }
668
- if (shadow) {
669
- ctx.shadowColor = "transparent";
670
- ctx.shadowBlur = 0;
671
- ctx.shadowOffsetX = 0;
672
- ctx.shadowOffsetY = 0;
673
- }
674
- if (layout.border.width > 0 && layout.border.color !== "transparent") {
675
- ctx.strokeStyle = layout.border.color;
676
- ctx.lineWidth = layout.border.width;
677
- if (radius > 0) {
678
- drawRoundedRect(ctx, x, y, width, height, radius);
679
- ctx.stroke();
680
- } else {
681
- ctx.strokeRect(x, y, width, height);
682
- }
683
- }
684
- const overflow = props.overflow || "visible";
685
- if (overflow === "hidden" || overflow === "scroll" || overflow === "auto") {
686
- ctx.save();
687
- ctx.beginPath();
688
- if (radius > 0) {
689
- drawRoundedRect(ctx, x, y, width, height, radius);
690
- } else {
691
- ctx.rect(x, y, width, height);
692
- }
693
- ctx.clip();
694
- node._needsRestore = true;
695
- }
696
- }
697
- function paintText(ctx, node) {
698
- const layout = node.layout;
699
- const props = node.props;
700
- let text = String(props[0] || props.text || "");
701
- const color = props.color || "#000000";
702
- const fontSize = parseFloat(props.fontSize) || 16;
703
- const fontWeight = props.fontWeight || "normal";
704
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
705
- const textAlign = props.textAlign || "left";
706
- const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
707
- const textDecoration = props.textDecoration || "none";
708
- const textTransform = props.textTransform || "none";
709
- const letterSpacing = parseFloat(props.letterSpacing) || 0;
710
- if (textTransform === "uppercase") {
711
- text = text.toUpperCase();
712
- } else if (textTransform === "lowercase") {
713
- text = text.toLowerCase();
714
- } else if (textTransform === "capitalize") {
715
- text = text.replace(/\b\w/g, (char) => char.toUpperCase());
716
- }
717
- const textShadow = props.textShadow || props.shadow;
718
- if (textShadow) {
719
- applyShadow(ctx, textShadow);
720
- }
721
- const x = layout.x + layout.contentX;
722
- const y = layout.y + layout.contentY;
723
- if (letterSpacing !== 0) {
724
- ctx.fillStyle = color;
725
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
726
- ctx.textAlign = "left";
727
- ctx.textBaseline = "top";
728
- let currentX = x;
729
- for (let i = 0;i < text.length; i++) {
730
- ctx.fillText(text[i], currentX, y);
731
- currentX += ctx.measureText(text[i]).width;
732
- if (i < text.length - 1) {
733
- currentX += letterSpacing;
734
- }
735
- }
736
- if (textDecoration !== "none") {
737
- applyTextDecoration(ctx, textDecoration, color, x, y, currentX - x, fontSize);
738
- }
739
- } else {
740
- renderText(ctx, text, x, y, layout.contentWidth, layout.contentHeight, {
741
- color,
742
- fontSize,
743
- fontWeight,
744
- fontFamily,
745
- textAlign,
746
- verticalAlign: "top",
747
- lineHeight
748
- });
749
- if (textDecoration !== "none") {
750
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
751
- const textWidth = ctx.measureText(text).width;
752
- applyTextDecoration(ctx, textDecoration, color, x, y, textWidth, fontSize);
753
- }
754
- }
755
- if (textShadow) {
756
- ctx.shadowColor = "transparent";
757
- ctx.shadowBlur = 0;
758
- ctx.shadowOffsetX = 0;
759
- ctx.shadowOffsetY = 0;
760
- }
761
- }
762
- function paintButton(ctx, node) {
763
- const layout = node.layout;
764
- const props = node.props;
765
- const x = layout.x;
766
- const y = layout.y;
767
- const width = layout.width;
768
- const height = layout.height;
769
- const radius = layout.border.radius || 4;
770
- const shadow = props.shadow || props.boxShadow;
771
- if (shadow) {
772
- applyShadow(ctx, shadow);
773
- }
774
- let backgroundColor = props.backgroundColor || "#007bff";
775
- if (node.hovered) {
776
- backgroundColor = props.hoverColor || "#0056b3";
777
- }
778
- if (node.focused) {
779
- backgroundColor = props.focusColor || "#004085";
780
- }
781
- if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
782
- ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
783
- } else {
784
- ctx.fillStyle = backgroundColor;
785
- }
786
- drawRoundedRect(ctx, x, y, width, height, radius);
787
- ctx.fill();
788
- if (shadow) {
789
- ctx.shadowColor = "transparent";
790
- ctx.shadowBlur = 0;
791
- ctx.shadowOffsetX = 0;
792
- ctx.shadowOffsetY = 0;
793
- }
794
- if (layout.border.width > 0) {
795
- ctx.strokeStyle = layout.border.color;
796
- ctx.lineWidth = layout.border.width;
797
- drawRoundedRect(ctx, x, y, width, height, radius);
798
- ctx.stroke();
799
- }
800
- for (const child of node.children) {
801
- paintNode(ctx, child);
802
- }
803
- }
804
- function paintInput(ctx, node) {
805
- const layout = node.layout;
806
- const props = node.props;
807
- const x = layout.x;
808
- const y = layout.y;
809
- const width = layout.width;
810
- const height = layout.height;
811
- const radius = layout.border.radius || 4;
812
- ctx.fillStyle = props.backgroundColor || "#ffffff";
813
- drawRoundedRect(ctx, x, y, width, height, radius);
814
- ctx.fill();
815
- const borderColor = node.focused ? "#007bff" : layout.border.color || "#cccccc";
816
- const borderWidth = node.focused ? 2 : layout.border.width || 1;
817
- ctx.strokeStyle = borderColor;
818
- ctx.lineWidth = borderWidth;
819
- drawRoundedRect(ctx, x, y, width, height, radius);
820
- ctx.stroke();
821
- const value = props.value || "";
822
- const placeholder = props.placeholder || "";
823
- const text = value || placeholder;
824
- const textColor = value ? props.color || "#000000" : "#999999";
825
- if (text) {
826
- const fontSize = parseFloat(props.fontSize) || 16;
827
- const fontWeight = props.fontWeight || "normal";
828
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
829
- const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
830
- renderText(ctx, text, layout.x + layout.contentX, layout.y + layout.contentY, layout.contentWidth, layout.contentHeight, {
831
- color: textColor,
832
- fontSize,
833
- fontWeight,
834
- fontFamily,
835
- textAlign: "left",
836
- verticalAlign: "middle",
837
- lineHeight
838
- });
839
- }
840
- }
841
- var MAX_IMAGE_CACHE_SIZE = 100;
842
- var MAX_FAIL_CACHE_SIZE = 200;
843
- var imageCache = new Map;
844
- var imagePending = new Set;
845
- var imageInFlight = new Map;
846
- var imageFailCache = new Map;
847
- var IMAGE_FAIL_COOLDOWN_MS = 30000;
848
- var IMAGE_MAX_ATTEMPTS = 3;
849
- function evictMap(map, max) {
850
- while (map.size > max) {
851
- const oldest = map.keys().next().value;
852
- if (oldest === undefined)
853
- break;
854
- map.delete(oldest);
855
- }
856
- }
857
- function purgeExpiredFailures() {
858
- const now = Date.now();
859
- for (const [key, entry] of imageFailCache) {
860
- if (entry.attempts >= IMAGE_MAX_ATTEMPTS)
861
- continue;
862
- if (now - entry.failedAt >= IMAGE_FAIL_COOLDOWN_MS) {
863
- imageFailCache.delete(key);
864
- }
865
- }
866
- }
867
- function clearImageCache() {
868
- for (const img of imageInFlight.values()) {
869
- img.onload = null;
870
- img.onerror = null;
871
- img.src = "";
872
- }
873
- imageInFlight.clear();
874
- for (const img of imageCache.values()) {
875
- img.src = "";
876
- }
877
- imageCache.clear();
878
- imagePending.clear();
879
- imageFailCache.clear();
880
- }
881
- function paintImage(ctx, node) {
882
- const layout = node.layout;
883
- const props = node.props;
884
- const src = props.src || props[0];
885
- if (!src)
886
- return;
887
- const x = layout.x;
888
- const y = layout.y;
889
- const width = layout.width;
890
- const height = layout.height;
891
- const cached = imageCache.get(src);
892
- if (cached && cached.complete && cached.naturalWidth > 0) {
893
- ctx.drawImage(cached, x, y, width, height);
894
- return;
895
- }
896
- ctx.fillStyle = "#e0e0e0";
897
- ctx.fillRect(x, y, width, height);
898
- ctx.strokeStyle = "#999999";
899
- ctx.lineWidth = 1;
900
- ctx.strokeRect(x, y, width, height);
901
- ctx.fillStyle = "#666666";
902
- ctx.font = "14px sans-serif";
903
- ctx.textAlign = "center";
904
- ctx.textBaseline = "middle";
905
- const failEntry = imageFailCache.get(src);
906
- if (failEntry) {
907
- if (failEntry.attempts >= IMAGE_MAX_ATTEMPTS || Date.now() - failEntry.failedAt < IMAGE_FAIL_COOLDOWN_MS) {
908
- ctx.fillText("Failed", x + width / 2, y + height / 2);
909
- return;
910
- }
911
- }
912
- ctx.fillText("Loading...", x + width / 2, y + height / 2);
913
- if (!imagePending.has(src)) {
914
- imagePending.add(src);
915
- const canvas = ctx.canvas;
916
- const img = new Image;
917
- img.crossOrigin = "anonymous";
918
- imageInFlight.set(src, img);
919
- img.onload = () => {
920
- imageInFlight.delete(src);
921
- imageCache.set(src, img);
922
- imagePending.delete(src);
923
- imageFailCache.delete(src);
924
- evictMap(imageCache, MAX_IMAGE_CACHE_SIZE);
925
- canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
926
- };
927
- img.onerror = () => {
928
- imageInFlight.delete(src);
929
- imagePending.delete(src);
930
- purgeExpiredFailures();
931
- imageFailCache.set(src, {
932
- failedAt: Date.now(),
933
- attempts: (imageFailCache.get(src)?.attempts ?? 0) + 1
934
- });
935
- evictMap(imageFailCache, MAX_FAIL_CACHE_SIZE);
936
- };
937
- img.src = src;
938
- }
939
- }
940
- function drawRoundedRect(ctx, x, y, width, height, radius) {
941
- if (radius <= 0) {
942
- ctx.rect(x, y, width, height);
943
- return;
944
- }
945
- ctx.beginPath();
946
- ctx.moveTo(x + radius, y);
947
- ctx.lineTo(x + width - radius, y);
948
- ctx.arcTo(x + width, y, x + width, y + radius, radius);
949
- ctx.lineTo(x + width, y + height - radius);
950
- ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius);
951
- ctx.lineTo(x + radius, y + height);
952
- ctx.arcTo(x, y + height, x, y + height - radius, radius);
953
- ctx.lineTo(x, y + radius);
954
- ctx.arcTo(x, y, x + radius, y, radius);
955
- ctx.closePath();
956
- }
957
- function applyShadow(ctx, shadow) {
958
- if (typeof shadow === "string") {
959
- const parts = shadow.trim().split(/\s+/);
960
- if (parts.length >= 3) {
961
- const offsetX = parseFloat(parts[0]);
962
- const offsetY = parseFloat(parts[1]);
963
- const blur = parseFloat(parts[2]);
964
- const color = parts.slice(3).join(" ") || "rgba(0,0,0,0.3)";
965
- ctx.shadowOffsetX = offsetX;
966
- ctx.shadowOffsetY = offsetY;
967
- ctx.shadowBlur = blur;
968
- ctx.shadowColor = color;
969
- }
970
- } else if (typeof shadow === "object") {
971
- ctx.shadowOffsetX = shadow.offsetX || 0;
972
- ctx.shadowOffsetY = shadow.offsetY || 0;
973
- ctx.shadowBlur = shadow.blur || 0;
974
- ctx.shadowColor = shadow.color || "rgba(0,0,0,0.3)";
975
- }
976
- }
977
- function parseGradient(ctx, gradientStr, x, y, width, height) {
978
- if (gradientStr.startsWith("linear-gradient")) {
979
- const match = gradientStr.match(/linear-gradient\((.*)\)/);
980
- if (!match)
981
- return gradientStr;
982
- const parts = match[1].split(",").map((s) => s.trim());
983
- let x0 = x, y0 = y, x1 = x, y1 = y + height;
984
- let colorStart = 0;
985
- if (parts[0].includes("deg") || parts[0].includes("to ")) {
986
- colorStart = 1;
987
- const direction = parts[0];
988
- if (direction.includes("to right") || direction === "90deg") {
989
- x1 = x + width;
990
- y1 = y;
991
- } else if (direction.includes("to left") || direction === "270deg") {
992
- x0 = x + width;
993
- x1 = x;
994
- y0 = y;
995
- y1 = y;
996
- } else if (direction.includes("to top") || direction === "0deg") {
997
- y0 = y + height;
998
- y1 = y;
999
- }
1000
- }
1001
- const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
1002
- const colors = parts.slice(colorStart);
1003
- colors.forEach((color, i) => {
1004
- const stop = i / (colors.length - 1);
1005
- gradient.addColorStop(stop, color.trim());
1006
- });
1007
- return gradient;
1008
- } else if (gradientStr.startsWith("radial-gradient")) {
1009
- const match = gradientStr.match(/radial-gradient\((.*)\)/);
1010
- if (!match)
1011
- return gradientStr;
1012
- const parts = match[1].split(",").map((s) => s.trim());
1013
- const centerX = x + width / 2;
1014
- const centerY = y + height / 2;
1015
- const radius = Math.max(width, height) / 2;
1016
- const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
1017
- parts.forEach((color, i) => {
1018
- const stop = i / (parts.length - 1);
1019
- gradient.addColorStop(stop, color.trim());
1020
- });
1021
- return gradient;
1022
- }
1023
- return gradientStr;
1024
- }
1025
- function applyTransforms(ctx, node) {
1026
- const props = node.props;
1027
- const layout = node.layout;
1028
- const originX = parseFloat(props.transformOriginX) || 0.5;
1029
- const originY = parseFloat(props.transformOriginY) || 0.5;
1030
- const centerX = layout.x + layout.width * originX;
1031
- const centerY = layout.y + layout.height * originY;
1032
- const translateX = parseFloat(props.translateX) || 0;
1033
- const translateY = parseFloat(props.translateY) || 0;
1034
- const rotate = parseFloat(props.rotate) || 0;
1035
- const scaleX = parseFloat(props.scaleX) || parseFloat(props.scale) || 1;
1036
- const scaleY = parseFloat(props.scaleY) || parseFloat(props.scale) || 1;
1037
- const skewX = parseFloat(props.skewX) || parseFloat(props.skew) || 0;
1038
- const skewY = parseFloat(props.skewY) || 0;
1039
- if (translateX !== 0 || translateY !== 0 || rotate !== 0 || scaleX !== 1 || scaleY !== 1 || skewX !== 0 || skewY !== 0) {
1040
- ctx.translate(centerX, centerY);
1041
- if (scaleX !== 1 || scaleY !== 1) {
1042
- ctx.scale(scaleX, scaleY);
1043
- }
1044
- if (rotate !== 0) {
1045
- ctx.rotate(rotate * Math.PI / 180);
1046
- }
1047
- if (skewX !== 0 || skewY !== 0) {
1048
- const skewXRad = skewX * Math.PI / 180;
1049
- const skewYRad = skewY * Math.PI / 180;
1050
- ctx.transform(1, Math.tan(skewYRad), Math.tan(skewXRad), 1, 0, 0);
1051
- }
1052
- ctx.translate(-centerX + translateX, -centerY + translateY);
1053
- }
1054
- if (props.transform && typeof props.transform === "string") {
1055
- parseTransformString(ctx, props.transform, centerX, centerY);
1056
- }
1057
- }
1058
- function parseTransformString(ctx, transformStr, originX, originY) {
1059
- const transforms = transformStr.match(/(\w+)\(([^)]+)\)/g);
1060
- if (!transforms)
1061
- return;
1062
- ctx.translate(originX, originY);
1063
- for (const transform of transforms) {
1064
- const match = transform.match(/(\w+)\(([^)]+)\)/);
1065
- if (!match)
1066
- continue;
1067
- const [, func, args] = match;
1068
- const values = args.split(",").map((v) => parseFloat(v.trim()));
1069
- switch (func.toLowerCase()) {
1070
- case "translate":
1071
- ctx.translate(values[0] || 0, values[1] || 0);
1072
- break;
1073
- case "rotate":
1074
- ctx.rotate(values[0] * Math.PI / 180);
1075
- break;
1076
- case "scale":
1077
- ctx.scale(values[0] || 1, values[1] || values[0] || 1);
1078
- break;
1079
- }
1080
- }
1081
- ctx.translate(-originX, -originY);
1082
- }
1083
- function paintDivider(ctx, node) {
1084
- const layout = node.layout;
1085
- const props = node.props;
1086
- const orientation = props.orientation || "horizontal";
1087
- const color = props.color || props.backgroundColor || "#e0e0e0";
1088
- const thickness = parseFloat(props.thickness) || 1;
1089
- ctx.strokeStyle = color;
1090
- ctx.lineWidth = thickness;
1091
- ctx.beginPath();
1092
- if (orientation === "vertical") {
1093
- const x = layout.x + layout.width / 2;
1094
- ctx.moveTo(x, layout.y);
1095
- ctx.lineTo(x, layout.y + layout.height);
1096
- } else {
1097
- const y = layout.y + layout.height / 2;
1098
- ctx.moveTo(layout.x, y);
1099
- ctx.lineTo(layout.x + layout.width, y);
1100
- }
1101
- ctx.stroke();
1102
- }
1103
- function paintCheckbox(ctx, node) {
1104
- const layout = node.layout;
1105
- const props = node.props;
1106
- const size = Math.min(layout.width, layout.height);
1107
- const x = layout.x;
1108
- const y = layout.y;
1109
- const checkedValue = props.checked !== undefined ? props.checked : props.value;
1110
- const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
1111
- const radius = parseFloat(props.borderRadius) || 2;
1112
- const bgColor = checked ? props.checkedColor || "#007bff" : props.backgroundColor || "#ffffff";
1113
- ctx.fillStyle = bgColor;
1114
- drawRoundedRect(ctx, x, y, size, size, radius);
1115
- ctx.fill();
1116
- const borderColor = checked ? props.checkedColor || "#007bff" : props.borderColor || "#cccccc";
1117
- ctx.strokeStyle = borderColor;
1118
- ctx.lineWidth = node.focused ? 2 : 1;
1119
- drawRoundedRect(ctx, x, y, size, size, radius);
1120
- ctx.stroke();
1121
- if (checked) {
1122
- ctx.strokeStyle = props.checkColor || "#ffffff";
1123
- ctx.lineWidth = 2;
1124
- ctx.lineCap = "round";
1125
- ctx.lineJoin = "round";
1126
- const padding = size * 0.25;
1127
- ctx.beginPath();
1128
- ctx.moveTo(x + padding, y + size / 2);
1129
- ctx.lineTo(x + size * 0.4, y + size - padding);
1130
- ctx.lineTo(x + size - padding, y + padding);
1131
- ctx.stroke();
1132
- }
1133
- }
1134
- function paintRadio(ctx, node) {
1135
- const layout = node.layout;
1136
- const props = node.props;
1137
- const size = Math.min(layout.width, layout.height);
1138
- const centerX = layout.x + size / 2;
1139
- const centerY = layout.y + size / 2;
1140
- const radius = size / 2;
1141
- const checkedValue = props.checked !== undefined ? props.checked : props.value;
1142
- const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
1143
- const bgColor = props.backgroundColor || "#ffffff";
1144
- ctx.fillStyle = bgColor;
1145
- ctx.beginPath();
1146
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1147
- ctx.fill();
1148
- const borderColor = checked ? props.checkedColor || "#007bff" : props.borderColor || "#cccccc";
1149
- ctx.strokeStyle = borderColor;
1150
- ctx.lineWidth = node.focused ? 2 : 1;
1151
- ctx.beginPath();
1152
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1153
- ctx.stroke();
1154
- if (checked) {
1155
- ctx.fillStyle = props.checkedColor || "#007bff";
1156
- ctx.beginPath();
1157
- ctx.arc(centerX, centerY, radius * 0.5, 0, Math.PI * 2);
1158
- ctx.fill();
1159
- }
1160
- }
1161
- function paintSwitch(ctx, node) {
1162
- const layout = node.layout;
1163
- const props = node.props;
1164
- const width = layout.width;
1165
- const height = layout.height;
1166
- const x = layout.x;
1167
- const y = layout.y;
1168
- const checkedValue = props.checked !== undefined ? props.checked : props.value;
1169
- const checked = checkedValue === true || checkedValue === "true" || checkedValue !== false && checkedValue !== "false" && !!checkedValue;
1170
- const radius = height / 2;
1171
- const trackColor = checked ? props.checkedColor || "#4caf50" : props.backgroundColor || "#cccccc";
1172
- ctx.fillStyle = trackColor;
1173
- drawRoundedRect(ctx, x, y, width, height, radius);
1174
- ctx.fill();
1175
- const thumbRadius = radius * 0.8;
1176
- const thumbX = checked ? x + width - radius : x + radius;
1177
- const thumbY = y + radius;
1178
- ctx.fillStyle = props.thumbColor || "#ffffff";
1179
- ctx.beginPath();
1180
- ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
1181
- ctx.fill();
1182
- if (props.shadow !== false) {
1183
- ctx.shadowColor = "rgba(0,0,0,0.2)";
1184
- ctx.shadowBlur = 2;
1185
- ctx.shadowOffsetY = 1;
1186
- ctx.beginPath();
1187
- ctx.arc(thumbX, thumbY, thumbRadius, 0, Math.PI * 2);
1188
- ctx.fill();
1189
- ctx.shadowColor = "transparent";
1190
- ctx.shadowBlur = 0;
1191
- ctx.shadowOffsetY = 0;
1192
- }
1193
- }
1194
- function paintSlider(ctx, node) {
1195
- const layout = node.layout;
1196
- const props = node.props;
1197
- const width = layout.width;
1198
- const height = layout.height;
1199
- const x = layout.x;
1200
- const y = layout.y;
1201
- const min = parseFloat(props.min) || 0;
1202
- const max = parseFloat(props.max) || 100;
1203
- const value = parseFloat(props.value) || min;
1204
- const percentage = (value - min) / (max - min);
1205
- const trackHeight = parseFloat(props.trackHeight) || 4;
1206
- const thumbSize = parseFloat(props.thumbSize) || 16;
1207
- const trackY = y + (height - trackHeight) / 2;
1208
- ctx.fillStyle = props.trackColor || "#e0e0e0";
1209
- drawRoundedRect(ctx, x, trackY, width, trackHeight, trackHeight / 2);
1210
- ctx.fill();
1211
- const fillWidth = width * percentage;
1212
- const fillColor = props.fillColor || props.color || "#007bff";
1213
- if (typeof fillColor === "string" && fillColor.includes("gradient")) {
1214
- ctx.fillStyle = parseGradient(ctx, fillColor, x, trackY, fillWidth, trackHeight);
1215
- } else {
1216
- ctx.fillStyle = fillColor;
1217
- }
1218
- drawRoundedRect(ctx, x, trackY, fillWidth, trackHeight, trackHeight / 2);
1219
- ctx.fill();
1220
- const thumbX = x + fillWidth;
1221
- const thumbY = y + height / 2;
1222
- ctx.fillStyle = props.thumbColor || "#007bff";
1223
- ctx.beginPath();
1224
- ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
1225
- ctx.fill();
1226
- ctx.strokeStyle = "#ffffff";
1227
- ctx.lineWidth = 2;
1228
- ctx.beginPath();
1229
- ctx.arc(thumbX, thumbY, thumbSize / 2, 0, Math.PI * 2);
1230
- ctx.stroke();
1231
- }
1232
- function paintProgress(ctx, node) {
1233
- const layout = node.layout;
1234
- const props = node.props;
1235
- const width = layout.width;
1236
- const height = layout.height;
1237
- const x = layout.x;
1238
- const y = layout.y;
1239
- const min = parseFloat(props.min) || 0;
1240
- const max = parseFloat(props.max) || 100;
1241
- const value = parseFloat(props.value) || 0;
1242
- const percentage = Math.min(Math.max((value - min) / (max - min), 0), 1);
1243
- const radius = layout.border.radius || height / 2;
1244
- ctx.fillStyle = props.backgroundColor || "#e0e0e0";
1245
- drawRoundedRect(ctx, x, y, width, height, radius);
1246
- ctx.fill();
1247
- if (percentage > 0) {
1248
- const fillWidth = width * percentage;
1249
- const fillColor = props.fillColor || props.color || "#007bff";
1250
- if (typeof fillColor === "string" && fillColor.includes("gradient")) {
1251
- ctx.fillStyle = parseGradient(ctx, fillColor, x, y, fillWidth, height);
1252
- } else {
1253
- ctx.fillStyle = fillColor;
1254
- }
1255
- drawRoundedRect(ctx, x, y, fillWidth, height, radius);
1256
- ctx.fill();
1257
- }
1258
- if (props.showLabel) {
1259
- const label = props.label || `${Math.round(percentage * 100)}%`;
1260
- ctx.fillStyle = props.labelColor || "#ffffff";
1261
- ctx.font = `${props.fontSize || 12}px ${props.fontFamily || "sans-serif"}`;
1262
- ctx.textAlign = "center";
1263
- ctx.textBaseline = "middle";
1264
- ctx.fillText(label, x + width / 2, y + height / 2);
1265
- }
1266
- }
1267
- function paintSpinner(ctx, node) {
1268
- const layout = node.layout;
1269
- const props = node.props;
1270
- const size = Math.min(layout.width, layout.height);
1271
- const centerX = layout.x + size / 2;
1272
- const centerY = layout.y + size / 2;
1273
- const radius = size / 2 - 4;
1274
- const thickness = parseFloat(props.thickness) || 4;
1275
- const color = props.color || "#007bff";
1276
- const rotation = Date.now() / 1000 * Math.PI;
1277
- ctx.strokeStyle = color;
1278
- ctx.lineWidth = thickness;
1279
- ctx.lineCap = "round";
1280
- ctx.beginPath();
1281
- ctx.arc(centerX, centerY, radius, rotation, rotation + Math.PI * 1.5);
1282
- ctx.stroke();
1283
- ctx.globalAlpha = 0.3;
1284
- ctx.beginPath();
1285
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1286
- ctx.stroke();
1287
- ctx.globalAlpha = 1;
1288
- }
1289
- function paintCard(ctx, node) {
1290
- const layout = node.layout;
1291
- const props = node.props;
1292
- const x = layout.x;
1293
- const y = layout.y;
1294
- const width = layout.width;
1295
- const height = layout.height;
1296
- const radius = layout.border.radius || 8;
1297
- const shadow = props.shadow || props.boxShadow || "0 2 8 rgba(0,0,0,0.1)";
1298
- applyShadow(ctx, shadow);
1299
- const backgroundColor = props.backgroundColor || "#ffffff";
1300
- if (typeof backgroundColor === "string" && backgroundColor.includes("gradient")) {
1301
- ctx.fillStyle = parseGradient(ctx, backgroundColor, x, y, width, height);
1302
- } else {
1303
- ctx.fillStyle = backgroundColor;
1304
- }
1305
- drawRoundedRect(ctx, x, y, width, height, radius);
1306
- ctx.fill();
1307
- ctx.shadowColor = "transparent";
1308
- ctx.shadowBlur = 0;
1309
- ctx.shadowOffsetX = 0;
1310
- ctx.shadowOffsetY = 0;
1311
- if (layout.border.width > 0) {
1312
- ctx.strokeStyle = layout.border.color;
1313
- ctx.lineWidth = layout.border.width;
1314
- drawRoundedRect(ctx, x, y, width, height, radius);
1315
- ctx.stroke();
1316
- }
1317
- for (const child of node.children) {
1318
- paintNode(ctx, child);
1319
- }
1320
- }
1321
- function paintBadge(ctx, node) {
1322
- const layout = node.layout;
1323
- const props = node.props;
1324
- const x = layout.x;
1325
- const y = layout.y;
1326
- const width = layout.width;
1327
- const height = layout.height;
1328
- const radius = layout.border.radius || height / 2;
1329
- const backgroundColor = props.backgroundColor || "#dc3545";
1330
- ctx.fillStyle = backgroundColor;
1331
- drawRoundedRect(ctx, x, y, width, height, radius);
1332
- ctx.fill();
1333
- const text = String(props[0] || props.text || "");
1334
- if (text) {
1335
- ctx.fillStyle = props.color || "#ffffff";
1336
- ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || 10}px ${props.fontFamily || "sans-serif"}`;
1337
- ctx.textAlign = "center";
1338
- ctx.textBaseline = "middle";
1339
- ctx.fillText(text, x + width / 2, y + height / 2);
1340
- }
1341
- }
1342
- function paintAvatar(ctx, node) {
1343
- const layout = node.layout;
1344
- const props = node.props;
1345
- const size = Math.min(layout.width, layout.height);
1346
- const centerX = layout.x + size / 2;
1347
- const centerY = layout.y + size / 2;
1348
- const radius = size / 2;
1349
- ctx.save();
1350
- ctx.beginPath();
1351
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1352
- ctx.clip();
1353
- const backgroundColor = props.backgroundColor || "#cccccc";
1354
- ctx.fillStyle = backgroundColor;
1355
- ctx.beginPath();
1356
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1357
- ctx.fill();
1358
- const text = String(props[0] || props.text || props.initials || "");
1359
- if (text) {
1360
- ctx.fillStyle = props.color || "#ffffff";
1361
- ctx.font = `${props.fontWeight || "bold"} ${props.fontSize || size / 2.5}px ${props.fontFamily || "sans-serif"}`;
1362
- ctx.textAlign = "center";
1363
- ctx.textBaseline = "middle";
1364
- ctx.fillText(text, centerX, centerY);
1365
- }
1366
- ctx.restore();
1367
- if (layout.border.width > 0) {
1368
- ctx.strokeStyle = layout.border.color;
1369
- ctx.lineWidth = layout.border.width;
1370
- ctx.beginPath();
1371
- ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
1372
- ctx.stroke();
1373
- }
1374
- }
1375
- function paintIcon(ctx, node) {
1376
- const layout = node.layout;
1377
- const props = node.props;
1378
- const size = Math.min(layout.width, layout.height);
1379
- const x = layout.x;
1380
- const y = layout.y;
1381
- const color = props.color || "#000000";
1382
- const iconName = props.icon || props.name || "circle";
1383
- ctx.fillStyle = color;
1384
- ctx.strokeStyle = color;
1385
- ctx.lineWidth = 2;
1386
- switch (iconName.toLowerCase()) {
1387
- case "circle":
1388
- ctx.beginPath();
1389
- ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1390
- ctx.fill();
1391
- break;
1392
- case "square":
1393
- const padding = size * 0.2;
1394
- ctx.fillRect(x + padding, y + padding, size - padding * 2, size - padding * 2);
1395
- break;
1396
- case "star":
1397
- drawStar(ctx, x + size / 2, y + size / 2, 5, size / 3, size / 6);
1398
- ctx.fill();
1399
- break;
1400
- case "check":
1401
- case "checkmark":
1402
- const p = size * 0.2;
1403
- ctx.beginPath();
1404
- ctx.moveTo(x + p, y + size / 2);
1405
- ctx.lineTo(x + size * 0.4, y + size - p);
1406
- ctx.lineTo(x + size - p, y + p);
1407
- ctx.stroke();
1408
- break;
1409
- case "x":
1410
- case "close":
1411
- const pd = size * 0.2;
1412
- ctx.beginPath();
1413
- ctx.moveTo(x + pd, y + pd);
1414
- ctx.lineTo(x + size - pd, y + size - pd);
1415
- ctx.moveTo(x + size - pd, y + pd);
1416
- ctx.lineTo(x + pd, y + size - pd);
1417
- ctx.stroke();
1418
- break;
1419
- default:
1420
- ctx.beginPath();
1421
- ctx.arc(x + size / 2, y + size / 2, size / 3, 0, Math.PI * 2);
1422
- ctx.fill();
1423
- }
1424
- }
1425
- function paintLink(ctx, node) {
1426
- const layout = node.layout;
1427
- const props = node.props;
1428
- const text = String(props[0] || props.text || "");
1429
- const color = node.hovered ? props.hoverColor || "#0056b3" : props.color || "#007bff";
1430
- const fontSize = parseFloat(props.fontSize) || 16;
1431
- const fontWeight = props.fontWeight || "normal";
1432
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
1433
- const textDecoration = props.textDecoration !== undefined ? props.textDecoration : "underline";
1434
- ctx.fillStyle = color;
1435
- ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
1436
- ctx.textAlign = "left";
1437
- ctx.textBaseline = "top";
1438
- const x = layout.x + layout.contentX;
1439
- const y = layout.y + layout.contentY;
1440
- ctx.fillText(text, x, y);
1441
- if (textDecoration === "underline") {
1442
- const textWidth = ctx.measureText(text).width;
1443
- ctx.strokeStyle = color;
1444
- ctx.lineWidth = 1;
1445
- ctx.beginPath();
1446
- ctx.moveTo(x, y + fontSize + 2);
1447
- ctx.lineTo(x + textWidth, y + fontSize + 2);
1448
- ctx.stroke();
1449
- }
1450
- }
1451
- function applyTextDecoration(ctx, decoration, color, x, y, width, fontSize) {
1452
- ctx.strokeStyle = color;
1453
- ctx.lineWidth = Math.max(1, fontSize / 16);
1454
- ctx.beginPath();
1455
- if (decoration === "underline") {
1456
- const underlineY = y + fontSize + 2;
1457
- ctx.moveTo(x, underlineY);
1458
- ctx.lineTo(x + width, underlineY);
1459
- } else if (decoration === "line-through" || decoration === "strikethrough") {
1460
- const lineThroughY = y + fontSize / 2;
1461
- ctx.moveTo(x, lineThroughY);
1462
- ctx.lineTo(x + width, lineThroughY);
1463
- } else if (decoration === "overline") {
1464
- ctx.moveTo(x, y);
1465
- ctx.lineTo(x + width, y);
1466
- }
1467
- ctx.stroke();
1468
- }
1469
- function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
1470
- let rot = Math.PI / 2 * 3;
1471
- let x = cx;
1472
- let y = cy;
1473
- const step = Math.PI / spikes;
1474
- ctx.beginPath();
1475
- ctx.moveTo(cx, cy - outerRadius);
1476
- for (let i = 0;i < spikes; i++) {
1477
- x = cx + Math.cos(rot) * outerRadius;
1478
- y = cy + Math.sin(rot) * outerRadius;
1479
- ctx.lineTo(x, y);
1480
- rot += step;
1481
- x = cx + Math.cos(rot) * innerRadius;
1482
- y = cy + Math.sin(rot) * innerRadius;
1483
- ctx.lineTo(x, y);
1484
- rot += step;
1485
- }
1486
- ctx.lineTo(cx, cy - outerRadius);
1487
- ctx.closePath();
1488
- }
1489
-
1490
- // src/canvas/layout.ts
1491
- function computeLayout(ctx, node, availableWidth, availableHeight, x = 0, y = 0) {
1492
- const props = node.props;
1493
- let margin = parseSpacing(props.margin || 0);
1494
- if (props.marginTop !== undefined)
1495
- margin.top = parseFloat(props.marginTop) || 0;
1496
- if (props.marginRight !== undefined)
1497
- margin.right = parseFloat(props.marginRight) || 0;
1498
- if (props.marginBottom !== undefined)
1499
- margin.bottom = parseFloat(props.marginBottom) || 0;
1500
- if (props.marginLeft !== undefined)
1501
- margin.left = parseFloat(props.marginLeft) || 0;
1502
- let padding = parseSpacing(props.padding || 0);
1503
- if (props.paddingTop !== undefined)
1504
- padding.top = parseFloat(props.paddingTop) || 0;
1505
- if (props.paddingRight !== undefined)
1506
- padding.right = parseFloat(props.paddingRight) || 0;
1507
- if (props.paddingBottom !== undefined)
1508
- padding.bottom = parseFloat(props.paddingBottom) || 0;
1509
- if (props.paddingLeft !== undefined)
1510
- padding.left = parseFloat(props.paddingLeft) || 0;
1511
- const borderWidth = parseFloat(props.borderWidth) || 0;
1512
- const borderColor = props.borderColor || "transparent";
1513
- const borderRadius = parseFloat(props.borderRadius) || 0;
1514
- const availableAfterMargin = {
1515
- width: availableWidth - margin.left - margin.right,
1516
- height: availableHeight - margin.top - margin.bottom
1517
- };
1518
- let width = parseSize(props.width);
1519
- let height = parseSize(props.height);
1520
- const type = node.type.toLowerCase();
1521
- if (type === "app") {
1522
- if (width === null)
1523
- width = availableAfterMargin.width;
1524
- if (height === null)
1525
- height = availableAfterMargin.height;
1526
- } else if (type === "spacer") {
1527
- if (width === null)
1528
- width = availableAfterMargin.width;
1529
- if (height === null)
1530
- height = availableAfterMargin.height;
1531
- } else if (type === "divider" || type === "separator") {
1532
- const orientation = props.orientation || "horizontal";
1533
- const thickness = parseFloat(props.thickness) || 1;
1534
- if (orientation === "vertical") {
1535
- if (width === null)
1536
- width = thickness;
1537
- if (height === null)
1538
- height = availableAfterMargin.height;
1539
- } else {
1540
- if (width === null)
1541
- width = availableAfterMargin.width;
1542
- if (height === null)
1543
- height = thickness;
1544
- }
1545
- } else if (type === "checkbox" || type === "radio") {
1546
- const size = parseFloat(props.size) || 20;
1547
- if (width === null)
1548
- width = size;
1549
- if (height === null)
1550
- height = size;
1551
- } else if (type === "switch" || type === "toggle") {
1552
- if (width === null)
1553
- width = 44;
1554
- if (height === null)
1555
- height = 24;
1556
- } else if (type === "slider") {
1557
- if (width === null)
1558
- width = 200;
1559
- if (height === null)
1560
- height = 20;
1561
- } else if (type === "progress" || type === "progressbar") {
1562
- if (width === null)
1563
- width = 200;
1564
- if (height === null)
1565
- height = 8;
1566
- } else if (type === "spinner" || type === "loading") {
1567
- const size = parseFloat(props.size) || 24;
1568
- if (width === null)
1569
- width = size;
1570
- if (height === null)
1571
- height = size;
1572
- } else if (type === "badge") {
1573
- if (width === null)
1574
- width = 20;
1575
- if (height === null)
1576
- height = 20;
1577
- } else if (type === "avatar") {
1578
- const size = parseFloat(props.size) || 40;
1579
- if (width === null)
1580
- width = size;
1581
- if (height === null)
1582
- height = size;
1583
- } else if (type === "icon") {
1584
- const size = parseFloat(props.size) || 24;
1585
- if (width === null)
1586
- width = size;
1587
- if (height === null)
1588
- height = size;
1589
- }
1590
- if (node.type === "text" && node.props[0]) {
1591
- const text = String(node.props[0] || "");
1592
- const fontSize = parseFloat(props.fontSize) || 16;
1593
- const fontWeight = props.fontWeight || "normal";
1594
- const fontFamily = props.fontFamily || "system-ui, sans-serif";
1595
- const lineHeight = parseFloat(props.lineHeight) || fontSize * 1.2;
1596
- const maxWidth2 = width || availableAfterMargin.width - padding.left - padding.right;
1597
- const metrics = measureText(ctx, text, { fontSize, fontWeight, fontFamily, lineHeight }, maxWidth2);
1598
- if (!width)
1599
- width = metrics.width + padding.left + padding.right;
1600
- if (!height)
1601
- height = metrics.height + padding.top + padding.bottom;
1602
- }
1603
- if (width === null)
1604
- width = availableAfterMargin.width;
1605
- if (height === null)
1606
- height = availableAfterMargin.height;
1607
- const minWidth = parseSize(props.minWidth);
1608
- const maxWidth = parseSize(props.maxWidth);
1609
- const minHeight = parseSize(props.minHeight);
1610
- const maxHeight = parseSize(props.maxHeight);
1611
- if (minWidth !== null)
1612
- width = Math.max(width, minWidth);
1613
- if (maxWidth !== null)
1614
- width = Math.min(width, maxWidth);
1615
- if (minHeight !== null)
1616
- height = Math.max(height, minHeight);
1617
- if (maxHeight !== null)
1618
- height = Math.min(height, maxHeight);
1619
- const layout = {
1620
- x: x + margin.left,
1621
- y: y + margin.top,
1622
- width,
1623
- height,
1624
- margin,
1625
- padding,
1626
- border: {
1627
- width: borderWidth,
1628
- color: borderColor,
1629
- radius: borderRadius
1630
- },
1631
- contentX: padding.left + borderWidth,
1632
- contentY: padding.top + borderWidth,
1633
- contentWidth: width - padding.left - padding.right - borderWidth * 2,
1634
- contentHeight: height - padding.top - padding.bottom - borderWidth * 2
1635
- };
1636
- node.layout = layout;
1637
- if (node.children.length > 0) {
1638
- layoutChildren(ctx, node);
1639
- }
1640
- }
1641
- function layoutChildren(ctx, parent) {
1642
- const layout = parent.layout;
1643
- const props = parent.props;
1644
- if (parent.type.toLowerCase() === "stack") {
1645
- layoutStackChildren(ctx, parent);
1646
- return;
1647
- }
1648
- const flexDirection = props.flexDirection || (parent.type === "column" ? "column" : "row");
1649
- const isColumn = flexDirection === "column";
1650
- const justifyContent = isColumn ? props.verticalAlignment || "flex-start" : props.horizontalAlignment || "flex-start";
1651
- const alignItems = isColumn ? props.horizontalAlignment || "flex-start" : props.verticalAlignment || "flex-start";
1652
- const gap = parseFloat(props.gap) || 0;
1653
- const availableWidth = layout.contentWidth;
1654
- const availableHeight = layout.contentHeight;
1655
- const childInfo = [];
1656
- let totalMainSize = 0;
1657
- let totalFlexGrow = 0;
1658
- let totalFlexShrink = 0;
1659
- for (const child of parent.children) {
1660
- const flexGrow = parseFloat(child.props.flexGrow) || parseFloat(child.props.flex) || 0;
1661
- const flexShrink = parseFloat(child.props.flexShrink) || 1;
1662
- const flexBasis = parseSize(child.props.flexBasis);
1663
- computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
1664
- const childLayout = child.layout;
1665
- let mainSize = isColumn ? childLayout.height : childLayout.width;
1666
- if (flexBasis !== null) {
1667
- mainSize = flexBasis;
1668
- if (isColumn) {
1669
- childLayout.height = flexBasis;
1670
- } else {
1671
- childLayout.width = flexBasis;
1672
- }
1673
- }
1674
- childInfo.push({
1675
- width: childLayout.width,
1676
- height: childLayout.height,
1677
- flexGrow,
1678
- flexShrink,
1679
- flexBasis
1680
- });
1681
- totalMainSize += mainSize;
1682
- totalFlexGrow += flexGrow;
1683
- totalFlexShrink += flexShrink;
1684
- }
1685
- const totalGap = gap * (parent.children.length - 1);
1686
- totalMainSize += totalGap;
1687
- const availableMain = isColumn ? availableHeight : availableWidth;
1688
- let remainingSpace = availableMain - totalMainSize;
1689
- if (remainingSpace > 0 && totalFlexGrow > 0) {
1690
- const spacePerFlex = remainingSpace / totalFlexGrow;
1691
- for (let i = 0;i < parent.children.length; i++) {
1692
- const info = childInfo[i];
1693
- if (info.flexGrow > 0) {
1694
- const extraSpace = spacePerFlex * info.flexGrow;
1695
- if (isColumn) {
1696
- info.height += extraSpace;
1697
- } else {
1698
- info.width += extraSpace;
87
+ // Schedule redraw
88
+ this.scheduleRedraw();
89
+ }
90
+ /**
91
+ * Apply single patch
92
+ */
93
+ applyPatch(patch) {
94
+ switch (patch.type) {
95
+ case "create":
96
+ this.onCreate(patch.id, patch.elementType, patch.props || {});
97
+ break;
98
+ case "setProp":
99
+ this.onSetProp(patch.id, patch.name, patch.value);
100
+ break;
101
+ case "removeProp":
102
+ this.onRemoveProp(patch.id, patch.name);
103
+ break;
104
+ case "setText":
105
+ this.onSetText(patch.id, patch.text);
106
+ break;
107
+ case "insert":
108
+ this.onInsert(patch.parentId, patch.id, patch.beforeId);
109
+ break;
110
+ case "move":
111
+ this.onMove(patch.parentId, patch.id, patch.beforeId);
112
+ break;
113
+ case "remove":
114
+ this.onRemove(patch.id);
115
+ break;
1699
116
  }
1700
- totalMainSize += extraSpace;
1701
- }
1702
117
  }
1703
- remainingSpace = 0;
1704
- }
1705
- if (remainingSpace < 0 && totalFlexShrink > 0) {
1706
- const shrinkPerFlex = Math.abs(remainingSpace) / totalFlexShrink;
1707
- for (let i = 0;i < parent.children.length; i++) {
1708
- const info = childInfo[i];
1709
- if (info.flexShrink > 0) {
1710
- const shrinkSpace = Math.min(shrinkPerFlex * info.flexShrink, isColumn ? info.height : info.width);
1711
- if (isColumn) {
1712
- info.height = Math.max(0, info.height - shrinkSpace);
1713
- } else {
1714
- info.width = Math.max(0, info.width - shrinkSpace);
118
+ /**
119
+ * Create new virtual node
120
+ */
121
+ onCreate(id, elementType, props) {
122
+ const node = {
123
+ id,
124
+ type: elementType,
125
+ props: props instanceof Map ? Object.fromEntries(props) : props,
126
+ children: [],
127
+ parent: null,
128
+ visible: true,
129
+ opacity: parseFloat(props.opacity) || 1,
130
+ clickable: elementType === "button" || !!props.onclick,
131
+ hoverable: true,
132
+ focusable: elementType === "input" || elementType === "textarea" || elementType === "button",
133
+ focused: false,
134
+ hovered: false,
135
+ };
136
+ this.nodes.set(id, node);
137
+ }
138
+ /**
139
+ * Set property on node
140
+ */
141
+ onSetProp(id, name, value) {
142
+ const node = this.nodes.get(id);
143
+ if (!node)
144
+ return;
145
+ node.props[name] = value;
146
+ // Update computed properties
147
+ if (name === "visible") {
148
+ node.visible = !!value;
149
+ }
150
+ if (name === "opacity") {
151
+ node.opacity = parseFloat(value) || 1;
152
+ }
153
+ // Update accessibility
154
+ this.accessibilityLayer.updateNode(node);
155
+ }
156
+ /**
157
+ * Remove a property from a node
158
+ */
159
+ onRemoveProp(id, name) {
160
+ const node = this.nodes.get(id);
161
+ if (!node)
162
+ return;
163
+ delete node.props[name];
164
+ if (name === "visible") {
165
+ node.visible = true;
166
+ }
167
+ if (name === "opacity") {
168
+ node.opacity = 1;
169
+ }
170
+ this.accessibilityLayer.updateNode(node);
171
+ }
172
+ /**
173
+ * Set text on node
174
+ */
175
+ onSetText(id, text) {
176
+ const node = this.nodes.get(id);
177
+ if (!node)
178
+ return;
179
+ node.props[0] = text;
180
+ // Update accessibility
181
+ this.accessibilityLayer.updateNode(node);
182
+ }
183
+ /**
184
+ * Insert node into tree
185
+ */
186
+ onInsert(parentId, id, beforeId) {
187
+ const child = this.nodes.get(id);
188
+ if (!child)
189
+ return;
190
+ // Check if this is setting the root node (parent_id === id === "root" or similar)
191
+ if (parentId === "root" && id === "root") {
192
+ this.rootNode = child;
193
+ this.eventManager.setRootNode(child);
194
+ return;
195
+ }
196
+ // Otherwise find the parent node
197
+ const parent = this.nodes.get(parentId);
198
+ if (!parent) {
199
+ // If parent is "root", this might be the first real root node
200
+ if (parentId === "root") {
201
+ this.rootNode = child;
202
+ this.eventManager.setRootNode(child);
203
+ }
204
+ return;
205
+ }
206
+ // Insert child into parent
207
+ child.parent = parent;
208
+ if (beforeId) {
209
+ const beforeIndex = parent.children.findIndex((c) => c.id === beforeId);
210
+ if (beforeIndex >= 0) {
211
+ parent.children.splice(beforeIndex, 0, child);
212
+ }
213
+ else {
214
+ parent.children.push(child);
215
+ }
216
+ }
217
+ else {
218
+ parent.children.push(child);
1715
219
  }
1716
- totalMainSize -= shrinkSpace;
1717
- }
1718
- }
1719
- remainingSpace = availableMain - totalMainSize;
1720
- }
1721
- let mainStart = 0;
1722
- let spacing = 0;
1723
- if (justifyContent === "center") {
1724
- mainStart = Math.max(0, remainingSpace / 2);
1725
- } else if (justifyContent === "flex-end") {
1726
- mainStart = Math.max(0, remainingSpace);
1727
- } else if (justifyContent === "space-between") {
1728
- spacing = remainingSpace / Math.max(1, parent.children.length - 1);
1729
- } else if (justifyContent === "space-around") {
1730
- spacing = remainingSpace / parent.children.length;
1731
- mainStart = spacing / 2;
1732
- }
1733
- let currentMain = mainStart;
1734
- for (let i = 0;i < parent.children.length; i++) {
1735
- const child = parent.children[i];
1736
- const childLayout = child.layout;
1737
- const info = childInfo[i];
1738
- childLayout.width = info.width;
1739
- childLayout.height = info.height;
1740
- let crossStart = 0;
1741
- const availableCross = isColumn ? availableWidth : availableHeight;
1742
- const childCross = isColumn ? info.width : info.height;
1743
- if (alignItems === "center") {
1744
- crossStart = (availableCross - childCross) / 2;
1745
- } else if (alignItems === "flex-end") {
1746
- crossStart = availableCross - childCross;
1747
- }
1748
- if (isColumn) {
1749
- childLayout.x = layout.x + layout.contentX + crossStart;
1750
- childLayout.y = layout.y + layout.contentY + currentMain;
1751
- currentMain += info.height + gap;
1752
- } else {
1753
- childLayout.x = layout.x + layout.contentX + currentMain;
1754
- childLayout.y = layout.y + layout.contentY + crossStart;
1755
- currentMain += info.width + gap;
1756
- }
1757
- if (justifyContent === "space-between" || justifyContent === "space-around") {
1758
- currentMain += spacing;
1759
- }
1760
- }
1761
- }
1762
- function layoutStackChildren(ctx, parent) {
1763
- const layout = parent.layout;
1764
- const props = parent.props;
1765
- const horizontalAlignment = props.horizontalAlignment || "flex-start";
1766
- const verticalAlignment = props.verticalAlignment || "flex-start";
1767
- const availableWidth = layout.contentWidth;
1768
- const availableHeight = layout.contentHeight;
1769
- for (const child of parent.children) {
1770
- computeLayout(ctx, child, availableWidth, availableHeight, 0, 0);
1771
- const childLayout = child.layout;
1772
- let x = 0;
1773
- let y = 0;
1774
- if (horizontalAlignment === "center") {
1775
- x = (availableWidth - childLayout.width) / 2;
1776
- } else if (horizontalAlignment === "flex-end") {
1777
- x = availableWidth - childLayout.width;
1778
- }
1779
- if (verticalAlignment === "center") {
1780
- y = (availableHeight - childLayout.height) / 2;
1781
- } else if (verticalAlignment === "flex-end") {
1782
- y = availableHeight - childLayout.height;
1783
- }
1784
- childLayout.x = layout.x + layout.contentX + x;
1785
- childLayout.y = layout.y + layout.contentY + y;
1786
- }
1787
- }
1788
-
1789
- // src/canvas/events.ts
1790
- class CanvasEventManager {
1791
- canvas;
1792
- engine;
1793
- rootNode = null;
1794
- hoveredNode = null;
1795
- focusedNode = null;
1796
- mouseDownNode = null;
1797
- boundOnMouseMove;
1798
- boundOnMouseDown;
1799
- boundOnMouseUp;
1800
- boundOnClick;
1801
- boundOnDoubleClick;
1802
- boundOnKeyDown;
1803
- boundOnKeyUp;
1804
- constructor(canvas, engine) {
1805
- this.canvas = canvas;
1806
- this.engine = engine;
1807
- this.setupEventListeners();
1808
- }
1809
- setRootNode(node) {
1810
- this.rootNode = node;
1811
- }
1812
- setupEventListeners() {
1813
- this.boundOnMouseMove = this.onMouseMove.bind(this);
1814
- this.boundOnMouseDown = this.onMouseDown.bind(this);
1815
- this.boundOnMouseUp = this.onMouseUp.bind(this);
1816
- this.boundOnClick = this.onClick.bind(this);
1817
- this.boundOnDoubleClick = this.onDoubleClick.bind(this);
1818
- this.boundOnKeyDown = this.onKeyDown.bind(this);
1819
- this.boundOnKeyUp = this.onKeyUp.bind(this);
1820
- this.canvas.addEventListener("mousemove", this.boundOnMouseMove);
1821
- this.canvas.addEventListener("mousedown", this.boundOnMouseDown);
1822
- this.canvas.addEventListener("mouseup", this.boundOnMouseUp);
1823
- this.canvas.addEventListener("click", this.boundOnClick);
1824
- this.canvas.addEventListener("dblclick", this.boundOnDoubleClick);
1825
- this.canvas.addEventListener("keydown", this.boundOnKeyDown);
1826
- this.canvas.addEventListener("keyup", this.boundOnKeyUp);
1827
- }
1828
- getCanvasCoordinates(e) {
1829
- const rect = this.canvas.getBoundingClientRect();
1830
- const scaleX = this.canvas.width / rect.width;
1831
- const scaleY = this.canvas.height / rect.height;
1832
- return {
1833
- x: (e.clientX - rect.left) * scaleX,
1834
- y: (e.clientY - rect.top) * scaleY
1835
- };
1836
- }
1837
- hitTest(point) {
1838
- if (!this.rootNode)
1839
- return null;
1840
- return this.hitTestNode(this.rootNode, point);
1841
- }
1842
- hitTestNode(node, point) {
1843
- if (!node.visible || !node.layout)
1844
- return null;
1845
- const bounds = getAbsoluteBounds(node);
1846
- if (!bounds)
1847
- return null;
1848
- for (let i = node.children.length - 1;i >= 0; i--) {
1849
- const child = node.children[i];
1850
- const hit = this.hitTestNode(child, point);
1851
- if (hit)
1852
- return hit;
1853
- }
1854
- const radius = node.layout.border.radius;
1855
- if (isPointInRoundedRect(point, bounds, radius)) {
1856
- return node;
1857
- }
1858
- return null;
1859
- }
1860
- onMouseMove(e) {
1861
- const point = this.getCanvasCoordinates(e);
1862
- const node = this.hitTest(point);
1863
- if (node !== this.hoveredNode) {
1864
- if (this.hoveredNode) {
1865
- this.hoveredNode.hovered = false;
1866
- this.dispatchNodeEvent(this.hoveredNode, "mouseleave", {});
1867
- }
1868
- this.hoveredNode = node;
1869
- if (node) {
1870
- node.hovered = true;
1871
- this.dispatchNodeEvent(node, "mouseenter", {});
1872
- }
1873
- this.updateCursor(node);
1874
- this.requestRedraw();
1875
- }
1876
- }
1877
- onMouseDown(e) {
1878
- const point = this.getCanvasCoordinates(e);
1879
- const node = this.hitTest(point);
1880
- this.mouseDownNode = node;
1881
- if (node && node.clickable) {
1882
- this.dispatchNodeEvent(node, "mousedown", {
1883
- button: e.button,
1884
- clientX: e.clientX,
1885
- clientY: e.clientY
1886
- });
1887
- }
1888
- if (node && node.focusable) {
1889
- this.setFocus(node);
1890
- } else {
1891
- this.setFocus(null);
1892
- }
1893
- }
1894
- onMouseUp(e) {
1895
- const point = this.getCanvasCoordinates(e);
1896
- const node = this.hitTest(point);
1897
- if (node && node.clickable) {
1898
- this.dispatchNodeEvent(node, "mouseup", {
1899
- button: e.button,
1900
- clientX: e.clientX,
1901
- clientY: e.clientY
1902
- });
1903
- }
1904
- this.mouseDownNode = null;
1905
- }
1906
- onClick(e) {
1907
- const point = this.getCanvasCoordinates(e);
1908
- const node = this.hitTest(point);
1909
- if (node && node.clickable && node === this.mouseDownNode) {
1910
- this.dispatchNodeEvent(node, "click", {
1911
- button: e.button,
1912
- clientX: e.clientX,
1913
- clientY: e.clientY
1914
- });
1915
- }
1916
- }
1917
- onDoubleClick(e) {
1918
- const point = this.getCanvasCoordinates(e);
1919
- const node = this.hitTest(point);
1920
- if (node && node.clickable) {
1921
- this.dispatchNodeEvent(node, "dblclick", {
1922
- button: e.button,
1923
- clientX: e.clientX,
1924
- clientY: e.clientY
1925
- });
1926
- }
1927
- }
1928
- onKeyDown(e) {
1929
- if (this.focusedNode) {
1930
- this.dispatchNodeEvent(this.focusedNode, "keydown", {
1931
- key: e.key,
1932
- code: e.code,
1933
- ctrlKey: e.ctrlKey,
1934
- shiftKey: e.shiftKey,
1935
- altKey: e.altKey
1936
- });
1937
- }
1938
- }
1939
- onKeyUp(e) {
1940
- if (this.focusedNode) {
1941
- this.dispatchNodeEvent(this.focusedNode, "keyup", {
1942
- key: e.key,
1943
- code: e.code,
1944
- ctrlKey: e.ctrlKey,
1945
- shiftKey: e.shiftKey,
1946
- altKey: e.altKey
1947
- });
1948
- }
1949
- }
1950
- setFocus(node) {
1951
- if (node === this.focusedNode)
1952
- return;
1953
- if (this.focusedNode) {
1954
- this.focusedNode.focused = false;
1955
- this.dispatchNodeEvent(this.focusedNode, "blur", {});
1956
- }
1957
- this.focusedNode = node;
1958
- if (node) {
1959
- node.focused = true;
1960
- this.dispatchNodeEvent(node, "focus", {});
1961
- }
1962
- this.requestRedraw();
1963
- }
1964
- updateCursor(node) {
1965
- if (!node) {
1966
- this.canvas.style.cursor = "default";
1967
- return;
1968
- }
1969
- const cursor = node.props.cursor || (node.clickable ? "pointer" : "default");
1970
- this.canvas.style.cursor = cursor;
1971
- }
1972
- dispatchNodeEvent(node, eventType, data) {
1973
- const actionName = node.props[`on${eventType}`] || node.props[eventType];
1974
- if (actionName && typeof actionName === "string") {
1975
- this.engine.dispatchAction(actionName, {
1976
- type: eventType,
1977
- nodeId: node.id,
1978
- timestamp: Date.now(),
1979
- ...data
1980
- });
1981
- }
1982
- }
1983
- requestRedraw() {
1984
- this.canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
1985
- }
1986
- destroy() {
1987
- this.canvas.removeEventListener("mousemove", this.boundOnMouseMove);
1988
- this.canvas.removeEventListener("mousedown", this.boundOnMouseDown);
1989
- this.canvas.removeEventListener("mouseup", this.boundOnMouseUp);
1990
- this.canvas.removeEventListener("click", this.boundOnClick);
1991
- this.canvas.removeEventListener("dblclick", this.boundOnDoubleClick);
1992
- this.canvas.removeEventListener("keydown", this.boundOnKeyDown);
1993
- this.canvas.removeEventListener("keyup", this.boundOnKeyUp);
1994
- this.rootNode = null;
1995
- this.hoveredNode = null;
1996
- this.focusedNode = null;
1997
- this.mouseDownNode = null;
1998
- }
1999
- }
2000
-
2001
- // src/canvas/renderer.ts
2002
- import { frameworkLoggers as frameworkLoggers2 } from "@hypen-space/core/logger";
2003
- var log2 = frameworkLoggers2.canvas;
2004
- var DEFAULT_OPTIONS = {
2005
- devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
2006
- backgroundColor: "#ffffff",
2007
- enableAccessibility: true,
2008
- enableHitTesting: true,
2009
- enableInputOverlay: true,
2010
- enableDirtyRects: false,
2011
- enableLayerCaching: false,
2012
- maxLayerCacheSize: 10,
2013
- showLayoutBounds: false,
2014
- showDirtyRects: false,
2015
- logPerformance: false
2016
- };
2017
-
2018
- class CanvasRenderer {
2019
- canvas;
2020
- ctx;
2021
- engine;
2022
- options;
2023
- rootNode = null;
2024
- nodes = new Map;
2025
- eventManager;
2026
- inputOverlay;
2027
- accessibilityLayer;
2028
- rafId = null;
2029
- needsRedraw = false;
2030
- frameCount = 0;
2031
- lastFrameTime = 0;
2032
- constructor(canvas, engine, options) {
2033
- this.canvas = canvas;
2034
- this.engine = engine;
2035
- this.options = { ...DEFAULT_OPTIONS, ...options };
2036
- const ctx = canvas.getContext("2d");
2037
- if (!ctx) {
2038
- throw new Error("Failed to get 2D context from canvas");
2039
- }
2040
- this.ctx = ctx;
2041
- this.setupHiDPI();
2042
- this.eventManager = new CanvasEventManager(canvas, engine);
2043
- this.inputOverlay = new InputOverlay(canvas.parentElement || (typeof document !== "undefined" ? document.body : null));
2044
- this.accessibilityLayer = new AccessibilityLayer(canvas.parentElement || (typeof document !== "undefined" ? document.body : null), this.options.enableAccessibility || false);
2045
- this.canvas.addEventListener("hypen:redraw", () => this.scheduleRedraw());
2046
- }
2047
- setupHiDPI() {
2048
- const dpr = this.options.devicePixelRatio || 1;
2049
- const rect = this.canvas.getBoundingClientRect();
2050
- this.canvas.width = rect.width * dpr;
2051
- this.canvas.height = rect.height * dpr;
2052
- this.ctx.scale(dpr, dpr);
2053
- this.canvas.style.width = `${rect.width}px`;
2054
- this.canvas.style.height = `${rect.height}px`;
2055
- }
2056
- applyPatches(patches) {
2057
- for (const patch of patches) {
2058
- this.applyPatch(patch);
2059
- }
2060
- if (this.rootNode) {
2061
- this.accessibilityLayer.syncTree(this.rootNode);
2062
- }
2063
- this.scheduleRedraw();
2064
- }
2065
- applyPatch(patch) {
2066
- switch (patch.type) {
2067
- case "create":
2068
- this.onCreate(patch.id, patch.elementType, patch.props || {});
2069
- break;
2070
- case "setProp":
2071
- this.onSetProp(patch.id, patch.name, patch.value);
2072
- break;
2073
- case "removeProp":
2074
- this.onRemoveProp(patch.id, patch.name);
2075
- break;
2076
- case "setText":
2077
- this.onSetText(patch.id, patch.text);
2078
- break;
2079
- case "insert":
2080
- this.onInsert(patch.parentId, patch.id, patch.beforeId);
2081
- break;
2082
- case "move":
2083
- this.onMove(patch.parentId, patch.id, patch.beforeId);
2084
- break;
2085
- case "remove":
2086
- this.onRemove(patch.id);
2087
- break;
2088
- }
2089
- }
2090
- onCreate(id, elementType, props) {
2091
- const node = {
2092
- id,
2093
- type: elementType,
2094
- props: props instanceof Map ? Object.fromEntries(props) : props,
2095
- children: [],
2096
- parent: null,
2097
- visible: true,
2098
- opacity: parseFloat(props.opacity) || 1,
2099
- clickable: elementType === "button" || !!props.onclick,
2100
- hoverable: true,
2101
- focusable: elementType === "input" || elementType === "textarea" || elementType === "button",
2102
- focused: false,
2103
- hovered: false
2104
- };
2105
- this.nodes.set(id, node);
2106
- }
2107
- onSetProp(id, name, value) {
2108
- const node = this.nodes.get(id);
2109
- if (!node)
2110
- return;
2111
- node.props[name] = value;
2112
- if (name === "visible") {
2113
- node.visible = !!value;
2114
- }
2115
- if (name === "opacity") {
2116
- node.opacity = parseFloat(value) || 1;
2117
- }
2118
- this.accessibilityLayer.updateNode(node);
2119
- }
2120
- onRemoveProp(id, name) {
2121
- const node = this.nodes.get(id);
2122
- if (!node)
2123
- return;
2124
- delete node.props[name];
2125
- if (name === "visible") {
2126
- node.visible = true;
2127
- }
2128
- if (name === "opacity") {
2129
- node.opacity = 1;
2130
- }
2131
- this.accessibilityLayer.updateNode(node);
2132
- }
2133
- onSetText(id, text) {
2134
- const node = this.nodes.get(id);
2135
- if (!node)
2136
- return;
2137
- node.props[0] = text;
2138
- this.accessibilityLayer.updateNode(node);
2139
- }
2140
- onInsert(parentId, id, beforeId) {
2141
- const child = this.nodes.get(id);
2142
- if (!child)
2143
- return;
2144
- if (parentId === "root" && id === "root") {
2145
- this.rootNode = child;
2146
- this.eventManager.setRootNode(child);
2147
- return;
2148
- }
2149
- const parent = this.nodes.get(parentId);
2150
- if (!parent) {
2151
- if (parentId === "root") {
2152
- this.rootNode = child;
2153
- this.eventManager.setRootNode(child);
2154
- }
2155
- return;
2156
- }
2157
- child.parent = parent;
2158
- if (beforeId) {
2159
- const beforeIndex = parent.children.findIndex((c) => c.id === beforeId);
2160
- if (beforeIndex >= 0) {
2161
- parent.children.splice(beforeIndex, 0, child);
2162
- } else {
2163
- parent.children.push(child);
2164
- }
2165
- } else {
2166
- parent.children.push(child);
2167
- }
2168
- }
2169
- onMove(parentId, id, beforeId) {
2170
- const node = this.nodes.get(id);
2171
- if (!node || !node.parent)
2172
- return;
2173
- const oldParent = node.parent;
2174
- const oldIndex = oldParent.children.indexOf(node);
2175
- if (oldIndex >= 0) {
2176
- oldParent.children.splice(oldIndex, 1);
2177
- }
2178
- this.onInsert(parentId, id, beforeId);
2179
- }
2180
- onRemove(id) {
2181
- const node = this.nodes.get(id);
2182
- if (!node)
2183
- return;
2184
- if (node.parent) {
2185
- const index = node.parent.children.indexOf(node);
2186
- if (index >= 0) {
2187
- node.parent.children.splice(index, 1);
2188
- }
2189
- }
2190
- if (this.rootNode === node) {
2191
- this.rootNode = null;
2192
- this.eventManager.setRootNode(null);
2193
- }
2194
- this.nodes.delete(id);
2195
- }
2196
- scheduleRedraw() {
2197
- if (this.rafId !== null)
2198
- return;
2199
- if (typeof requestAnimationFrame !== "undefined") {
2200
- this.rafId = requestAnimationFrame(() => {
2201
- this.render();
2202
- this.rafId = null;
2203
- });
2204
- } else {
2205
- this.render();
2206
- }
2207
- }
2208
- render() {
2209
- const startTime = performance.now();
2210
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
2211
- if (this.options.backgroundColor) {
2212
- this.ctx.fillStyle = this.options.backgroundColor;
2213
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
2214
220
  }
2215
- if (this.rootNode) {
2216
- const dpr = this.options.devicePixelRatio || 1;
2217
- computeLayout(this.ctx, this.rootNode, this.canvas.width / dpr, this.canvas.height / dpr);
2218
- paintNode(this.ctx, this.rootNode);
2219
- if (this.options.showLayoutBounds) {
2220
- this.drawLayoutBounds(this.rootNode);
2221
- }
221
+ /**
222
+ * Move node in tree
223
+ */
224
+ onMove(parentId, id, beforeId) {
225
+ // Remove from old parent
226
+ const node = this.nodes.get(id);
227
+ if (!node || !node.parent)
228
+ return;
229
+ const oldParent = node.parent;
230
+ const oldIndex = oldParent.children.indexOf(node);
231
+ if (oldIndex >= 0) {
232
+ oldParent.children.splice(oldIndex, 1);
233
+ }
234
+ // Insert into new location
235
+ this.onInsert(parentId, id, beforeId);
236
+ }
237
+ /**
238
+ * Remove node from tree
239
+ */
240
+ onRemove(id) {
241
+ const node = this.nodes.get(id);
242
+ if (!node)
243
+ return;
244
+ // Remove from parent
245
+ if (node.parent) {
246
+ const index = node.parent.children.indexOf(node);
247
+ if (index >= 0) {
248
+ node.parent.children.splice(index, 1);
249
+ }
250
+ }
251
+ // Remove from root
252
+ if (this.rootNode === node) {
253
+ this.rootNode = null;
254
+ this.eventManager.setRootNode(null);
255
+ }
256
+ // Remove from nodes map
257
+ this.nodes.delete(id);
258
+ }
259
+ /**
260
+ * Schedule redraw
261
+ */
262
+ scheduleRedraw() {
263
+ if (this.rafId !== null)
264
+ return;
265
+ // Use requestAnimationFrame if available (browser), otherwise render immediately (tests)
266
+ if (typeof requestAnimationFrame !== "undefined") {
267
+ this.rafId = requestAnimationFrame(() => {
268
+ this.render();
269
+ this.rafId = null;
270
+ });
271
+ }
272
+ else {
273
+ // In non-browser environments (tests), render immediately
274
+ this.render();
275
+ }
2222
276
  }
2223
- if (this.options.logPerformance) {
2224
- const elapsed = performance.now() - startTime;
2225
- this.frameCount++;
2226
- if (performance.now() - this.lastFrameTime > 1000) {
2227
- log2.debug(`Canvas FPS: ${this.frameCount}, Last frame: ${elapsed.toFixed(2)}ms`);
2228
- this.frameCount = 0;
2229
- this.lastFrameTime = performance.now();
2230
- }
277
+ /**
278
+ * Main render function
279
+ */
280
+ render() {
281
+ const startTime = performance.now();
282
+ // Clear canvas
283
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
284
+ // Draw background
285
+ if (this.options.backgroundColor) {
286
+ this.ctx.fillStyle = this.options.backgroundColor;
287
+ this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
288
+ }
289
+ // Layout and paint
290
+ if (this.rootNode) {
291
+ // Compute layout
292
+ const dpr = this.options.devicePixelRatio || 1;
293
+ computeLayout(this.ctx, this.rootNode, this.canvas.width / dpr, this.canvas.height / dpr);
294
+ // Paint
295
+ paintNode(this.ctx, this.rootNode);
296
+ // Debug: show layout bounds
297
+ if (this.options.showLayoutBounds) {
298
+ this.drawLayoutBounds(this.rootNode);
299
+ }
300
+ }
301
+ // Performance logging
302
+ if (this.options.logPerformance) {
303
+ const elapsed = performance.now() - startTime;
304
+ this.frameCount++;
305
+ if (performance.now() - this.lastFrameTime > 1000) {
306
+ log.debug(`Canvas FPS: ${this.frameCount}, Last frame: ${elapsed.toFixed(2)}ms`);
307
+ this.frameCount = 0;
308
+ this.lastFrameTime = performance.now();
309
+ }
310
+ }
2231
311
  }
2232
- }
2233
- drawLayoutBounds(node) {
2234
- if (!node.layout)
2235
- return;
2236
- const layout = node.layout;
2237
- this.ctx.strokeStyle = "#ff0000";
2238
- this.ctx.lineWidth = 1;
2239
- this.ctx.strokeRect(layout.x, layout.y, layout.width, layout.height);
2240
- for (const child of node.children) {
2241
- this.drawLayoutBounds(child);
312
+ /**
313
+ * Draw layout bounds for debugging
314
+ */
315
+ drawLayoutBounds(node) {
316
+ if (!node.layout)
317
+ return;
318
+ const layout = node.layout;
319
+ this.ctx.strokeStyle = "#ff0000";
320
+ this.ctx.lineWidth = 1;
321
+ this.ctx.strokeRect(layout.x, layout.y, layout.width, layout.height);
322
+ for (const child of node.children) {
323
+ this.drawLayoutBounds(child);
324
+ }
2242
325
  }
2243
- }
2244
- getNode(id) {
2245
- return this.nodes.get(id);
2246
- }
2247
- clear() {
2248
- this.rootNode = null;
2249
- this.nodes.clear();
2250
- this.eventManager.setRootNode(null);
2251
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
2252
- }
2253
- registerPainter(type, painter) {
2254
- registerPainter(type, painter);
2255
- }
2256
- setOptions(options) {
2257
- this.options = { ...this.options, ...options };
2258
- this.accessibilityLayer.setEnabled(this.options.enableAccessibility || false);
2259
- }
2260
- destroy() {
2261
- if (this.rafId !== null) {
2262
- cancelAnimationFrame(this.rafId);
326
+ /**
327
+ * Get node by ID
328
+ */
329
+ getNode(id) {
330
+ return this.nodes.get(id);
331
+ }
332
+ /**
333
+ * Clear renderer
334
+ */
335
+ clear() {
336
+ this.rootNode = null;
337
+ this.nodes.clear();
338
+ this.eventManager.setRootNode(null);
339
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
340
+ }
341
+ /**
342
+ * Register custom painter
343
+ */
344
+ registerPainter(type, painter) {
345
+ registerPainter(type, painter);
346
+ }
347
+ /**
348
+ * Set renderer options
349
+ */
350
+ setOptions(options) {
351
+ this.options = { ...this.options, ...options };
352
+ this.accessibilityLayer.setEnabled(this.options.enableAccessibility || false);
353
+ }
354
+ /**
355
+ * Destroy renderer
356
+ */
357
+ destroy() {
358
+ if (this.rafId !== null) {
359
+ cancelAnimationFrame(this.rafId);
360
+ }
361
+ this.eventManager.destroy();
362
+ this.accessibilityLayer.destroy();
2263
363
  }
2264
- this.eventManager.destroy();
2265
- this.accessibilityLayer.destroy();
2266
- }
2267
364
  }
2268
- export {
2269
- CanvasRenderer
2270
- };
2271
-
2272
- //# debugId=F583F289DA278D9964756E2164756E21
365
+ //# sourceMappingURL=renderer.js.map