@hypen-space/web 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2s02mkzs.js +32 -0
- package/dist/chunk-2s02mkzs.js.map +9 -0
- package/dist/src/canvas/accessibility.js +152 -0
- package/dist/src/canvas/accessibility.js.map +10 -0
- package/dist/src/canvas/events.js +198 -0
- package/dist/src/canvas/events.js.map +10 -0
- package/dist/src/canvas/index.js +28 -0
- package/dist/src/canvas/index.js.map +9 -0
- package/dist/src/canvas/input.js +132 -0
- package/dist/src/canvas/input.js.map +10 -0
- package/dist/src/canvas/layout.js +309 -0
- package/dist/src/canvas/layout.js.map +10 -0
- package/dist/src/canvas/paint.js +878 -0
- package/dist/src/canvas/paint.js.map +10 -0
- package/dist/src/canvas/renderer.js +276 -0
- package/dist/src/canvas/renderer.js.map +10 -0
- package/dist/src/canvas/text.js +118 -0
- package/dist/src/canvas/text.js.map +10 -0
- package/dist/src/canvas/types.js +2 -0
- package/dist/src/canvas/types.js.map +9 -0
- package/dist/src/canvas/utils.js +139 -0
- package/dist/src/canvas/utils.js.map +10 -0
- package/dist/src/dom/applicators/advanced-layout.js +111 -0
- package/dist/src/dom/applicators/advanced-layout.js.map +10 -0
- package/dist/src/dom/applicators/background.js +54 -0
- package/dist/src/dom/applicators/background.js.map +10 -0
- package/dist/src/dom/applicators/border.js +33 -0
- package/dist/src/dom/applicators/border.js.map +10 -0
- package/dist/src/dom/applicators/color.js +36 -0
- package/dist/src/dom/applicators/color.js.map +10 -0
- package/dist/src/dom/applicators/display.js +57 -0
- package/dist/src/dom/applicators/display.js.map +10 -0
- package/dist/src/dom/applicators/effects.js +89 -0
- package/dist/src/dom/applicators/effects.js.map +10 -0
- package/dist/src/dom/applicators/events.js +518 -0
- package/dist/src/dom/applicators/events.js.map +10 -0
- package/dist/src/dom/applicators/font.js +39 -0
- package/dist/src/dom/applicators/font.js.map +10 -0
- package/dist/src/dom/applicators/index.js +296 -0
- package/dist/src/dom/applicators/index.js.map +10 -0
- package/dist/src/dom/applicators/layout.js +86 -0
- package/dist/src/dom/applicators/layout.js.map +10 -0
- package/dist/src/dom/applicators/margin.js +32 -0
- package/dist/src/dom/applicators/margin.js.map +10 -0
- package/dist/src/dom/applicators/padding.js +35 -0
- package/dist/src/dom/applicators/padding.js.map +10 -0
- package/dist/src/dom/applicators/size.js +42 -0
- package/dist/src/dom/applicators/size.js.map +10 -0
- package/dist/src/dom/applicators/transform.js +92 -0
- package/dist/src/dom/applicators/transform.js.map +10 -0
- package/dist/src/dom/applicators/transition.js +66 -0
- package/dist/src/dom/applicators/transition.js.map +10 -0
- package/dist/src/dom/applicators/typography.js +87 -0
- package/dist/src/dom/applicators/typography.js.map +10 -0
- package/dist/src/dom/canvas/index.js +50 -0
- package/dist/src/dom/canvas/index.js.map +10 -0
- package/dist/src/dom/components/audio.js +48 -0
- package/dist/src/dom/components/audio.js.map +10 -0
- package/dist/src/dom/components/avatar.js +58 -0
- package/dist/src/dom/components/avatar.js.map +10 -0
- package/dist/src/dom/components/badge.js +55 -0
- package/dist/src/dom/components/badge.js.map +10 -0
- package/dist/src/dom/components/button.js +29 -0
- package/dist/src/dom/components/button.js.map +10 -0
- package/dist/src/dom/components/card.js +33 -0
- package/dist/src/dom/components/card.js.map +10 -0
- package/dist/src/dom/components/center.js +32 -0
- package/dist/src/dom/components/center.js.map +10 -0
- package/dist/src/dom/components/checkbox.js +54 -0
- package/dist/src/dom/components/checkbox.js.map +10 -0
- package/dist/src/dom/components/column.js +31 -0
- package/dist/src/dom/components/column.js.map +10 -0
- package/dist/src/dom/components/container.js +29 -0
- package/dist/src/dom/components/container.js.map +10 -0
- package/dist/src/dom/components/divider.js +45 -0
- package/dist/src/dom/components/divider.js.map +10 -0
- package/dist/src/dom/components/grid.js +44 -0
- package/dist/src/dom/components/grid.js.map +10 -0
- package/dist/src/dom/components/heading.js +47 -0
- package/dist/src/dom/components/heading.js.map +10 -0
- package/dist/src/dom/components/image.js +39 -0
- package/dist/src/dom/components/image.js.map +10 -0
- package/dist/src/dom/components/index.js +217 -0
- package/dist/src/dom/components/index.js.map +10 -0
- package/dist/src/dom/components/input.js +41 -0
- package/dist/src/dom/components/input.js.map +10 -0
- package/dist/src/dom/components/link.js +42 -0
- package/dist/src/dom/components/link.js.map +10 -0
- package/dist/src/dom/components/list.js +42 -0
- package/dist/src/dom/components/list.js.map +10 -0
- package/dist/src/dom/components/paragraph.js +35 -0
- package/dist/src/dom/components/paragraph.js.map +10 -0
- package/dist/src/dom/components/progressbar.js +57 -0
- package/dist/src/dom/components/progressbar.js.map +10 -0
- package/dist/src/dom/components/route.js +44 -0
- package/dist/src/dom/components/route.js.map +10 -0
- package/dist/src/dom/components/router.js +33 -0
- package/dist/src/dom/components/router.js.map +10 -0
- package/dist/src/dom/components/row.js +31 -0
- package/dist/src/dom/components/row.js.map +10 -0
- package/dist/src/dom/components/select.js +57 -0
- package/dist/src/dom/components/select.js.map +10 -0
- package/dist/src/dom/components/slider.js +48 -0
- package/dist/src/dom/components/slider.js.map +10 -0
- package/dist/src/dom/components/spacer.js +30 -0
- package/dist/src/dom/components/spacer.js.map +10 -0
- package/dist/src/dom/components/spinner.js +65 -0
- package/dist/src/dom/components/spinner.js.map +10 -0
- package/dist/src/dom/components/stack.js +45 -0
- package/dist/src/dom/components/stack.js.map +10 -0
- package/dist/src/dom/components/switch.js +83 -0
- package/dist/src/dom/components/switch.js.map +10 -0
- package/dist/src/dom/components/text.js +37 -0
- package/dist/src/dom/components/text.js.map +10 -0
- package/dist/src/dom/components/textarea.js +51 -0
- package/dist/src/dom/components/textarea.js.map +10 -0
- package/dist/src/dom/components/video.js +51 -0
- package/dist/src/dom/components/video.js.map +10 -0
- package/dist/src/dom/debug.js +170 -0
- package/dist/src/dom/debug.js.map +10 -0
- package/dist/src/dom/events.js +112 -0
- package/dist/src/dom/events.js.map +10 -0
- package/dist/src/dom/index.js +73 -0
- package/dist/src/dom/index.js.map +9 -0
- package/dist/src/dom/renderer.js +277 -0
- package/dist/src/dom/renderer.js.map +10 -0
- package/dist/src/index.js +89 -0
- package/dist/src/index.js.map +9 -0
- package/package.json +84 -0
- package/src/canvas/QUICKSTART.md +421 -0
- package/src/canvas/README.md +376 -0
- package/src/canvas/accessibility.ts +218 -0
- package/src/canvas/events.ts +307 -0
- package/src/canvas/index.ts +35 -0
- package/src/canvas/input.ts +210 -0
- package/src/canvas/layout.ts +401 -0
- package/src/canvas/paint.ts +1321 -0
- package/src/canvas/renderer.ts +422 -0
- package/src/canvas/text.ts +182 -0
- package/src/canvas/types.ts +137 -0
- package/src/canvas/utils.ts +218 -0
- package/src/dom/README.md +265 -0
- package/src/dom/applicators/advanced-layout.ts +128 -0
- package/src/dom/applicators/background.ts +50 -0
- package/src/dom/applicators/border.ts +19 -0
- package/src/dom/applicators/color.ts +23 -0
- package/src/dom/applicators/display.ts +54 -0
- package/src/dom/applicators/effects.ts +97 -0
- package/src/dom/applicators/events.ts +689 -0
- package/src/dom/applicators/font.ts +27 -0
- package/src/dom/applicators/index.ts +354 -0
- package/src/dom/applicators/layout.ts +92 -0
- package/src/dom/applicators/margin.ts +18 -0
- package/src/dom/applicators/padding.ts +18 -0
- package/src/dom/applicators/size.ts +31 -0
- package/src/dom/applicators/transform.ts +93 -0
- package/src/dom/applicators/transition.ts +65 -0
- package/src/dom/applicators/typography.ts +91 -0
- package/src/dom/canvas/index.ts +60 -0
- package/src/dom/components/audio.ts +45 -0
- package/src/dom/components/avatar.ts +49 -0
- package/src/dom/components/badge.ts +45 -0
- package/src/dom/components/button.ts +13 -0
- package/src/dom/components/card.ts +19 -0
- package/src/dom/components/center.ts +16 -0
- package/src/dom/components/checkbox.ts +54 -0
- package/src/dom/components/column.ts +15 -0
- package/src/dom/components/container.ts +13 -0
- package/src/dom/components/divider.ts +37 -0
- package/src/dom/components/grid.ts +40 -0
- package/src/dom/components/heading.ts +41 -0
- package/src/dom/components/image.ts +27 -0
- package/src/dom/components/index.ts +115 -0
- package/src/dom/components/input.ts +29 -0
- package/src/dom/components/link.ts +35 -0
- package/src/dom/components/list.ts +30 -0
- package/src/dom/components/paragraph.ts +23 -0
- package/src/dom/components/progressbar.ts +51 -0
- package/src/dom/components/route.ts +37 -0
- package/src/dom/components/router.ts +22 -0
- package/src/dom/components/row.ts +15 -0
- package/src/dom/components/select.ts +56 -0
- package/src/dom/components/slider.ts +45 -0
- package/src/dom/components/spacer.ts +16 -0
- package/src/dom/components/spinner.ts +60 -0
- package/src/dom/components/stack.ts +34 -0
- package/src/dom/components/switch.ts +86 -0
- package/src/dom/components/text.ts +24 -0
- package/src/dom/components/textarea.ts +50 -0
- package/src/dom/components/video.ts +50 -0
- package/src/dom/debug.ts +247 -0
- package/src/dom/events.ts +168 -0
- package/src/dom/index.ts +11 -0
- package/src/dom/renderer.ts +327 -0
- package/src/index.ts +56 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event System
|
|
3
|
+
*
|
|
4
|
+
* Hit testing and event handling for canvas nodes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VirtualNode, Point } from "./types.js";
|
|
8
|
+
import { isPointInRoundedRect, getAbsoluteBounds } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
// Interface for the engine that CanvasEventManager needs
|
|
11
|
+
interface IEngine {
|
|
12
|
+
dispatchAction(name: string, payload?: any): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Canvas Event Manager
|
|
17
|
+
*/
|
|
18
|
+
export class CanvasEventManager {
|
|
19
|
+
private canvas: HTMLCanvasElement;
|
|
20
|
+
private engine: IEngine;
|
|
21
|
+
private rootNode: VirtualNode | null = null;
|
|
22
|
+
private hoveredNode: VirtualNode | null = null;
|
|
23
|
+
private focusedNode: VirtualNode | null = null;
|
|
24
|
+
private mouseDownNode: VirtualNode | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(canvas: HTMLCanvasElement, engine: IEngine) {
|
|
27
|
+
this.canvas = canvas;
|
|
28
|
+
this.engine = engine;
|
|
29
|
+
this.setupEventListeners();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Set the root node for hit testing
|
|
34
|
+
*/
|
|
35
|
+
setRootNode(node: VirtualNode | null): void {
|
|
36
|
+
this.rootNode = node;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Setup canvas event listeners
|
|
41
|
+
*/
|
|
42
|
+
private setupEventListeners(): void {
|
|
43
|
+
this.canvas.addEventListener("mousemove", this.onMouseMove.bind(this));
|
|
44
|
+
this.canvas.addEventListener("mousedown", this.onMouseDown.bind(this));
|
|
45
|
+
this.canvas.addEventListener("mouseup", this.onMouseUp.bind(this));
|
|
46
|
+
this.canvas.addEventListener("click", this.onClick.bind(this));
|
|
47
|
+
this.canvas.addEventListener("dblclick", this.onDoubleClick.bind(this));
|
|
48
|
+
this.canvas.addEventListener("keydown", this.onKeyDown.bind(this));
|
|
49
|
+
this.canvas.addEventListener("keyup", this.onKeyUp.bind(this));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get canvas coordinates from mouse event
|
|
54
|
+
*/
|
|
55
|
+
private getCanvasCoordinates(e: MouseEvent): Point {
|
|
56
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
57
|
+
const scaleX = this.canvas.width / rect.width;
|
|
58
|
+
const scaleY = this.canvas.height / rect.height;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
x: (e.clientX - rect.left) * scaleX,
|
|
62
|
+
y: (e.clientY - rect.top) * scaleY,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Find node at canvas coordinates
|
|
68
|
+
*/
|
|
69
|
+
private hitTest(point: Point): VirtualNode | null {
|
|
70
|
+
if (!this.rootNode) return null;
|
|
71
|
+
return this.hitTestNode(this.rootNode, point);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Recursively test node and children
|
|
76
|
+
*/
|
|
77
|
+
private hitTestNode(node: VirtualNode, point: Point): VirtualNode | null {
|
|
78
|
+
if (!node.visible || !node.layout) return null;
|
|
79
|
+
|
|
80
|
+
const bounds = getAbsoluteBounds(node);
|
|
81
|
+
if (!bounds) return null;
|
|
82
|
+
|
|
83
|
+
// Test children first (front to back)
|
|
84
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
85
|
+
const child = node.children[i];
|
|
86
|
+
const hit = this.hitTestNode(child, point);
|
|
87
|
+
if (hit) return hit;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Test this node
|
|
91
|
+
const radius = node.layout.border.radius;
|
|
92
|
+
if (isPointInRoundedRect(point, bounds, radius)) {
|
|
93
|
+
return node;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Handle mouse move
|
|
101
|
+
*/
|
|
102
|
+
private onMouseMove(e: MouseEvent): void {
|
|
103
|
+
const point = this.getCanvasCoordinates(e);
|
|
104
|
+
const node = this.hitTest(point);
|
|
105
|
+
|
|
106
|
+
// Update hover state
|
|
107
|
+
if (node !== this.hoveredNode) {
|
|
108
|
+
// Leave old node
|
|
109
|
+
if (this.hoveredNode) {
|
|
110
|
+
this.hoveredNode.hovered = false;
|
|
111
|
+
this.dispatchNodeEvent(this.hoveredNode, "mouseleave", {});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Enter new node
|
|
115
|
+
this.hoveredNode = node;
|
|
116
|
+
if (node) {
|
|
117
|
+
node.hovered = true;
|
|
118
|
+
this.dispatchNodeEvent(node, "mouseenter", {});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Update cursor
|
|
122
|
+
this.updateCursor(node);
|
|
123
|
+
|
|
124
|
+
// Request redraw for hover effects
|
|
125
|
+
this.requestRedraw();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Handle mouse down
|
|
131
|
+
*/
|
|
132
|
+
private onMouseDown(e: MouseEvent): void {
|
|
133
|
+
const point = this.getCanvasCoordinates(e);
|
|
134
|
+
const node = this.hitTest(point);
|
|
135
|
+
|
|
136
|
+
this.mouseDownNode = node;
|
|
137
|
+
|
|
138
|
+
if (node && node.clickable) {
|
|
139
|
+
this.dispatchNodeEvent(node, "mousedown", {
|
|
140
|
+
button: e.button,
|
|
141
|
+
clientX: e.clientX,
|
|
142
|
+
clientY: e.clientY,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Update focus
|
|
147
|
+
if (node && node.focusable) {
|
|
148
|
+
this.setFocus(node);
|
|
149
|
+
} else {
|
|
150
|
+
this.setFocus(null);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Handle mouse up
|
|
156
|
+
*/
|
|
157
|
+
private onMouseUp(e: MouseEvent): void {
|
|
158
|
+
const point = this.getCanvasCoordinates(e);
|
|
159
|
+
const node = this.hitTest(point);
|
|
160
|
+
|
|
161
|
+
if (node && node.clickable) {
|
|
162
|
+
this.dispatchNodeEvent(node, "mouseup", {
|
|
163
|
+
button: e.button,
|
|
164
|
+
clientX: e.clientX,
|
|
165
|
+
clientY: e.clientY,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.mouseDownNode = null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Handle click
|
|
174
|
+
*/
|
|
175
|
+
private onClick(e: MouseEvent): void {
|
|
176
|
+
const point = this.getCanvasCoordinates(e);
|
|
177
|
+
const node = this.hitTest(point);
|
|
178
|
+
|
|
179
|
+
if (node && node.clickable && node === this.mouseDownNode) {
|
|
180
|
+
this.dispatchNodeEvent(node, "click", {
|
|
181
|
+
button: e.button,
|
|
182
|
+
clientX: e.clientX,
|
|
183
|
+
clientY: e.clientY,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Handle double click
|
|
190
|
+
*/
|
|
191
|
+
private onDoubleClick(e: MouseEvent): void {
|
|
192
|
+
const point = this.getCanvasCoordinates(e);
|
|
193
|
+
const node = this.hitTest(point);
|
|
194
|
+
|
|
195
|
+
if (node && node.clickable) {
|
|
196
|
+
this.dispatchNodeEvent(node, "dblclick", {
|
|
197
|
+
button: e.button,
|
|
198
|
+
clientX: e.clientX,
|
|
199
|
+
clientY: e.clientY,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handle keyboard events
|
|
206
|
+
*/
|
|
207
|
+
private onKeyDown(e: KeyboardEvent): void {
|
|
208
|
+
if (this.focusedNode) {
|
|
209
|
+
this.dispatchNodeEvent(this.focusedNode, "keydown", {
|
|
210
|
+
key: e.key,
|
|
211
|
+
code: e.code,
|
|
212
|
+
ctrlKey: e.ctrlKey,
|
|
213
|
+
shiftKey: e.shiftKey,
|
|
214
|
+
altKey: e.altKey,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private onKeyUp(e: KeyboardEvent): void {
|
|
220
|
+
if (this.focusedNode) {
|
|
221
|
+
this.dispatchNodeEvent(this.focusedNode, "keyup", {
|
|
222
|
+
key: e.key,
|
|
223
|
+
code: e.code,
|
|
224
|
+
ctrlKey: e.ctrlKey,
|
|
225
|
+
shiftKey: e.shiftKey,
|
|
226
|
+
altKey: e.altKey,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Set focused node
|
|
233
|
+
*/
|
|
234
|
+
private setFocus(node: VirtualNode | null): void {
|
|
235
|
+
if (node === this.focusedNode) return;
|
|
236
|
+
|
|
237
|
+
if (this.focusedNode) {
|
|
238
|
+
this.focusedNode.focused = false;
|
|
239
|
+
this.dispatchNodeEvent(this.focusedNode, "blur", {});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.focusedNode = node;
|
|
243
|
+
|
|
244
|
+
if (node) {
|
|
245
|
+
node.focused = true;
|
|
246
|
+
this.dispatchNodeEvent(node, "focus", {});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.requestRedraw();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Update cursor based on node
|
|
254
|
+
*/
|
|
255
|
+
private updateCursor(node: VirtualNode | null): void {
|
|
256
|
+
if (!node) {
|
|
257
|
+
this.canvas.style.cursor = "default";
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const cursor = node.props.cursor || (node.clickable ? "pointer" : "default");
|
|
262
|
+
this.canvas.style.cursor = cursor;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Dispatch event to engine
|
|
267
|
+
*/
|
|
268
|
+
private dispatchNodeEvent(node: VirtualNode, eventType: string, data: any): void {
|
|
269
|
+
// Check if node has action handler for this event
|
|
270
|
+
const actionName = node.props[`on${eventType}`] || node.props[eventType];
|
|
271
|
+
|
|
272
|
+
if (actionName && typeof actionName === "string") {
|
|
273
|
+
// Dispatch to engine
|
|
274
|
+
this.engine.dispatchAction(actionName, {
|
|
275
|
+
type: eventType,
|
|
276
|
+
nodeId: node.id,
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
...data,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Request redraw from renderer
|
|
285
|
+
*/
|
|
286
|
+
private requestRedraw(): void {
|
|
287
|
+
// This will be called via a callback set by the renderer
|
|
288
|
+
// For now, dispatch a custom event
|
|
289
|
+
this.canvas.dispatchEvent(new CustomEvent("hypen:redraw"));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Cleanup
|
|
294
|
+
*/
|
|
295
|
+
destroy(): void {
|
|
296
|
+
// Remove event listeners if needed
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Renderer for Hypen
|
|
3
|
+
*
|
|
4
|
+
* Browser-only module for rendering Hypen UI to Canvas
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { CanvasRenderer } from "./renderer.js";
|
|
8
|
+
export { registerPainter } from "./paint.js";
|
|
9
|
+
export { CanvasEventManager } from "./events.js";
|
|
10
|
+
export { InputOverlay } from "./input.js";
|
|
11
|
+
export { AccessibilityLayer } from "./accessibility.js";
|
|
12
|
+
|
|
13
|
+
export type {
|
|
14
|
+
VirtualNode,
|
|
15
|
+
Layout,
|
|
16
|
+
Rectangle,
|
|
17
|
+
Point,
|
|
18
|
+
FontStyle,
|
|
19
|
+
TextStyle,
|
|
20
|
+
TextMetrics,
|
|
21
|
+
CanvasRendererOptions,
|
|
22
|
+
PainterFunction,
|
|
23
|
+
LayoutFunction,
|
|
24
|
+
} from "./types.js";
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Overlay System
|
|
3
|
+
*
|
|
4
|
+
* Handle text input using DOM overlay elements
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { VirtualNode, Rectangle } from "./types.js";
|
|
8
|
+
import { getAbsoluteBounds } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Input Overlay Manager
|
|
12
|
+
*/
|
|
13
|
+
export class InputOverlay {
|
|
14
|
+
private container: HTMLElement;
|
|
15
|
+
private overlay: HTMLInputElement | HTMLTextAreaElement | null = null;
|
|
16
|
+
private focusedNode: VirtualNode | null = null;
|
|
17
|
+
private onChangeCallback: ((value: string) => void) | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(container: HTMLElement | null) {
|
|
20
|
+
this.container = container || ({} as HTMLElement);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Show input overlay for a node
|
|
25
|
+
*/
|
|
26
|
+
showInput(
|
|
27
|
+
node: VirtualNode,
|
|
28
|
+
canvasBounds: DOMRect,
|
|
29
|
+
onChange: (value: string) => void
|
|
30
|
+
): void {
|
|
31
|
+
// Skip if document is not available (non-browser environment)
|
|
32
|
+
if (typeof document === "undefined") return;
|
|
33
|
+
|
|
34
|
+
this.hideInput();
|
|
35
|
+
|
|
36
|
+
const bounds = getAbsoluteBounds(node);
|
|
37
|
+
if (!bounds) return;
|
|
38
|
+
|
|
39
|
+
const isMultiline = node.type === "textarea";
|
|
40
|
+
this.overlay = isMultiline
|
|
41
|
+
? document.createElement("textarea")
|
|
42
|
+
: document.createElement("input");
|
|
43
|
+
|
|
44
|
+
// Style overlay
|
|
45
|
+
this.styleOverlay(node, bounds, canvasBounds);
|
|
46
|
+
|
|
47
|
+
// Set initial value
|
|
48
|
+
const value = node.props.value || "";
|
|
49
|
+
this.overlay.value = value;
|
|
50
|
+
|
|
51
|
+
// Setup event handlers
|
|
52
|
+
this.onChangeCallback = onChange;
|
|
53
|
+
this.overlay.addEventListener("input", this.onInput.bind(this));
|
|
54
|
+
this.overlay.addEventListener("blur", this.onBlur.bind(this));
|
|
55
|
+
this.overlay.addEventListener("keydown", this.onKeyDown.bind(this) as EventListener);
|
|
56
|
+
|
|
57
|
+
// Add to DOM and focus
|
|
58
|
+
this.container.appendChild(this.overlay);
|
|
59
|
+
this.overlay.focus();
|
|
60
|
+
|
|
61
|
+
this.focusedNode = node;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hide input overlay
|
|
66
|
+
*/
|
|
67
|
+
hideInput(): void {
|
|
68
|
+
if (this.overlay) {
|
|
69
|
+
this.overlay.remove();
|
|
70
|
+
this.overlay = null;
|
|
71
|
+
}
|
|
72
|
+
this.focusedNode = null;
|
|
73
|
+
this.onChangeCallback = null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Update overlay position
|
|
78
|
+
*/
|
|
79
|
+
updatePosition(node: VirtualNode, canvasBounds: DOMRect): void {
|
|
80
|
+
if (!this.overlay || node !== this.focusedNode) return;
|
|
81
|
+
|
|
82
|
+
const bounds = getAbsoluteBounds(node);
|
|
83
|
+
if (!bounds) return;
|
|
84
|
+
|
|
85
|
+
this.positionOverlay(bounds, canvasBounds);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Style overlay to match canvas node
|
|
90
|
+
*/
|
|
91
|
+
private styleOverlay(
|
|
92
|
+
node: VirtualNode,
|
|
93
|
+
bounds: Rectangle,
|
|
94
|
+
canvasBounds: DOMRect
|
|
95
|
+
): void {
|
|
96
|
+
if (!this.overlay) return;
|
|
97
|
+
|
|
98
|
+
const props = node.props;
|
|
99
|
+
|
|
100
|
+
// Position
|
|
101
|
+
this.positionOverlay(bounds, canvasBounds);
|
|
102
|
+
|
|
103
|
+
// Font and text styling
|
|
104
|
+
const fontSize = parseFloat(props.fontSize) || 16;
|
|
105
|
+
const fontWeight = props.fontWeight || "normal";
|
|
106
|
+
const fontFamily = props.fontFamily || "system-ui, sans-serif";
|
|
107
|
+
const color = props.color || "#000000";
|
|
108
|
+
|
|
109
|
+
Object.assign(this.overlay.style, {
|
|
110
|
+
fontSize: `${fontSize}px`,
|
|
111
|
+
fontWeight: fontWeight,
|
|
112
|
+
fontFamily: fontFamily,
|
|
113
|
+
color: color,
|
|
114
|
+
border: "none",
|
|
115
|
+
outline: "2px solid #007bff",
|
|
116
|
+
backgroundColor: props.backgroundColor || "#ffffff",
|
|
117
|
+
padding: `${props.padding || 8}px`,
|
|
118
|
+
borderRadius: `${props.borderRadius || 4}px`,
|
|
119
|
+
boxSizing: "border-box",
|
|
120
|
+
resize: "none",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Placeholder
|
|
124
|
+
if (props.placeholder) {
|
|
125
|
+
this.overlay.placeholder = props.placeholder;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Input type
|
|
129
|
+
if (this.overlay instanceof HTMLInputElement && props.type) {
|
|
130
|
+
this.overlay.type = props.type;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Position overlay over canvas node
|
|
136
|
+
*/
|
|
137
|
+
private positionOverlay(bounds: Rectangle, canvasBounds: DOMRect): void {
|
|
138
|
+
if (!this.overlay) return;
|
|
139
|
+
|
|
140
|
+
// Calculate scale factor between canvas and display
|
|
141
|
+
const canvas = this.container.querySelector("canvas") as HTMLCanvasElement;
|
|
142
|
+
const scaleX = canvasBounds.width / canvas.width;
|
|
143
|
+
const scaleY = canvasBounds.height / canvas.height;
|
|
144
|
+
|
|
145
|
+
const left = bounds.x * scaleX;
|
|
146
|
+
const top = bounds.y * scaleY;
|
|
147
|
+
const width = bounds.width * scaleX;
|
|
148
|
+
const height = bounds.height * scaleY;
|
|
149
|
+
|
|
150
|
+
Object.assign(this.overlay.style, {
|
|
151
|
+
position: "absolute",
|
|
152
|
+
left: `${left}px`,
|
|
153
|
+
top: `${top}px`,
|
|
154
|
+
width: `${width}px`,
|
|
155
|
+
height: `${height}px`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle input event
|
|
161
|
+
*/
|
|
162
|
+
private onInput(e: Event): void {
|
|
163
|
+
if (!this.overlay || !this.onChangeCallback) return;
|
|
164
|
+
|
|
165
|
+
const value = this.overlay.value;
|
|
166
|
+
this.onChangeCallback(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle blur event
|
|
171
|
+
*/
|
|
172
|
+
private onBlur(): void {
|
|
173
|
+
// Hide overlay when focus is lost
|
|
174
|
+
this.hideInput();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle keyboard events
|
|
179
|
+
*/
|
|
180
|
+
private onKeyDown(e: KeyboardEvent): void {
|
|
181
|
+
if (!this.overlay) return;
|
|
182
|
+
|
|
183
|
+
// Enter key submits (unless multiline)
|
|
184
|
+
if (e.key === "Enter" && this.overlay instanceof HTMLInputElement) {
|
|
185
|
+
e.preventDefault();
|
|
186
|
+
this.overlay.blur();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Escape cancels
|
|
190
|
+
if (e.key === "Escape") {
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
this.overlay.blur();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if input is currently shown
|
|
198
|
+
*/
|
|
199
|
+
isShown(): boolean {
|
|
200
|
+
return this.overlay !== null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get current focused node
|
|
205
|
+
*/
|
|
206
|
+
getFocusedNode(): VirtualNode | null {
|
|
207
|
+
return this.focusedNode;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|