@hypen-space/web 0.4.980 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/dist/canvas/accessibility.d.ts +118 -11
- package/dist/canvas/accessibility.js +470 -108
- package/dist/canvas/accessibility.js.map +1 -1
- package/dist/canvas/dispatch.d.ts +24 -0
- package/dist/canvas/dispatch.js +62 -0
- package/dist/canvas/dispatch.js.map +1 -0
- package/dist/canvas/editing.d.ts +178 -0
- package/dist/canvas/editing.js +590 -0
- package/dist/canvas/editing.js.map +1 -0
- package/dist/canvas/events.d.ts +28 -23
- package/dist/canvas/events.js +90 -103
- package/dist/canvas/events.js.map +1 -1
- package/dist/canvas/focus.d.ts +78 -0
- package/dist/canvas/focus.js +182 -0
- package/dist/canvas/focus.js.map +1 -0
- package/dist/canvas/index.d.ts +3 -2
- package/dist/canvas/index.js +3 -2
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/layout.js +68 -98
- package/dist/canvas/layout.js.map +1 -1
- package/dist/canvas/paint.d.ts +16 -3
- package/dist/canvas/paint.js +449 -110
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +28 -4
- package/dist/canvas/renderer.js +201 -81
- package/dist/canvas/renderer.js.map +1 -1
- package/dist/canvas/selection.js +31 -177
- package/dist/canvas/selection.js.map +1 -1
- package/dist/canvas/text-geometry.d.ts +72 -0
- package/dist/canvas/text-geometry.js +244 -0
- package/dist/canvas/text-geometry.js.map +1 -0
- package/dist/canvas/text.js +44 -18
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +10 -1
- package/dist/canvas/utils.d.ts +0 -23
- package/dist/canvas/utils.js +3 -67
- package/dist/canvas/utils.js.map +1 -1
- package/dist/canvas/variants.d.ts +52 -0
- package/dist/canvas/variants.js +156 -0
- package/dist/canvas/variants.js.map +1 -0
- package/dist/dom/a11y-styles.d.ts +20 -0
- package/dist/dom/a11y-styles.js +61 -0
- package/dist/dom/a11y-styles.js.map +1 -0
- package/dist/dom/applicators/aria.d.ts +19 -0
- package/dist/dom/applicators/aria.js +36 -0
- package/dist/dom/applicators/aria.js.map +1 -0
- package/dist/dom/applicators/events.d.ts +7 -0
- package/dist/dom/applicators/events.js +36 -8
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/font.d.ts +11 -6
- package/dist/dom/applicators/font.js +16 -27
- package/dist/dom/applicators/font.js.map +1 -1
- package/dist/dom/applicators/index.js +51 -45
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/canvas/index.js +2 -2
- package/dist/dom/canvas/index.js.map +1 -1
- package/dist/dom/components/app.js +2 -2
- package/dist/dom/components/app.js.map +1 -1
- package/dist/dom/components/audio.js +2 -2
- package/dist/dom/components/audio.js.map +1 -1
- package/dist/dom/components/avatar.js +3 -4
- package/dist/dom/components/avatar.js.map +1 -1
- package/dist/dom/components/badge.js +3 -4
- package/dist/dom/components/badge.js.map +1 -1
- package/dist/dom/components/button.js +2 -2
- package/dist/dom/components/button.js.map +1 -1
- package/dist/dom/components/card.js +2 -2
- package/dist/dom/components/card.js.map +1 -1
- package/dist/dom/components/center.js +2 -2
- package/dist/dom/components/center.js.map +1 -1
- package/dist/dom/components/checkbox.js +4 -5
- package/dist/dom/components/checkbox.js.map +1 -1
- package/dist/dom/components/column.js +2 -2
- package/dist/dom/components/column.js.map +1 -1
- package/dist/dom/components/container.js +2 -2
- package/dist/dom/components/container.js.map +1 -1
- package/dist/dom/components/divider.js +2 -2
- package/dist/dom/components/divider.js.map +1 -1
- package/dist/dom/components/grid.js +10 -10
- package/dist/dom/components/grid.js.map +1 -1
- package/dist/dom/components/heading.js +4 -6
- package/dist/dom/components/heading.js.map +1 -1
- package/dist/dom/components/hypenapp.d.ts +25 -1
- package/dist/dom/components/hypenapp.js +215 -247
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/icon.js +4 -5
- package/dist/dom/components/icon.js.map +1 -1
- package/dist/dom/components/image.js +2 -2
- package/dist/dom/components/image.js.map +1 -1
- package/dist/dom/components/index.d.ts +2 -2
- package/dist/dom/components/index.js +9 -2
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/input.js +2 -2
- package/dist/dom/components/input.js.map +1 -1
- package/dist/dom/components/link.js +2 -2
- package/dist/dom/components/link.js.map +1 -1
- package/dist/dom/components/list.js +2 -2
- package/dist/dom/components/list.js.map +1 -1
- package/dist/dom/components/paragraph.js +3 -4
- package/dist/dom/components/paragraph.js.map +1 -1
- package/dist/dom/components/progressbar.js +3 -3
- package/dist/dom/components/progressbar.js.map +1 -1
- package/dist/dom/components/route.js +2 -2
- package/dist/dom/components/route.js.map +1 -1
- package/dist/dom/components/router.js +2 -2
- package/dist/dom/components/router.js.map +1 -1
- package/dist/dom/components/row.js +10 -10
- package/dist/dom/components/row.js.map +1 -1
- package/dist/dom/components/select.js +3 -4
- package/dist/dom/components/select.js.map +1 -1
- package/dist/dom/components/slider.js +2 -2
- package/dist/dom/components/slider.js.map +1 -1
- package/dist/dom/components/spacer.js +2 -2
- package/dist/dom/components/spacer.js.map +1 -1
- package/dist/dom/components/spinner.js +27 -13
- package/dist/dom/components/spinner.js.map +1 -1
- package/dist/dom/components/stack.js +10 -10
- package/dist/dom/components/stack.js.map +1 -1
- package/dist/dom/components/switch.js +5 -6
- package/dist/dom/components/switch.js.map +1 -1
- package/dist/dom/components/tabs.d.ts +27 -0
- package/dist/dom/components/tabs.js +69 -0
- package/dist/dom/components/tabs.js.map +1 -0
- package/dist/dom/components/text.js +4 -7
- package/dist/dom/components/text.js.map +1 -1
- package/dist/dom/components/textarea.js +2 -2
- package/dist/dom/components/textarea.js.map +1 -1
- package/dist/dom/components/video.js +2 -2
- package/dist/dom/components/video.js.map +1 -1
- package/dist/dom/components/visuallyhidden.d.ts +9 -0
- package/dist/dom/components/visuallyhidden.js +26 -0
- package/dist/dom/components/visuallyhidden.js.map +1 -0
- package/dist/dom/debug.js +6 -8
- package/dist/dom/debug.js.map +1 -1
- package/dist/dom/index.d.ts +0 -1
- package/dist/dom/index.js +0 -1
- package/dist/dom/index.js.map +1 -1
- package/dist/dom/operability.d.ts +100 -0
- package/dist/dom/operability.js +298 -0
- package/dist/dom/operability.js.map +1 -0
- package/dist/dom/renderer.d.ts +98 -5
- package/dist/dom/renderer.js +400 -66
- package/dist/dom/renderer.js.map +1 -1
- package/dist/dom/route-focus.d.ts +42 -0
- package/dist/dom/route-focus.js +88 -0
- package/dist/dom/route-focus.js.map +1 -0
- package/dist/dom/semantics.d.ts +35 -0
- package/dist/dom/semantics.js +184 -0
- package/dist/dom/semantics.js.map +1 -0
- package/dist/variants.d.ts +90 -0
- package/dist/variants.js +206 -0
- package/dist/variants.js.map +1 -0
- package/package.json +2 -2
- package/src/canvas/QUICKSTART.md +16 -11
- package/src/canvas/README.md +63 -34
- package/src/canvas/accessibility.ts +507 -115
- package/src/canvas/dispatch.ts +78 -0
- package/src/canvas/editing.ts +697 -0
- package/src/canvas/events.ts +104 -115
- package/src/canvas/focus.ts +216 -0
- package/src/canvas/index.ts +8 -9
- package/src/canvas/layout.ts +68 -103
- package/src/canvas/paint.ts +509 -109
- package/src/canvas/renderer.ts +239 -99
- package/src/canvas/selection.ts +52 -210
- package/src/canvas/text-geometry.ts +311 -0
- package/src/canvas/text.ts +45 -18
- package/src/canvas/types.ts +28 -1
- package/src/canvas/utils.ts +3 -69
- package/src/canvas/variants.ts +172 -0
- package/src/dom/README.md +8 -7
- package/src/dom/a11y-styles.ts +65 -0
- package/src/dom/applicators/aria.ts +41 -0
- package/src/dom/applicators/events.ts +39 -7
- package/src/dom/applicators/font.ts +16 -29
- package/src/dom/applicators/index.ts +61 -46
- package/src/dom/canvas/index.ts +2 -2
- package/src/dom/components/app.ts +2 -2
- package/src/dom/components/audio.ts +2 -2
- package/src/dom/components/avatar.ts +3 -4
- package/src/dom/components/badge.ts +3 -4
- package/src/dom/components/button.ts +2 -2
- package/src/dom/components/card.ts +2 -2
- package/src/dom/components/center.ts +2 -2
- package/src/dom/components/checkbox.ts +4 -5
- package/src/dom/components/column.ts +2 -2
- package/src/dom/components/container.ts +2 -2
- package/src/dom/components/divider.ts +2 -2
- package/src/dom/components/grid.ts +10 -10
- package/src/dom/components/heading.ts +4 -6
- package/src/dom/components/hypenapp.ts +246 -274
- package/src/dom/components/icon.ts +4 -5
- package/src/dom/components/image.ts +2 -2
- package/src/dom/components/index.ts +10 -3
- package/src/dom/components/input.ts +2 -2
- package/src/dom/components/link.ts +2 -2
- package/src/dom/components/list.ts +2 -2
- package/src/dom/components/paragraph.ts +3 -4
- package/src/dom/components/progressbar.ts +3 -3
- package/src/dom/components/route.ts +2 -2
- package/src/dom/components/router.ts +2 -2
- package/src/dom/components/row.ts +10 -10
- package/src/dom/components/select.ts +3 -4
- package/src/dom/components/slider.ts +2 -2
- package/src/dom/components/spacer.ts +2 -2
- package/src/dom/components/spinner.ts +34 -16
- package/src/dom/components/stack.ts +10 -10
- package/src/dom/components/switch.ts +5 -6
- package/src/dom/components/tabs.ts +76 -0
- package/src/dom/components/text.ts +4 -7
- package/src/dom/components/textarea.ts +2 -2
- package/src/dom/components/video.ts +2 -2
- package/src/dom/components/visuallyhidden.ts +30 -0
- package/src/dom/debug.ts +6 -8
- package/src/dom/index.ts +0 -8
- package/src/dom/operability.ts +312 -0
- package/src/dom/renderer.ts +457 -66
- package/src/dom/route-focus.ts +98 -0
- package/src/dom/semantics.ts +199 -0
- package/src/variants.ts +249 -0
- package/dist/canvas/input.d.ts +0 -76
- package/dist/canvas/input.js +0 -207
- package/dist/canvas/input.js.map +0 -1
- package/dist/client.d.ts +0 -36
- package/dist/client.js +0 -47
- package/dist/client.js.map +0 -1
- package/src/canvas/input.ts +0 -251
- package/src/client.ts +0 -88
package/src/canvas/renderer.ts
CHANGED
|
@@ -21,10 +21,13 @@ import type {
|
|
|
21
21
|
LayoutFunction,
|
|
22
22
|
} from "./types.js";
|
|
23
23
|
import { computeLayout, initTaffyLayout } from "./layout.js";
|
|
24
|
-
import {
|
|
24
|
+
import { clearTextCache } from "./text.js";
|
|
25
|
+
import { paintNode, registerPainter, clearCharAdvanceCache } from "./paint.js";
|
|
25
26
|
import { CanvasEventManager } from "./events.js";
|
|
26
|
-
import { InputOverlay } from "./input.js";
|
|
27
27
|
import { AccessibilityLayer } from "./accessibility.js";
|
|
28
|
+
import { FocusManager } from "./focus.js";
|
|
29
|
+
import { TextEditController, isEditableNode } from "./editing.js";
|
|
30
|
+
import { setTextEditor } from "./paint.js";
|
|
28
31
|
import { findNodeById } from "./utils.js";
|
|
29
32
|
import { ScrollManager } from "./scroll.js";
|
|
30
33
|
import { SelectionManager } from "./selection.js";
|
|
@@ -35,13 +38,13 @@ import {
|
|
|
35
38
|
refreshApplicator,
|
|
36
39
|
parseApplicatorBase,
|
|
37
40
|
} from "./props.js";
|
|
41
|
+
import { applyVariants, invalidateVariantCache, deriveNodeComputed } from "./variants.js";
|
|
38
42
|
|
|
39
43
|
const DEFAULT_OPTIONS: CanvasRendererOptions = {
|
|
40
44
|
devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
|
|
41
45
|
backgroundColor: "#ffffff",
|
|
42
46
|
enableAccessibility: true,
|
|
43
47
|
enableHitTesting: true,
|
|
44
|
-
enableInputOverlay: true,
|
|
45
48
|
enableDirtyRects: false,
|
|
46
49
|
enableLayerCaching: false,
|
|
47
50
|
maxLayerCacheSize: 10,
|
|
@@ -65,21 +68,34 @@ export class CanvasRenderer implements Renderer {
|
|
|
65
68
|
private eventManager: CanvasEventManager;
|
|
66
69
|
private scrollManager: ScrollManager;
|
|
67
70
|
private selectionManager: SelectionManager;
|
|
68
|
-
private inputOverlay: InputOverlay;
|
|
69
71
|
private accessibilityLayer: AccessibilityLayer;
|
|
72
|
+
private focusManager: FocusManager;
|
|
73
|
+
private textEditor: TextEditController;
|
|
70
74
|
|
|
71
75
|
private dirtyTracker: DirtyRectTracker;
|
|
72
76
|
|
|
73
77
|
private rafId: number | null = null;
|
|
74
78
|
private needsRedraw = false;
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
// Whether the next frame must re-run layout. Patches, resize, font loads,
|
|
81
|
+
// and image decodes set it; pure paint frames (scroll, hover, caret blink,
|
|
82
|
+
// scrollbar fade) leave it clear so the frame skips the Taffy solve.
|
|
83
|
+
private layoutDirty = true;
|
|
79
84
|
|
|
80
85
|
private frameCount = 0;
|
|
81
86
|
private lastFrameTime = 0;
|
|
82
87
|
|
|
88
|
+
private boundFontsLoaded = () => {
|
|
89
|
+
// Metrics measured against the fallback font are stale now — both the
|
|
90
|
+
// wrapped-text metrics and the per-glyph advances used by the manual
|
|
91
|
+
// letter-spacing path (keyed by CSS font string, which does not change
|
|
92
|
+
// when the real font replaces the fallback).
|
|
93
|
+
clearTextCache();
|
|
94
|
+
clearCharAdvanceCache();
|
|
95
|
+
this.layoutDirty = true;
|
|
96
|
+
this.scheduleRedraw();
|
|
97
|
+
};
|
|
98
|
+
|
|
83
99
|
constructor(canvas: HTMLCanvasElement, engine: IEngine, options?: Partial<CanvasRendererOptions>) {
|
|
84
100
|
this.canvas = canvas;
|
|
85
101
|
this.engine = engine;
|
|
@@ -105,66 +121,84 @@ export class CanvasRenderer implements Renderer {
|
|
|
105
121
|
this.scrollManager = new ScrollManager(canvas, () => this.scheduleRedraw());
|
|
106
122
|
this.selectionManager = new SelectionManager(canvas, () => this.scheduleRedraw());
|
|
107
123
|
setSelectionManager(this.selectionManager);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
124
|
+
// The mirror is a transparent positioned overlay above the canvas: its
|
|
125
|
+
// elements are exposed to AT with REAL geometry (screen-reader browse
|
|
126
|
+
// modes are geometry-driven) and join the native tab order, while the
|
|
127
|
+
// canvas paints all pixels and keeps all pointer events.
|
|
111
128
|
this.accessibilityLayer = new AccessibilityLayer(
|
|
112
|
-
|
|
129
|
+
canvas as unknown as HTMLElement,
|
|
113
130
|
this.options.enableAccessibility || false
|
|
114
131
|
);
|
|
115
132
|
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
this.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
// Text editing happens IN the focused mirror element (the browser owns
|
|
134
|
+
// value/caret/selection/IME there); the controller reads that state and
|
|
135
|
+
// the canvas paints text, selection, and caret natively.
|
|
136
|
+
this.textEditor = new TextEditController(canvas, engine, {
|
|
137
|
+
scheduleRedraw: () => this.scheduleRedraw(),
|
|
138
|
+
markDirty: (node) => {
|
|
139
|
+
if (this.options.enableDirtyRects) {
|
|
140
|
+
this.dirtyTracker.markNodeDirty(node);
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
// Proxy blurred to somewhere outside the canvas: node.focused follows.
|
|
144
|
+
onEditBlur: () => this.focusManager.clearFocus(),
|
|
145
|
+
});
|
|
146
|
+
setTextEditor(this.textEditor);
|
|
147
|
+
|
|
148
|
+
// DOM focus on mirror elements (canvas fallback content) is the single
|
|
149
|
+
// source of truth for focus: Tab/Shift+Tab traverse the mirror natively,
|
|
150
|
+
// and the pointer path (hit-test in CanvasEventManager) funnels into the
|
|
151
|
+
// same place by focusing the node's mirror element.
|
|
152
|
+
this.focusManager = new FocusManager(this.accessibilityLayer, engine, {
|
|
153
|
+
getNode: (id) => this.nodes.get(id),
|
|
154
|
+
isAuxFocusTarget: (el) => this.textEditor.isProxyElement(el),
|
|
155
|
+
onFocusChange: (next) => {
|
|
156
|
+
if (next && isEditableNode(next)) {
|
|
157
|
+
const el = this.accessibilityLayer.getElement(next.id);
|
|
158
|
+
if (el) {
|
|
159
|
+
this.textEditor.beginEditing(
|
|
160
|
+
next,
|
|
161
|
+
el as HTMLInputElement | HTMLTextAreaElement,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
this.textEditor.endEditing();
|
|
166
|
+
}
|
|
167
|
+
// Repaint so focus styling (input border, button ring) updates.
|
|
168
|
+
this.scheduleRedraw();
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
this.eventManager.setFocusManager(this.focusManager);
|
|
172
|
+
this.eventManager.setEditablePointerHandler((node, point) => {
|
|
173
|
+
this.textEditor.placeCaretFromPoint(node, point);
|
|
137
174
|
});
|
|
138
175
|
|
|
139
|
-
// Listen for redraw requests from event manager
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
176
|
+
// Listen for redraw requests from event manager (paint-only) and image
|
|
177
|
+
// loads (which carry `detail.layout` — a decoded intrinsic size can
|
|
178
|
+
// change the layout, see `getImageNaturalAspect`).
|
|
179
|
+
this.canvas.addEventListener("hypen:redraw", (e: Event) => {
|
|
180
|
+
if ((e as CustomEvent).detail?.layout) {
|
|
181
|
+
this.layoutDirty = true;
|
|
182
|
+
}
|
|
183
|
+
this.scheduleRedraw();
|
|
184
|
+
});
|
|
144
185
|
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
this.resizeObserver = new ResizeObserver(() => {
|
|
150
|
-
this.setupHiDPI();
|
|
151
|
-
this.scheduleRedraw();
|
|
152
|
-
});
|
|
153
|
-
this.resizeObserver.observe(this.canvas);
|
|
186
|
+
// A late-loading web font changes every measurement made against the
|
|
187
|
+
// fallback font — re-run layout once the real metrics are available.
|
|
188
|
+
if (typeof document !== "undefined") {
|
|
189
|
+
(document as any).fonts?.addEventListener?.("loadingdone", this.boundFontsLoaded);
|
|
154
190
|
}
|
|
155
191
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
this.setupHiDPI();
|
|
192
|
+
// Eagerly initialise Taffy WASM for layout (non-blocking — fallback used
|
|
193
|
+
// until ready). Repaint once it arrives: the fallback's first frame is
|
|
194
|
+
// approximate, and without this redraw its layout would stick until the
|
|
195
|
+
// next state change.
|
|
196
|
+
initTaffyLayout().then((ready) => {
|
|
197
|
+
if (ready) {
|
|
198
|
+
this.layoutDirty = true;
|
|
164
199
|
this.scheduleRedraw();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
168
202
|
|
|
169
203
|
// Don't schedule initial render - wait for patches
|
|
170
204
|
}
|
|
@@ -176,18 +210,15 @@ export class CanvasRenderer implements Renderer {
|
|
|
176
210
|
const dpr = this.options.devicePixelRatio || 1;
|
|
177
211
|
const rect = this.canvas.getBoundingClientRect();
|
|
178
212
|
|
|
179
|
-
// Skip when the host is collapsed — a 0-pixel backing store throws
|
|
180
|
-
// in some 2D contexts and the next non-zero resize will reinitialise.
|
|
181
|
-
if (rect.width === 0 || rect.height === 0) return;
|
|
182
|
-
|
|
183
213
|
this.canvas.width = rect.width * dpr;
|
|
184
214
|
this.canvas.height = rect.height * dpr;
|
|
185
215
|
|
|
186
|
-
// Reset transform first — `scale()` is cumulative across calls, so on
|
|
187
|
-
// a resize the second call would compound (dpr²) and shrink content.
|
|
188
|
-
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
189
216
|
this.ctx.scale(dpr, dpr);
|
|
190
217
|
|
|
218
|
+
// Update canvas display size
|
|
219
|
+
this.canvas.style.width = `${rect.width}px`;
|
|
220
|
+
this.canvas.style.height = `${rect.height}px`;
|
|
221
|
+
|
|
191
222
|
// Sync dirty tracker with new canvas size
|
|
192
223
|
if (this.dirtyTracker) {
|
|
193
224
|
this.dirtyTracker.setCanvasSize(rect.width, rect.height);
|
|
@@ -203,6 +234,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
203
234
|
this.canvas.width = width;
|
|
204
235
|
this.canvas.height = height;
|
|
205
236
|
this.setupHiDPI();
|
|
237
|
+
this.layoutDirty = true;
|
|
206
238
|
this.scheduleRedraw();
|
|
207
239
|
}
|
|
208
240
|
|
|
@@ -221,10 +253,12 @@ export class CanvasRenderer implements Renderer {
|
|
|
221
253
|
this.dirtyTracker.markFullDirty();
|
|
222
254
|
}
|
|
223
255
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
256
|
+
// The accessibility mirror is synced incrementally by the per-patch
|
|
257
|
+
// handlers above — no per-batch rebuild, so mirror element identity
|
|
258
|
+
// (and with it AT focus/virtual-cursor position) survives updates.
|
|
259
|
+
|
|
260
|
+
// Any patch can affect layout (props, tree shape, text).
|
|
261
|
+
this.layoutDirty = true;
|
|
228
262
|
|
|
229
263
|
// Schedule redraw
|
|
230
264
|
this.scheduleRedraw();
|
|
@@ -236,7 +270,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
236
270
|
private applyPatch(patch: Patch): void {
|
|
237
271
|
switch (patch.type) {
|
|
238
272
|
case "create":
|
|
239
|
-
this.onCreate(patch.id!, patch.elementType!, patch.props || {});
|
|
273
|
+
this.onCreate(patch.id!, patch.elementType!, patch.props || {}, patch.semantics);
|
|
240
274
|
break;
|
|
241
275
|
|
|
242
276
|
case "setProp":
|
|
@@ -270,13 +304,38 @@ export class CanvasRenderer implements Renderer {
|
|
|
270
304
|
case "attach":
|
|
271
305
|
this.onInsert(patch.parentId!, patch.id!, patch.beforeId);
|
|
272
306
|
break;
|
|
307
|
+
|
|
308
|
+
case "setSemantics":
|
|
309
|
+
this.onSetSemantics(patch.id!, patch.semantics);
|
|
310
|
+
break;
|
|
273
311
|
}
|
|
274
312
|
}
|
|
275
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Reactive accessibility update: swap in the node's complete re-resolved
|
|
316
|
+
* semantics block and re-apply it to the shadow element. This is the only
|
|
317
|
+
* channel that keeps the canvas shadow tree's accessible name/state live —
|
|
318
|
+
* painted text is invisible to AT, so prop deltas alone can't do it.
|
|
319
|
+
*/
|
|
320
|
+
private onSetSemantics(
|
|
321
|
+
id: string,
|
|
322
|
+
semantics?: import("@hypen-space/core/types").Semantics,
|
|
323
|
+
): void {
|
|
324
|
+
const node = this.nodes.get(id);
|
|
325
|
+
if (!node) return;
|
|
326
|
+
node.semantics = semantics;
|
|
327
|
+
this.accessibilityLayer.updateNode(node);
|
|
328
|
+
}
|
|
329
|
+
|
|
276
330
|
/**
|
|
277
331
|
* Create new virtual node
|
|
278
332
|
*/
|
|
279
|
-
private onCreate(
|
|
333
|
+
private onCreate(
|
|
334
|
+
id: string,
|
|
335
|
+
elementType: string,
|
|
336
|
+
props: Record<string, any>,
|
|
337
|
+
semantics?: import("@hypen-space/core/types").Semantics,
|
|
338
|
+
): void {
|
|
280
339
|
// Engine may send a Map (from WASM) or a plain object. Copy either way so
|
|
281
340
|
// we own the prop bag and can mutate it during applicator normalisation.
|
|
282
341
|
const rawProps: Record<string, any> =
|
|
@@ -293,6 +352,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
293
352
|
id,
|
|
294
353
|
type: elementType,
|
|
295
354
|
props: rawProps,
|
|
355
|
+
semantics,
|
|
296
356
|
children: [],
|
|
297
357
|
parent: null,
|
|
298
358
|
visible: true,
|
|
@@ -309,6 +369,9 @@ export class CanvasRenderer implements Renderer {
|
|
|
309
369
|
};
|
|
310
370
|
|
|
311
371
|
this.nodes.set(id, node);
|
|
372
|
+
|
|
373
|
+
// Mirror the create (element stays detached until its insert patch).
|
|
374
|
+
this.accessibilityLayer.createNode(node);
|
|
312
375
|
}
|
|
313
376
|
|
|
314
377
|
/**
|
|
@@ -325,6 +388,10 @@ export class CanvasRenderer implements Renderer {
|
|
|
325
388
|
|
|
326
389
|
node.props[name] = value;
|
|
327
390
|
|
|
391
|
+
// A new key may introduce a variant marker (e.g. `padding@md.0`) — drop the
|
|
392
|
+
// cached variant-base set so the next frame rescans.
|
|
393
|
+
invalidateVariantCache(node);
|
|
394
|
+
|
|
328
395
|
// If this is an applicator-namespaced key (e.g. `flex.0`, `onClick.to`),
|
|
329
396
|
// rebuild the derived flat/aggregate entry under the base name so layout,
|
|
330
397
|
// paint, and event dispatch see the updated value.
|
|
@@ -358,6 +425,12 @@ export class CanvasRenderer implements Renderer {
|
|
|
358
425
|
|
|
359
426
|
// Update accessibility
|
|
360
427
|
this.accessibilityLayer.updateNode(node);
|
|
428
|
+
|
|
429
|
+
// Engine value echo for an actively edited input: same value → no-op
|
|
430
|
+
// (the caret must not move); a rewritten value re-seeds the element.
|
|
431
|
+
if (name === "value") {
|
|
432
|
+
this.textEditor.onEngineValueEcho(node);
|
|
433
|
+
}
|
|
361
434
|
}
|
|
362
435
|
|
|
363
436
|
/**
|
|
@@ -373,6 +446,8 @@ export class CanvasRenderer implements Renderer {
|
|
|
373
446
|
|
|
374
447
|
delete node.props[name];
|
|
375
448
|
|
|
449
|
+
invalidateVariantCache(node);
|
|
450
|
+
|
|
376
451
|
const base = parseApplicatorBase(name);
|
|
377
452
|
if (base !== null) {
|
|
378
453
|
refreshApplicator(node.props, base);
|
|
@@ -429,6 +504,10 @@ export class CanvasRenderer implements Renderer {
|
|
|
429
504
|
const child = this.nodes.get(id);
|
|
430
505
|
if (!child) return;
|
|
431
506
|
|
|
507
|
+
// Mirror the insert/attach/move — insertNode resolves root addressing
|
|
508
|
+
// and unknown-beforeId fallback with the same rules as the code below.
|
|
509
|
+
this.accessibilityLayer.insertNode(parentId, id, beforeId);
|
|
510
|
+
|
|
432
511
|
// Check if this is setting the root node (parent_id === id === "root" or similar)
|
|
433
512
|
if (parentId === "root" && id === "root") {
|
|
434
513
|
this.rootNode = child;
|
|
@@ -512,6 +591,15 @@ export class CanvasRenderer implements Renderer {
|
|
|
512
591
|
const node = this.nodes.get(id);
|
|
513
592
|
if (!node) return;
|
|
514
593
|
|
|
594
|
+
// An edit session inside the detached subtree ends (flushing any
|
|
595
|
+
// pending composition value) before the subtree leaves the tree, and
|
|
596
|
+
// focus state follows.
|
|
597
|
+
this.textEditor.endIfWithin(node);
|
|
598
|
+
this.focusManager.clearIfWithin(node);
|
|
599
|
+
|
|
600
|
+
// Mirror keeps the element (and subtree ids) alive for re-attach.
|
|
601
|
+
this.accessibilityLayer.detachNode(id);
|
|
602
|
+
|
|
515
603
|
// Mark dirty so the next paint doesn't leave stale pixels behind.
|
|
516
604
|
if (this.options.enableDirtyRects) {
|
|
517
605
|
this.dirtyTracker.markNodeDirty(node);
|
|
@@ -543,6 +631,13 @@ export class CanvasRenderer implements Renderer {
|
|
|
543
631
|
const node = this.nodes.get(id);
|
|
544
632
|
if (!node) return;
|
|
545
633
|
|
|
634
|
+
// End any edit session inside the removed subtree; focus state follows.
|
|
635
|
+
this.textEditor.endIfWithin(node);
|
|
636
|
+
this.focusManager.clearIfWithin(node);
|
|
637
|
+
|
|
638
|
+
// Drop the mirror element and its subtree's id mappings.
|
|
639
|
+
this.accessibilityLayer.removeNode(node);
|
|
640
|
+
|
|
546
641
|
// Mark dirty before removal
|
|
547
642
|
if (this.options.enableDirtyRects) {
|
|
548
643
|
this.dirtyTracker.markNodeDirty(node);
|
|
@@ -599,6 +694,16 @@ export class CanvasRenderer implements Renderer {
|
|
|
599
694
|
this.renderFull(dpr);
|
|
600
695
|
}
|
|
601
696
|
|
|
697
|
+
// Layout may have moved the edited input — keep the IME proxy (and with
|
|
698
|
+
// it the IME candidate window) pinned to the painted caret.
|
|
699
|
+
if (this.textEditor.isActive()) {
|
|
700
|
+
this.textEditor.syncProxyPosition();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Keep the semantics overlay's element boxes on the painted bounds so
|
|
704
|
+
// screen-reader browse modes (geometry-driven) track layout changes.
|
|
705
|
+
this.accessibilityLayer.syncPositions(this.rootNode);
|
|
706
|
+
|
|
602
707
|
// Performance logging
|
|
603
708
|
if (this.options.logPerformance) {
|
|
604
709
|
const elapsed = performance.now() - startTime;
|
|
@@ -611,6 +716,51 @@ export class CanvasRenderer implements Renderer {
|
|
|
611
716
|
}
|
|
612
717
|
}
|
|
613
718
|
|
|
719
|
+
/**
|
|
720
|
+
* Re-derive cached computed fields that may have been overridden by variant
|
|
721
|
+
* resolution. `node.opacity` is read straight off `node.props.opacity` in
|
|
722
|
+
* paint, but it is cached on the node at create/setProp time — after a
|
|
723
|
+
* variant pass changes `props.opacity` (e.g. `opacity:disabled`), the cache
|
|
724
|
+
* must be refreshed or the paint would use the stale value.
|
|
725
|
+
*/
|
|
726
|
+
private refreshComputedProps(node: VirtualNode): void {
|
|
727
|
+
deriveNodeComputed(node);
|
|
728
|
+
for (const child of node.children) {
|
|
729
|
+
this.refreshComputedProps(child);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Resolve variants and re-run layout when (and only when) something that
|
|
735
|
+
* affects layout changed since the last frame. Variants are resolved every
|
|
736
|
+
* frame — hover/focus/pressed winners must reach paint — but the Taffy
|
|
737
|
+
* solve, computed-prop refresh, and scroll-bounds pass only run when a
|
|
738
|
+
* patch/resize/font/image marked the layout dirty or a variant winner
|
|
739
|
+
* actually changed (variants can rewrite spacing/size props).
|
|
740
|
+
*/
|
|
741
|
+
private runLayoutIfNeeded(dpr: number): void {
|
|
742
|
+
if (!this.rootNode) return;
|
|
743
|
+
|
|
744
|
+
// Resolve responsive + state variants against the current content width
|
|
745
|
+
// BEFORE layout so spacing/size winners feed the layout engine and
|
|
746
|
+
// colour/opacity winners feed paint.
|
|
747
|
+
const contentWidth = this.canvas.width / dpr;
|
|
748
|
+
const variantsChanged = applyVariants(this.rootNode, contentWidth);
|
|
749
|
+
if (!variantsChanged && !this.layoutDirty) return;
|
|
750
|
+
|
|
751
|
+
this.refreshComputedProps(this.rootNode);
|
|
752
|
+
|
|
753
|
+
computeLayout(
|
|
754
|
+
this.ctx,
|
|
755
|
+
this.rootNode,
|
|
756
|
+
contentWidth,
|
|
757
|
+
this.canvas.height / dpr
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
ScrollManager.updateScrollBounds(this.rootNode);
|
|
761
|
+
this.layoutDirty = false;
|
|
762
|
+
}
|
|
763
|
+
|
|
614
764
|
/**
|
|
615
765
|
* Full canvas repaint (default behavior when dirty rects disabled)
|
|
616
766
|
*/
|
|
@@ -626,14 +776,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
626
776
|
|
|
627
777
|
// Layout and paint
|
|
628
778
|
if (this.rootNode) {
|
|
629
|
-
|
|
630
|
-
this.ctx,
|
|
631
|
-
this.rootNode,
|
|
632
|
-
this.canvas.width / dpr,
|
|
633
|
-
this.canvas.height / dpr
|
|
634
|
-
);
|
|
635
|
-
|
|
636
|
-
ScrollManager.updateScrollBounds(this.rootNode);
|
|
779
|
+
this.runLayoutIfNeeded(dpr);
|
|
637
780
|
|
|
638
781
|
paintNode(this.ctx, this.rootNode);
|
|
639
782
|
|
|
@@ -647,16 +790,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
647
790
|
* Optimized render that only repaints dirty regions
|
|
648
791
|
*/
|
|
649
792
|
private renderWithDirtyRects(dpr: number): void {
|
|
650
|
-
|
|
651
|
-
if (this.rootNode) {
|
|
652
|
-
computeLayout(
|
|
653
|
-
this.ctx,
|
|
654
|
-
this.rootNode,
|
|
655
|
-
this.canvas.width / dpr,
|
|
656
|
-
this.canvas.height / dpr
|
|
657
|
-
);
|
|
658
|
-
ScrollManager.updateScrollBounds(this.rootNode);
|
|
659
|
-
}
|
|
793
|
+
this.runLayoutIfNeeded(dpr);
|
|
660
794
|
|
|
661
795
|
const dirtyRegion = this.dirtyTracker.getDirtyRegion();
|
|
662
796
|
this.dirtyTracker.clear();
|
|
@@ -682,9 +816,11 @@ export class CanvasRenderer implements Renderer {
|
|
|
682
816
|
this.ctx.fillRect(dirtyRegion.x, dirtyRegion.y, dirtyRegion.width, dirtyRegion.height);
|
|
683
817
|
}
|
|
684
818
|
|
|
685
|
-
// Repaint the
|
|
819
|
+
// Repaint the tree — the clip path bounds rasterized pixels and the
|
|
820
|
+
// dirty region is threaded through paintNode so subtrees that cannot
|
|
821
|
+
// reach it are pruned from the traversal entirely.
|
|
686
822
|
if (this.rootNode) {
|
|
687
|
-
paintNode(this.ctx, this.rootNode);
|
|
823
|
+
paintNode(this.ctx, this.rootNode, dirtyRegion);
|
|
688
824
|
|
|
689
825
|
if (this.options.showLayoutBounds) {
|
|
690
826
|
this.drawLayoutBounds(this.rootNode);
|
|
@@ -733,11 +869,13 @@ export class CanvasRenderer implements Renderer {
|
|
|
733
869
|
* Clear renderer
|
|
734
870
|
*/
|
|
735
871
|
clear(): void {
|
|
872
|
+
this.textEditor.endEditing();
|
|
736
873
|
this.rootNode = null;
|
|
737
874
|
this.nodes.clear();
|
|
738
875
|
this.eventManager.setRootNode(null);
|
|
739
876
|
this.scrollManager.setRootNode(null);
|
|
740
877
|
this.selectionManager.setRootNode(null);
|
|
878
|
+
this.accessibilityLayer.rebuild(null);
|
|
741
879
|
this.dirtyTracker.clear();
|
|
742
880
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
743
881
|
}
|
|
@@ -754,7 +892,12 @@ export class CanvasRenderer implements Renderer {
|
|
|
754
892
|
*/
|
|
755
893
|
setOptions(options: Partial<CanvasRendererOptions>): void {
|
|
756
894
|
this.options = { ...this.options, ...options };
|
|
757
|
-
|
|
895
|
+
// Re-enabling rebuilds the mirror from the current tree — incremental
|
|
896
|
+
// sync has no history to replay for the disabled period.
|
|
897
|
+
this.accessibilityLayer.setEnabled(
|
|
898
|
+
this.options.enableAccessibility || false,
|
|
899
|
+
this.rootNode,
|
|
900
|
+
);
|
|
758
901
|
}
|
|
759
902
|
|
|
760
903
|
/**
|
|
@@ -764,19 +907,16 @@ export class CanvasRenderer implements Renderer {
|
|
|
764
907
|
if (this.rafId !== null) {
|
|
765
908
|
cancelAnimationFrame(this.rafId);
|
|
766
909
|
}
|
|
767
|
-
if (
|
|
768
|
-
this.
|
|
769
|
-
this.resizeObserver = null;
|
|
770
|
-
}
|
|
771
|
-
if (this.dprMediaQuery && this.dprChangeHandler) {
|
|
772
|
-
this.dprMediaQuery.removeEventListener("change", this.dprChangeHandler);
|
|
773
|
-
this.dprMediaQuery = null;
|
|
774
|
-
this.dprChangeHandler = null;
|
|
910
|
+
if (typeof document !== "undefined") {
|
|
911
|
+
(document as any).fonts?.removeEventListener?.("loadingdone", this.boundFontsLoaded);
|
|
775
912
|
}
|
|
776
913
|
this.eventManager.destroy();
|
|
777
914
|
this.scrollManager.destroy();
|
|
778
915
|
this.selectionManager.destroy();
|
|
779
916
|
setSelectionManager(null);
|
|
917
|
+
this.textEditor.destroy();
|
|
918
|
+
setTextEditor(null);
|
|
919
|
+
this.focusManager.destroy();
|
|
780
920
|
this.accessibilityLayer.destroy();
|
|
781
921
|
}
|
|
782
922
|
}
|