drawnui-react 0.1.0-preview.14 → 0.1.0-preview.16
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/SKIPPED.md +5 -4
- package/dist/controls/SkiaBackdrop.d.ts +2 -0
- package/dist/controls/SkiaBackdrop.js +2 -0
- package/dist/controls/SkiaButton.d.ts +2 -0
- package/dist/controls/SkiaButton.js +2 -0
- package/dist/controls/SkiaCarousel.d.ts +2 -0
- package/dist/controls/SkiaCarousel.js +2 -0
- package/dist/controls/SkiaDrawer.d.ts +2 -0
- package/dist/controls/SkiaDrawer.js +2 -0
- package/dist/controls/SkiaEditor.js +10 -1
- package/dist/controls/SkiaLabel.d.ts +2 -0
- package/dist/controls/SkiaLabel.js +13 -2
- package/dist/controls/SkiaLayout.d.ts +15 -0
- package/dist/controls/SkiaLayout.js +75 -6
- package/dist/controls/SkiaProgress.js +2 -2
- package/dist/controls/SkiaScroll.d.ts +4 -0
- package/dist/controls/SkiaScroll.js +9 -0
- package/dist/controls/SkiaSlider.d.ts +2 -0
- package/dist/controls/SkiaSlider.js +5 -3
- package/dist/controls/SkiaToggle.d.ts +2 -0
- package/dist/controls/SkiaToggle.js +3 -1
- package/dist/controls/SnappingLayout.d.ts +2 -0
- package/dist/controls/SnappingLayout.js +3 -0
- package/dist/core/Accessibility.d.ts +8 -0
- package/dist/core/Accessibility.js +56 -5
- package/dist/core/Canvas.d.ts +23 -0
- package/dist/core/Canvas.js +65 -3
- package/dist/core/Hover.d.ts +37 -0
- package/dist/core/Hover.js +118 -0
- package/dist/core/KeyboardManager.d.ts +13 -0
- package/dist/core/KeyboardManager.js +18 -0
- package/dist/core/SkiaControl.d.ts +87 -1
- package/dist/core/SkiaControl.js +229 -18
- package/dist/core/SkiaEffect.d.ts +3 -0
- package/dist/core/SkiaEffect.js +8 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +2 -2
- package/dist/react/reconciler.js +29 -10
- package/package.json +4 -1
|
@@ -26,6 +26,16 @@ export const Aria = {
|
|
|
26
26
|
return role === "list" || role === "listbox" || role === "grid" || role === "toolbar" || role === "radiogroup"
|
|
27
27
|
|| role === "tablist" || role === "menu" || role === "menubar";
|
|
28
28
|
},
|
|
29
|
+
/**
|
|
30
|
+
* Roles a user operates (C# Aria.IsInteractiveRole, as DrawnUi.Rust is_control_role): a node with one of them that
|
|
31
|
+
* cannot take input is read as disabled (aria-disabled). C# also lists listbox and scrollbar; here a listbox is an
|
|
32
|
+
* arrow-key group and groups are never disabled.
|
|
33
|
+
*/
|
|
34
|
+
IsInteractiveRole(role) {
|
|
35
|
+
return role === "button" || role === "link" || role === "checkbox" || role === "radio" || role === "switch"
|
|
36
|
+
|| role === "slider" || role === "spinbutton" || role === "textbox" || role === "searchbox" || role === "combobox"
|
|
37
|
+
|| role === "option" || role === "tab" || role === "menuitem" || role === "menuitemcheckbox" || role === "menuitemradio";
|
|
38
|
+
},
|
|
29
39
|
};
|
|
30
40
|
const asLayout = (c) => ("ChildrenFactory" in c && "Split" in c ? c : undefined);
|
|
31
41
|
const asScroll = (c) => ("ScrollToIndex" in c && "ViewportOffsetY" in c ? c : undefined);
|
|
@@ -150,7 +160,7 @@ export class SkiaAccessibilityManager {
|
|
|
150
160
|
const current = this.groupRequest?.Group === group ? this.groupRequest.Index : IndexOf(group, item);
|
|
151
161
|
if (current < 0 || count < 1)
|
|
152
162
|
return false;
|
|
153
|
-
const row = axis === "Both" ? RowLength(group
|
|
163
|
+
const row = axis === "Both" ? RowLength(group) : 1;
|
|
154
164
|
const vertical = axis !== "Horizontal";
|
|
155
165
|
const page = () => PageSize(scroll, item, vertical) * row;
|
|
156
166
|
let target, step;
|
|
@@ -282,7 +292,7 @@ export class SkiaAccessibilityManager {
|
|
|
282
292
|
Rebuild(scale, canvasWidthPx, canvasHeightPx) {
|
|
283
293
|
this.dirty = false;
|
|
284
294
|
this.lastRebuild = performance.now();
|
|
285
|
-
|
|
295
|
+
let list = [];
|
|
286
296
|
for (const n of this.nodes) {
|
|
287
297
|
if (!n.Superview) {
|
|
288
298
|
this.nodes.delete(n);
|
|
@@ -304,9 +314,11 @@ export class SkiaAccessibilityManager {
|
|
|
304
314
|
Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole,
|
|
305
315
|
Rect: new SKRect(px.Left / scale, px.Top / scale, px.Right / scale, px.Bottom / scale),
|
|
306
316
|
CanInteract: n.AccessibilityCanInteract, IsPressed: n.AccessibilityIsPressed, Live: n.AccessibilityLive, Source: n,
|
|
317
|
+
Disabled: !n.AccessibilityCanInteract && Aria.IsInteractiveRole(n.AccessibilityRole),
|
|
307
318
|
TextLines: n.AccessibilityTextSelectable ? n.GetAccessibilityTextLines(scale) : undefined,
|
|
308
319
|
});
|
|
309
320
|
}
|
|
321
|
+
list = SayNamesOnce(list);
|
|
310
322
|
list.sort((a, b) => a.Rect.Top - b.Rect.Top || a.Rect.Left - b.Rect.Left);
|
|
311
323
|
// reading order: nodes whose tops are within half the smaller height form one row, read left to right, so a row of
|
|
312
324
|
// vertically centered controls of different heights keeps its visual order (C# RectComparer + LeftComparer)
|
|
@@ -334,7 +346,7 @@ export class SkiaAccessibilityManager {
|
|
|
334
346
|
return false;
|
|
335
347
|
for (let i = 0; i < a.length; i++) {
|
|
336
348
|
const x = a[i], y = b[i];
|
|
337
|
-
if (x.Source !== y.Source || x.Label !== y.Label || x.Hint !== y.Hint || x.Role !== y.Role || x.CanInteract !== y.CanInteract
|
|
349
|
+
if (x.Source !== y.Source || x.Label !== y.Label || x.Hint !== y.Hint || x.Role !== y.Role || x.CanInteract !== y.CanInteract || x.Disabled !== y.Disabled
|
|
338
350
|
|| x.IsPressed !== y.IsPressed || x.Live !== y.Live
|
|
339
351
|
|| Math.abs(x.Rect.Left - y.Rect.Left) > 0.5 || Math.abs(x.Rect.Top - y.Rect.Top) > 0.5
|
|
340
352
|
|| Math.abs(x.Rect.Right - y.Rect.Right) > 0.5 || Math.abs(x.Rect.Bottom - y.Rect.Bottom) > 0.5)
|
|
@@ -353,6 +365,30 @@ export class SkiaAccessibilityManager {
|
|
|
353
365
|
return true;
|
|
354
366
|
}
|
|
355
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Each name is said once in the flat overlay (DrawnUi.Rust said_by_child): a text or a heading whose label repeats the
|
|
370
|
+
* label of the node it is in (its nearest ancestor node) is said by one of them. Under an interactive node (a card
|
|
371
|
+
* button) the child is left out, the button keeps its name; under any other node (a group card and its title) the
|
|
372
|
+
* node loses its name and the heading says it. Selectable text (TextLines) always stays.
|
|
373
|
+
*/
|
|
374
|
+
function SayNamesOnce(list) {
|
|
375
|
+
const bySource = new Map(list.map((x) => [x.Source, x]));
|
|
376
|
+
const said = new Set();
|
|
377
|
+
for (const x of list) {
|
|
378
|
+
if (!x.Label || x.TextLines || (x.Role !== Aria.RoleText && x.Role !== Aria.RoleHeading))
|
|
379
|
+
continue;
|
|
380
|
+
let owner;
|
|
381
|
+
for (let p = x.Source.Parent; p && !owner; p = p.Parent)
|
|
382
|
+
owner = bySource.get(p);
|
|
383
|
+
if (!owner || owner.Label !== x.Label)
|
|
384
|
+
continue;
|
|
385
|
+
if (owner.CanInteract)
|
|
386
|
+
said.add(x);
|
|
387
|
+
else
|
|
388
|
+
owner.Label = undefined;
|
|
389
|
+
}
|
|
390
|
+
return said.size ? list.filter((x) => !said.has(x)) : list;
|
|
391
|
+
}
|
|
356
392
|
// ---- group helpers (duck-typed layouts and scrolls) ----
|
|
357
393
|
/** Children of a control (a layout's Views, a scroll's Content). */
|
|
358
394
|
function ViewsOf(c) {
|
|
@@ -383,11 +419,18 @@ function AxisOf(group) {
|
|
|
383
419
|
return "Both";
|
|
384
420
|
}
|
|
385
421
|
/** Items per row of a 2D group: Split when set, else the items sharing the row of `item`. */
|
|
386
|
-
|
|
422
|
+
/**
|
|
423
|
+
* Items per row of a 2D group: the Split, else the items on the row of the group's first item. C# counts the focused
|
|
424
|
+
* item's row, which the short last row of a wrap leaves short (Up from it skipped items); DrawnUi.Rust afef66f.
|
|
425
|
+
*/
|
|
426
|
+
function RowLength(group) {
|
|
387
427
|
const l = asLayout(group);
|
|
388
428
|
if (l && l.Split > 1)
|
|
389
429
|
return l.Split;
|
|
390
|
-
const
|
|
430
|
+
const first = ItemAt(group, 0) ?? FirstRealized(group);
|
|
431
|
+
if (!first)
|
|
432
|
+
return 1;
|
|
433
|
+
const top = first.GetAccessibilityPixelRect();
|
|
391
434
|
if (top.Height <= 0)
|
|
392
435
|
return 1;
|
|
393
436
|
const mid = (top.Top + top.Bottom) / 2;
|
|
@@ -416,6 +459,14 @@ function ItemAt(group, index) {
|
|
|
416
459
|
}
|
|
417
460
|
return index >= 0 && index < ViewsOf(group).length ? ViewsOf(group)[index] : undefined;
|
|
418
461
|
}
|
|
462
|
+
/** The realized item with the lowest index (item 0 of a scrolled recycled group may not be realized). */
|
|
463
|
+
function FirstRealized(group) {
|
|
464
|
+
let best;
|
|
465
|
+
for (const v of ViewsOf(group))
|
|
466
|
+
if (!best || IndexOf(group, v) < IndexOf(group, best))
|
|
467
|
+
best = v;
|
|
468
|
+
return best;
|
|
469
|
+
}
|
|
419
470
|
/** Arranged on the canvas: realized and laid out (a templated layout keeps only the cells it draws in use). */
|
|
420
471
|
function IsDrawn(control) {
|
|
421
472
|
const r = control.GetAccessibilityPixelRect();
|
package/dist/core/Canvas.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Surface } from "canvaskit-wasm";
|
|
|
2
2
|
import type { DrawingContext, SkiaControl } from "./SkiaControl";
|
|
3
3
|
import type { AnimatorBase } from "./Animators";
|
|
4
4
|
import { SkiaAccessibilityManager } from "./Accessibility";
|
|
5
|
+
import { HoverManager } from "./Hover";
|
|
5
6
|
import { type Color, type RenderingModeType } from "./Types";
|
|
6
7
|
import { ContextMenuEventArgs, type GesturesMode, SkiaGesturesParameters, TouchActionEventArgs } from "./Gestures";
|
|
7
8
|
/**
|
|
@@ -37,6 +38,8 @@ export declare class Canvas {
|
|
|
37
38
|
WasDrawn?: (sender: Canvas) => void;
|
|
38
39
|
/** Registry + rate-limited snapshot of accessible controls, rendered by the DOM overlay (DrawnUi AccessibilityManager). */
|
|
39
40
|
readonly AccessibilityManager: SkiaAccessibilityManager;
|
|
41
|
+
/** Mouse hover of the controls (drawnui-cross 6m): ReceivesHover / IsHovered / HoverChanged. */
|
|
42
|
+
readonly Hover: HoverManager;
|
|
40
43
|
private content?;
|
|
41
44
|
get Content(): SkiaControl | undefined;
|
|
42
45
|
set Content(value: SkiaControl | undefined);
|
|
@@ -47,6 +50,13 @@ export declare class Canvas {
|
|
|
47
50
|
private frameId;
|
|
48
51
|
private disposed;
|
|
49
52
|
private readonly observer;
|
|
53
|
+
/** The WebGL context is lost: no frames until the browser restores it. */
|
|
54
|
+
private contextLost;
|
|
55
|
+
/**
|
|
56
|
+
* Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
|
|
57
|
+
* replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
|
|
58
|
+
*/
|
|
59
|
+
GpuEpoch: number;
|
|
50
60
|
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
51
61
|
constructor(Element: HTMLCanvasElement, init?: (canvas: Canvas) => void);
|
|
52
62
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
@@ -61,6 +71,17 @@ export declare class Canvas {
|
|
|
61
71
|
* redrawn content lands in the same paint and no blank frame is ever shown while dragging.
|
|
62
72
|
*/
|
|
63
73
|
private OnResized;
|
|
74
|
+
/** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
|
|
75
|
+
private readonly onContextLost;
|
|
76
|
+
/**
|
|
77
|
+
* WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
|
|
78
|
+
* gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
|
|
79
|
+
* any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
|
|
80
|
+
* `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
|
|
81
|
+
* GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
|
|
82
|
+
* context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
|
|
83
|
+
*/
|
|
84
|
+
private readonly onContextRestored;
|
|
64
85
|
/** Draws one frame immediately (outside the rAF loop) and presents it. */
|
|
65
86
|
private DrawNow;
|
|
66
87
|
private Draw;
|
|
@@ -108,6 +129,8 @@ export declare class Canvas {
|
|
|
108
129
|
private readonly previousTouchArgs;
|
|
109
130
|
private readonly pendingGestures;
|
|
110
131
|
private readonly onPointer;
|
|
132
|
+
/** The mouse left the canvas: hover ends at once, also while content moves. */
|
|
133
|
+
private readonly onPointerLeave;
|
|
111
134
|
private readonly preventTouch;
|
|
112
135
|
/** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
|
|
113
136
|
private readonly claimedTouches;
|
package/dist/core/Canvas.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Super } from "./Super";
|
|
2
2
|
import { SkiaAccessibilityManager } from "./Accessibility";
|
|
3
|
+
import { HoverManager } from "./Hover";
|
|
3
4
|
import { Colors, SKRect } from "./Types";
|
|
4
5
|
import { ContextMenuEventArgs, GestureEventProcessingInfo, PointerData, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
|
|
5
6
|
/**
|
|
@@ -35,6 +36,8 @@ export class Canvas {
|
|
|
35
36
|
WasDrawn;
|
|
36
37
|
/** Registry + rate-limited snapshot of accessible controls, rendered by the DOM overlay (DrawnUi AccessibilityManager). */
|
|
37
38
|
AccessibilityManager = new SkiaAccessibilityManager();
|
|
39
|
+
/** Mouse hover of the controls (drawnui-cross 6m): ReceivesHover / IsHovered / HoverChanged. */
|
|
40
|
+
Hover = new HoverManager();
|
|
38
41
|
content;
|
|
39
42
|
get Content() { return this.content; }
|
|
40
43
|
set Content(value) {
|
|
@@ -45,6 +48,7 @@ export class Canvas {
|
|
|
45
48
|
value.Parent = undefined;
|
|
46
49
|
value._superview = this;
|
|
47
50
|
}
|
|
51
|
+
this.Hover.RequestCheck();
|
|
48
52
|
this.Update();
|
|
49
53
|
}
|
|
50
54
|
surface;
|
|
@@ -54,19 +58,28 @@ export class Canvas {
|
|
|
54
58
|
frameId = 0;
|
|
55
59
|
disposed = false;
|
|
56
60
|
observer;
|
|
61
|
+
/** The WebGL context is lost: no frames until the browser restores it. */
|
|
62
|
+
contextLost = false;
|
|
63
|
+
/**
|
|
64
|
+
* Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
|
|
65
|
+
* replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
|
|
66
|
+
*/
|
|
67
|
+
GpuEpoch = 0;
|
|
57
68
|
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
58
69
|
constructor(Element, init) {
|
|
59
70
|
this.Element = Element;
|
|
60
71
|
if (!Super.CK)
|
|
61
72
|
throw new Error("DrawnUi: call Super.UseDrawnUi()...BuildAsync() before creating a Canvas");
|
|
62
73
|
init?.(this);
|
|
74
|
+
Element.addEventListener("webglcontextlost", this.onContextLost);
|
|
75
|
+
Element.addEventListener("webglcontextrestored", this.onContextRestored);
|
|
63
76
|
this.observer = new ResizeObserver(() => this.OnResized());
|
|
64
77
|
this.observer.observe(Element);
|
|
65
78
|
this.OnResized();
|
|
66
79
|
}
|
|
67
80
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
68
81
|
Update() {
|
|
69
|
-
if (this.frameId || !this.surface || this.disposed)
|
|
82
|
+
if (this.frameId || !this.surface || this.disposed || this.contextLost)
|
|
70
83
|
return;
|
|
71
84
|
const surface = this.surface;
|
|
72
85
|
this.frameId = surface.requestAnimationFrame((c) => {
|
|
@@ -117,16 +130,53 @@ export class Canvas {
|
|
|
117
130
|
this.RenderingScale = dpr;
|
|
118
131
|
this.Element.width = w;
|
|
119
132
|
this.Element.height = h;
|
|
133
|
+
if (this.contextLost)
|
|
134
|
+
return; // the restore makes the surface at this size
|
|
120
135
|
this.ReleaseSurface();
|
|
121
136
|
this.surface = this.CreateSurface(w, h);
|
|
122
137
|
if (!this.surface)
|
|
123
138
|
throw new Error("DrawnUi: cannot create surface");
|
|
124
139
|
this.DrawNow();
|
|
125
140
|
}
|
|
141
|
+
/** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
|
|
142
|
+
onContextLost = (e) => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
this.contextLost = true;
|
|
145
|
+
if (this.frameId) {
|
|
146
|
+
cancelAnimationFrame(this.frameId);
|
|
147
|
+
this.frameId = 0;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
|
|
152
|
+
* gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
|
|
153
|
+
* any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
|
|
154
|
+
* `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
|
|
155
|
+
* GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
|
|
156
|
+
* context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
|
|
157
|
+
*/
|
|
158
|
+
onContextRestored = () => {
|
|
159
|
+
if (this.disposed)
|
|
160
|
+
return;
|
|
161
|
+
if (this.grContext)
|
|
162
|
+
this.grContext.releaseResourcesAndAbandonContext();
|
|
163
|
+
this.surface?.delete();
|
|
164
|
+
this.surface = undefined;
|
|
165
|
+
this.grContext?.delete();
|
|
166
|
+
this.grContext = undefined;
|
|
167
|
+
if (this.glHandle) {
|
|
168
|
+
Super.CK.deleteContext(this.glHandle);
|
|
169
|
+
this.glHandle = undefined;
|
|
170
|
+
}
|
|
171
|
+
this.contextLost = false;
|
|
172
|
+
this.GpuEpoch++;
|
|
173
|
+
this.surface = this.CreateSurface(this.Element.width, this.Element.height);
|
|
174
|
+
this.DrawNow();
|
|
175
|
+
};
|
|
126
176
|
/** Draws one frame immediately (outside the rAF loop) and presents it. */
|
|
127
177
|
DrawNow() {
|
|
128
178
|
const surface = this.surface;
|
|
129
|
-
if (!surface || this.disposed)
|
|
179
|
+
if (!surface || this.disposed || this.contextLost)
|
|
130
180
|
return;
|
|
131
181
|
this.Draw(surface.getCanvas());
|
|
132
182
|
surface.flush();
|
|
@@ -155,6 +205,7 @@ export class Canvas {
|
|
|
155
205
|
root.Render({ Context: { Canvas: canvas, Surface: this.surface }, Destination: new SKRect(0, 0, w, h), Scale: scale });
|
|
156
206
|
}
|
|
157
207
|
this.AccessibilityManager.OnFrameEnd(this.RenderingScale, this.Element.width, this.Element.height, () => this.Update());
|
|
208
|
+
this.Hover.AfterFrame(this.content, this.activeTouchIds.size > 0);
|
|
158
209
|
const now = performance.now();
|
|
159
210
|
this.FrameTime = now - started;
|
|
160
211
|
this.FrameIndex++;
|
|
@@ -260,6 +311,8 @@ export class Canvas {
|
|
|
260
311
|
}
|
|
261
312
|
Dispose() {
|
|
262
313
|
this.disposed = true;
|
|
314
|
+
this.Element.removeEventListener("webglcontextlost", this.onContextLost);
|
|
315
|
+
this.Element.removeEventListener("webglcontextrestored", this.onContextRestored);
|
|
263
316
|
this.Gestures = "Disabled";
|
|
264
317
|
this.observer.disconnect();
|
|
265
318
|
this.content?.Dispose();
|
|
@@ -296,10 +349,14 @@ export class Canvas {
|
|
|
296
349
|
if (!type)
|
|
297
350
|
return;
|
|
298
351
|
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
|
|
352
|
+
if (e.pointerType === "touch")
|
|
353
|
+
return; // touch never hovers
|
|
299
354
|
if (e.pointerType === "mouse")
|
|
300
355
|
this.UpdateCursor(e.offsetX, e.offsetY);
|
|
356
|
+
if (this.activeTouchIds.size === 0)
|
|
357
|
+
this.Hover.Move(this.content, e.offsetX * this.RenderingScale, e.offsetY * this.RenderingScale);
|
|
301
358
|
return;
|
|
302
|
-
}
|
|
359
|
+
}
|
|
303
360
|
if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
|
|
304
361
|
return; // Up of a pointer that never pressed here
|
|
305
362
|
// Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
|
|
@@ -329,6 +386,9 @@ export class Canvas {
|
|
|
329
386
|
this.UpdateTouchAction();
|
|
330
387
|
}
|
|
331
388
|
};
|
|
389
|
+
/** The mouse left the canvas: hover ends at once, also while content moves. */
|
|
390
|
+
onPointerLeave = (e) => { if (e.pointerType !== "touch")
|
|
391
|
+
this.Hover.Leave(); };
|
|
332
392
|
preventTouch = (e) => e.preventDefault();
|
|
333
393
|
/** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
|
|
334
394
|
claimedTouches = new Set();
|
|
@@ -471,6 +531,7 @@ export class Canvas {
|
|
|
471
531
|
el.style.userSelect = "none";
|
|
472
532
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
473
533
|
el.addEventListener(t, this.onPointer);
|
|
534
|
+
el.addEventListener("pointerleave", this.onPointerLeave);
|
|
474
535
|
el.addEventListener("contextmenu", this.onContextMenu);
|
|
475
536
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
476
537
|
if (this.gestures === "Lock")
|
|
@@ -487,6 +548,7 @@ export class Canvas {
|
|
|
487
548
|
el.style.userSelect = "";
|
|
488
549
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
489
550
|
el.removeEventListener(t, this.onPointer);
|
|
551
|
+
el.removeEventListener("pointerleave", this.onPointerLeave);
|
|
490
552
|
el.removeEventListener("contextmenu", this.onContextMenu);
|
|
491
553
|
el.removeEventListener("wheel", this.onWheel);
|
|
492
554
|
el.removeEventListener("touchmove", this.preventTouch);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SkiaControl } from "./SkiaControl";
|
|
2
|
+
/**
|
|
3
|
+
* DrawnUI hover (drawnui-cross 6m, C# DrawnView ReportHover / CommitHover): every control under the mouse that takes
|
|
4
|
+
* hover (`ReceivesHover`) is hovered, a card and the button inside it alike, as CSS :hover. Decided from the hit path
|
|
5
|
+
* of one pointer position, by the routing rules of ProcessGestures (visible, not InputTransparent, inside, children
|
|
6
|
+
* not locked, nothing below a control that blocks gestures below).
|
|
7
|
+
* While content moves under the pointer (a scroll glide, fling or pan, a carousel or drawer transition) hover is not
|
|
8
|
+
* tracked and what was hovered stays; one check at the last pointer position follows when it stops, and after
|
|
9
|
+
* controls are added or removed, a cell is rebound, a hovered control is hidden or a scroll jumps. Leaving the canvas
|
|
10
|
+
* clears hover at once. Touch never hovers. No allocation on frames where nothing changes.
|
|
11
|
+
*/
|
|
12
|
+
export declare class HoverManager {
|
|
13
|
+
/** The hovered controls, root first. */
|
|
14
|
+
readonly Hovered: SkiaControl[];
|
|
15
|
+
private readonly next;
|
|
16
|
+
private readonly path;
|
|
17
|
+
private readonly movers;
|
|
18
|
+
private hasPointer;
|
|
19
|
+
private x;
|
|
20
|
+
private y;
|
|
21
|
+
private paused;
|
|
22
|
+
private check;
|
|
23
|
+
/** The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. */
|
|
24
|
+
Move(root: SkiaControl | undefined, x: number, y: number): void;
|
|
25
|
+
/** The pointer left the canvas: nothing is hovered, also while content moves. */
|
|
26
|
+
Leave(): void;
|
|
27
|
+
/** The controls under a still pointer may have changed: checked once at the end of the frame. */
|
|
28
|
+
RequestCheck(): void;
|
|
29
|
+
/** A control that moves its content (MovesContent) while drawn: hover waits while any of them moves. */
|
|
30
|
+
RegisterMover(control: SkiaControl): void;
|
|
31
|
+
/** End of a frame: pause while content moves, else run a pending check (at most one a frame, none while pressed). */
|
|
32
|
+
AfterFrame(root: SkiaControl | undefined, pressed: boolean): void;
|
|
33
|
+
private static Shown;
|
|
34
|
+
private Update;
|
|
35
|
+
/** `next` becomes the hovered set: the ones that left get HoverChanged(false), then the new ones (true). */
|
|
36
|
+
private Apply;
|
|
37
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { SKPoint } from "./Gestures";
|
|
2
|
+
/**
|
|
3
|
+
* DrawnUI hover (drawnui-cross 6m, C# DrawnView ReportHover / CommitHover): every control under the mouse that takes
|
|
4
|
+
* hover (`ReceivesHover`) is hovered, a card and the button inside it alike, as CSS :hover. Decided from the hit path
|
|
5
|
+
* of one pointer position, by the routing rules of ProcessGestures (visible, not InputTransparent, inside, children
|
|
6
|
+
* not locked, nothing below a control that blocks gestures below).
|
|
7
|
+
* While content moves under the pointer (a scroll glide, fling or pan, a carousel or drawer transition) hover is not
|
|
8
|
+
* tracked and what was hovered stays; one check at the last pointer position follows when it stops, and after
|
|
9
|
+
* controls are added or removed, a cell is rebound, a hovered control is hidden or a scroll jumps. Leaving the canvas
|
|
10
|
+
* clears hover at once. Touch never hovers. No allocation on frames where nothing changes.
|
|
11
|
+
*/
|
|
12
|
+
export class HoverManager {
|
|
13
|
+
/** The hovered controls, root first. */
|
|
14
|
+
Hovered = [];
|
|
15
|
+
next = [];
|
|
16
|
+
path = [];
|
|
17
|
+
movers = [];
|
|
18
|
+
hasPointer = false;
|
|
19
|
+
x = 0;
|
|
20
|
+
y = 0;
|
|
21
|
+
paused = false;
|
|
22
|
+
check = false;
|
|
23
|
+
/** The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. */
|
|
24
|
+
Move(root, x, y) {
|
|
25
|
+
this.hasPointer = true;
|
|
26
|
+
this.x = x;
|
|
27
|
+
this.y = y;
|
|
28
|
+
if (this.paused)
|
|
29
|
+
return; // content moves under the pointer: what was hovered stays, one check when it stops
|
|
30
|
+
this.check = false;
|
|
31
|
+
this.Update(root);
|
|
32
|
+
}
|
|
33
|
+
/** The pointer left the canvas: nothing is hovered, also while content moves. */
|
|
34
|
+
Leave() {
|
|
35
|
+
this.hasPointer = false;
|
|
36
|
+
this.check = false;
|
|
37
|
+
this.next.length = 0;
|
|
38
|
+
this.Apply();
|
|
39
|
+
}
|
|
40
|
+
/** The controls under a still pointer may have changed: checked once at the end of the frame. */
|
|
41
|
+
RequestCheck() { if (this.hasPointer)
|
|
42
|
+
this.check = true; }
|
|
43
|
+
/** A control that moves its content (MovesContent) while drawn: hover waits while any of them moves. */
|
|
44
|
+
RegisterMover(control) { if (this.movers.indexOf(control) < 0)
|
|
45
|
+
this.movers.push(control); }
|
|
46
|
+
/** End of a frame: pause while content moves, else run a pending check (at most one a frame, none while pressed). */
|
|
47
|
+
AfterFrame(root, pressed) {
|
|
48
|
+
let moving = false;
|
|
49
|
+
for (let i = this.movers.length - 1; i >= 0; i--) {
|
|
50
|
+
const m = this.movers[i];
|
|
51
|
+
if (m.IsDisposed || !m.Superview) {
|
|
52
|
+
this.movers.splice(i, 1);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!moving && m.IsVisible && m.MovesContent())
|
|
56
|
+
moving = true;
|
|
57
|
+
}
|
|
58
|
+
if (moving) {
|
|
59
|
+
this.paused = true;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (this.paused) {
|
|
63
|
+
this.paused = false;
|
|
64
|
+
this.check = true;
|
|
65
|
+
}
|
|
66
|
+
// a hovered control (or an ancestor) hidden or removed under a still pointer stops being hovered
|
|
67
|
+
for (let i = 0; i < this.Hovered.length && !this.check; i++)
|
|
68
|
+
if (!HoverManager.Shown(this.Hovered[i]))
|
|
69
|
+
this.check = true;
|
|
70
|
+
if (!this.check)
|
|
71
|
+
return;
|
|
72
|
+
this.check = false;
|
|
73
|
+
if (!this.hasPointer || pressed)
|
|
74
|
+
return;
|
|
75
|
+
this.Update(root);
|
|
76
|
+
}
|
|
77
|
+
static Shown(control) {
|
|
78
|
+
if (control.IsDisposed || !control.Superview)
|
|
79
|
+
return false;
|
|
80
|
+
for (let c = control; c; c = c.Parent)
|
|
81
|
+
if (!c.IsVisible)
|
|
82
|
+
return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
Update(root) {
|
|
86
|
+
const path = this.path, next = this.next;
|
|
87
|
+
path.length = 0;
|
|
88
|
+
next.length = 0;
|
|
89
|
+
if (root && root.IsVisible && !root.InputTransparent) {
|
|
90
|
+
const point = root.TransformPointToLocalSpace(new SKPoint(this.x, this.y));
|
|
91
|
+
if (root.HitIsInside(point.X, point.Y))
|
|
92
|
+
root.CollectHovered(point, path);
|
|
93
|
+
}
|
|
94
|
+
for (const c of path)
|
|
95
|
+
if (c.ReceivesHover)
|
|
96
|
+
next.push(c);
|
|
97
|
+
this.Apply();
|
|
98
|
+
}
|
|
99
|
+
/** `next` becomes the hovered set: the ones that left get HoverChanged(false), then the new ones (true). */
|
|
100
|
+
Apply() {
|
|
101
|
+
const now = this.Hovered, next = this.next;
|
|
102
|
+
if (now.length === next.length) {
|
|
103
|
+
let same = true;
|
|
104
|
+
for (let i = 0; i < now.length && same; i++)
|
|
105
|
+
same = now[i] === next[i];
|
|
106
|
+
if (same)
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const was = now.splice(0);
|
|
110
|
+
now.push(...next);
|
|
111
|
+
for (const c of was)
|
|
112
|
+
if (now.indexOf(c) < 0)
|
|
113
|
+
c.ApplyHover(false);
|
|
114
|
+
for (const c of now)
|
|
115
|
+
if (was.indexOf(c) < 0)
|
|
116
|
+
c.ApplyHover(true);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -20,6 +20,19 @@ export declare class KeyboardManager {
|
|
|
20
20
|
static AttachToKeyboard(): void;
|
|
21
21
|
static Subscribe(down: (key: InputKey, e: KeyboardEvent) => void, char: (ch: string, e: KeyboardEvent) => void, up?: (key: InputKey, e: KeyboardEvent) => void): void;
|
|
22
22
|
static Unsubscribe(down: (key: InputKey, e: KeyboardEvent) => void, char: (ch: string, e: KeyboardEvent) => void, up?: (key: InputKey, e: KeyboardEvent) => void): void;
|
|
23
|
+
/**
|
|
24
|
+
* Hooks observe keys, they never take them from the element that owns them (C# 845b26e9, GitHub #231). True when a
|
|
25
|
+
* focused DOM element is the key's target: a page input, textarea, select, button, link or contenteditable, an
|
|
26
|
+
* accessibility overlay node, the drawn editor's hidden textarea. False when the key goes to the page itself (body,
|
|
27
|
+
* html) or the canvas. A handler that acts on keys (edits, moves, calls preventDefault) skips owned keys, except
|
|
28
|
+
* those of its own element.
|
|
29
|
+
*/
|
|
30
|
+
static IsOwnedByElement(e: KeyboardEvent): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The key belongs to the input method (an IME composition, a soft keyboard): isComposing, keyCode 229, key "Process"
|
|
33
|
+
* or "Unidentified". Its result arrives as input events (TextInputProxy), never act on the key itself.
|
|
34
|
+
*/
|
|
35
|
+
static IsInputMethodKey(e: KeyboardEvent): boolean;
|
|
23
36
|
/** JS blurExternalTextInput: a page text input outside the canvas would keep receiving the keys. */
|
|
24
37
|
static BlurExternalTextInput(): void;
|
|
25
38
|
}
|
|
@@ -45,6 +45,24 @@ export class KeyboardManager {
|
|
|
45
45
|
if (up)
|
|
46
46
|
KeyboardManager.keyUp.delete(up);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Hooks observe keys, they never take them from the element that owns them (C# 845b26e9, GitHub #231). True when a
|
|
50
|
+
* focused DOM element is the key's target: a page input, textarea, select, button, link or contenteditable, an
|
|
51
|
+
* accessibility overlay node, the drawn editor's hidden textarea. False when the key goes to the page itself (body,
|
|
52
|
+
* html) or the canvas. A handler that acts on keys (edits, moves, calls preventDefault) skips owned keys, except
|
|
53
|
+
* those of its own element.
|
|
54
|
+
*/
|
|
55
|
+
static IsOwnedByElement(e) {
|
|
56
|
+
const tag = e.target?.tagName;
|
|
57
|
+
return !!tag && tag !== "BODY" && tag !== "HTML" && tag !== "CANVAS";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The key belongs to the input method (an IME composition, a soft keyboard): isComposing, keyCode 229, key "Process"
|
|
61
|
+
* or "Unidentified". Its result arrives as input events (TextInputProxy), never act on the key itself.
|
|
62
|
+
*/
|
|
63
|
+
static IsInputMethodKey(e) {
|
|
64
|
+
return e.isComposing || e.keyCode === 229 || e.key === "Process" || e.key === "Unidentified";
|
|
65
|
+
}
|
|
48
66
|
/** JS blurExternalTextInput: a page text input outside the canvas would keep receiving the keys. */
|
|
49
67
|
static BlurExternalTextInput() {
|
|
50
68
|
const a = typeof document !== "undefined" ? document.activeElement : null;
|