drawnui-react 0.1.0-preview.15 → 0.1.0-preview.17

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 (64) hide show
  1. package/README.md +1 -1
  2. package/SKIPPED.md +9 -8
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +4 -1
  6. package/dist/controls/DrawnGame.js +7 -3
  7. package/dist/controls/GridStructure.d.ts +13 -1
  8. package/dist/controls/GridStructure.js +38 -2
  9. package/dist/controls/SkiaBackdrop.d.ts +2 -0
  10. package/dist/controls/SkiaBackdrop.js +2 -0
  11. package/dist/controls/SkiaButton.d.ts +2 -0
  12. package/dist/controls/SkiaButton.js +2 -0
  13. package/dist/controls/SkiaCarousel.d.ts +3 -1
  14. package/dist/controls/SkiaCarousel.js +5 -2
  15. package/dist/controls/SkiaDrawer.d.ts +2 -0
  16. package/dist/controls/SkiaDrawer.js +2 -0
  17. package/dist/controls/SkiaEditor.d.ts +5 -1
  18. package/dist/controls/SkiaEditor.js +29 -18
  19. package/dist/controls/SkiaGif.d.ts +3 -0
  20. package/dist/controls/SkiaGif.js +11 -1
  21. package/dist/controls/SkiaImage.js +15 -10
  22. package/dist/controls/SkiaLabel.d.ts +54 -3
  23. package/dist/controls/SkiaLabel.js +235 -52
  24. package/dist/controls/SkiaLayout.d.ts +20 -0
  25. package/dist/controls/SkiaLayout.js +78 -6
  26. package/dist/controls/SkiaLottie.js +3 -1
  27. package/dist/controls/SkiaProgress.d.ts +2 -1
  28. package/dist/controls/SkiaProgress.js +6 -3
  29. package/dist/controls/SkiaScroll.d.ts +16 -0
  30. package/dist/controls/SkiaScroll.js +85 -8
  31. package/dist/controls/SkiaShape.d.ts +13 -2
  32. package/dist/controls/SkiaShape.js +50 -10
  33. package/dist/controls/SkiaSlider.d.ts +7 -1
  34. package/dist/controls/SkiaSlider.js +12 -4
  35. package/dist/controls/SkiaSprite.d.ts +1 -0
  36. package/dist/controls/SkiaSprite.js +4 -0
  37. package/dist/controls/SkiaToggle.d.ts +2 -0
  38. package/dist/controls/SkiaToggle.js +3 -1
  39. package/dist/controls/SnappingLayout.d.ts +7 -0
  40. package/dist/controls/SnappingLayout.js +11 -0
  41. package/dist/core/Accessibility.d.ts +21 -0
  42. package/dist/core/Accessibility.js +66 -9
  43. package/dist/core/Animators.d.ts +28 -1
  44. package/dist/core/Animators.js +76 -8
  45. package/dist/core/Canvas.d.ts +26 -4
  46. package/dist/core/Canvas.js +136 -23
  47. package/dist/core/Easing.d.ts +12 -1
  48. package/dist/core/Easing.js +25 -1
  49. package/dist/core/Gestures.d.ts +24 -1
  50. package/dist/core/Gestures.js +75 -2
  51. package/dist/core/Hover.d.ts +47 -0
  52. package/dist/core/Hover.js +134 -0
  53. package/dist/core/KeyboardManager.d.ts +13 -0
  54. package/dist/core/KeyboardManager.js +18 -0
  55. package/dist/core/ScrollAnimators.js +9 -0
  56. package/dist/core/SkiaControl.d.ts +97 -1
  57. package/dist/core/SkiaControl.js +271 -38
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.js +1 -0
  60. package/dist/react/SkiaShell.js +33 -14
  61. package/dist/react/index.d.ts +1 -1
  62. package/dist/react/index.js +30 -3
  63. package/dist/react/reconciler.js +29 -10
  64. package/package.json +18 -1
@@ -50,6 +50,8 @@ export declare abstract class SnappingLayout extends SkiaLayout {
50
50
  protected StopSnapAnimators(): void;
51
51
  /** C# SnappingLayout.Render: the transition state follows the position every frame. */
52
52
  Render(ctx: DrawingContext): void;
53
+ /** Hover waits while the content slides (C# InTransition) or follows a pan. */
54
+ MovesContent(): boolean;
53
55
  ClampOffset(x: number, y: number, rubber: boolean): SKPoint;
54
56
  /** C# GetContentOffsetBounds: the extent covered by the snap points. */
55
57
  protected BoundsFromSnapPoints(): SKRect;
@@ -59,6 +61,11 @@ export declare abstract class SnappingLayout extends SkiaLayout {
59
61
  protected FindNearestAnchorInternal(current: SKPoint, _velocity: SKPoint): SKPoint;
60
62
  /** The closest anchor lying in the direction of the velocity (dot product), else the origin. */
61
63
  SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
64
+ /**
65
+ * Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
66
+ * anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
67
+ */
68
+ SnapVelocityThreshold: number;
62
69
  ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
63
70
  get CanAnimate(): boolean;
64
71
  protected GetAutoVelocity(displacement: SKPoint): SKPoint;
@@ -80,9 +80,12 @@ export class SnappingLayout extends SkiaLayout {
80
80
  }
81
81
  /** C# SnappingLayout.Render: the transition state follows the position every frame. */
82
82
  Render(ctx) {
83
+ this.Superview?.Hover?.RegisterMover(this);
83
84
  super.Render(ctx);
84
85
  this.InTransition = !this.CheckTransitionEnded();
85
86
  }
87
+ /** Hover waits while the content slides (C# InTransition) or follows a pan. */
88
+ MovesContent() { return this.InTransition || this.IsUserPanning; }
86
89
  // ---- clamping ----
87
90
  ClampOffset(x, y, rubber) {
88
91
  const b = this.ContentOffsetBounds;
@@ -135,9 +138,17 @@ export class SnappingLayout extends SkiaLayout {
135
138
  }
136
139
  return origin;
137
140
  }
141
+ /**
142
+ * Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
143
+ * anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
144
+ */
145
+ SnapVelocityThreshold = 100;
138
146
  ScrollToNearestAnchor(location, velocity) {
139
147
  if (this.SnapPoints.length === 0)
140
148
  return;
149
+ if (Math.abs(velocity.X) < this.SnapVelocityThreshold || Math.abs(velocity.Y) < this.SnapVelocityThreshold) {
150
+ velocity = new SKPoint(Math.abs(velocity.X) < this.SnapVelocityThreshold ? 0 : velocity.X, Math.abs(velocity.Y) < this.SnapVelocityThreshold ? 0 : velocity.Y);
151
+ }
141
152
  const origin = this.FindNearestAnchorInternal(location, velocity);
142
153
  const target = this.SelectNextAnchor(origin, velocity);
143
154
  if (SnappingLayout.Dist(location, target) >= 0.5)
@@ -52,6 +52,12 @@ export declare const Aria: {
52
52
  * its current item, and the arrow keys move between its items.
53
53
  */
54
54
  readonly IsCompositeRole: (role?: string) => boolean;
55
+ /**
56
+ * Roles a user operates (C# Aria.IsInteractiveRole, as DrawnUi.Rust is_control_role): a node with one of them that
57
+ * cannot take input is read as disabled (aria-disabled). C# also lists listbox and scrollbar; here a listbox is an
58
+ * arrow-key group and groups are never disabled.
59
+ */
60
+ readonly IsInteractiveRole: (role?: string) => boolean;
55
61
  };
56
62
  /** One laid-out text line exposed for native selection (AccessibilityTextSelectable): CSS px relative to the node. */
57
63
  export interface AccessibilityTextLine {
@@ -64,6 +70,17 @@ export interface AccessibilityTextLine {
64
70
  FontWeight: number;
65
71
  FontSize: number;
66
72
  }
73
+ /**
74
+ * The value of a range control (a slider, a progress bar): what a screen reader reads and adjusts (drawnui-cross 6c,
75
+ * C# AccessibilityValue): aria-valuenow / min / max, and aria-valuetext when Text is not empty ("65%", "20 – 80").
76
+ */
77
+ export interface AccessibilityValue {
78
+ Now: number;
79
+ Min: number;
80
+ Max: number;
81
+ Step: number;
82
+ Text: string;
83
+ }
67
84
  /** One entry of the accessibility snapshot (DrawnUi AccessibilityNode). Rect is CSS pixels relative to the canvas. */
68
85
  export interface AccessibilityNode {
69
86
  Id: number;
@@ -72,9 +89,13 @@ export interface AccessibilityNode {
72
89
  Role: string;
73
90
  Rect: SKRect;
74
91
  CanInteract: boolean;
92
+ /** A role a user operates (Aria.IsInteractiveRole) that cannot take input: aria-disabled, no tab stop. */
93
+ Disabled: boolean;
75
94
  IsPressed?: boolean;
76
95
  Live?: string;
77
96
  Source: SkiaControl;
97
+ /** A range control's value (aria-valuenow / min / max / valuetext); its name stays the app's label. */
98
+ Value?: AccessibilityValue;
78
99
  /** Real text lines rendered into the overlay so the browser can select / copy them (opt-in per control). */
79
100
  TextLines?: AccessibilityTextLine[];
80
101
  }
@@ -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);
@@ -70,6 +80,7 @@ export class SkiaAccessibilityManager {
70
80
  if (!node || !node.AccessibilityCanInteract)
71
81
  return false;
72
82
  node.OnAccessibilityActivated();
83
+ node.Superview?.AccessibilityManager.ForceRebuildOnNextFrame(); // a screen reader reads the new state right back
73
84
  return true;
74
85
  }
75
86
  /**
@@ -80,8 +91,10 @@ export class SkiaAccessibilityManager {
80
91
  static Key(node, key) {
81
92
  if (!node)
82
93
  return false;
83
- if (node.CanReceiveGesture("Panning") && node.OnAccessibilityKey(key))
94
+ if (node.CanReceiveGesture("Panning") && node.OnAccessibilityKey(key)) {
95
+ node.Superview?.AccessibilityManager.ForceRebuildOnNextFrame(); // the new value is read right back, not a second later
84
96
  return true;
97
+ }
85
98
  return node.Superview?.AccessibilityManager.MoveInGroup(node, key) ?? false;
86
99
  }
87
100
  // ---- arrow-key groups (C# SkiaAccessibilityManager, "Arrow-key groups") ----
@@ -150,7 +163,7 @@ export class SkiaAccessibilityManager {
150
163
  const current = this.groupRequest?.Group === group ? this.groupRequest.Index : IndexOf(group, item);
151
164
  if (current < 0 || count < 1)
152
165
  return false;
153
- const row = axis === "Both" ? RowLength(group, item) : 1;
166
+ const row = axis === "Both" ? RowLength(group) : 1;
154
167
  const vertical = axis !== "Horizontal";
155
168
  const page = () => PageSize(scroll, item, vertical) * row;
156
169
  let target, step;
@@ -245,7 +258,7 @@ export class SkiaAccessibilityManager {
245
258
  for (const cb of this.liveUpdated)
246
259
  cb(node);
247
260
  }
248
- ForceRebuildOnNextFrame() { this.lastRebuild = 0; this.dirty = true; }
261
+ ForceRebuildOnNextFrame() { this.lastRebuild = -Infinity; this.dirty = true; }
249
262
  Unregister(node) {
250
263
  if (this.nodes.delete(node)) {
251
264
  node.OnAccessibilityUnregistered();
@@ -282,7 +295,7 @@ export class SkiaAccessibilityManager {
282
295
  Rebuild(scale, canvasWidthPx, canvasHeightPx) {
283
296
  this.dirty = false;
284
297
  this.lastRebuild = performance.now();
285
- const list = [];
298
+ let list = [];
286
299
  for (const n of this.nodes) {
287
300
  if (!n.Superview) {
288
301
  this.nodes.delete(n);
@@ -301,12 +314,14 @@ export class SkiaAccessibilityManager {
301
314
  if (px.Right < -canvasWidthPx || px.Bottom < -canvasHeightPx || px.Left > 2 * canvasWidthPx || px.Top > 2 * canvasHeightPx)
302
315
  continue;
303
316
  list.push({
304
- Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole,
317
+ Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole, Value: n.AccessibilityValue,
305
318
  Rect: new SKRect(px.Left / scale, px.Top / scale, px.Right / scale, px.Bottom / scale),
306
319
  CanInteract: n.AccessibilityCanInteract, IsPressed: n.AccessibilityIsPressed, Live: n.AccessibilityLive, Source: n,
320
+ Disabled: !n.AccessibilityCanInteract && Aria.IsInteractiveRole(n.AccessibilityRole),
307
321
  TextLines: n.AccessibilityTextSelectable ? n.GetAccessibilityTextLines(scale) : undefined,
308
322
  });
309
323
  }
324
+ list = SayNamesOnce(list);
310
325
  list.sort((a, b) => a.Rect.Top - b.Rect.Top || a.Rect.Left - b.Rect.Left);
311
326
  // reading order: nodes whose tops are within half the smaller height form one row, read left to right, so a row of
312
327
  // vertically centered controls of different heights keeps its visual order (C# RectComparer + LeftComparer)
@@ -334,8 +349,8 @@ export class SkiaAccessibilityManager {
334
349
  return false;
335
350
  for (let i = 0; i < a.length; i++) {
336
351
  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
338
- || x.IsPressed !== y.IsPressed || x.Live !== y.Live
352
+ 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
353
+ || x.IsPressed !== y.IsPressed || x.Live !== y.Live || !SameValue(x.Value, y.Value)
339
354
  || Math.abs(x.Rect.Left - y.Rect.Left) > 0.5 || Math.abs(x.Rect.Top - y.Rect.Top) > 0.5
340
355
  || Math.abs(x.Rect.Right - y.Rect.Right) > 0.5 || Math.abs(x.Rect.Bottom - y.Rect.Bottom) > 0.5)
341
356
  return false;
@@ -353,6 +368,33 @@ export class SkiaAccessibilityManager {
353
368
  return true;
354
369
  }
355
370
  }
371
+ /**
372
+ * Each name is said once in the flat overlay (DrawnUi.Rust said_by_child): a text or a heading whose label repeats the
373
+ * label of the node it is in (its nearest ancestor node) is said by one of them. Under an interactive node (a card
374
+ * button) the child is left out, the button keeps its name; under any other node (a group card and its title) the
375
+ * node loses its name and the heading says it. Selectable text (TextLines) always stays.
376
+ */
377
+ function SayNamesOnce(list) {
378
+ const bySource = new Map(list.map((x) => [x.Source, x]));
379
+ const said = new Set();
380
+ for (const x of list) {
381
+ if (!x.Label || x.TextLines || (x.Role !== Aria.RoleText && x.Role !== Aria.RoleHeading))
382
+ continue;
383
+ let owner;
384
+ for (let p = x.Source.Parent; p && !owner; p = p.Parent)
385
+ owner = bySource.get(p);
386
+ if (!owner || owner.Label !== x.Label)
387
+ continue;
388
+ if (owner.CanInteract)
389
+ said.add(x);
390
+ else
391
+ owner.Label = undefined;
392
+ }
393
+ return said.size ? list.filter((x) => !said.has(x)) : list;
394
+ }
395
+ function SameValue(a, b) {
396
+ return a === b || (!!a && !!b && a.Now === b.Now && a.Min === b.Min && a.Max === b.Max && a.Step === b.Step && a.Text === b.Text);
397
+ }
356
398
  // ---- group helpers (duck-typed layouts and scrolls) ----
357
399
  /** Children of a control (a layout's Views, a scroll's Content). */
358
400
  function ViewsOf(c) {
@@ -383,11 +425,18 @@ function AxisOf(group) {
383
425
  return "Both";
384
426
  }
385
427
  /** Items per row of a 2D group: Split when set, else the items sharing the row of `item`. */
386
- function RowLength(group, item) {
428
+ /**
429
+ * 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
430
+ * item's row, which the short last row of a wrap leaves short (Up from it skipped items); DrawnUi.Rust afef66f.
431
+ */
432
+ function RowLength(group) {
387
433
  const l = asLayout(group);
388
434
  if (l && l.Split > 1)
389
435
  return l.Split;
390
- const top = item.GetAccessibilityPixelRect();
436
+ const first = ItemAt(group, 0) ?? FirstRealized(group);
437
+ if (!first)
438
+ return 1;
439
+ const top = first.GetAccessibilityPixelRect();
391
440
  if (top.Height <= 0)
392
441
  return 1;
393
442
  const mid = (top.Top + top.Bottom) / 2;
@@ -416,6 +465,14 @@ function ItemAt(group, index) {
416
465
  }
417
466
  return index >= 0 && index < ViewsOf(group).length ? ViewsOf(group)[index] : undefined;
418
467
  }
468
+ /** The realized item with the lowest index (item 0 of a scrolled recycled group may not be realized). */
469
+ function FirstRealized(group) {
470
+ let best;
471
+ for (const v of ViewsOf(group))
472
+ if (!best || IndexOf(group, v) < IndexOf(group, best))
473
+ best = v;
474
+ return best;
475
+ }
419
476
  /** Arranged on the canvas: realized and laid out (a templated layout keeps only the cells it draws in use). */
420
477
  function IsDrawn(control) {
421
478
  const r = control.GetAccessibilityPixelRect();
@@ -18,11 +18,28 @@ export declare class AnimatorBase {
18
18
  StartFrameTimeNanos: number;
19
19
  OnStart?: () => void;
20
20
  OnStop?: () => void;
21
+ /** Paused: the canvas does not tick it and it wakes no frames; Resume goes on from where it was (C# Pause / Resume). */
22
+ IsPaused: boolean;
23
+ /** Paused by the canvas because its control is hidden; resumed when it shows again. */
24
+ PausedWhileHidden: boolean;
25
+ /**
26
+ * Frame time (ns) before which nothing it shows changes: the canvas does not tick it and it keeps no frames coming
27
+ * until then, a timer wakes the frame loop at that time (a GIF or a sprite between two frames; DrawnUi.Rust frame
28
+ * animators sleep between changes). 0 = tick every frame.
29
+ */
30
+ SleepUntilNanos: number;
21
31
  private delayHandle;
32
+ private delayEnd;
33
+ private delayLeft;
34
+ private pausedAt;
22
35
  constructor(parent: SkiaControl);
23
36
  protected Register(): boolean;
24
37
  protected Unregister(): void;
25
38
  Start(delayMs?: number): void;
39
+ /** Freezes the run (and what is left of a start delay); Resume goes on from there. */
40
+ Pause(): void;
41
+ /** Goes on from where Pause froze it: the paused time is not counted. */
42
+ Resume(): void;
26
43
  Stop(): void;
27
44
  /** Returns true when there is nothing more to do (not running / finished). */
28
45
  TickFrame(frameTimeNanos: number): boolean;
@@ -55,10 +72,20 @@ export declare class SkiaValueAnimator extends AnimatorBase {
55
72
  TickFrame(frameTimeNanos: number): boolean;
56
73
  /** Passed over mValue; subclasses derive their own reported value here. */
57
74
  protected TransformReportedValue(_deltaT: number): number;
58
- /** Updates mValue from elapsed time; true when the target was reached. */
75
+ /**
76
+ * Updates mValue from elapsed time; true when the run is over. The eased value is not clamped to the range: a spring
77
+ * easing goes past its ends, and a run whose mMinValue is above mMaxValue (a ping-pong's way back) goes down.
78
+ */
59
79
  protected UpdateValue(_deltaT: number, deltaFromStart: number): boolean;
60
80
  protected FinishedRunning(): boolean;
61
81
  }
82
+ /** DrawnUi PingPongAnimator: every repeat runs the other way (Repeat -1 = forever by default). */
83
+ export declare class PingPongAnimator extends SkiaValueAnimator {
84
+ /** One way done, the next starts (C# CycleFInished). */
85
+ CycleFinished?: () => void;
86
+ constructor(parent: SkiaControl);
87
+ protected FinishedRunning(): boolean;
88
+ }
62
89
  /** DrawnUi RenderingAnimator: a value animator that paints itself above its Parent (IOverlayEffect). */
63
90
  export declare class RenderingAnimator extends SkiaValueAnimator implements IOverlayEffect {
64
91
  constructor(parent: SkiaControl);
@@ -16,7 +16,20 @@ export class AnimatorBase {
16
16
  StartFrameTimeNanos = 0;
17
17
  OnStart;
18
18
  OnStop;
19
+ /** Paused: the canvas does not tick it and it wakes no frames; Resume goes on from where it was (C# Pause / Resume). */
20
+ IsPaused = false;
21
+ /** Paused by the canvas because its control is hidden; resumed when it shows again. */
22
+ PausedWhileHidden = false;
23
+ /**
24
+ * Frame time (ns) before which nothing it shows changes: the canvas does not tick it and it keeps no frames coming
25
+ * until then, a timer wakes the frame loop at that time (a GIF or a sprite between two frames; DrawnUi.Rust frame
26
+ * animators sleep between changes). 0 = tick every frame.
27
+ */
28
+ SleepUntilNanos = 0;
19
29
  delayHandle = 0;
30
+ delayEnd = 0;
31
+ delayLeft = 0;
32
+ pausedAt = 0;
20
33
  constructor(parent) { this.Parent = parent; }
21
34
  Register() {
22
35
  const parent = this.Parent;
@@ -60,6 +73,8 @@ export class AnimatorBase {
60
73
  this.delayHandle = 0;
61
74
  }
62
75
  if (delayMs > 0) {
76
+ // the delay waits without frames
77
+ this.delayEnd = performance.now() + delayMs;
63
78
  this.delayHandle = window.setTimeout(() => { this.delayHandle = 0; this.Start(); }, delayMs);
64
79
  return;
65
80
  }
@@ -72,7 +87,43 @@ export class AnimatorBase {
72
87
  }
73
88
  }
74
89
  }
90
+ /** Freezes the run (and what is left of a start delay); Resume goes on from there. */
91
+ Pause() {
92
+ if (this.IsPaused)
93
+ return;
94
+ this.IsPaused = true;
95
+ this.pausedAt = performance.now();
96
+ if (this.delayHandle) {
97
+ clearTimeout(this.delayHandle);
98
+ this.delayHandle = 0;
99
+ this.delayLeft = Math.max(0, this.delayEnd - this.pausedAt);
100
+ }
101
+ }
102
+ /** Goes on from where Pause froze it: the paused time is not counted. */
103
+ Resume() {
104
+ if (!this.IsPaused)
105
+ return;
106
+ this.IsPaused = false;
107
+ const paused = (performance.now() - this.pausedAt) * 1_000_000;
108
+ if (this.StartFrameTimeNanos !== 0) {
109
+ this.StartFrameTimeNanos += paused;
110
+ this.LastFrameTimeNanos += paused;
111
+ }
112
+ if (this.SleepUntilNanos !== 0)
113
+ this.SleepUntilNanos += paused;
114
+ if (this.delayLeft > 0) {
115
+ const left = this.delayLeft;
116
+ this.delayLeft = 0;
117
+ this.Start(left);
118
+ return;
119
+ }
120
+ this.Parent?.Repaint(); // frames again
121
+ }
75
122
  Stop() {
123
+ this.IsPaused = false;
124
+ this.PausedWhileHidden = false;
125
+ this.SleepUntilNanos = 0;
126
+ this.delayLeft = 0;
76
127
  if (this.delayHandle) {
77
128
  clearTimeout(this.delayHandle);
78
129
  this.delayHandle = 0;
@@ -155,23 +206,21 @@ export class SkiaValueAnimator extends AnimatorBase {
155
206
  }
156
207
  /** Passed over mValue; subclasses derive their own reported value here. */
157
208
  TransformReportedValue(_deltaT) { return this.mValue; }
158
- /** Updates mValue from elapsed time; true when the target was reached. */
209
+ /**
210
+ * Updates mValue from elapsed time; true when the run is over. The eased value is not clamped to the range: a spring
211
+ * easing goes past its ends, and a run whose mMinValue is above mMaxValue (a ping-pong's way back) goes down.
212
+ */
159
213
  UpdateValue(_deltaT, deltaFromStart) {
160
214
  const elapsedMs = deltaFromStart / 1_000_000;
161
215
  const progress = this.Speed > 0 ? elapsedMs / this.Speed : 1;
162
216
  const deltaValue = this.mMaxValue - this.mMinValue;
163
217
  const eased = this.Easing.Ease(Math.min(progress, 1));
164
218
  this.Progress = eased;
165
- const value = deltaValue * eased + this.mMinValue;
166
- if (value < this.mMinValue) {
167
- this.mValue = this.mMinValue;
168
- return false;
169
- }
170
- if (value >= this.mMaxValue || progress >= 1) {
219
+ if (progress >= 1) {
171
220
  this.mValue = this.mMaxValue;
172
221
  return true;
173
222
  }
174
- this.mValue = value;
223
+ this.mValue = deltaValue * eased + this.mMinValue;
175
224
  return false;
176
225
  }
177
226
  FinishedRunning() {
@@ -188,6 +237,25 @@ export class SkiaValueAnimator extends AnimatorBase {
188
237
  return true;
189
238
  }
190
239
  }
240
+ /** DrawnUi PingPongAnimator: every repeat runs the other way (Repeat -1 = forever by default). */
241
+ export class PingPongAnimator extends SkiaValueAnimator {
242
+ /** One way done, the next starts (C# CycleFInished). */
243
+ CycleFinished;
244
+ constructor(parent) { super(parent); this.Repeat = -1; }
245
+ FinishedRunning() {
246
+ if (this.Repeat !== 0) {
247
+ this.CycleFinished?.();
248
+ if (this.Repeat > 0)
249
+ this.Repeat--;
250
+ [this.mMinValue, this.mMaxValue] = [this.mMaxValue, this.mMinValue];
251
+ this.mValue = this.mMinValue;
252
+ this.LastFrameTimeNanos = 0;
253
+ this.StartFrameTimeNanos = 0;
254
+ return false;
255
+ }
256
+ return super.FinishedRunning();
257
+ }
258
+ }
191
259
  /** DrawnUi RenderingAnimator: a value animator that paints itself above its Parent (IOverlayEffect). */
192
260
  export class RenderingAnimator extends SkiaValueAnimator {
193
261
  constructor(parent) { super(parent); }
@@ -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
  /**
@@ -14,6 +15,11 @@ export declare class Canvas {
14
15
  readonly Element: HTMLCanvasElement;
15
16
  /** Points a pointer may travel between Down and Up and still count as a tap (AppoMobi TouchEffect default). */
16
17
  static TappedCancelMoveThresholdPoints: number;
18
+ /**
19
+ * A press held this long without moving past TappedCancelMoveThresholdPoints is a LongPressing, at the press point
20
+ * (AppoMobi TouchEffect, DrawnUi.Rust LONG_PRESS_MS); its release sends no Tapped. A timer, no frames while waiting.
21
+ */
22
+ static LongPressTimeMs: number;
17
23
  BackgroundColor: Color;
18
24
  /** Accelerated = WebGL surface, Default = software. Read once at first frame. */
19
25
  RenderingMode: RenderingModeType;
@@ -37,6 +43,8 @@ export declare class Canvas {
37
43
  WasDrawn?: (sender: Canvas) => void;
38
44
  /** Registry + rate-limited snapshot of accessible controls, rendered by the DOM overlay (DrawnUi AccessibilityManager). */
39
45
  readonly AccessibilityManager: SkiaAccessibilityManager;
46
+ /** Mouse hover of the controls (drawnui-cross 6m): ReceivesHover / IsHovered / HoverChanged. */
47
+ readonly Hover: HoverManager;
40
48
  private content?;
41
49
  get Content(): SkiaControl | undefined;
42
50
  set Content(value: SkiaControl | undefined);
@@ -93,6 +101,10 @@ export declare class Canvas {
93
101
  UnregisterAnimator(uid: number): void;
94
102
  /** Ticks every registered animator once; returns how many ran. */
95
103
  protected ExecuteAnimators(frameTimeNanos: number): number;
104
+ private wakeTimer;
105
+ private wakeAtNanos;
106
+ /** One timer for the earliest sleeping animator: the frame loop wakes then, no frames run in between. */
107
+ private WakeAt;
96
108
  /** Frames drawn so far (SkiaBackdrop uses it to refresh once after a cached record). */
97
109
  FrameIndex: number;
98
110
  /**
@@ -126,6 +138,8 @@ export declare class Canvas {
126
138
  private readonly previousTouchArgs;
127
139
  private readonly pendingGestures;
128
140
  private readonly onPointer;
141
+ /** The mouse left the canvas: hover ends at once, also while content moves. */
142
+ private readonly onPointerLeave;
129
143
  private readonly preventTouch;
130
144
  /** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
131
145
  private readonly claimedTouches;
@@ -138,10 +152,11 @@ export declare class Canvas {
138
152
  private cursorPointer;
139
153
  /**
140
154
  * DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
141
- * is pointer-events:none, so the mouse position is tested against the accessibility snapshot (the accessible
142
- * controls' rects in points, already sorted and rate-limited), each hit control answering WantsPointerCursor
143
- * (itself tappable, or a tappable span of a label), and the canvas element's cursor is switched only when the
144
- * answer changes. Mouse moves only, no work without a mouse and none in the frame loop.
155
+ * is pointer-events:none, so the canvas decides from the hover hit path (Hover.Path: the controls under the mouse by
156
+ * the routing rules of ProcessGestures, so nothing below a popup that blocks gestures below): a control in the
157
+ * accessibility tree (it has a role) that answers WantsPointerCursor (itself tappable, or a tappable span of a label)
158
+ * shows the hand. A popup's close-on-background-tap wrapper has no role, so its backdrop shows none. The canvas
159
+ * element's cursor is switched only when the answer changes. Mouse moves only, none in the frame loop.
145
160
  */
146
161
  private UpdateCursor;
147
162
  /**
@@ -174,9 +189,16 @@ export declare class Canvas {
174
189
  private readonly UpdateTouchAction;
175
190
  private pageObserver?;
176
191
  private AttachInput;
192
+ /** The page's own overscroll-behavior before Lock set it to none (html, body), put back on detach. */
193
+ private pageOverscroll?;
177
194
  private DetachInput;
178
195
  /** Port of DrawnUi.Blazor Canvas.OnTouchAction: per-pointer state machine producing Down / Panning / Tapped / Up. */
179
196
  OnTouchAction(args: TouchActionEventArgs): void;
197
+ private readonly longPressTimers;
198
+ private readonly longPressed;
199
+ /** The long press of a new press: due LongPressTimeMs later unless the pointer moves away or lets go. */
200
+ private ArmLongPress;
201
+ private DisarmLongPress;
180
202
  private OnGestureEvent;
181
203
  private ProcessPendingGestures;
182
204
  /**