@hypen-space/web 0.5.2 → 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/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 +76 -128
- 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 -1
- package/dist/canvas/index.js +3 -1
- package/dist/canvas/index.js.map +1 -1
- package/dist/canvas/paint.d.ts +16 -3
- package/dist/canvas/paint.js +283 -41
- package/dist/canvas/paint.js.map +1 -1
- package/dist/canvas/renderer.d.ts +20 -1
- package/dist/canvas/renderer.js +181 -50
- 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 +42 -9
- package/dist/canvas/text.js.map +1 -1
- package/dist/canvas/types.d.ts +7 -1
- package/dist/canvas/variants.d.ts +4 -1
- package/dist/canvas/variants.js +22 -3
- package/dist/canvas/variants.js.map +1 -1
- 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 +35 -8
- package/dist/dom/applicators/events.js.map +1 -1
- package/dist/dom/applicators/index.js +4 -0
- package/dist/dom/applicators/index.js.map +1 -1
- package/dist/dom/components/hypenapp.d.ts +25 -1
- package/dist/dom/components/hypenapp.js +213 -245
- package/dist/dom/components/hypenapp.js.map +1 -1
- package/dist/dom/components/index.js +7 -0
- package/dist/dom/components/index.js.map +1 -1
- package/dist/dom/components/spinner.js +24 -10
- package/dist/dom/components/spinner.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/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/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 +398 -63
- 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/package.json +2 -2
- package/src/canvas/QUICKSTART.md +4 -4
- package/src/canvas/README.md +55 -29
- 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 +89 -142
- package/src/canvas/focus.ts +216 -0
- package/src/canvas/index.ts +8 -1
- package/src/canvas/paint.ts +339 -42
- package/src/canvas/renderer.ts +218 -70
- package/src/canvas/selection.ts +52 -210
- package/src/canvas/text-geometry.ts +311 -0
- package/src/canvas/text.ts +43 -9
- package/src/canvas/types.ts +14 -1
- package/src/canvas/variants.ts +24 -5
- package/src/dom/a11y-styles.ts +65 -0
- package/src/dom/applicators/aria.ts +41 -0
- package/src/dom/applicators/events.ts +38 -7
- package/src/dom/applicators/index.ts +5 -0
- package/src/dom/components/hypenapp.ts +244 -272
- package/src/dom/components/index.ts +7 -0
- package/src/dom/components/spinner.ts +31 -13
- package/src/dom/components/tabs.ts +76 -0
- package/src/dom/components/visuallyhidden.ts +30 -0
- package/src/dom/operability.ts +312 -0
- package/src/dom/renderer.ts +455 -63
- package/src/dom/route-focus.ts +98 -0
- package/src/dom/semantics.ts +199 -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/src/canvas/input.ts +0 -251
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";
|
|
@@ -42,7 +45,6 @@ const DEFAULT_OPTIONS: CanvasRendererOptions = {
|
|
|
42
45
|
backgroundColor: "#ffffff",
|
|
43
46
|
enableAccessibility: true,
|
|
44
47
|
enableHitTesting: true,
|
|
45
|
-
enableInputOverlay: true,
|
|
46
48
|
enableDirtyRects: false,
|
|
47
49
|
enableLayerCaching: false,
|
|
48
50
|
maxLayerCacheSize: 10,
|
|
@@ -66,17 +68,34 @@ export class CanvasRenderer implements Renderer {
|
|
|
66
68
|
private eventManager: CanvasEventManager;
|
|
67
69
|
private scrollManager: ScrollManager;
|
|
68
70
|
private selectionManager: SelectionManager;
|
|
69
|
-
private inputOverlay: InputOverlay;
|
|
70
71
|
private accessibilityLayer: AccessibilityLayer;
|
|
72
|
+
private focusManager: FocusManager;
|
|
73
|
+
private textEditor: TextEditController;
|
|
71
74
|
|
|
72
75
|
private dirtyTracker: DirtyRectTracker;
|
|
73
76
|
|
|
74
77
|
private rafId: number | null = null;
|
|
75
78
|
private needsRedraw = false;
|
|
76
79
|
|
|
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;
|
|
84
|
+
|
|
77
85
|
private frameCount = 0;
|
|
78
86
|
private lastFrameTime = 0;
|
|
79
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
|
+
|
|
80
99
|
constructor(canvas: HTMLCanvasElement, engine: IEngine, options?: Partial<CanvasRendererOptions>) {
|
|
81
100
|
this.canvas = canvas;
|
|
82
101
|
this.engine = engine;
|
|
@@ -102,42 +121,84 @@ export class CanvasRenderer implements Renderer {
|
|
|
102
121
|
this.scrollManager = new ScrollManager(canvas, () => this.scheduleRedraw());
|
|
103
122
|
this.selectionManager = new SelectionManager(canvas, () => this.scheduleRedraw());
|
|
104
123
|
setSelectionManager(this.selectionManager);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
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.
|
|
108
128
|
this.accessibilityLayer = new AccessibilityLayer(
|
|
109
|
-
|
|
129
|
+
canvas as unknown as HTMLElement,
|
|
110
130
|
this.options.enableAccessibility || false
|
|
111
131
|
);
|
|
112
132
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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);
|
|
174
|
+
});
|
|
175
|
+
|
|
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;
|
|
133
182
|
}
|
|
183
|
+
this.scheduleRedraw();
|
|
134
184
|
});
|
|
135
185
|
|
|
136
|
-
//
|
|
137
|
-
|
|
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);
|
|
190
|
+
}
|
|
138
191
|
|
|
139
|
-
// Eagerly initialise Taffy WASM for layout (non-blocking — fallback used
|
|
140
|
-
|
|
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;
|
|
199
|
+
this.scheduleRedraw();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
141
202
|
|
|
142
203
|
// Don't schedule initial render - wait for patches
|
|
143
204
|
}
|
|
@@ -173,6 +234,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
173
234
|
this.canvas.width = width;
|
|
174
235
|
this.canvas.height = height;
|
|
175
236
|
this.setupHiDPI();
|
|
237
|
+
this.layoutDirty = true;
|
|
176
238
|
this.scheduleRedraw();
|
|
177
239
|
}
|
|
178
240
|
|
|
@@ -191,10 +253,12 @@ export class CanvasRenderer implements Renderer {
|
|
|
191
253
|
this.dirtyTracker.markFullDirty();
|
|
192
254
|
}
|
|
193
255
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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;
|
|
198
262
|
|
|
199
263
|
// Schedule redraw
|
|
200
264
|
this.scheduleRedraw();
|
|
@@ -206,7 +270,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
206
270
|
private applyPatch(patch: Patch): void {
|
|
207
271
|
switch (patch.type) {
|
|
208
272
|
case "create":
|
|
209
|
-
this.onCreate(patch.id!, patch.elementType!, patch.props || {});
|
|
273
|
+
this.onCreate(patch.id!, patch.elementType!, patch.props || {}, patch.semantics);
|
|
210
274
|
break;
|
|
211
275
|
|
|
212
276
|
case "setProp":
|
|
@@ -240,13 +304,38 @@ export class CanvasRenderer implements Renderer {
|
|
|
240
304
|
case "attach":
|
|
241
305
|
this.onInsert(patch.parentId!, patch.id!, patch.beforeId);
|
|
242
306
|
break;
|
|
307
|
+
|
|
308
|
+
case "setSemantics":
|
|
309
|
+
this.onSetSemantics(patch.id!, patch.semantics);
|
|
310
|
+
break;
|
|
243
311
|
}
|
|
244
312
|
}
|
|
245
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
|
+
|
|
246
330
|
/**
|
|
247
331
|
* Create new virtual node
|
|
248
332
|
*/
|
|
249
|
-
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 {
|
|
250
339
|
// Engine may send a Map (from WASM) or a plain object. Copy either way so
|
|
251
340
|
// we own the prop bag and can mutate it during applicator normalisation.
|
|
252
341
|
const rawProps: Record<string, any> =
|
|
@@ -263,6 +352,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
263
352
|
id,
|
|
264
353
|
type: elementType,
|
|
265
354
|
props: rawProps,
|
|
355
|
+
semantics,
|
|
266
356
|
children: [],
|
|
267
357
|
parent: null,
|
|
268
358
|
visible: true,
|
|
@@ -279,6 +369,9 @@ export class CanvasRenderer implements Renderer {
|
|
|
279
369
|
};
|
|
280
370
|
|
|
281
371
|
this.nodes.set(id, node);
|
|
372
|
+
|
|
373
|
+
// Mirror the create (element stays detached until its insert patch).
|
|
374
|
+
this.accessibilityLayer.createNode(node);
|
|
282
375
|
}
|
|
283
376
|
|
|
284
377
|
/**
|
|
@@ -332,6 +425,12 @@ export class CanvasRenderer implements Renderer {
|
|
|
332
425
|
|
|
333
426
|
// Update accessibility
|
|
334
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
|
+
}
|
|
335
434
|
}
|
|
336
435
|
|
|
337
436
|
/**
|
|
@@ -405,6 +504,10 @@ export class CanvasRenderer implements Renderer {
|
|
|
405
504
|
const child = this.nodes.get(id);
|
|
406
505
|
if (!child) return;
|
|
407
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
|
+
|
|
408
511
|
// Check if this is setting the root node (parent_id === id === "root" or similar)
|
|
409
512
|
if (parentId === "root" && id === "root") {
|
|
410
513
|
this.rootNode = child;
|
|
@@ -488,6 +591,15 @@ export class CanvasRenderer implements Renderer {
|
|
|
488
591
|
const node = this.nodes.get(id);
|
|
489
592
|
if (!node) return;
|
|
490
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
|
+
|
|
491
603
|
// Mark dirty so the next paint doesn't leave stale pixels behind.
|
|
492
604
|
if (this.options.enableDirtyRects) {
|
|
493
605
|
this.dirtyTracker.markNodeDirty(node);
|
|
@@ -519,6 +631,13 @@ export class CanvasRenderer implements Renderer {
|
|
|
519
631
|
const node = this.nodes.get(id);
|
|
520
632
|
if (!node) return;
|
|
521
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
|
+
|
|
522
641
|
// Mark dirty before removal
|
|
523
642
|
if (this.options.enableDirtyRects) {
|
|
524
643
|
this.dirtyTracker.markNodeDirty(node);
|
|
@@ -575,6 +694,16 @@ export class CanvasRenderer implements Renderer {
|
|
|
575
694
|
this.renderFull(dpr);
|
|
576
695
|
}
|
|
577
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
|
+
|
|
578
707
|
// Performance logging
|
|
579
708
|
if (this.options.logPerformance) {
|
|
580
709
|
const elapsed = performance.now() - startTime;
|
|
@@ -601,6 +730,37 @@ export class CanvasRenderer implements Renderer {
|
|
|
601
730
|
}
|
|
602
731
|
}
|
|
603
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
|
+
|
|
604
764
|
/**
|
|
605
765
|
* Full canvas repaint (default behavior when dirty rects disabled)
|
|
606
766
|
*/
|
|
@@ -616,21 +776,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
616
776
|
|
|
617
777
|
// Layout and paint
|
|
618
778
|
if (this.rootNode) {
|
|
619
|
-
|
|
620
|
-
// BEFORE layout so spacing/size winners feed the layout engine and
|
|
621
|
-
// colour/opacity winners feed paint.
|
|
622
|
-
const contentWidth = this.canvas.width / dpr;
|
|
623
|
-
applyVariants(this.rootNode, contentWidth);
|
|
624
|
-
this.refreshComputedProps(this.rootNode);
|
|
625
|
-
|
|
626
|
-
computeLayout(
|
|
627
|
-
this.ctx,
|
|
628
|
-
this.rootNode,
|
|
629
|
-
contentWidth,
|
|
630
|
-
this.canvas.height / dpr
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
ScrollManager.updateScrollBounds(this.rootNode);
|
|
779
|
+
this.runLayoutIfNeeded(dpr);
|
|
634
780
|
|
|
635
781
|
paintNode(this.ctx, this.rootNode);
|
|
636
782
|
|
|
@@ -644,20 +790,7 @@ export class CanvasRenderer implements Renderer {
|
|
|
644
790
|
* Optimized render that only repaints dirty regions
|
|
645
791
|
*/
|
|
646
792
|
private renderWithDirtyRects(dpr: number): void {
|
|
647
|
-
|
|
648
|
-
if (this.rootNode) {
|
|
649
|
-
const contentWidth = this.canvas.width / dpr;
|
|
650
|
-
applyVariants(this.rootNode, contentWidth);
|
|
651
|
-
this.refreshComputedProps(this.rootNode);
|
|
652
|
-
|
|
653
|
-
computeLayout(
|
|
654
|
-
this.ctx,
|
|
655
|
-
this.rootNode,
|
|
656
|
-
contentWidth,
|
|
657
|
-
this.canvas.height / dpr
|
|
658
|
-
);
|
|
659
|
-
ScrollManager.updateScrollBounds(this.rootNode);
|
|
660
|
-
}
|
|
793
|
+
this.runLayoutIfNeeded(dpr);
|
|
661
794
|
|
|
662
795
|
const dirtyRegion = this.dirtyTracker.getDirtyRegion();
|
|
663
796
|
this.dirtyTracker.clear();
|
|
@@ -683,9 +816,11 @@ export class CanvasRenderer implements Renderer {
|
|
|
683
816
|
this.ctx.fillRect(dirtyRegion.x, dirtyRegion.y, dirtyRegion.width, dirtyRegion.height);
|
|
684
817
|
}
|
|
685
818
|
|
|
686
|
-
// 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.
|
|
687
822
|
if (this.rootNode) {
|
|
688
|
-
paintNode(this.ctx, this.rootNode);
|
|
823
|
+
paintNode(this.ctx, this.rootNode, dirtyRegion);
|
|
689
824
|
|
|
690
825
|
if (this.options.showLayoutBounds) {
|
|
691
826
|
this.drawLayoutBounds(this.rootNode);
|
|
@@ -734,11 +869,13 @@ export class CanvasRenderer implements Renderer {
|
|
|
734
869
|
* Clear renderer
|
|
735
870
|
*/
|
|
736
871
|
clear(): void {
|
|
872
|
+
this.textEditor.endEditing();
|
|
737
873
|
this.rootNode = null;
|
|
738
874
|
this.nodes.clear();
|
|
739
875
|
this.eventManager.setRootNode(null);
|
|
740
876
|
this.scrollManager.setRootNode(null);
|
|
741
877
|
this.selectionManager.setRootNode(null);
|
|
878
|
+
this.accessibilityLayer.rebuild(null);
|
|
742
879
|
this.dirtyTracker.clear();
|
|
743
880
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
744
881
|
}
|
|
@@ -755,7 +892,12 @@ export class CanvasRenderer implements Renderer {
|
|
|
755
892
|
*/
|
|
756
893
|
setOptions(options: Partial<CanvasRendererOptions>): void {
|
|
757
894
|
this.options = { ...this.options, ...options };
|
|
758
|
-
|
|
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
|
+
);
|
|
759
901
|
}
|
|
760
902
|
|
|
761
903
|
/**
|
|
@@ -765,10 +907,16 @@ export class CanvasRenderer implements Renderer {
|
|
|
765
907
|
if (this.rafId !== null) {
|
|
766
908
|
cancelAnimationFrame(this.rafId);
|
|
767
909
|
}
|
|
910
|
+
if (typeof document !== "undefined") {
|
|
911
|
+
(document as any).fonts?.removeEventListener?.("loadingdone", this.boundFontsLoaded);
|
|
912
|
+
}
|
|
768
913
|
this.eventManager.destroy();
|
|
769
914
|
this.scrollManager.destroy();
|
|
770
915
|
this.selectionManager.destroy();
|
|
771
916
|
setSelectionManager(null);
|
|
917
|
+
this.textEditor.destroy();
|
|
918
|
+
setTextEditor(null);
|
|
919
|
+
this.focusManager.destroy();
|
|
772
920
|
this.accessibilityLayer.destroy();
|
|
773
921
|
}
|
|
774
922
|
}
|