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.
- package/README.md +1 -1
- package/SKIPPED.md +9 -8
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +4 -1
- package/dist/controls/DrawnGame.js +7 -3
- package/dist/controls/GridStructure.d.ts +13 -1
- package/dist/controls/GridStructure.js +38 -2
- package/dist/controls/SkiaBackdrop.d.ts +2 -0
- package/dist/controls/SkiaBackdrop.js +2 -0
- package/dist/controls/SkiaButton.d.ts +2 -0
- package/dist/controls/SkiaButton.js +2 -0
- package/dist/controls/SkiaCarousel.d.ts +3 -1
- package/dist/controls/SkiaCarousel.js +5 -2
- package/dist/controls/SkiaDrawer.d.ts +2 -0
- package/dist/controls/SkiaDrawer.js +2 -0
- package/dist/controls/SkiaEditor.d.ts +5 -1
- package/dist/controls/SkiaEditor.js +29 -18
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +11 -1
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +54 -3
- package/dist/controls/SkiaLabel.js +235 -52
- package/dist/controls/SkiaLayout.d.ts +20 -0
- package/dist/controls/SkiaLayout.js +78 -6
- package/dist/controls/SkiaLottie.js +3 -1
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +6 -3
- package/dist/controls/SkiaScroll.d.ts +16 -0
- package/dist/controls/SkiaScroll.js +85 -8
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +7 -1
- package/dist/controls/SkiaSlider.js +12 -4
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaToggle.d.ts +2 -0
- package/dist/controls/SkiaToggle.js +3 -1
- package/dist/controls/SnappingLayout.d.ts +7 -0
- package/dist/controls/SnappingLayout.js +11 -0
- package/dist/core/Accessibility.d.ts +21 -0
- package/dist/core/Accessibility.js +66 -9
- package/dist/core/Animators.d.ts +28 -1
- package/dist/core/Animators.js +76 -8
- package/dist/core/Canvas.d.ts +26 -4
- package/dist/core/Canvas.js +136 -23
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +24 -1
- package/dist/core/Gestures.js +75 -2
- package/dist/core/Hover.d.ts +47 -0
- package/dist/core/Hover.js +134 -0
- package/dist/core/KeyboardManager.d.ts +13 -0
- package/dist/core/KeyboardManager.js +18 -0
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +97 -1
- package/dist/core/SkiaControl.js +271 -38
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +30 -3
- package/dist/react/reconciler.js +29 -10
- package/package.json +18 -1
package/dist/core/Canvas.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Super } from "./Super";
|
|
2
2
|
import { SkiaAccessibilityManager } from "./Accessibility";
|
|
3
|
+
import { HoverManager } from "./Hover";
|
|
3
4
|
import { Colors, SKRect } from "./Types";
|
|
4
5
|
import { ContextMenuEventArgs, GestureEventProcessingInfo, PointerData, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
|
|
5
6
|
/**
|
|
@@ -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(
|
|
400
|
+
this.UpdateCursor();
|
|
349
401
|
return;
|
|
350
|
-
}
|
|
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
|
|
406
|
-
*
|
|
407
|
-
* (itself tappable, or a tappable span of a label)
|
|
408
|
-
*
|
|
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(
|
|
466
|
+
UpdateCursor() {
|
|
411
467
|
let hit = false;
|
|
412
|
-
const
|
|
413
|
-
for (
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
739
|
+
if (!wheel)
|
|
740
|
+
this.gestureOwner = args.Type === "Up" ? null : consumed;
|
|
632
741
|
return consumed;
|
|
633
742
|
}
|
|
634
|
-
|
|
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
|
-
|
|
749
|
+
if (!wheel)
|
|
750
|
+
this.gestureOwner = consumed && args.Type !== "Up" ? consumed : null;
|
|
638
751
|
return consumed;
|
|
639
752
|
}
|
|
640
753
|
}
|
package/dist/core/Easing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Mirrors MAUI Easing
|
|
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
|
}
|
package/dist/core/Easing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Mirrors MAUI Easing
|
|
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
|
}
|
package/dist/core/Gestures.d.ts
CHANGED
|
@@ -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
|
|
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.
|
package/dist/core/Gestures.js
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
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
|
+
}
|