drawnui-react 0.1.0-preview.1 → 0.1.0-preview.10
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 +87 -29
- package/SKIPPED.md +20 -15
- package/dist/controls/SkiaButton.d.ts +4 -1
- package/dist/controls/SkiaButton.js +20 -7
- package/dist/controls/SkiaCarousel.d.ts +6 -0
- package/dist/controls/SkiaCarousel.js +7 -1
- package/dist/controls/SkiaCheckbox.d.ts +0 -1
- package/dist/controls/SkiaCheckbox.js +0 -6
- package/dist/controls/SkiaEditor.js +8 -0
- package/dist/controls/SkiaLabel.d.ts +2 -0
- package/dist/controls/SkiaLabel.js +9 -0
- package/dist/controls/SkiaLayout.d.ts +10 -0
- package/dist/controls/SkiaLayout.js +64 -1
- package/dist/controls/SkiaRadioButton.js +1 -2
- package/dist/controls/SkiaScroll.d.ts +9 -0
- package/dist/controls/SkiaScroll.js +120 -13
- package/dist/controls/SkiaScrollBar.d.ts +27 -0
- package/dist/controls/SkiaScrollBar.js +51 -2
- package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
- package/dist/controls/SkiaShaderCarousel.js +5 -0
- package/dist/controls/SkiaSwitch.d.ts +0 -1
- package/dist/controls/SkiaSwitch.js +0 -6
- package/dist/controls/SkiaToggle.d.ts +18 -4
- package/dist/controls/SkiaToggle.js +45 -3
- package/dist/core/Canvas.d.ts +82 -4
- package/dist/core/Canvas.js +255 -9
- package/dist/core/Gestures.d.ts +47 -1
- package/dist/core/Gestures.js +43 -1
- package/dist/core/SkiaControl.d.ts +30 -1
- package/dist/core/SkiaControl.js +59 -3
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +10 -1
- package/dist/core/Super.js +20 -3
- package/dist/core/Types.d.ts +7 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/SkiaShell.js +5 -2
- package/dist/react/index.d.ts +22 -4
- package/dist/react/index.js +25 -4
- package/dist/react/reconciler.js +2 -0
- package/dist/vite/index.d.ts +74 -0
- package/dist/vite/index.js +204 -0
- package/package.json +35 -4
- package/PARITY.md +0 -287
package/dist/core/Canvas.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Surface } from "canvaskit-wasm";
|
|
2
|
-
import type { SkiaControl } from "./SkiaControl";
|
|
2
|
+
import type { DrawingContext, SkiaControl } from "./SkiaControl";
|
|
3
3
|
import type { AnimatorBase } from "./Animators";
|
|
4
4
|
import { SkiaAccessibilityManager } from "./Accessibility";
|
|
5
5
|
import { type Color, type RenderingModeType } from "./Types";
|
|
6
|
-
import { type GesturesMode, SkiaGesturesParameters, TouchActionEventArgs } from "./Gestures";
|
|
6
|
+
import { ContextMenuEventArgs, type GesturesMode, SkiaGesturesParameters, TouchActionEventArgs } from "./Gestures";
|
|
7
7
|
/**
|
|
8
8
|
* Mirrors DrawnUi Canvas (DrawnView): hosts one Content control on an HTML canvas element,
|
|
9
9
|
* owns RenderingScale (devicePixelRatio), the surface, the on-demand frame loop and raw input.
|
|
@@ -23,6 +23,18 @@ export declare class Canvas {
|
|
|
23
23
|
/** Frames per second over the last second of drawn frames. */
|
|
24
24
|
FPS: number;
|
|
25
25
|
private frameTimes;
|
|
26
|
+
/** DrawnView.WasRendered: a frame has been drawn on a ready surface (set after the first one completes). */
|
|
27
|
+
WasRendered: boolean;
|
|
28
|
+
/** DrawnView.CanRender: the surface (and its GPU context when accelerated) is ready to draw. */
|
|
29
|
+
get CanRender(): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* DrawnView.WillFirstTimeDraw: raised once, right before the first frame drawn on a ready surface, with that
|
|
32
|
+
* frame's drawing context. The engine draws its first frame while it is being constructed (so the page never shows
|
|
33
|
+
* an unpainted canvas): pass it through the constructor's `init` to receive that frame.
|
|
34
|
+
*/
|
|
35
|
+
WillFirstTimeDraw?: (sender: Canvas, context: DrawingContext["Context"]) => void;
|
|
36
|
+
/** DrawnView.WasDrawn: raised after every frame has been drawn (the C# event passes no context). */
|
|
37
|
+
WasDrawn?: (sender: Canvas) => void;
|
|
26
38
|
/** Registry + rate-limited snapshot of accessible controls, rendered by the DOM overlay (DrawnUi AccessibilityManager). */
|
|
27
39
|
readonly AccessibilityManager: SkiaAccessibilityManager;
|
|
28
40
|
private content?;
|
|
@@ -35,7 +47,8 @@ export declare class Canvas {
|
|
|
35
47
|
private frameId;
|
|
36
48
|
private disposed;
|
|
37
49
|
private readonly observer;
|
|
38
|
-
|
|
50
|
+
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
51
|
+
constructor(Element: HTMLCanvasElement, init?: (canvas: Canvas) => void);
|
|
39
52
|
/** Request a redraw (full measure + arrange + render) on the next animation frame. */
|
|
40
53
|
Update(): void;
|
|
41
54
|
/** Deletes the current surface; a pending frame on it is cancelled first (drawing on a deleted surface faults). */
|
|
@@ -66,6 +79,18 @@ export declare class Canvas {
|
|
|
66
79
|
FrameIndex: number;
|
|
67
80
|
/** On-screen surface (SkiaBackdrop snapshots it). */
|
|
68
81
|
get Surface(): Surface | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* A picture of what is on screen right now, as PNG bytes (C# DrawnView.TakeScreenShot).
|
|
84
|
+
*
|
|
85
|
+
* The on-screen canvas cannot simply be read: an accelerated surface lives in a WebGL drawing
|
|
86
|
+
* buffer the browser clears after compositing, so both `canvas.toDataURL()` and a snapshot of
|
|
87
|
+
* the live surface come back blank. The content is therefore drawn once more into an offscreen
|
|
88
|
+
* RASTER surface, which can be read back anywhere.
|
|
89
|
+
*
|
|
90
|
+
* This is a still picture, not a frame of the loop: animators are not ticked and the frame
|
|
91
|
+
* counters do not move, so taking one never changes what the next real frame shows.
|
|
92
|
+
*/
|
|
93
|
+
TakeScreenShot(): Uint8Array | null;
|
|
69
94
|
Dispose(): void;
|
|
70
95
|
private gestures;
|
|
71
96
|
get Gestures(): GesturesMode;
|
|
@@ -76,14 +101,67 @@ export declare class Canvas {
|
|
|
76
101
|
private readonly pendingGestures;
|
|
77
102
|
private readonly onPointer;
|
|
78
103
|
private readonly preventTouch;
|
|
79
|
-
/**
|
|
104
|
+
/** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
|
|
105
|
+
private readonly claimedTouches;
|
|
106
|
+
/** Gestures="Enabled": while a claimed touch is down the page may not take its drag. */
|
|
107
|
+
private readonly preventClaimedTouch;
|
|
108
|
+
/** Called when no control handled a context-menu request; return true to suppress the browser's canvas menu. */
|
|
109
|
+
ContextMenu?: (sender: Canvas, e: ContextMenuEventArgs) => boolean | void;
|
|
110
|
+
/** DOM contextmenu (right click / long press / Menu key) -> ContextMenuEventArgs routed through the tree like a tap. */
|
|
111
|
+
private readonly onContextMenu;
|
|
112
|
+
private cursorPointer;
|
|
113
|
+
/**
|
|
114
|
+
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
115
|
+
* is pointer-events:none, so the mouse position is tested against the accessibility snapshot (the accessible
|
|
116
|
+
* controls' rects in points, already sorted and rate-limited), each hit control answering WantsPointerCursor
|
|
117
|
+
* (itself tappable, or a tappable span of a label), and the canvas element's cursor is switched only when the
|
|
118
|
+
* answer changes. Mouse moves only, no work without a mouse and none in the frame loop.
|
|
119
|
+
*/
|
|
120
|
+
private UpdateCursor;
|
|
121
|
+
/**
|
|
122
|
+
* Mouse wheel -> TouchActionResult.Wheel.
|
|
123
|
+
* Lock keeps every wheel on the canvas. Enabled shares it with the page, like DrawnUi.Blazor's
|
|
124
|
+
* TouchHandlingStyle.Manual: the page scrolls unless a control consumed this wheel. The browser needs that answer
|
|
125
|
+
* inside this handler, so the wheel is processed now instead of at the next frame (Blazor does the same, for the
|
|
126
|
+
* same reason); pending gestures are flushed first so the event order is kept.
|
|
127
|
+
*/
|
|
80
128
|
private readonly onWheel;
|
|
129
|
+
/**
|
|
130
|
+
* Processes one gesture immediately (after whatever is queued) and reports whether a control USED it: a consumer
|
|
131
|
+
* that marked the event Handled, or any consumer that is not a `BlockGesturesBelow` layer. A blocker returns itself
|
|
132
|
+
* for every gesture it keeps from the controls below (a shell page, a popup backdrop), which is not a use: counting
|
|
133
|
+
* it would block the page wheel over every shell app.
|
|
134
|
+
*/
|
|
135
|
+
private ProcessGestureNow;
|
|
136
|
+
/**
|
|
137
|
+
* The page axes a touch pan over the canvas hands to the browser: "pan-y" when the page (or a scrolling ancestor) can
|
|
138
|
+
* scroll vertically, "pan-x" horizontally, both, or "none". Mirrors MAUI's Gestures="Enabled" inside a native scroll
|
|
139
|
+
* view, where the parent takes the pans it scrolls in.
|
|
140
|
+
*/
|
|
141
|
+
private static PageScrollAxes;
|
|
142
|
+
/**
|
|
143
|
+
* Gestures="Enabled" shares touch pans with the page like MAUI's Enabled inside a native scroll view: along an axis
|
|
144
|
+
* the page can scroll, the browser takes the pan (and sends pointercancel), taps and the other axis stay on the
|
|
145
|
+
* canvas. A page that cannot scroll, and Lock, keep every touch. The browser reads touch-action when a touch starts,
|
|
146
|
+
* so it is kept current ahead of time: on attach, when the window / html / body resize, and after every touch.
|
|
147
|
+
*/
|
|
148
|
+
private readonly UpdateTouchAction;
|
|
149
|
+
private pageObserver?;
|
|
81
150
|
private AttachInput;
|
|
82
151
|
private DetachInput;
|
|
83
152
|
/** Port of DrawnUi.Blazor Canvas.OnTouchAction: per-pointer state machine producing Down / Panning / Tapped / Up. */
|
|
84
153
|
OnTouchAction(args: TouchActionEventArgs): void;
|
|
85
154
|
private OnGestureEvent;
|
|
86
155
|
private ProcessPendingGestures;
|
|
156
|
+
/**
|
|
157
|
+
* The control that consumed the current gesture keeps it until it lets go (DrawnUi Canvas.HadInput). Without this a
|
|
158
|
+
* gesture is re-routed by the pointer position on every event, so anything the finger drags away from — a drag
|
|
159
|
+
* handle, a slider thumb, a button being held — stops receiving Panning the moment the pointer leaves its box.
|
|
160
|
+
* Single pointer, which is what the web gives us here; C# keeps a set for multi-touch.
|
|
161
|
+
*/
|
|
162
|
+
private gestureOwner;
|
|
163
|
+
/** DrawnUi IsSavedGesture: the types replayed to the owner instead of being routed again. */
|
|
164
|
+
private static IsSavedGesture;
|
|
87
165
|
/** Entry into the control tree, same shape as DrawnUi Canvas.ProcessGestures. */
|
|
88
166
|
protected ProcessGestures(root: SkiaControl, args: SkiaGesturesParameters): SkiaControl | null;
|
|
89
167
|
}
|
package/dist/core/Canvas.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Super } from "./Super";
|
|
2
2
|
import { SkiaAccessibilityManager } from "./Accessibility";
|
|
3
3
|
import { Colors, SKRect } from "./Types";
|
|
4
|
-
import { GestureEventProcessingInfo, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
|
|
4
|
+
import { ContextMenuEventArgs, GestureEventProcessingInfo, PointerData, SKPoint, SkiaGesturesParameters, TouchActionEventArgs, } from "./Gestures";
|
|
5
5
|
/**
|
|
6
6
|
* Mirrors DrawnUi Canvas (DrawnView): hosts one Content control on an HTML canvas element,
|
|
7
7
|
* owns RenderingScale (devicePixelRatio), the surface, the on-demand frame loop and raw input.
|
|
@@ -21,6 +21,18 @@ export class Canvas {
|
|
|
21
21
|
/** Frames per second over the last second of drawn frames. */
|
|
22
22
|
FPS = 0;
|
|
23
23
|
frameTimes = [];
|
|
24
|
+
/** DrawnView.WasRendered: a frame has been drawn on a ready surface (set after the first one completes). */
|
|
25
|
+
WasRendered = false;
|
|
26
|
+
/** DrawnView.CanRender: the surface (and its GPU context when accelerated) is ready to draw. */
|
|
27
|
+
get CanRender() { return !!this.surface && !this.disposed; }
|
|
28
|
+
/**
|
|
29
|
+
* DrawnView.WillFirstTimeDraw: raised once, right before the first frame drawn on a ready surface, with that
|
|
30
|
+
* frame's drawing context. The engine draws its first frame while it is being constructed (so the page never shows
|
|
31
|
+
* an unpainted canvas): pass it through the constructor's `init` to receive that frame.
|
|
32
|
+
*/
|
|
33
|
+
WillFirstTimeDraw;
|
|
34
|
+
/** DrawnView.WasDrawn: raised after every frame has been drawn (the C# event passes no context). */
|
|
35
|
+
WasDrawn;
|
|
24
36
|
/** Registry + rate-limited snapshot of accessible controls, rendered by the DOM overlay (DrawnUi AccessibilityManager). */
|
|
25
37
|
AccessibilityManager = new SkiaAccessibilityManager();
|
|
26
38
|
content;
|
|
@@ -42,10 +54,12 @@ export class Canvas {
|
|
|
42
54
|
frameId = 0;
|
|
43
55
|
disposed = false;
|
|
44
56
|
observer;
|
|
45
|
-
|
|
57
|
+
/** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
|
|
58
|
+
constructor(Element, init) {
|
|
46
59
|
this.Element = Element;
|
|
47
60
|
if (!Super.CK)
|
|
48
61
|
throw new Error("DrawnUi: call Super.UseDrawnUi()...BuildAsync() before creating a Canvas");
|
|
62
|
+
init?.(this);
|
|
49
63
|
this.observer = new ResizeObserver(() => this.OnResized());
|
|
50
64
|
this.observer.observe(Element);
|
|
51
65
|
this.OnResized();
|
|
@@ -122,6 +136,9 @@ export class Canvas {
|
|
|
122
136
|
const started = performance.now();
|
|
123
137
|
this.ProcessPendingGestures();
|
|
124
138
|
const executed = this.ExecuteAnimators(Math.round(started * 1_000_000));
|
|
139
|
+
const canRender = this.CanRender;
|
|
140
|
+
if (canRender && !this.WasRendered)
|
|
141
|
+
this.WillFirstTimeDraw?.(this, { Canvas: canvas, Surface: this.surface });
|
|
125
142
|
canvas.clear(Super.ParseColor(this.BackgroundColor));
|
|
126
143
|
const root = this.content;
|
|
127
144
|
if (root) {
|
|
@@ -141,6 +158,10 @@ export class Canvas {
|
|
|
141
158
|
this.FPS = this.frameTimes.length;
|
|
142
159
|
if (executed > 0)
|
|
143
160
|
this.Update(); // animators running: keep frames coming
|
|
161
|
+
// C# order: WasDrawn at the end of the frame, then the frame counts as rendered
|
|
162
|
+
this.WasDrawn?.(this);
|
|
163
|
+
if (!this.WasRendered && canRender)
|
|
164
|
+
this.WasRendered = true;
|
|
144
165
|
}
|
|
145
166
|
// ---- deferred disposal (DrawnUi DisposeObject: never delete Skia objects mid-frame) ----
|
|
146
167
|
disposeQueue = [];
|
|
@@ -179,6 +200,50 @@ export class Canvas {
|
|
|
179
200
|
FrameIndex = 0;
|
|
180
201
|
/** On-screen surface (SkiaBackdrop snapshots it). */
|
|
181
202
|
get Surface() { return this.surface; }
|
|
203
|
+
/**
|
|
204
|
+
* A picture of what is on screen right now, as PNG bytes (C# DrawnView.TakeScreenShot).
|
|
205
|
+
*
|
|
206
|
+
* The on-screen canvas cannot simply be read: an accelerated surface lives in a WebGL drawing
|
|
207
|
+
* buffer the browser clears after compositing, so both `canvas.toDataURL()` and a snapshot of
|
|
208
|
+
* the live surface come back blank. The content is therefore drawn once more into an offscreen
|
|
209
|
+
* RASTER surface, which can be read back anywhere.
|
|
210
|
+
*
|
|
211
|
+
* This is a still picture, not a frame of the loop: animators are not ticked and the frame
|
|
212
|
+
* counters do not move, so taking one never changes what the next real frame shows.
|
|
213
|
+
*/
|
|
214
|
+
TakeScreenShot() {
|
|
215
|
+
const CK = Super.CK;
|
|
216
|
+
const w = this.Element.width, h = this.Element.height;
|
|
217
|
+
if (!CK || !w || !h)
|
|
218
|
+
return null;
|
|
219
|
+
const surface = CK.MakeSurface(w, h);
|
|
220
|
+
if (!surface)
|
|
221
|
+
return null;
|
|
222
|
+
try {
|
|
223
|
+
const canvas = surface.getCanvas();
|
|
224
|
+
canvas.clear(Super.ParseColor(this.BackgroundColor));
|
|
225
|
+
const root = this.content;
|
|
226
|
+
if (root) {
|
|
227
|
+
const scale = this.RenderingScale;
|
|
228
|
+
root.Measure(w, h, scale);
|
|
229
|
+
root.Arrange(new SKRect(0, 0, w, h), root.WidthRequest, root.HeightRequest, scale);
|
|
230
|
+
root.Render({ Context: { Canvas: canvas, Surface: surface }, Destination: new SKRect(0, 0, w, h), Scale: scale });
|
|
231
|
+
}
|
|
232
|
+
surface.flush();
|
|
233
|
+
const image = surface.makeImageSnapshot();
|
|
234
|
+
if (!image)
|
|
235
|
+
return null;
|
|
236
|
+
try {
|
|
237
|
+
return image.encodeToBytes();
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
image.delete();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
surface.delete();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
182
247
|
Dispose() {
|
|
183
248
|
this.disposed = true;
|
|
184
249
|
this.Gestures = "Disabled";
|
|
@@ -216,8 +281,13 @@ export class Canvas {
|
|
|
216
281
|
e.type === "pointercancel" ? "Cancelled" : undefined;
|
|
217
282
|
if (!type)
|
|
218
283
|
return;
|
|
219
|
-
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId))
|
|
220
|
-
|
|
284
|
+
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
|
|
285
|
+
if (e.pointerType === "mouse")
|
|
286
|
+
this.UpdateCursor(e.offsetX, e.offsetY);
|
|
287
|
+
return;
|
|
288
|
+
} // hover not ported (TouchActionResult.Pointer)
|
|
289
|
+
if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
|
|
290
|
+
return; // Up of a pointer that never pressed here
|
|
221
291
|
// Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
|
|
222
292
|
if (type === "Pressed") {
|
|
223
293
|
try {
|
|
@@ -231,12 +301,76 @@ export class Canvas {
|
|
|
231
301
|
args.Type = type;
|
|
232
302
|
args.Scale = this.RenderingScale;
|
|
233
303
|
args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
|
|
304
|
+
// every mouse button is delivered with its PointerData (AppoMobi.Gestures): a right click is a Down / Up / Tapped
|
|
305
|
+
// with Button "Right" for games and custom controls, and a ContextMenu on top
|
|
306
|
+
const pd = new PointerData();
|
|
307
|
+
pd.Button = e.button === 0 ? "Left" : e.button === 1 ? "Middle" : e.button === 2 ? "Right" : e.button === 3 ? "XButton1" : e.button === 4 ? "XButton2" : "Extended";
|
|
308
|
+
pd.ButtonNumber = e.button === 1 ? 3 : e.button === 2 ? 2 : e.button + 1;
|
|
309
|
+
pd.DeviceType = e.pointerType === "touch" ? "Touch" : e.pointerType === "pen" ? "Pen" : "Mouse";
|
|
310
|
+
pd.PressedButtons = e.buttons;
|
|
311
|
+
args.Pointer = pd;
|
|
234
312
|
this.OnTouchAction(args);
|
|
313
|
+
if (type === "Released" || type === "Cancelled") {
|
|
314
|
+
this.claimedTouches.delete(e.pointerId);
|
|
315
|
+
this.UpdateTouchAction();
|
|
316
|
+
}
|
|
235
317
|
};
|
|
236
318
|
preventTouch = (e) => e.preventDefault();
|
|
237
|
-
/**
|
|
319
|
+
/** Gestures="Enabled": touch / pen pointers whose Down a control used (see OnTouchAction). */
|
|
320
|
+
claimedTouches = new Set();
|
|
321
|
+
/** Gestures="Enabled": while a claimed touch is down the page may not take its drag. */
|
|
322
|
+
preventClaimedTouch = (e) => { if (this.claimedTouches.size > 0 && e.cancelable)
|
|
323
|
+
e.preventDefault(); };
|
|
324
|
+
/** Called when no control handled a context-menu request; return true to suppress the browser's canvas menu. */
|
|
325
|
+
ContextMenu;
|
|
326
|
+
/** DOM contextmenu (right click / long press / Menu key) -> ContextMenuEventArgs routed through the tree like a tap. */
|
|
327
|
+
onContextMenu = (e) => {
|
|
328
|
+
const rect = this.Element.getBoundingClientRect();
|
|
329
|
+
const scale = this.RenderingScale;
|
|
330
|
+
const x = e.clientX - rect.left, y = e.clientY - rect.top;
|
|
331
|
+
const pointerType = e.pointerType;
|
|
332
|
+
const source = pointerType === "touch" || pointerType === "pen" ? "touch" : pointerType === "mouse" || e.button === 2 ? "mouse" : "keyboard";
|
|
333
|
+
const args = new ContextMenuEventArgs(new SKPoint(x, y), new SKPoint(x * scale, y * scale), source, e);
|
|
334
|
+
let handled = this.content?.ProcessContextMenu(args.Pixels, args) ?? false;
|
|
335
|
+
if (!handled && this.ContextMenu)
|
|
336
|
+
handled = this.ContextMenu(this, args) === true;
|
|
337
|
+
if (handled)
|
|
338
|
+
e.preventDefault();
|
|
339
|
+
};
|
|
340
|
+
cursorPointer = false;
|
|
341
|
+
/**
|
|
342
|
+
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
343
|
+
* is pointer-events:none, so the mouse position is tested against the accessibility snapshot (the accessible
|
|
344
|
+
* controls' rects in points, already sorted and rate-limited), each hit control answering WantsPointerCursor
|
|
345
|
+
* (itself tappable, or a tappable span of a label), and the canvas element's cursor is switched only when the
|
|
346
|
+
* answer changes. Mouse moves only, no work without a mouse and none in the frame loop.
|
|
347
|
+
*/
|
|
348
|
+
UpdateCursor(x, y) {
|
|
349
|
+
let hit = false;
|
|
350
|
+
const scale = this.RenderingScale;
|
|
351
|
+
for (const n of this.AccessibilityManager.Snapshot) {
|
|
352
|
+
if (x < n.Rect.Left || x >= n.Rect.Right || y < n.Rect.Top || y >= n.Rect.Bottom)
|
|
353
|
+
continue;
|
|
354
|
+
// the control decides (whole control, or a tappable span of a label); point in pixels relative to the control's
|
|
355
|
+
// origin, taken from the snapshot rect (already carries the scroll / cache offset; a rotated control gets its bbox)
|
|
356
|
+
if (n.Source.WantsPointerCursor((x - n.Rect.Left) * scale, (y - n.Rect.Top) * scale)) {
|
|
357
|
+
hit = true;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (hit !== this.cursorPointer) {
|
|
362
|
+
this.cursorPointer = hit;
|
|
363
|
+
this.Element.style.cursor = hit ? "pointer" : "";
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Mouse wheel -> TouchActionResult.Wheel.
|
|
368
|
+
* Lock keeps every wheel on the canvas. Enabled shares it with the page, like DrawnUi.Blazor's
|
|
369
|
+
* TouchHandlingStyle.Manual: the page scrolls unless a control consumed this wheel. The browser needs that answer
|
|
370
|
+
* inside this handler, so the wheel is processed now instead of at the next frame (Blazor does the same, for the
|
|
371
|
+
* same reason); pending gestures are flushed first so the event order is kept.
|
|
372
|
+
*/
|
|
238
373
|
onWheel = (e) => {
|
|
239
|
-
e.preventDefault();
|
|
240
374
|
const rect = this.Element.getBoundingClientRect();
|
|
241
375
|
const args = new TouchActionEventArgs();
|
|
242
376
|
args.Id = -1;
|
|
@@ -245,26 +379,101 @@ export class Canvas {
|
|
|
245
379
|
args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
|
|
246
380
|
args.StartingLocation = args.Location;
|
|
247
381
|
args.Wheel = { Delta: e.deltaY !== 0 ? e.deltaY : e.deltaX };
|
|
248
|
-
this.
|
|
382
|
+
if (this.gestures === "Lock") {
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
this.OnGestureEvent(args, "Wheel");
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (this.ProcessGestureNow(args, "Wheel"))
|
|
388
|
+
e.preventDefault();
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* Processes one gesture immediately (after whatever is queued) and reports whether a control USED it: a consumer
|
|
392
|
+
* that marked the event Handled, or any consumer that is not a `BlockGesturesBelow` layer. A blocker returns itself
|
|
393
|
+
* for every gesture it keeps from the controls below (a shell page, a popup backdrop), which is not a use: counting
|
|
394
|
+
* it would block the page wheel over every shell app.
|
|
395
|
+
*/
|
|
396
|
+
ProcessGestureNow(args, result) {
|
|
397
|
+
this.ProcessPendingGestures();
|
|
398
|
+
const root = this.content;
|
|
399
|
+
const consumed = root ? this.ProcessGestures(root, SkiaGesturesParameters.Create(result, args)) : null;
|
|
400
|
+
this.Update();
|
|
401
|
+
return !!consumed && (args.Handled || !consumed.BlockGesturesBelow);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* The page axes a touch pan over the canvas hands to the browser: "pan-y" when the page (or a scrolling ancestor) can
|
|
405
|
+
* scroll vertically, "pan-x" horizontally, both, or "none". Mirrors MAUI's Gestures="Enabled" inside a native scroll
|
|
406
|
+
* view, where the parent takes the pans it scrolls in.
|
|
407
|
+
*/
|
|
408
|
+
static PageScrollAxes(el) {
|
|
409
|
+
const scrolls = (v) => v === "auto" || v === "scroll" || v === "overlay";
|
|
410
|
+
const clips = (v) => v === "hidden" || v === "clip";
|
|
411
|
+
let x = false, y = false;
|
|
412
|
+
for (let n = el.parentElement; n && n !== document.body && n !== document.documentElement; n = n.parentElement) {
|
|
413
|
+
const st = getComputedStyle(n);
|
|
414
|
+
if (!y && scrolls(st.overflowY) && n.scrollHeight > n.clientHeight + 1)
|
|
415
|
+
y = true;
|
|
416
|
+
if (!x && scrolls(st.overflowX) && n.scrollWidth > n.clientWidth + 1)
|
|
417
|
+
x = true;
|
|
418
|
+
}
|
|
419
|
+
// the viewport scrolls unless html or body clips it
|
|
420
|
+
const root = document.scrollingElement ?? document.documentElement;
|
|
421
|
+
const hs = getComputedStyle(document.documentElement), bs = getComputedStyle(document.body);
|
|
422
|
+
if (!y && !clips(hs.overflowY) && !clips(bs.overflowY) && root.scrollHeight > root.clientHeight + 1)
|
|
423
|
+
y = true;
|
|
424
|
+
if (!x && !clips(hs.overflowX) && !clips(bs.overflowX) && root.scrollWidth > root.clientWidth + 1)
|
|
425
|
+
x = true;
|
|
426
|
+
// a canvas filling a framed document (a widget in an iframe): the page that scrolls is the parent, which a
|
|
427
|
+
// cross-origin frame cannot inspect, so assume it scrolls vertically; horizontal pans and taps stay on the canvas
|
|
428
|
+
if (!y && window.self !== window.top)
|
|
429
|
+
y = true;
|
|
430
|
+
return x && y ? "pan-x pan-y" : y ? "pan-y" : x ? "pan-x" : "none";
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Gestures="Enabled" shares touch pans with the page like MAUI's Enabled inside a native scroll view: along an axis
|
|
434
|
+
* the page can scroll, the browser takes the pan (and sends pointercancel), taps and the other axis stay on the
|
|
435
|
+
* canvas. A page that cannot scroll, and Lock, keep every touch. The browser reads touch-action when a touch starts,
|
|
436
|
+
* so it is kept current ahead of time: on attach, when the window / html / body resize, and after every touch.
|
|
437
|
+
*/
|
|
438
|
+
UpdateTouchAction = () => {
|
|
439
|
+
if (this.gestures === "Disabled")
|
|
440
|
+
return;
|
|
441
|
+
const pan = this.gestures === "Enabled" ? Canvas.PageScrollAxes(this.Element) : "none";
|
|
442
|
+
if (this.Element.style.touchAction !== pan)
|
|
443
|
+
this.Element.style.touchAction = pan;
|
|
249
444
|
};
|
|
445
|
+
pageObserver;
|
|
250
446
|
AttachInput() {
|
|
251
447
|
const el = this.Element;
|
|
252
|
-
|
|
448
|
+
this.UpdateTouchAction();
|
|
449
|
+
window.addEventListener("resize", this.UpdateTouchAction);
|
|
450
|
+
this.pageObserver = new ResizeObserver(this.UpdateTouchAction);
|
|
451
|
+
this.pageObserver.observe(document.documentElement);
|
|
452
|
+
this.pageObserver.observe(document.body);
|
|
253
453
|
el.style.userSelect = "none";
|
|
254
454
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
255
455
|
el.addEventListener(t, this.onPointer);
|
|
456
|
+
el.addEventListener("contextmenu", this.onContextMenu);
|
|
256
457
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
257
458
|
if (this.gestures === "Lock")
|
|
258
459
|
el.addEventListener("touchmove", this.preventTouch, { passive: false });
|
|
460
|
+
else if (this.gestures === "Enabled")
|
|
461
|
+
el.addEventListener("touchmove", this.preventClaimedTouch, { passive: false });
|
|
259
462
|
}
|
|
260
463
|
DetachInput() {
|
|
261
464
|
const el = this.Element;
|
|
262
465
|
el.style.touchAction = "";
|
|
466
|
+
window.removeEventListener("resize", this.UpdateTouchAction);
|
|
467
|
+
this.pageObserver?.disconnect();
|
|
468
|
+
this.pageObserver = undefined;
|
|
263
469
|
el.style.userSelect = "";
|
|
264
470
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
265
471
|
el.removeEventListener(t, this.onPointer);
|
|
472
|
+
el.removeEventListener("contextmenu", this.onContextMenu);
|
|
266
473
|
el.removeEventListener("wheel", this.onWheel);
|
|
267
474
|
el.removeEventListener("touchmove", this.preventTouch);
|
|
475
|
+
el.removeEventListener("touchmove", this.preventClaimedTouch);
|
|
476
|
+
this.claimedTouches.clear();
|
|
268
477
|
this.activeTouchIds.clear();
|
|
269
478
|
this.pointerDownArgs.clear();
|
|
270
479
|
this.previousTouchArgs.clear();
|
|
@@ -281,6 +490,16 @@ export class Canvas {
|
|
|
281
490
|
args.IsInContact = true;
|
|
282
491
|
this.pointerDownArgs.set(id, args);
|
|
283
492
|
this.previousTouchArgs.set(id, args);
|
|
493
|
+
// Gestures="Enabled", touch or pen: page scrolling was left to touch-action alone, and iOS Safari still takes an
|
|
494
|
+
// off-axis drag from the page axis (pointercancel mid-gesture, w3c/pointerevents#303). So the Down is processed
|
|
495
|
+
// now, like the wheel (queued gestures first, same "used" rule): when a control used it the touch is claimed and
|
|
496
|
+
// its touchmoves are default-prevented until it ends. It is not queued as well, so it is processed exactly once and
|
|
497
|
+
// the Panning / Up that follow keep their order. An unclaimed touch shares the page as before.
|
|
498
|
+
if (this.gestures === "Enabled" && args.Pointer && args.Pointer.DeviceType !== "Mouse") {
|
|
499
|
+
if (this.ProcessGestureNow(args, "Down"))
|
|
500
|
+
this.claimedTouches.add(id);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
284
503
|
this.OnGestureEvent(args, "Down");
|
|
285
504
|
return;
|
|
286
505
|
}
|
|
@@ -321,8 +540,35 @@ export class Canvas {
|
|
|
321
540
|
for (const args of batch)
|
|
322
541
|
this.ProcessGestures(root, args);
|
|
323
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* The control that consumed the current gesture keeps it until it lets go (DrawnUi Canvas.HadInput). Without this a
|
|
545
|
+
* gesture is re-routed by the pointer position on every event, so anything the finger drags away from — a drag
|
|
546
|
+
* handle, a slider thumb, a button being held — stops receiving Panning the moment the pointer leaves its box.
|
|
547
|
+
* Single pointer, which is what the web gives us here; C# keeps a set for multi-touch.
|
|
548
|
+
*/
|
|
549
|
+
gestureOwner = null;
|
|
550
|
+
/** DrawnUi IsSavedGesture: the types replayed to the owner instead of being routed again. */
|
|
551
|
+
static IsSavedGesture(type) {
|
|
552
|
+
return type === "Panning" || type === "Wheel" || type === "Up";
|
|
553
|
+
}
|
|
324
554
|
/** Entry into the control tree, same shape as DrawnUi Canvas.ProcessGestures. */
|
|
325
555
|
ProcessGestures(root, args) {
|
|
326
|
-
|
|
556
|
+
const info = () => new GestureEventProcessingInfo(args.Event.Location, SKPoint.Empty, SKPoint.Empty, null);
|
|
557
|
+
if (args.Type === "Down")
|
|
558
|
+
this.gestureOwner = null;
|
|
559
|
+
else if (this.gestureOwner && Canvas.IsSavedGesture(args.Type)) {
|
|
560
|
+
const owner = this.gestureOwner;
|
|
561
|
+
const alive = !!owner.Superview && owner.IsVisible && !owner.InputTransparent;
|
|
562
|
+
const consumed = alive ? owner.OnSkiaGestureEvent(args, info()) : null;
|
|
563
|
+
if (consumed) {
|
|
564
|
+
// it still wants the gesture: nobody else sees this one (C# skips the tree pass for a saved gesture)
|
|
565
|
+
this.gestureOwner = args.Type === "Up" ? null : consumed;
|
|
566
|
+
return consumed;
|
|
567
|
+
}
|
|
568
|
+
this.gestureOwner = null; // it let go (a button whose press turned into a pan): route normally again
|
|
569
|
+
}
|
|
570
|
+
const consumed = root.ProcessGestures(args, info());
|
|
571
|
+
this.gestureOwner = consumed && args.Type !== "Up" ? consumed : null;
|
|
572
|
+
return consumed;
|
|
327
573
|
}
|
|
328
574
|
}
|
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/Pointer/Touch declared for parity, not produced yet. */
|
|
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,9 +21,23 @@ export declare class DistanceInfo {
|
|
|
21
21
|
Total: SKPoint;
|
|
22
22
|
Velocity: SKPoint;
|
|
23
23
|
}
|
|
24
|
+
/** AppoMobi.Gestures MouseButton: which button pressed / released (DOM button 0..4). */
|
|
25
|
+
export type MouseButton = "Left" | "Middle" | "Right" | "XButton1" | "XButton2" | "Extended";
|
|
26
|
+
export type PointerDeviceType = "Mouse" | "Touch" | "Pen";
|
|
27
|
+
/** AppoMobi.Gestures PointerData: the device and button behind a Down / Up / Tapped (every button is delivered). */
|
|
28
|
+
export declare class PointerData {
|
|
29
|
+
Button: MouseButton;
|
|
30
|
+
/** 1 = Left, 2 = Right, 3 = Middle, 4+ = extended, like AppoMobi.Gestures. */
|
|
31
|
+
ButtonNumber: number;
|
|
32
|
+
DeviceType: PointerDeviceType;
|
|
33
|
+
/** DOM `buttons` bitmask of the buttons held (1 left, 2 right, 4 middle, 8 back, 16 forward). */
|
|
34
|
+
PressedButtons: number;
|
|
35
|
+
}
|
|
24
36
|
/** AppoMobi.Gestures TouchActionEventArgs. */
|
|
25
37
|
export declare class TouchActionEventArgs {
|
|
26
38
|
Id: number;
|
|
39
|
+
/** Device and button of this event (mouse, pen, touch); undefined for wheel. */
|
|
40
|
+
Pointer?: PointerData;
|
|
27
41
|
Type: TouchActionType;
|
|
28
42
|
/** Pixels. */
|
|
29
43
|
Location: SKPoint;
|
|
@@ -39,6 +53,11 @@ export declare class TouchActionEventArgs {
|
|
|
39
53
|
Wheel: {
|
|
40
54
|
Delta: number;
|
|
41
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* AppoMobi TouchActionEventArgs.Handled: set by a control that actually used the event. The canvas prevents the
|
|
58
|
+
* browser default (the page scrolling under the wheel) only for a Handled event in `Gestures="Enabled"`.
|
|
59
|
+
*/
|
|
60
|
+
Handled: boolean;
|
|
42
61
|
/** Same as the .NET helper: derives Start/End/Delta/Total from the previous event of the same pointer. */
|
|
43
62
|
static FillDistanceInfo(current: TouchActionEventArgs, previous: TouchActionEventArgs | undefined): void;
|
|
44
63
|
}
|
|
@@ -66,6 +85,33 @@ export declare class SkiaGesturesInfo {
|
|
|
66
85
|
constructor(Args: SkiaGesturesParameters, Info: GestureEventProcessingInfo);
|
|
67
86
|
}
|
|
68
87
|
/** DrawnUi ControlTappedEventArgs. */
|
|
88
|
+
/** Where a context-menu request came from: right click, long press (Android fires contextmenu), or the keyboard Menu key. */
|
|
89
|
+
export type ContextMenuSource = "mouse" | "touch" | "keyboard";
|
|
90
|
+
/**
|
|
91
|
+
* Arguments of SkiaControl.ContextMenu / Canvas.ContextMenu: a browser `contextmenu` request on the canvas (right
|
|
92
|
+
* click, long press on touch, the Menu key). Handlers return true to take it: the browser's own menu ("Save image")
|
|
93
|
+
* is then suppressed; otherwise it shows as usual.
|
|
94
|
+
*/
|
|
95
|
+
export declare class ContextMenuEventArgs {
|
|
96
|
+
/** Point on the canvas, in points (CSS px). */
|
|
97
|
+
Location: SKPoint;
|
|
98
|
+
/** Same point in pixels (canvas space). */
|
|
99
|
+
Pixels: SKPoint;
|
|
100
|
+
Source: ContextMenuSource;
|
|
101
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
102
|
+
Native: MouseEvent;
|
|
103
|
+
/** Deepest control under the point that had a ContextMenu handler (set while routing). */
|
|
104
|
+
Control?: SkiaControl;
|
|
105
|
+
/** Point inside Control, in pixels relative to its DrawingRect origin (set while routing). */
|
|
106
|
+
Local: SKPoint;
|
|
107
|
+
constructor(
|
|
108
|
+
/** Point on the canvas, in points (CSS px). */
|
|
109
|
+
Location: SKPoint,
|
|
110
|
+
/** Same point in pixels (canvas space). */
|
|
111
|
+
Pixels: SKPoint, Source: ContextMenuSource,
|
|
112
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
113
|
+
Native: MouseEvent);
|
|
114
|
+
}
|
|
69
115
|
export declare class ControlTappedEventArgs {
|
|
70
116
|
Control: SkiaControl;
|
|
71
117
|
Parameters: SkiaGesturesParameters;
|
package/dist/core/Gestures.js
CHANGED
|
@@ -18,9 +18,20 @@ export class DistanceInfo {
|
|
|
18
18
|
Total = SKPoint.Empty;
|
|
19
19
|
Velocity = SKPoint.Empty;
|
|
20
20
|
}
|
|
21
|
+
/** AppoMobi.Gestures PointerData: the device and button behind a Down / Up / Tapped (every button is delivered). */
|
|
22
|
+
export class PointerData {
|
|
23
|
+
Button = "Left";
|
|
24
|
+
/** 1 = Left, 2 = Right, 3 = Middle, 4+ = extended, like AppoMobi.Gestures. */
|
|
25
|
+
ButtonNumber = 1;
|
|
26
|
+
DeviceType = "Mouse";
|
|
27
|
+
/** DOM `buttons` bitmask of the buttons held (1 left, 2 right, 4 middle, 8 back, 16 forward). */
|
|
28
|
+
PressedButtons = 0;
|
|
29
|
+
}
|
|
21
30
|
/** AppoMobi.Gestures TouchActionEventArgs. */
|
|
22
31
|
export class TouchActionEventArgs {
|
|
23
32
|
Id = 0;
|
|
33
|
+
/** Device and button of this event (mouse, pen, touch); undefined for wheel. */
|
|
34
|
+
Pointer;
|
|
24
35
|
Type = "Pressed";
|
|
25
36
|
/** Pixels. */
|
|
26
37
|
Location = SKPoint.Empty;
|
|
@@ -34,6 +45,11 @@ export class TouchActionEventArgs {
|
|
|
34
45
|
DeltaTimeMs = 0;
|
|
35
46
|
/** Mouse wheel: Delta > 0 = wheel down (browser deltaY sign). */
|
|
36
47
|
Wheel = { Delta: 0 };
|
|
48
|
+
/**
|
|
49
|
+
* AppoMobi TouchActionEventArgs.Handled: set by a control that actually used the event. The canvas prevents the
|
|
50
|
+
* browser default (the page scrolling under the wheel) only for a Handled event in `Gestures="Enabled"`.
|
|
51
|
+
*/
|
|
52
|
+
Handled = false;
|
|
37
53
|
/** Same as the .NET helper: derives Start/End/Delta/Total from the previous event of the same pointer. */
|
|
38
54
|
static FillDistanceInfo(current, previous) {
|
|
39
55
|
if (!previous) {
|
|
@@ -91,7 +107,33 @@ export class SkiaGesturesInfo {
|
|
|
91
107
|
this.Info = Info;
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Arguments of SkiaControl.ContextMenu / Canvas.ContextMenu: a browser `contextmenu` request on the canvas (right
|
|
112
|
+
* click, long press on touch, the Menu key). Handlers return true to take it: the browser's own menu ("Save image")
|
|
113
|
+
* is then suppressed; otherwise it shows as usual.
|
|
114
|
+
*/
|
|
115
|
+
export class ContextMenuEventArgs {
|
|
116
|
+
Location;
|
|
117
|
+
Pixels;
|
|
118
|
+
Source;
|
|
119
|
+
Native;
|
|
120
|
+
/** Deepest control under the point that had a ContextMenu handler (set while routing). */
|
|
121
|
+
Control;
|
|
122
|
+
/** Point inside Control, in pixels relative to its DrawingRect origin (set while routing). */
|
|
123
|
+
Local = SKPoint.Empty;
|
|
124
|
+
constructor(
|
|
125
|
+
/** Point on the canvas, in points (CSS px). */
|
|
126
|
+
Location,
|
|
127
|
+
/** Same point in pixels (canvas space). */
|
|
128
|
+
Pixels, Source,
|
|
129
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
130
|
+
Native) {
|
|
131
|
+
this.Location = Location;
|
|
132
|
+
this.Pixels = Pixels;
|
|
133
|
+
this.Source = Source;
|
|
134
|
+
this.Native = Native;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
95
137
|
export class ControlTappedEventArgs {
|
|
96
138
|
Control;
|
|
97
139
|
Parameters;
|