drawnui-react 0.1.0-preview.15 → 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 (39) 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 +5 -0
  27. package/dist/core/Canvas.js +15 -1
  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 +85 -1
  33. package/dist/core/SkiaControl.js +217 -16
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +1 -0
  36. package/dist/react/index.d.ts +1 -1
  37. package/dist/react/index.js +2 -2
  38. package/dist/react/reconciler.js +29 -10
  39. 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);
@@ -126,6 +129,8 @@ export declare class Canvas {
126
129
  private readonly previousTouchArgs;
127
130
  private readonly pendingGestures;
128
131
  private readonly onPointer;
132
+ /** The mouse left the canvas: hover ends at once, also while content moves. */
133
+ private readonly onPointerLeave;
129
134
  private readonly preventTouch;
130
135
  /** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
131
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;
@@ -201,6 +205,7 @@ export class Canvas {
201
205
  root.Render({ Context: { Canvas: canvas, Surface: this.surface }, Destination: new SKRect(0, 0, w, h), Scale: scale });
202
206
  }
203
207
  this.AccessibilityManager.OnFrameEnd(this.RenderingScale, this.Element.width, this.Element.height, () => this.Update());
208
+ this.Hover.AfterFrame(this.content, this.activeTouchIds.size > 0);
204
209
  const now = performance.now();
205
210
  this.FrameTime = now - started;
206
211
  this.FrameIndex++;
@@ -344,10 +349,14 @@ export class Canvas {
344
349
  if (!type)
345
350
  return;
346
351
  if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
352
+ if (e.pointerType === "touch")
353
+ return; // touch never hovers
347
354
  if (e.pointerType === "mouse")
348
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);
349
358
  return;
350
- } // hover not ported (TouchActionResult.Pointer)
359
+ }
351
360
  if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
352
361
  return; // Up of a pointer that never pressed here
353
362
  // Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
@@ -377,6 +386,9 @@ export class Canvas {
377
386
  this.UpdateTouchAction();
378
387
  }
379
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(); };
380
392
  preventTouch = (e) => e.preventDefault();
381
393
  /** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
382
394
  claimedTouches = new Set();
@@ -519,6 +531,7 @@ export class Canvas {
519
531
  el.style.userSelect = "none";
520
532
  for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
521
533
  el.addEventListener(t, this.onPointer);
534
+ el.addEventListener("pointerleave", this.onPointerLeave);
522
535
  el.addEventListener("contextmenu", this.onContextMenu);
523
536
  el.addEventListener("wheel", this.onWheel, { passive: false });
524
537
  if (this.gestures === "Lock")
@@ -535,6 +548,7 @@ export class Canvas {
535
548
  el.style.userSelect = "";
536
549
  for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
537
550
  el.removeEventListener(t, this.onPointer);
551
+ el.removeEventListener("pointerleave", this.onPointerLeave);
538
552
  el.removeEventListener("contextmenu", this.onContextMenu);
539
553
  el.removeEventListener("wheel", this.onWheel);
540
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;
@@ -138,18 +138,43 @@ export declare class SkiaControl {
138
138
  private compositeSurface?;
139
139
  /** Own content or structure changed: the next composite record is a full one. */
140
140
  private compositeFull;
141
+ /** Where the kept composite surface starts relative to DrawingRect (the effects margin it was laid out with). */
142
+ private compositeInsetX;
143
+ private compositeInsetY;
141
144
  /** Canvas-pixel bounds this control covered when its parent last recorded it (composite erase region). */
142
145
  private lastCompositeBounds?;
143
- /** What the last composite record did (diagnostics): "full" or "partial", the number of children re-recorded and which ones. */
146
+ /**
147
+ * What the last composite record did (diagnostics): "full" or "partial", the number of children re-recorded and which
148
+ * ones; Areas: the pixels erased and drawn again for changes deeper than a child (C# Areas), Changed: the controls
149
+ * those areas belong to (C# Changed).
150
+ */
144
151
  LastCompositeRecord: {
145
152
  Mode: "full" | "partial";
146
153
  Children: number;
147
154
  Dirty: readonly SkiaControl[];
155
+ Areas: readonly SKRect[];
156
+ Changed: readonly SkiaControl[];
148
157
  };
158
+ /** C# MaxCompositionAreas: more areas than this in one composite record draw it whole instead. */
159
+ static MaxCompositionAreas: number;
160
+ /** C# MaxCompositionShare: areas covering more than this share of a composite draw it whole instead. */
161
+ static MaxCompositionShare: number;
162
+ /**
163
+ * Changes deeper than a direct child since the last composite record (drawnui-cross 6m): the control, the direct child
164
+ * it is in, where it was drawn before the change (its own drawn bounds, as drawn) and its rect then.
165
+ */
166
+ private compositeDeep?;
167
+ /** Set while an UpdateDraw climbs: the ancestors re-measure but an ImageComposite among them keeps its partial record. */
168
+ private static drawOrigin;
169
+ /** Nonzero while a composite redraws only some areas: layouts skip children entirely outside the clip. */
170
+ static CompositionCulling: number;
149
171
  /** The children a composite record can re-record individually; layouts return their views. */
150
172
  protected GetCompositeChildren(): readonly SkiaControl[];
151
173
  /** C# TrackChildAsDirty (DirtyChildrenTracker): the child changed without a remeasure of this control. */
152
174
  TrackChildAsDirty(child: SkiaControl): void;
175
+ private static readonly rejectRect;
176
+ /** During a partial composite record: true when this control draws nothing inside the canvas clip (skipped whole). */
177
+ OutsideCompositionClip(canvas: SkCanvas): boolean;
153
178
  /** Drawn bounds in canvas pixels including transforms and effects margins (C# GetTransformedDirtyBounds). */
154
179
  GetTransformedDirtyBounds(): SKRect;
155
180
  /** Current cache, if any. */
@@ -160,6 +185,36 @@ export declare class SkiaControl {
160
185
  /** Consume every gesture that lands on this control so nothing below (z-order) receives it. */
161
186
  BlockGesturesBelow: boolean;
162
187
  LockChildrenGestures: LockTouch;
188
+ private receivesHover?;
189
+ /**
190
+ * Whether this control takes mouse hover: the canvas sets IsHovered while the mouse is over it. Opt-in: false by
191
+ * default, true by default only in the controls that always had hover (SkiaButton, SkiaSlider, the toggles,
192
+ * SkiaRadioButton, SkiaCarousel, SkiaDrawer), never because a control has a tap handler. A HoverChanged handler turns
193
+ * it on (C# fluent OnHovered) unless it is set.
194
+ */
195
+ get ReceivesHover(): boolean;
196
+ set ReceivesHover(value: boolean);
197
+ /** The value of ReceivesHover until it is set. */
198
+ protected ReceivesHoverByDefault(): boolean;
199
+ private isHovered;
200
+ /**
201
+ * True while the mouse is over this control and it takes hover. Every such control under the pointer is hovered at
202
+ * once (a card and the button inside it, as CSS :hover); it stays as it is while content moves under the pointer and
203
+ * is checked again when that stops. Touch never hovers.
204
+ */
205
+ get IsHovered(): boolean;
206
+ /** IsHovered changed: true when the mouse came over this control, false when it left (C# HoverChanged / OnHovered). */
207
+ HoverChanged?: (sender: SkiaControl, hovered: boolean) => void;
208
+ /** Set by the canvas's HoverManager. */
209
+ ApplyHover(on: boolean): void;
210
+ /** A scroll, carousel or drawer moving its content right now: hover waits until it stops (C# IsScrolling / InTransition). */
211
+ MovesContent(): boolean;
212
+ /**
213
+ * Hover hit path (HoverManager): this control is under the point (in its own space), its children under it follow,
214
+ * top-most first, by the routing rules of ProcessGestures. True when this subtree blocks gestures below it, so the
215
+ * controls under it get no hover (a popup over a list).
216
+ */
217
+ CollectHovered(point: SKPoint, into: SkiaControl[]): boolean;
163
218
  TouchEffectColor: Color;
164
219
  /** Plays a ripple (from the tap point) when Tapped fires. */
165
220
  AnimationTapped: SkiaTouchAnimation;
@@ -211,6 +266,11 @@ export declare class SkiaControl {
211
266
  * built in code-behind is styled on its first measure and keeps whatever the code already set.
212
267
  */
213
268
  ApplyInitialStyles(atConstruction?: boolean): void;
269
+ /**
270
+ * C# IsChildrenItem: added by the app (JSX children or the `Children` list), not a part the control made itself. A new
271
+ * `Children` list drops only these, so a control's own parts keep their place.
272
+ */
273
+ IsChildrenItem: boolean;
214
274
  AddSubView(_control: SkiaControl): void;
215
275
  InsertSubView(_index: number, control: SkiaControl): void;
216
276
  RemoveSubView(_control: SkiaControl): void;
@@ -416,6 +476,30 @@ export declare class SkiaControl {
416
476
  * cache holds its composited output, so those are staled before redrawing (DrawnUi RedrawCanvas + parent invalidation).
417
477
  */
418
478
  RepaintComposition(): void;
479
+ /**
480
+ * Update() for a change of how this control looks, not of its size (a color set through props, C# NeedDraw): the same
481
+ * re-measure and cache invalidation, but an ImageComposite above redraws only this control's area instead of
482
+ * recording whole (drawnui-cross 6m). The record still goes whole when the control's rect changed after all.
483
+ */
484
+ UpdateDraw(): void;
485
+ /**
486
+ * A change of this control that keeps its layout, to every ImageComposite above: a direct child is drawn again whole
487
+ * (C# TrackChildAsDirty), a deeper control by its area, read now as it was drawn (its last matrix and effects margin).
488
+ */
489
+ private ReportToComposites;
490
+ /** The matrix this control will be drawn with (Render), from its current properties; undefined when none. */
491
+ private CurrentMatrix;
492
+ /** Where this control will be drawn: its rect grown by its effects margin, through its current matrix. */
493
+ private DrawnBoundsNow;
494
+ private static MapRect;
495
+ /** Its output depends on all of its content (a visual effect; a backdrop samples below): a change inside redraws it whole. */
496
+ protected get SamplesContent(): boolean;
497
+ /**
498
+ * Where a deep change shows inside the composite (drawnui-cross 6m rule b): the control as drawn before and as drawn
499
+ * now, then through the transform of every ancestor up to the composite. undefined: draw the child whole (an effect
500
+ * or a backdrop on the way, the control gone from the child); null: the layout changed, record whole.
501
+ */
502
+ private static DeepArea;
419
503
  /**
420
504
  * DrawnUi IsParentIndependent: this control's size never changes the layout around it, so its own invalidation
421
505
  * stops here (ancestors composite again, nothing above re-measures). The parent re-measures it on draw when