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.
Files changed (41) hide show
  1. package/SKIPPED.md +5 -4
  2. package/dist/controls/SkiaBackdrop.d.ts +2 -0
  3. package/dist/controls/SkiaBackdrop.js +2 -0
  4. package/dist/controls/SkiaButton.d.ts +2 -0
  5. package/dist/controls/SkiaButton.js +2 -0
  6. package/dist/controls/SkiaCarousel.d.ts +2 -0
  7. package/dist/controls/SkiaCarousel.js +2 -0
  8. package/dist/controls/SkiaDrawer.d.ts +2 -0
  9. package/dist/controls/SkiaDrawer.js +2 -0
  10. package/dist/controls/SkiaEditor.js +10 -1
  11. package/dist/controls/SkiaLabel.d.ts +2 -0
  12. package/dist/controls/SkiaLabel.js +13 -2
  13. package/dist/controls/SkiaLayout.d.ts +15 -0
  14. package/dist/controls/SkiaLayout.js +75 -6
  15. package/dist/controls/SkiaProgress.js +2 -2
  16. package/dist/controls/SkiaScroll.d.ts +4 -0
  17. package/dist/controls/SkiaScroll.js +9 -0
  18. package/dist/controls/SkiaSlider.d.ts +2 -0
  19. package/dist/controls/SkiaSlider.js +5 -3
  20. package/dist/controls/SkiaToggle.d.ts +2 -0
  21. package/dist/controls/SkiaToggle.js +3 -1
  22. package/dist/controls/SnappingLayout.d.ts +2 -0
  23. package/dist/controls/SnappingLayout.js +3 -0
  24. package/dist/core/Accessibility.d.ts +8 -0
  25. package/dist/core/Accessibility.js +56 -5
  26. package/dist/core/Canvas.d.ts +23 -0
  27. package/dist/core/Canvas.js +65 -3
  28. package/dist/core/Hover.d.ts +37 -0
  29. package/dist/core/Hover.js +118 -0
  30. package/dist/core/KeyboardManager.d.ts +13 -0
  31. package/dist/core/KeyboardManager.js +18 -0
  32. package/dist/core/SkiaControl.d.ts +87 -1
  33. package/dist/core/SkiaControl.js +229 -18
  34. package/dist/core/SkiaEffect.d.ts +3 -0
  35. package/dist/core/SkiaEffect.js +8 -3
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.js +1 -0
  38. package/dist/react/index.d.ts +1 -1
  39. package/dist/react/index.js +2 -2
  40. package/dist/react/reconciler.js +29 -10
  41. 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, item) : 1;
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
- const list = [];
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
- function RowLength(group, item) {
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 top = item.GetAccessibilityPixelRect();
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();
@@ -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;
@@ -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
- } // hover not ported (TouchActionResult.Pointer)
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;