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
@@ -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
  /**
@@ -12,6 +13,11 @@ export class Canvas {
12
13
  Element;
13
14
  /** Points a pointer may travel between Down and Up and still count as a tap (AppoMobi TouchEffect default). */
14
15
  static TappedCancelMoveThresholdPoints = 16;
16
+ /**
17
+ * A press held this long without moving past TappedCancelMoveThresholdPoints is a LongPressing, at the press point
18
+ * (AppoMobi TouchEffect, DrawnUi.Rust LONG_PRESS_MS); its release sends no Tapped. A timer, no frames while waiting.
19
+ */
20
+ static LongPressTimeMs = 1500;
15
21
  BackgroundColor = Colors.Transparent;
16
22
  /** Accelerated = WebGL surface, Default = software. Read once at first frame. */
17
23
  RenderingMode = "Accelerated";
@@ -35,6 +41,8 @@ export class Canvas {
35
41
  WasDrawn;
36
42
  /** Registry + rate-limited snapshot of accessible controls, rendered by the DOM overlay (DrawnUi AccessibilityManager). */
37
43
  AccessibilityManager = new SkiaAccessibilityManager();
44
+ /** Mouse hover of the controls (drawnui-cross 6m): ReceivesHover / IsHovered / HoverChanged. */
45
+ Hover = new HoverManager();
38
46
  content;
39
47
  get Content() { return this.content; }
40
48
  set Content(value) {
@@ -45,6 +53,7 @@ export class Canvas {
45
53
  value.Parent = undefined;
46
54
  value._superview = this;
47
55
  }
56
+ this.Hover.RequestCheck();
48
57
  this.Update();
49
58
  }
50
59
  surface;
@@ -201,6 +210,7 @@ export class Canvas {
201
210
  root.Render({ Context: { Canvas: canvas, Surface: this.surface }, Destination: new SKRect(0, 0, w, h), Scale: scale });
202
211
  }
203
212
  this.AccessibilityManager.OnFrameEnd(this.RenderingScale, this.Element.width, this.Element.height, () => this.Update());
213
+ this.Hover.AfterFrame(this.content, this.activeTouchIds.size > 0);
204
214
  const now = performance.now();
205
215
  this.FrameTime = now - started;
206
216
  this.FrameIndex++;
@@ -237,17 +247,51 @@ export class Canvas {
237
247
  UnregisterAnimator(uid) { this.AnimatingControls.delete(uid); }
238
248
  /** Ticks every registered animator once; returns how many ran. */
239
249
  ExecuteAnimators(frameTimeNanos) {
240
- let executed = 0;
250
+ let executed = 0, wake = Infinity;
241
251
  for (const a of [...this.AnimatingControls.values()]) {
242
252
  if (!a.Parent) {
243
253
  this.AnimatingControls.delete(a.Uid);
244
254
  continue;
245
255
  }
256
+ // the control or a parent is hidden: its animator pauses, no frames, and goes on from where it was when shown
257
+ if (a.Parent.HiddenInTree()) {
258
+ if (!a.IsPaused) {
259
+ a.Pause();
260
+ a.PausedWhileHidden = true;
261
+ }
262
+ continue;
263
+ }
264
+ if (a.PausedWhileHidden) {
265
+ a.PausedWhileHidden = false;
266
+ a.Resume();
267
+ }
268
+ if (a.IsPaused)
269
+ continue; // a paused animator neither ticks nor keeps frames coming (C# DrawnView)
270
+ if (a.SleepUntilNanos > frameTimeNanos) {
271
+ wake = Math.min(wake, a.SleepUntilNanos);
272
+ continue;
273
+ } // nothing changes yet
246
274
  a.TickFrame(frameTimeNanos);
247
275
  executed++;
276
+ if (a.SleepUntilNanos > frameTimeNanos)
277
+ wake = Math.min(wake, a.SleepUntilNanos);
248
278
  }
279
+ this.WakeAt(wake, frameTimeNanos);
249
280
  return executed;
250
281
  }
282
+ wakeTimer = 0;
283
+ wakeAtNanos = Infinity;
284
+ /** One timer for the earliest sleeping animator: the frame loop wakes then, no frames run in between. */
285
+ WakeAt(nanos, nowNanos) {
286
+ if (!isFinite(nanos))
287
+ return;
288
+ if (this.wakeTimer && this.wakeAtNanos <= nanos)
289
+ return;
290
+ if (this.wakeTimer)
291
+ clearTimeout(this.wakeTimer);
292
+ this.wakeAtNanos = nanos;
293
+ this.wakeTimer = window.setTimeout(() => { this.wakeTimer = 0; this.wakeAtNanos = Infinity; this.Update(); }, Math.max(0, (nanos - nowNanos) / 1_000_000));
294
+ }
251
295
  /** Frames drawn so far (SkiaBackdrop uses it to refresh once after a cached record). */
252
296
  FrameIndex = 0;
253
297
  /**
@@ -306,6 +350,10 @@ export class Canvas {
306
350
  }
307
351
  Dispose() {
308
352
  this.disposed = true;
353
+ if (this.wakeTimer) {
354
+ clearTimeout(this.wakeTimer);
355
+ this.wakeTimer = 0;
356
+ }
309
357
  this.Element.removeEventListener("webglcontextlost", this.onContextLost);
310
358
  this.Element.removeEventListener("webglcontextrestored", this.onContextRestored);
311
359
  this.Gestures = "Disabled";
@@ -344,10 +392,14 @@ export class Canvas {
344
392
  if (!type)
345
393
  return;
346
394
  if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
395
+ if (e.pointerType === "touch")
396
+ return; // touch never hovers
397
+ if (this.activeTouchIds.size === 0)
398
+ this.Hover.Move(this.content, e.offsetX * this.RenderingScale, e.offsetY * this.RenderingScale);
347
399
  if (e.pointerType === "mouse")
348
- this.UpdateCursor(e.offsetX, e.offsetY);
400
+ this.UpdateCursor();
349
401
  return;
350
- } // hover not ported (TouchActionResult.Pointer)
402
+ }
351
403
  if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
352
404
  return; // Up of a pointer that never pressed here
353
405
  // Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
@@ -377,6 +429,9 @@ export class Canvas {
377
429
  this.UpdateTouchAction();
378
430
  }
379
431
  };
432
+ /** The mouse left the canvas: hover ends at once, also while content moves. */
433
+ onPointerLeave = (e) => { if (e.pointerType !== "touch")
434
+ this.Hover.Leave(); };
380
435
  preventTouch = (e) => e.preventDefault();
381
436
  /** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
382
437
  claimedTouches = new Set();
@@ -402,23 +457,19 @@ export class Canvas {
402
457
  cursorPointer = false;
403
458
  /**
404
459
  * DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
405
- * is pointer-events:none, so the mouse position is tested against the accessibility snapshot (the accessible
406
- * controls' rects in points, already sorted and rate-limited), each hit control answering WantsPointerCursor
407
- * (itself tappable, or a tappable span of a label), and the canvas element's cursor is switched only when the
408
- * answer changes. Mouse moves only, no work without a mouse and none in the frame loop.
460
+ * is pointer-events:none, so the canvas decides from the hover hit path (Hover.Path: the controls under the mouse by
461
+ * the routing rules of ProcessGestures, so nothing below a popup that blocks gestures below): a control in the
462
+ * accessibility tree (it has a role) that answers WantsPointerCursor (itself tappable, or a tappable span of a label)
463
+ * shows the hand. A popup's close-on-background-tap wrapper has no role, so its backdrop shows none. The canvas
464
+ * element's cursor is switched only when the answer changes. Mouse moves only, none in the frame loop.
409
465
  */
410
- UpdateCursor(x, y) {
466
+ UpdateCursor() {
411
467
  let hit = false;
412
- const scale = this.RenderingScale;
413
- for (const n of this.AccessibilityManager.Snapshot) {
414
- if (x < n.Rect.Left || x >= n.Rect.Right || y < n.Rect.Top || y >= n.Rect.Bottom)
415
- continue;
416
- // the control decides (whole control, or a tappable span of a label); point in pixels relative to the control's
417
- // origin, taken from the snapshot rect (already carries the scroll / cache offset; a rotated control gets its bbox)
418
- if (n.Source.WantsPointerCursor((x - n.Rect.Left) * scale, (y - n.Rect.Top) * scale)) {
468
+ const path = this.Hover.Path, xs = this.Hover.PathX, ys = this.Hover.PathY;
469
+ for (let i = path.length - 1; i >= 0 && !hit; i--) {
470
+ const c = path[i];
471
+ if (c.IsAccessibilityElement && c.WantsPointerCursor(xs[i] - c.DrawingRect.Left, ys[i] - c.DrawingRect.Top))
419
472
  hit = true;
420
- break;
421
- }
422
473
  }
423
474
  if (hit !== this.cursorPointer) {
424
475
  this.cursorPointer = hit;
@@ -519,13 +570,25 @@ export class Canvas {
519
570
  el.style.userSelect = "none";
520
571
  for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
521
572
  el.addEventListener(t, this.onPointer);
573
+ el.addEventListener("pointerleave", this.onPointerLeave);
522
574
  el.addEventListener("contextmenu", this.onContextMenu);
523
575
  el.addEventListener("wheel", this.onWheel, { passive: false });
524
- if (this.gestures === "Lock")
576
+ if (this.gestures === "Lock") {
525
577
  el.addEventListener("touchmove", this.preventTouch, { passive: false });
578
+ // the canvas owns every touch (DrawnUi.Web applyGestureStyle, DrawnUi.Rust host): no iOS callout or text
579
+ // selection on a long press, and no rubber band / pull-down of the page starting on it
580
+ const st = el.style;
581
+ st.webkitUserSelect = "none";
582
+ st.webkitTouchCallout = "none";
583
+ this.pageOverscroll = [document.documentElement.style.overscrollBehavior, document.body.style.overscrollBehavior];
584
+ document.documentElement.style.overscrollBehavior = "none";
585
+ document.body.style.overscrollBehavior = "none";
586
+ }
526
587
  else if (this.gestures === "Enabled")
527
588
  el.addEventListener("touchmove", this.preventClaimedTouch, { passive: false });
528
589
  }
590
+ /** The page's own overscroll-behavior before Lock set it to none (html, body), put back on detach. */
591
+ pageOverscroll;
529
592
  DetachInput() {
530
593
  const el = this.Element;
531
594
  el.style.touchAction = "";
@@ -535,11 +598,22 @@ export class Canvas {
535
598
  el.style.userSelect = "";
536
599
  for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
537
600
  el.removeEventListener(t, this.onPointer);
601
+ el.removeEventListener("pointerleave", this.onPointerLeave);
538
602
  el.removeEventListener("contextmenu", this.onContextMenu);
539
603
  el.removeEventListener("wheel", this.onWheel);
540
604
  el.removeEventListener("touchmove", this.preventTouch);
541
605
  el.removeEventListener("touchmove", this.preventClaimedTouch);
606
+ if (this.pageOverscroll) {
607
+ const st = el.style;
608
+ st.webkitUserSelect = "";
609
+ st.webkitTouchCallout = "";
610
+ [document.documentElement.style.overscrollBehavior, document.body.style.overscrollBehavior] = this.pageOverscroll;
611
+ this.pageOverscroll = undefined;
612
+ }
542
613
  this.claimedTouches.clear();
614
+ for (const id of [...this.longPressTimers.keys()])
615
+ this.DisarmLongPress(id);
616
+ this.longPressed.clear();
543
617
  this.activeTouchIds.clear();
544
618
  this.pointerDownArgs.clear();
545
619
  this.previousTouchArgs.clear();
@@ -550,6 +624,7 @@ export class Canvas {
550
624
  return;
551
625
  const id = args.Id;
552
626
  if (args.Type === "Pressed") {
627
+ this.ArmLongPress(id, args);
553
628
  this.activeTouchIds.add(id);
554
629
  args.NumberOfTouches = this.activeTouchIds.size;
555
630
  args.StartingLocation = args.Location;
@@ -574,6 +649,9 @@ export class Canvas {
574
649
  const downArgs = this.pointerDownArgs.get(id);
575
650
  args.StartingLocation = downArgs ? downArgs.StartingLocation : args.Location;
576
651
  if (args.Type === "Moved") {
652
+ const threshold = Canvas.TappedCancelMoveThresholdPoints * Math.max(0.1, this.RenderingScale);
653
+ if (Math.abs(args.Distance.Total.X) >= threshold || Math.abs(args.Distance.Total.Y) >= threshold)
654
+ this.DisarmLongPress(id);
577
655
  if (args.Distance.Delta.X !== 0 || args.Distance.Delta.Y !== 0)
578
656
  this.OnGestureEvent(args, "Panning");
579
657
  this.previousTouchArgs.set(id, args);
@@ -581,7 +659,9 @@ export class Canvas {
581
659
  }
582
660
  if (args.Type === "Released" || args.Type === "Cancelled") {
583
661
  args.IsInContact = args.NumberOfTouches > 1;
584
- if (!args.IsInContact && downArgs && args.Type === "Released") {
662
+ const longPressed = this.longPressed.delete(id);
663
+ this.DisarmLongPress(id);
664
+ if (!args.IsInContact && downArgs && args.Type === "Released" && !longPressed) {
585
665
  const threshold = Canvas.TappedCancelMoveThresholdPoints * Math.max(0.1, this.RenderingScale);
586
666
  if (Math.abs(args.Distance.Total.X) < threshold && Math.abs(args.Distance.Total.Y) < threshold)
587
667
  this.OnGestureEvent(args, "Tapped");
@@ -592,6 +672,30 @@ export class Canvas {
592
672
  this.activeTouchIds.delete(id);
593
673
  }
594
674
  }
675
+ longPressTimers = new Map();
676
+ longPressed = new Set();
677
+ /** The long press of a new press: due LongPressTimeMs later unless the pointer moves away or lets go. */
678
+ ArmLongPress(id, down) {
679
+ this.DisarmLongPress(id);
680
+ this.longPressed.delete(id);
681
+ this.longPressTimers.set(id, window.setTimeout(() => {
682
+ this.longPressTimers.delete(id);
683
+ if (!this.activeTouchIds.has(id))
684
+ return;
685
+ this.longPressed.add(id);
686
+ const args = new TouchActionEventArgs();
687
+ Object.assign(args, down);
688
+ args.Location = down.StartingLocation ?? down.Location;
689
+ this.OnGestureEvent(args, "LongPressing");
690
+ }, Canvas.LongPressTimeMs));
691
+ }
692
+ DisarmLongPress(id) {
693
+ const t = this.longPressTimers.get(id);
694
+ if (t) {
695
+ clearTimeout(t);
696
+ this.longPressTimers.delete(id);
697
+ }
698
+ }
595
699
  OnGestureEvent(args, result) {
596
700
  this.pendingGestures.push(SkiaGesturesParameters.Create(result, args));
597
701
  this.Update();
@@ -620,21 +724,30 @@ export class Canvas {
620
724
  /** Entry into the control tree, same shape as DrawnUi Canvas.ProcessGestures. */
621
725
  ProcessGestures(root, args) {
622
726
  const info = () => new GestureEventProcessingInfo(args.Event.Location, SKPoint.Empty, SKPoint.Empty, null);
727
+ // the wheel goes to the press owner only while a press is held; between presses it goes by position, and it
728
+ // never makes an owner: a wheel over a second scroll moves that one, not the last one the wheel moved
729
+ // (DrawnUi.Rust input_router "the wheel goes to the owner of the press first"; C# keeps the last wheel consumer)
730
+ const wheel = args.Type === "Wheel";
623
731
  if (args.Type === "Down")
624
732
  this.gestureOwner = null;
625
- else if (this.gestureOwner && Canvas.IsSavedGesture(args.Type)) {
733
+ else if (this.gestureOwner && Canvas.IsSavedGesture(args.Type) && (!wheel || this.activeTouchIds.size > 0)) {
626
734
  const owner = this.gestureOwner;
627
735
  const alive = !!owner.Superview && owner.IsVisible && !owner.InputTransparent;
628
736
  const consumed = alive ? owner.OnSkiaGestureEvent(args, info()) : null;
629
737
  if (consumed) {
630
738
  // it still wants the gesture: nobody else sees this one (C# skips the tree pass for a saved gesture)
631
- this.gestureOwner = args.Type === "Up" ? null : consumed;
739
+ if (!wheel)
740
+ this.gestureOwner = args.Type === "Up" ? null : consumed;
632
741
  return consumed;
633
742
  }
634
- this.gestureOwner = null; // it let go (a button whose press turned into a pan): route normally again
743
+ // it let go (a button whose press turned into a pan): route normally again; a wheel it does not use goes by
744
+ // position and leaves it the press
745
+ if (!wheel)
746
+ this.gestureOwner = null;
635
747
  }
636
748
  const consumed = root.ProcessGestures(args, info());
637
- this.gestureOwner = consumed && args.Type !== "Up" ? consumed : null;
749
+ if (!wheel)
750
+ this.gestureOwner = consumed && args.Type !== "Up" ? consumed : null;
638
751
  return consumed;
639
752
  }
640
753
  }
@@ -1,4 +1,4 @@
1
- /** Mirrors MAUI Easing (subset): Ease(t) with t in 0..1. */
1
+ /** Mirrors MAUI Easing: Ease(t) with t in 0..1 (Spring* leave 0..1 on the way). */
2
2
  export declare class Easing {
3
3
  private readonly fn;
4
4
  constructor(fn: (x: number) => number);
@@ -9,5 +9,16 @@ export declare class Easing {
9
9
  static readonly CubicInOut: Easing;
10
10
  static readonly SinOut: Easing;
11
11
  static readonly SinIn: Easing;
12
+ /** Half a cosine: slow at both ends. */
13
+ static readonly SinInOut: Easing;
14
+ /** Leaps to the end, bounces three times and settles. */
15
+ static readonly BounceOut: Easing;
16
+ /** Bounces three times, then leaps to the end. */
17
+ static readonly BounceIn: Easing;
18
+ /** Moves away first, then leaps to the end. */
19
+ static readonly SpringIn: Easing;
20
+ /** Goes a little past the end and comes back. */
21
+ static readonly SpringOut: Easing;
22
+ private static Bounce;
12
23
  static readonly Default: Easing;
13
24
  }
@@ -1,4 +1,4 @@
1
- /** Mirrors MAUI Easing (subset): Ease(t) with t in 0..1. */
1
+ /** Mirrors MAUI Easing: Ease(t) with t in 0..1 (Spring* leave 0..1 on the way). */
2
2
  export class Easing {
3
3
  fn;
4
4
  constructor(fn) {
@@ -11,5 +11,29 @@ export class Easing {
11
11
  static CubicInOut = new Easing((x) => (x < 0.5 ? 4 * x ** 3 : (x - 1) * (2 * x - 2) ** 2 + 1));
12
12
  static SinOut = new Easing((x) => Math.sin((x * Math.PI) / 2));
13
13
  static SinIn = new Easing((x) => 1 - Math.cos((x * Math.PI) / 2));
14
+ /** Half a cosine: slow at both ends. */
15
+ static SinInOut = new Easing((x) => -Math.cos(Math.PI * x) / 2 + 0.5);
16
+ /** Leaps to the end, bounces three times and settles. */
17
+ static BounceOut = new Easing(Easing.Bounce);
18
+ /** Bounces three times, then leaps to the end. */
19
+ static BounceIn = new Easing((x) => 1 - Easing.Bounce(1 - x));
20
+ /** Moves away first, then leaps to the end. */
21
+ static SpringIn = new Easing((x) => x * x * ((1.70158 + 1) * x - 1.70158));
22
+ /** Goes a little past the end and comes back. */
23
+ static SpringOut = new Easing((x) => (x - 1) * (x - 1) * ((1.70158 + 1) * (x - 1) + 1.70158) + 1);
24
+ static Bounce(p) {
25
+ if (p < 1 / 2.75)
26
+ return 7.5625 * p * p;
27
+ if (p < 2 / 2.75) {
28
+ p -= 1.5 / 2.75;
29
+ return 7.5625 * p * p + 0.75;
30
+ }
31
+ if (p < 2.5 / 2.75) {
32
+ p -= 2.25 / 2.75;
33
+ return 7.5625 * p * p + 0.9375;
34
+ }
35
+ p -= 2.625 / 2.75;
36
+ return 7.5625 * p * p + 0.984375;
37
+ }
14
38
  static Default = Easing.CubicInOut;
15
39
  }
@@ -9,7 +9,7 @@ export declare class SKPoint {
9
9
  }
10
10
  /** Raw platform action (subset of TouchActionType). */
11
11
  export type TouchActionType = "Pressed" | "Moved" | "Released" | "Cancelled" | "Wheel" | "Pointer";
12
- /** Recognized gesture (TouchActionResult). LongPressing/Pointer/Touch declared for parity, not produced yet. */
12
+ /** Recognized gesture (TouchActionResult). LongPressing after Canvas.LongPressTimeMs held still; Pointer / Touch declared for parity, not produced. */
13
13
  export type TouchActionResult = "Touch" | "Down" | "Up" | "Tapped" | "LongPressing" | "Panning" | "Wheel" | "Pointer";
14
14
  export type GesturesMode = "Disabled" | "Enabled" | "Lock";
15
15
  export type LockTouch = "Disabled" | "Enabled" | "PassNone" | "PassTap" | "PassTapAndLongPress";
@@ -21,6 +21,27 @@ export declare class DistanceInfo {
21
21
  Total: SKPoint;
22
22
  Velocity: SKPoint;
23
23
  }
24
+ /**
25
+ * The last positions of a press with their times (DrawnUi.Rust gestures `Trail`, 6d525f7). Measured only against the
26
+ * previous move, irregular delivery made the velocity worthless: moves handled in one burst (queued pointer events
27
+ * handed over back to back by a busy main thread, WSLg pairs 0.01 ms apart) gave millions of px/s and every fling at
28
+ * the limit. Moves under BurstMs apart are one position; a move's velocity is its displacement over the last WindowMs,
29
+ * the earlier position interpolated; when the previous move is already that old, its own velocity exactly (evenly
30
+ * spaced input at 60 Hz measures as before).
31
+ */
32
+ export declare class VelocityTrail {
33
+ /** A move's velocity is its displacement over this much time. */
34
+ static readonly WindowMs = 16;
35
+ /** Below this since the press no velocity is measured yet: the first moves of a burst. */
36
+ static readonly MinMs = 4;
37
+ /** Moves closer than this came in one burst: one position, at the burst's first time. */
38
+ static readonly BurstMs = 1;
39
+ private static readonly Capacity;
40
+ private readonly points;
41
+ Push(x: number, y: number, t: number): void;
42
+ /** The velocity at (x, y) at time t (ms), pixels per second; `fallback` right after the press. */
43
+ Velocity(x: number, y: number, t: number, fallback: SKPoint): SKPoint;
44
+ }
24
45
  /** AppoMobi.Gestures MouseButton: which button pressed / released (DOM button 0..4). */
25
46
  export type MouseButton = "Left" | "Middle" | "Right" | "XButton1" | "XButton2" | "Extended";
26
47
  export type PointerDeviceType = "Mouse" | "Touch" | "Pen";
@@ -49,6 +70,8 @@ export declare class TouchActionEventArgs {
49
70
  Timestamp: number;
50
71
  /** ms since the previous event of the same pointer. */
51
72
  DeltaTimeMs: number;
73
+ /** The press's recent positions the velocity is measured over, carried from event to event of one pointer. */
74
+ Trail?: VelocityTrail;
52
75
  /**
53
76
  * Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
54
77
  * wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
@@ -18,6 +18,67 @@ export class DistanceInfo {
18
18
  Total = SKPoint.Empty;
19
19
  Velocity = SKPoint.Empty;
20
20
  }
21
+ /**
22
+ * The last positions of a press with their times (DrawnUi.Rust gestures `Trail`, 6d525f7). Measured only against the
23
+ * previous move, irregular delivery made the velocity worthless: moves handled in one burst (queued pointer events
24
+ * handed over back to back by a busy main thread, WSLg pairs 0.01 ms apart) gave millions of px/s and every fling at
25
+ * the limit. Moves under BurstMs apart are one position; a move's velocity is its displacement over the last WindowMs,
26
+ * the earlier position interpolated; when the previous move is already that old, its own velocity exactly (evenly
27
+ * spaced input at 60 Hz measures as before).
28
+ */
29
+ export class VelocityTrail {
30
+ /** A move's velocity is its displacement over this much time. */
31
+ static WindowMs = 16;
32
+ /** Below this since the press no velocity is measured yet: the first moves of a burst. */
33
+ static MinMs = 4;
34
+ /** Moves closer than this came in one burst: one position, at the burst's first time. */
35
+ static BurstMs = 1;
36
+ static Capacity = 16;
37
+ points = [];
38
+ Push(x, y, t) {
39
+ const last = this.points[this.points.length - 1];
40
+ if (last && t - last.T < VelocityTrail.BurstMs) {
41
+ last.X = x;
42
+ last.Y = y;
43
+ return;
44
+ }
45
+ if (this.points.length === VelocityTrail.Capacity)
46
+ this.points.shift();
47
+ this.points.push({ X: x, Y: y, T: t });
48
+ }
49
+ /** The velocity at (x, y) at time t (ms), pixels per second; `fallback` right after the press. */
50
+ Velocity(x, y, t, fallback) {
51
+ let n = this.points.length;
52
+ // a move in the burst of the last position measures from the positions before it
53
+ if (n > 0 && t - this.points[n - 1].T < VelocityTrail.BurstMs)
54
+ n--;
55
+ if (n === 0)
56
+ return fallback;
57
+ const since = t - this.points[0].T;
58
+ if (since < VelocityTrail.MinMs)
59
+ return fallback;
60
+ const window = Math.min(VelocityTrail.WindowMs, since), at = t - window;
61
+ const last = this.points[n - 1];
62
+ if (last.T <= at) {
63
+ const secs = (t - last.T) / 1000;
64
+ return new SKPoint((x - last.X) / secs, (y - last.Y) / secs);
65
+ }
66
+ // the position at `at`: between the two positions around it (the current one included)
67
+ let fromX = this.points[0].X, fromY = this.points[0].Y, prev = this.points[0];
68
+ for (let i = 1; i <= n; i++) {
69
+ const p = i < n ? this.points[i] : { X: x, Y: y, T: t };
70
+ if (p.T >= at) {
71
+ const share = p.T - prev.T < 1e-6 ? 1 : (at - prev.T) / (p.T - prev.T);
72
+ fromX = prev.X + (p.X - prev.X) * share;
73
+ fromY = prev.Y + (p.Y - prev.Y) * share;
74
+ break;
75
+ }
76
+ prev = p;
77
+ }
78
+ const secs = window / 1000;
79
+ return new SKPoint((x - fromX) / secs, (y - fromY) / secs);
80
+ }
81
+ }
21
82
  /** AppoMobi.Gestures PointerData: the device and button behind a Down / Up / Tapped (every button is delivered). */
22
83
  export class PointerData {
23
84
  Button = "Left";
@@ -43,6 +104,8 @@ export class TouchActionEventArgs {
43
104
  Timestamp = performance.now();
44
105
  /** ms since the previous event of the same pointer. */
45
106
  DeltaTimeMs = 0;
107
+ /** The press's recent positions the velocity is measured over, carried from event to event of one pointer. */
108
+ Trail;
46
109
  /**
47
110
  * Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
48
111
  * wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
@@ -68,8 +131,18 @@ export class TouchActionEventArgs {
68
131
  d.End = released ? previous.Location : current.Location;
69
132
  d.Delta = released ? SKPoint.Empty : current.Location.Subtract(previous.Location);
70
133
  d.Total = previous.Distance.Total.Add(d.Delta);
71
- const secs = current.DeltaTimeMs / 1000;
72
- d.Velocity = secs > 0 && !released ? new SKPoint(d.Delta.X / secs, d.Delta.Y / secs) : previous.Distance.Velocity;
134
+ if (released)
135
+ d.Velocity = previous.Distance.Velocity;
136
+ else {
137
+ let trail = previous.Trail;
138
+ if (!trail) {
139
+ trail = new VelocityTrail();
140
+ trail.Push(previous.Location.X, previous.Location.Y, previous.Timestamp);
141
+ }
142
+ d.Velocity = trail.Velocity(current.Location.X, current.Location.Y, current.Timestamp, previous.Distance.Velocity);
143
+ trail.Push(current.Location.X, current.Location.Y, current.Timestamp);
144
+ current.Trail = trail;
145
+ }
73
146
  current.Distance = d;
74
147
  }
75
148
  }
@@ -0,0 +1,47 @@
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
+ /** The controls under the pointer at the last hit test, root first, and the pointer in each one's own space. */
17
+ readonly Path: SkiaControl[];
18
+ readonly PathX: number[];
19
+ readonly PathY: number[];
20
+ private readonly movers;
21
+ private hasPointer;
22
+ private x;
23
+ private y;
24
+ private paused;
25
+ private check;
26
+ /**
27
+ * The mouse (or a pen in range) moved to (x, y), canvas pixels, nothing pressed. The hit path is taken also while
28
+ * hover waits (the cursor follows it), hover changes only when nothing moves.
29
+ */
30
+ Move(root: SkiaControl | undefined, x: number, y: number): void;
31
+ /** Fills Path with the controls under the pointer by the routing rules of ProcessGestures. */
32
+ private HitTest;
33
+ /** The pointer left the canvas: nothing is hovered, also while content moves. */
34
+ Leave(): void;
35
+ /** The controls under a still pointer may have changed: checked once at the end of the frame. */
36
+ RequestCheck(): void;
37
+ /** A control that moves its content (MovesContent) while drawn: hover waits while any of them moves. */
38
+ RegisterMover(control: SkiaControl): void;
39
+ /** End of a frame: pause while content moves, else run a pending check (at most one a frame, none while pressed). */
40
+ AfterFrame(root: SkiaControl | undefined, pressed: boolean): void;
41
+ private static Shown;
42
+ /**
43
+ * The hovered set becomes the controls of the hit path that take hover (`fromPath`), or none: the ones that left get
44
+ * HoverChanged(false), then the new ones (true).
45
+ */
46
+ private Apply;
47
+ }