drawnui-react 0.1.0-preview.1 → 0.1.0-preview.2
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 +85 -29
- package/SKIPPED.md +9 -6
- package/dist/controls/SkiaLabel.d.ts +2 -0
- package/dist/controls/SkiaLabel.js +9 -0
- package/dist/core/Canvas.d.ts +26 -1
- package/dist/core/Canvas.js +100 -3
- package/dist/core/Gestures.d.ts +28 -1
- package/dist/core/Gestures.js +27 -1
- package/dist/core/SkiaControl.d.ts +17 -1
- package/dist/core/SkiaControl.js +30 -0
- package/dist/core/Types.d.ts +7 -2
- package/dist/react/SkiaShell.js +5 -2
- package/dist/react/index.d.ts +13 -3
- package/dist/react/index.js +16 -1
- package/dist/vite/index.d.ts +74 -0
- package/dist/vite/index.js +204 -0
- package/package.json +31 -4
- package/PARITY.md +0 -287
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# DrawnUi.React
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[DrawnUi](https://drawnui.net) engine ptototype for React, TypeScript on top of
|
|
4
4
|
[CanvasKit](https://skia.org/docs/user/modules/canvaskit/) (Skia for the browser), composed with React
|
|
5
5
|
through a custom `react-reconciler` renderer.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Check out latest playbook: [helloreact.drawnui.net](https://helloreact.drawnui.net/) 👈
|
|
8
|
+
|
|
9
|
+
*Work in progress*: the goal is same API surface and semantics as DrawnUi (.NET) — same control names, same PascalCase
|
|
8
10
|
property names, same measure/arrange/paint contract — so knowledge and docs transfer 1:1.
|
|
9
11
|
|
|
10
12
|
```tsx
|
|
@@ -20,17 +22,31 @@ await Super.UseDrawnUi()
|
|
|
20
22
|
</Canvas>
|
|
21
23
|
```
|
|
22
24
|
|
|
23
|
-
##
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm i drawnui-react@preview react react-dom
|
|
29
|
+
```
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- `src/react` — reconciler host config + typed JSX tags + `<Canvas>` bridge component.
|
|
29
|
-
- `samples/demo/` — the deployed demo: root menu + pages (`pages/ImagesPage.tsx`, `SvgPage.tsx`, `CellsPage.tsx` with `ContactCell.ts`) navigated by the React-level `SkiaShell`.
|
|
30
|
-
- `samples/<name>/` — one folder per sample: `index.html`, `main.tsx`, two-line `vite.config.ts` (`defineSample`). Shared assets (fonts) in `samples/public`.
|
|
31
|
-
- `dev/build-samples.mjs` — builds every sample into `dist/<name>/` + a `dist/index.html` list; used by the Pages workflow.
|
|
31
|
+
`drawnui-react` = React tags + every engine type, `drawnui-react/core` = the engine only. Ships ES modules + `.d.ts`;
|
|
32
|
+
CanvasKit's `.wasm` is referenced with a `?url` import, so use Vite (or any bundler that understands `?url`) and
|
|
33
|
+
put your fonts under `public/fonts`. Preview releases carry the `preview` dist-tag (the first publish also became `latest`, as npm always does).
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
## Run
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
npm install
|
|
39
|
+
npm run dev # samples/demo at http://localhost:5173
|
|
40
|
+
npx vite samples/<name> # any other sample
|
|
41
|
+
npm run build # typecheck + build all samples into dist/<name>/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Skills for AI agents
|
|
45
|
+
|
|
46
|
+
`skills/drawnui-react/SKILL.md` teaches an agent this library (install, startup, composition rules, scroll / shell
|
|
47
|
+
extras, caching, effects, accessibility). Save it under `~/.claude/skills/drawnui-react/SKILL.md` or fetch it from
|
|
48
|
+
the demo site: https://helloreact.drawnui.net/skills/drawnui-react/SKILL.md — `llms.txt` / `llms-full.txt` at the
|
|
49
|
+
site root point to it. Pair it with the DrawnUI framework skill from https://drawnui.net/llms.txt.
|
|
34
50
|
|
|
35
51
|
## Where React ends and DrawnUi begins
|
|
36
52
|
|
|
@@ -49,6 +65,23 @@ accessibility all live in the engine and would work identically under Vue, Svelt
|
|
|
49
65
|
That is also why the demo pages describe DrawnUi features, not React ones: the same pages are meant to be reused as
|
|
50
66
|
the showcase for other frameworks on this engine.
|
|
51
67
|
|
|
68
|
+
## Context menu (right click)
|
|
69
|
+
|
|
70
|
+
A right click on the canvas normally opens the browser's own menu ("Save image as…"). Only the primary mouse
|
|
71
|
+
button starts a gesture, as in DrawnUi.Blazor, so a right click never fires `Tapped`; to take it, handle
|
|
72
|
+
`ContextMenu` on any control, exactly like `Tapped`:
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<SkiaShape ContextMenu={(sender, e) => { openMyMenu(e.Location); return true; }} />
|
|
76
|
+
<Canvas ContextMenu={(canvas, e) => true /* nothing hit: swallow the browser menu everywhere */} />
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The request is routed like a tap: deepest visible control under the point first (through transforms), then its
|
|
80
|
+
parents, then the `Canvas` prop. The first handler returning `true` takes it and the browser menu is suppressed;
|
|
81
|
+
with no handler the browser menu shows as before. `e` carries `Location` (points on the canvas), `Pixels`, `Local`
|
|
82
|
+
(pixels inside the handling control), `Source` (`"mouse"`, `"touch"` for a long press, `"keyboard"` for the Menu
|
|
83
|
+
key) and `Native` (the DOM event, for modifiers). Web-only: DrawnUi.Net has no mouse buttons.
|
|
84
|
+
|
|
52
85
|
## Accessibility
|
|
53
86
|
|
|
54
87
|
Same model as DrawnUi.Blazor: the `<canvas>` is `aria-hidden`, an invisible DOM overlay mirrors every
|
|
@@ -68,25 +101,48 @@ label readable and every button focusable without touching each control.
|
|
|
68
101
|
The overlay has `pointer-events: none`, so hover and all pointer gestures still reach the canvas — the
|
|
69
102
|
Blazor "accessible control loses hover" limitation does not apply.
|
|
70
103
|
|
|
71
|
-
|
|
104
|
+
Text selection (React extension, opt-in): `AccessibilityTextSelectable` on a `SkiaLabel` renders its laid-out lines
|
|
105
|
+
as real, invisible DOM text in the overlay — one span per drawn line, in the same font (registered fonts are also
|
|
106
|
+
installed as CSS `FontFace`s) and stretched to the drawn line width — with pointer events on. The browser then
|
|
107
|
+
selects, copies (Ctrl+C / context menu) and reads it like an HTML paragraph, and the wheel over it still scrolls the
|
|
108
|
+
drawn content. It is off by default and must stay off on anything gesture-driven (buttons, carousels, drawers): the
|
|
109
|
+
selectable text owns the pointer, so taps and pans under it never reach the drawn control. Everything else in the
|
|
110
|
+
overlay is `user-select: none`, so select-all only highlights opted-in text.
|
|
72
111
|
|
|
73
|
-
|
|
74
|
-
npm install
|
|
75
|
-
npm run dev # samples/demo at http://localhost:5173
|
|
76
|
-
npx vite samples/<name> # any other sample
|
|
77
|
-
npm run build # typecheck + build all samples into dist/<name>/
|
|
78
|
-
```
|
|
112
|
+
## Crawlers and AI agents: static HTML from the accessibility tree
|
|
79
113
|
|
|
80
|
-
|
|
114
|
+
A drawn app serves `<div id="root"></div>`; a crawler that does not run JavaScript sees nothing. The
|
|
115
|
+
`drawnUiStatic` Vite plugin fixes that at build time, from the app itself:
|
|
81
116
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
| Secret | What it is | Where to get it |
|
|
88
|
-
|---|---|---|
|
|
89
|
-
| `CLOUDFLARE_API_TOKEN` | Cloudflare API token with **Account → Cloudflare Pages → Edit** (deploy needs nothing else). | dash.cloudflare.com → My Profile → API Tokens → Create Token → "Edit Cloudflare Workers" template or custom with the Pages permission; copy the value once. Set with `gh secret set CLOUDFLARE_API_TOKEN --repo DrawnUi/DrawnUi.React` (paste the value on stdin). |
|
|
90
|
-
| `CLOUDFLARE_ACCOUNT_ID` | The Cloudflare account that owns the Pages project. | dash.cloudflare.com → any zone → Overview → right column "Account ID", or `npx wrangler whoami`. `gh secret set CLOUDFLARE_ACCOUNT_ID --repo DrawnUi/DrawnUi.React`. |
|
|
117
|
+
```ts
|
|
118
|
+
// vite.config.ts
|
|
119
|
+
import { drawnUiStatic } from "drawnui-react/vite";
|
|
120
|
+
export default defineConfig({ plugins: [react(), drawnUiStatic()] });
|
|
121
|
+
```
|
|
91
122
|
|
|
92
|
-
|
|
123
|
+
After `vite build` it serves the build, opens it in headless Chrome (`playwright-core`, your dev dependency; GitHub's
|
|
124
|
+
ubuntu runners ship a Chrome), lets the engine draw, reads the accessibility snapshot of the root page and of every
|
|
125
|
+
page a root button opens (or the `routes` you list), and writes ordinary visible HTML into `dist/index.html`: a
|
|
126
|
+
heading is a heading, a button is a link to the page it opened, a label is a paragraph. The overlay's own markup
|
|
127
|
+
(transparent text over the canvas) is never copied — in a static file it would read as hidden text. Nothing is
|
|
128
|
+
hand-written, so it cannot drift from what the canvas draws.
|
|
129
|
+
|
|
130
|
+
Where it lands: right after the mount element (`#root`), or wherever you put `<!-- drawnui-static -->` in
|
|
131
|
+
`index.html`. With the mount element filling the viewport that is below the fold: a visitor sees the app area
|
|
132
|
+
loading, the text is a scroll away, genuinely visible, nothing collapsed or hidden. The first `<Canvas>` that mounts
|
|
133
|
+
removes it once its engine has drawn frame 1 (one `querySelectorAll`, nothing in the frame loop, nothing to pay for
|
|
134
|
+
an app built without the plugin). If the app never boots it stays. The mount element keeps its size, so nothing
|
|
135
|
+
above the fold moves; a page that scrolled only because of the block loses its scrollbar (classic scrollbars give
|
|
136
|
+
the canvas those ~15 px back — `html { scrollbar-gutter: stable }` keeps the gutter if you prefer). If the mount
|
|
137
|
+
element is a box inside a longer page, put the marker at the end of the body so your own content does not move.
|
|
138
|
+
Look: a default stylesheet ships with the block (inherits your `body` font and color, translucent surfaces); override
|
|
139
|
+
by styling `.drawnui-static` or pass `render: { style: false }`.
|
|
140
|
+
|
|
141
|
+
Two things to know. Check it with `curl` or view-source, not DevTools after boot. And crawlers that do not run
|
|
142
|
+
JavaScript (GPTBot, ClaudeBot, CCBot, link previews) read this HTML, while Googlebot renders JavaScript and sees the
|
|
143
|
+
booted page, i.e. the accessibility overlay; both come from the same tree, and a control without an
|
|
144
|
+
`AccessibilityRole` is invisible to both.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
Repo layout, build scripts, skill sync and how the demo / npm package are published: [dev/DEVELOPMENT.md](dev/DEVELOPMENT.md).
|
package/SKIPPED.md
CHANGED
|
@@ -7,10 +7,10 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
7
7
|
|
|
8
8
|
| Area | Status | Notes |
|
|
9
9
|
|---|---|---|
|
|
10
|
-
| `UseCache` | partial | `None`, `Operations` (SkPicture replay), `Image` (offscreen surface snapshot, GPU-backed on WebGL, nearest-sampled blit)
|
|
10
|
+
| `UseCache` | partial | `None`, `Operations` / `OperationsFull` (SkPicture replay), `Image` (offscreen surface snapshot, GPU-backed on WebGL, nearest-sampled blit), `ImageDoubleBuffered` (`RenderObjectPrevious` kept and drawn when a new cache cannot be produced yet, `DrawPlaceholder(ctx)` hook when there is none; recording itself is synchronous — no background thread, same as DrawnUi.Blazor with `CanUseCacheDoubleBuffering = false`), `ImageComposite` / `ImageCompositeGPU` (per-child composite, see the `UseCache="ImageComposite"` row below). `GPU` resolves to `Image` and `ImageCompositeGPU` to `ImageComposite`: no separate GPU-only path. `Super.CacheEnabled`, `RenderObject`/`CachedObject`, `InvalidateCache`, `DestroyRenderingObject`, `Canvas.DisposeObject` (deleted after flush). Defaults per C#: `SkiaLabel`/`SkiaSvg` Operations, everything else None. Not ported: `CacheSharing`, `AllowCaching`, offscreen bake threads, cache validity by GRContext. |
|
|
11
11
|
| Incremental invalidation | partial | `Measure` returns the previous size when `!NeedMeasure` and constraints+scale are unchanged; `Update()`/`InvalidateMeasure()` bubble up and stale every ancestor cache; `Repaint()` keeps caches. Arrange still runs per frame for the whole tree; no dirty regions, no `DirtyChildrenTracker`. Post-animators on a control nested INSIDE a cached ancestor are drawn only when that ancestor re-records. |
|
|
12
|
-
| Gestures core: `Canvas.Gestures` (Disabled/Enabled/Lock), `ProcessGestures(args, apply)`, `ConsumeGestures`, `Tapped`/`ChildTapped`, `InputTransparent`, `BlockGesturesBelow`, `LockChildrenGestures`, `HitBoxAuto`/`HitIsInside`/`IsGestureForChild`, `SkiaGesturesParameters`/`GestureEventProcessingInfo`/`SkiaGesturesInfo`/`ControlTappedEventArgs` | ported | Pointer events -> per-pointer `OnTouchAction` state machine (port of DrawnUi.Blazor) -> Down/Panning/Tapped/Up, queued and processed at frame start. Tap slop = 16pt like AppoMobi TouchEffect. |
|
|
13
|
-
| Gestures: `LongPressing`, `Pointer` (hover), multi-touch pinch,
|
|
12
|
+
| Gestures core: `Canvas.Gestures` (Disabled/Enabled/Lock), `ProcessGestures(args, apply)`, `ConsumeGestures`, `Tapped`/`ChildTapped`, `InputTransparent`, `BlockGesturesBelow`, `LockChildrenGestures`, `HitBoxAuto`/`HitIsInside`/`IsGestureForChild`, `SkiaGesturesParameters`/`GestureEventProcessingInfo`/`SkiaGesturesInfo`/`ControlTappedEventArgs` | ported | Pointer events -> per-pointer `OnTouchAction` state machine (port of DrawnUi.Blazor) -> Down/Panning/Tapped/Up, queued and processed at frame start. Tap slop = 16pt like AppoMobi TouchEffect. Only the primary mouse button starts a gesture (right / middle clicks are ignored, like DrawnUi.Blazor). |
|
|
13
|
+
| Gestures: `LongPressing`, `Pointer` (hover), multi-touch pinch, `SoftLock`, `AddGestures` attached props, `OnGestures` delegate | skipped | The enum members exist for parity and are never produced; there is no pinch member at all. Ported instead of skipped: velocity (`Gestures.ts` fills `Distance.Velocity` per move, the scroll / snapping physics run on it) and transform-aware mapping — a child receives the point through the inverse `RenderTransformMatrix` (`TransformPointToLocalSpace`), so rotated / scaled / translated controls hit-test correctly. `ChildOffset` stays zero because that inverse matrix replaces the C# cache-offset math (`TranslateInputCoords`), not because transforms are missing. `Wheel` IS produced (`Canvas.onWheel`). |
|
|
14
14
|
| C# multi-subscriber `event`s | changed shape | One callback per event prop (`Tapped={fn}`), same names. `Command*` (ICommand) variants not ported. |
|
|
15
15
|
| `Opacity`, `TranslationX/Y`, `Rotation`, `ScaleX/Y` (+`Scale`), `SkewX/Y`, `AnchorX/Y`, `HasTransform`, `RenderTransformMatrix`, `TransformPointToLocalSpace` | ported | Same matrix order as C# `ApplyTransforms` (pivot → rotation → scale/skew → translation), opacity = `saveLayer` alpha over the subtree, gestures mapped through the inverse matrix (`IsGestureForChild`, child receives the local point), accessibility rect = transformed bounds. Animations: `AnimateAsync`, `AnimateRangeAsync`, `FadeToAsync`, `ScaleToAsync`, `TranslateToAsync`, `RotateToAsync` as Promises with `AbortSignal` (one running per kind, like the C# per-property cancellation). Not ported: `RotationX/Y/Z`, `TranslationZ`, `Perspective1/2`, `CameraAngle`, `LinkTransforms`, `AddTranslationX/Y`, `UseTranslation*` overrides, `CustomizeLayerPaint`, `SkipRenderingOutOfBounds`. |
|
|
16
16
|
| `Left` / `Top` | ported (as a translate) | Same paint-time offset in points, but folded into the render matrix (a `canvas.translate`, hit-testing follows) for every control, cached or not; C# applies them only when blitting a cache. |
|
|
@@ -29,7 +29,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
29
29
|
| `HorizontalFillRatio`/`VerticalFillRatio` | ported | Applied at arrange like C# `DefineAvailableSize` (the Fill box is scaled, alignment stays inside the full box); measure is not affected (C# passes `useModifiers=false` there too). |
|
|
30
30
|
| `ZIndex` | ported | Static children of a layout are drawn in ZIndex order (stable for equal values), gestures go to the top-most first; the sorted list is cached until a child or a ZIndex changes (C# `GetOrderedSubviews`). Templated cells keep index order. |
|
|
31
31
|
| `FillGradient`, `SkiaShape.StrokeGradient` | ported | `SkiaGradient` as a plain object: `Type` Linear (Start/End ratios or CSS `Angle`), Circular / Conical (radius = half the smaller side), Oval (radial scaled to the box), Sweep (around the center, `Value1`/`Value2` angles), `ColorPositions`, `TileMode`, `Light` (< 1 darker, > 1 lighter — HSL lightness, C# uses `MakeDarker/MakeLighter`, exact curve unverified), `Opacity`, `BlendMode`; C# `SetupGradient` paint setup (white base); shaders cached per gradient object and rect (C# `Version` cache: a new React literal replaces the old shaders). `SkiaLabel.FillGradient` paints the glyphs (`GradientByLines` default true = per line bounds, false = the text block) and the background only when `BackgroundColor` is set, like the C# label override. `SkiaLabel` text stroke (`StrokeColor`, `StrokeWidth` points, `StrokeGradient`) and drop shadow (`DropShadowColor`, `DropShadowSize`, `DropShadowOffsetX/Y`) in the C# order shadow → stroke → fill, measurement inflated by stroke × 2 and shadow size + offset like C#. Not ported: `Background` MAUI brushes, `FillBlendMode`. |
|
|
32
|
-
| `SkiaShell` | partial (React-level) | `Routes` (route -> JSX page factory), `GoToAsync(route, animated)` / `GoBackAsync(animated)` / `PopToRootAsync` with the SkiaViewSwitcher slide (`PagesAnimationSpeed` 200 ms, pushed pages opaque over the page below, lower pages hidden when idle), `NavigationStack`, `CanGoBack`, `Route`, nav bar with Back + title, `useShell()`; `OpenPopupAsync(content, { animated, closeWhenBackgroundTapped, showOverlay, backgroundColor })` (C# PopupWrapper: dimmed backdrop, scale 0.5→1 + fade over `PopupsAnimationSpeed`, tap outside the content closes), `ClosePopupAsync`, `CloseAllPopups`; `PushModalAsync(content, { useGestures, animated, freezeBackground })` (C# ModalWrapper: full-screen `SkiaDrawer` FromBottom HeaderSize 0, drag-to-close with `useGestures`), `PopModalAsync`; `ShowToast(text | content, ms)` (C# layout: bottom banner, slide + fade 300/250 ms, `ShellDefaults.Toast*`), `CloseAllToasts`; `PopupsCount`/`ModalsCount`/`ToastsCount`; `ShellDefaults` = the C# statics (`PopupBackgroundColor`, `PopupsBackgroundBlur`, `PopupsAnimationSpeed`, `PopupsCancelAnimationsAfterMs`, `ZIndex*`, `Toast*`, tab colors); popups/modals sit on a `SkiaBackdrop` (blur + tint); `Tabs` (C# SkiaViewSwitcher tabs: bottom tab bar, per-tab page stacks, `SelectedTab`, `SelectTabAsync`, `PopTabToRootAsync`); `GoBackAsync` follows C# `GoBackDefault` (top popup, then modal, then page); React extension `UseBrowserHistory` (default true): pages in the URL hash `#/a/b`, one history entry per page / popup / modal, browser Back unwinds in the same order, deep links restore the page stack. Not ported: engine-level shell / `NavigationLayout` view switcher, frozen-screenshot backgrounds, `pixelsScaleInFrom`, `Navigating`/`Navigated`/`RouteChanged` events, `RegisterRoute` by type with arguments, `IHandleGoBack`, insets, `PushAsync(page instance)`; `AnimateTabs` + `TabsAnimationSpeed` (150) ported with the C# SelectLeftTab/SelectRightTab slide (0.75 width, fade, back-ease 0.55) — only the tab roots animate, pages pushed inside the leaving tab drop instantly. |
|
|
32
|
+
| `SkiaShell` | partial (React-level) | `Routes` (route -> JSX page factory), `GoToAsync(route, animated)` / `GoBackAsync(animated)` / `PopToRootAsync` with the SkiaViewSwitcher slide (`PagesAnimationSpeed` 200 ms, pushed pages opaque over the page below, lower pages hidden when idle), `NavigationStack`, `CanGoBack`, `Route`, nav bar with Back + title, `useShell()`; `OpenPopupAsync(content, { animated, closeWhenBackgroundTapped, showOverlay, backgroundColor })` (C# PopupWrapper: dimmed backdrop, scale 0.5→1 + fade over `PopupsAnimationSpeed`, tap outside the content closes), `ClosePopupAsync`, `CloseAllPopups`; `PushModalAsync(content, { useGestures, animated, freezeBackground })` (C# ModalWrapper: full-screen `SkiaDrawer` FromBottom HeaderSize 0, drag-to-close with `useGestures`), `PopModalAsync`; `ShowToast(text | content, ms)` (C# layout: bottom banner, slide + fade 300/250 ms, `ShellDefaults.Toast*`), `CloseAllToasts`; `PopupsCount`/`ModalsCount`/`ToastsCount`; `ShellDefaults` = the C# statics (`PopupBackgroundColor`, `PopupsBackgroundBlur`, `PopupsAnimationSpeed`, `PopupsCancelAnimationsAfterMs`, `ZIndex*`, `Toast*`, tab colors); popups/modals sit on a `SkiaBackdrop` (blur + tint); `Tabs` (C# SkiaViewSwitcher tabs: bottom tab bar, per-tab page stacks, `SelectedTab`, `SelectTabAsync`, `PopTabToRootAsync`); `GoBackAsync` follows C# `GoBackDefault` (top popup, then modal, then page); React extension `UseBrowserHistory` (default true): pages in the URL hash `#/a/b`, one history entry per page / popup / modal, browser Back unwinds in the same order, deep links restore the page stack. Not ported: engine-level shell / `NavigationLayout` view switcher, frozen-screenshot backgrounds, `pixelsScaleInFrom`, `Navigating`/`Navigated`/`RouteChanged` events, `RegisterRoute` by type with arguments, `IHandleGoBack`, insets, `PushAsync(page instance)`; `AnimateTabs` + `TabsAnimationSpeed` (150) ported with the C# SelectLeftTab/SelectRightTab slide (0.75 width, fade, back-ease 0.55) — only the tab roots animate, pages pushed inside the leaving tab drop instantly. Browser history: a `popstate` at the same depth with another hash (a plain `<a href="#/route">` link, a typed hash) rebuilds the stack like a forward navigation. |
|
|
33
33
|
| Styles / `ConfigureStyles` | skipped | |
|
|
34
34
|
| Animators core: `AnimatorBase` / `SkiaValueAnimator` / `RenderingAnimator` (`IOverlayEffect`), `Canvas.RegisterAnimator`/`AnimatingControls`, `PostAnimators` + `ExecutePostAnimators`, `Easing` (Linear/Cubic*) | ported | Frame-driven: a running animator keeps frames coming, idle canvas draws nothing. |
|
|
35
35
|
| Touch feedback: `AnimationTapped="Ripple"` + `TouchEffectColor` + `AnimationTappedSpeed` on any control, `SkiaButton.ApplyEffect="Ripple"` (on Down), `PlayRippleAnimation`, `RippleAnimator`, `ClipEffects`/`CreateClip` | ported | Same numbers as C#: 500ms CubicIn, radius 300pt, opacity 0.20 fading over 1.15x progress. |
|
|
@@ -48,9 +48,11 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `RenderingMode` | partial | `Accelerated` (WebGL) with automatic fallback to software; read once at first surface creation. |
|
|
50
50
|
| `Gestures` param | ported | Enabled applies `touch-action:none; user-select:none`; Lock also blocks `touchmove` default. |
|
|
51
|
+
| Mouse cursor over interactive controls (Blazor `Canvas.razor` `.xaml-a11y-interactive { cursor: pointer }`) | ported (differently) | The overlay here is `pointer-events: none`, so `Canvas.UpdateCursor` hit-tests mouse moves with no active pointer against the accessibility snapshot rects (`CanInteract` = has `Tapped`, buttons, links) and asks each hit control `WantsPointerCursor(x, y)` (default `AccessibilityCanInteract`; `SkiaLabel` also answers for its tappable `TextSpan`s), setting the canvas element's `cursor` only when the answer changes. Touch pointers do nothing. Engine hover (`IsHovered` / `OnHover`, `TouchActionResult.Pointer`) stays skipped, see Gestures. |
|
|
52
|
+
| `ContextMenu` (right click / long press / Menu key, React extension) | ported (web-only) | `SkiaControl.ContextMenu` / `Canvas.ContextMenu` handlers with `ContextMenuEventArgs` (`Location` points, `Pixels`, `Local` px in the control, `Source`, `Native`); the DOM `contextmenu` event is routed like a tap (`ProcessContextMenu`: visible non-transparent children top-most first through `TransformPointToLocalSpace`, then the control, then the canvas). `true` = handled, browser menu suppressed; nothing handled = browser menu. Not in DrawnUi.Net. |
|
|
51
53
|
| Keyboard, focus (`FocusedChild`) | skipped | |
|
|
52
|
-
| FPS
|
|
53
|
-
| Insets / safe areas |
|
|
54
|
+
| Rendering stats | partial | `Canvas.FPS` (frames drawn over the last second) and `Canvas.FrameTime` (ms of the last measure + arrange + render, GPU flush excluded) are computed every frame and reachable from React through the `<Canvas>` ref. Not ported: the `SkiaLabelFps` control, `Super.EnableRenderingStats`. |
|
|
55
|
+
| Insets / safe areas | ported | `Super.Insets` measures `env(safe-area-inset-*)` off a probe element (once, and again on resize / orientation), `Super.OnInsetsChanged` notifies; `SkiaShell` pads its nav and tab bars with them. Not ported: the C# per-page `IgnoreSafeArea` / MAUI inset plumbing. |
|
|
54
56
|
|
|
55
57
|
## Controls
|
|
56
58
|
|
|
@@ -102,6 +104,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
102
104
|
| `SkiaButton.ControlStyle` | ported | Cupertino / Material / Material3 / Windows accent, corner radius, font size/weight and minimum height from the C# style builders, applied only when the user left `BackgroundColor`/`CornerRadius`(8)/`FontSize`(15)/`MinimumHeightRequest` unset; default look is now the C# Crimson accent. Not ported: `Background` brushes, pressed-state shadow changes. |
|
|
103
105
|
| `SkiaButton.FontFamilyFallback` | React extension | Pass-through to the inner label so icon-glyph buttons render with `AddSymbols()` faces. |
|
|
104
106
|
| Accessibility (`ISkiaAccessibilityNode` + `SkiaAccessibilityManager` + Blazor ARIA overlay) | ported | `AccessibilityRole/Label/Hint/CanInteract/IsPressed/Live`, `Aria` constants, `IsAccessibilityElement`, `GetAccessibilityPixelRect`, `NotifyAccessibility`, `OnAccessibilityActivated` (synthetic Tapped), `OnAccessibilityFocused`, snapshot rate-limited by `MinUpdateIntervalMs`, top-left reading order, `SkiaButton` hides its inner label. Extensions: per-class `DefaultAccessibilityRole`, label/interaction defaults derived from text / `Tapped`, `pointer-events:none` overlay, detached/off-canvas nodes pruned at rebuild (no explicit unregister on removal). Not ported: `WithAccessibility*` fluent helpers, `FocusChanged` event consumers, UIA/AT-SPI (browser only), `aria-level` for headings. |
|
|
107
|
+
| Static HTML for crawlers (`drawnui-react/vite` `drawnUiStatic()`, React extension) | ported | Build-time only: after `vite build` the plugin serves the build, opens it in headless Chrome (`playwright-core`, optional peer), reads the accessibility snapshot of the root page and of every page a root button opens (or `routes`), and writes visible semantic HTML (heading → heading, button → `<a href="#/route">`, text → paragraph, other controls → list items; `img` and `presentation` dropped) after the mount element or at `<!-- drawnui-static -->`, with `DEFAULT_STATIC_STYLE` (`render.style` to override / disable). Runtime: the first `<Canvas>` mount removes every `[data-drawnui-static]` element right after its engine drew frame 1 (`removeStaticContent`, one querySelectorAll, nothing in the frame loop). Not in DrawnUi.Net (Blazor sites hand-write static SEO content, see the drawnui-blazor notes). Not done: per-route documents (pages live in the hash, one URL), `<title>` / meta per page, sitemap entries. |
|
|
105
108
|
| React context bridging across the `<Canvas>` boundary | skipped | Contexts from the DOM tree are not visible inside the drawn tree. |
|
|
106
109
|
| Refs to engine controls from JSX | skipped | `getPublicInstance` returns the control, `ref` not wired/typed. |
|
|
107
110
|
| Fluent code-behind API (`.Assign`, `.OnTapped`, `.ObserveProperty`) | skipped | Engine classes are plain TS classes; React is the composition layer. |
|
|
@@ -155,6 +155,8 @@ export declare class SkiaLabel extends SkiaControl {
|
|
|
155
155
|
protected FillGradientPaintsBackground(): boolean;
|
|
156
156
|
protected Paint(ctx: DrawingContext): void;
|
|
157
157
|
ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
|
|
158
|
+
/** Pointer cursor over a tappable span (the label itself may not be tappable). */
|
|
159
|
+
WantsPointerCursor(x: number, y: number): boolean;
|
|
158
160
|
/** Return null to not consume the tap. */
|
|
159
161
|
protected OnSpanTapped(span: TextSpan, args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
|
|
160
162
|
}
|
|
@@ -572,6 +572,15 @@ export class SkiaLabel extends SkiaControl {
|
|
|
572
572
|
}
|
|
573
573
|
return super.ProcessGestures(args, apply);
|
|
574
574
|
}
|
|
575
|
+
/** Pointer cursor over a tappable span (the label itself may not be tappable). */
|
|
576
|
+
WantsPointerCursor(x, y) {
|
|
577
|
+
if (super.WantsPointerCursor(x, y))
|
|
578
|
+
return true;
|
|
579
|
+
for (const span of this.Spans)
|
|
580
|
+
if (span.HasTapHandler && span.HitIsInside(x, y))
|
|
581
|
+
return true;
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
575
584
|
/** Return null to not consume the tap. */
|
|
576
585
|
OnSpanTapped(span, args, apply) {
|
|
577
586
|
span.FireTap(new ControlTappedEventArgs(this, args, apply));
|
package/dist/core/Canvas.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { 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.
|
|
@@ -66,6 +66,18 @@ export declare class Canvas {
|
|
|
66
66
|
FrameIndex: number;
|
|
67
67
|
/** On-screen surface (SkiaBackdrop snapshots it). */
|
|
68
68
|
get Surface(): Surface | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* A picture of what is on screen right now, as PNG bytes (C# DrawnView.TakeScreenShot).
|
|
71
|
+
*
|
|
72
|
+
* The on-screen canvas cannot simply be read: an accelerated surface lives in a WebGL drawing
|
|
73
|
+
* buffer the browser clears after compositing, so both `canvas.toDataURL()` and a snapshot of
|
|
74
|
+
* the live surface come back blank. The content is therefore drawn once more into an offscreen
|
|
75
|
+
* RASTER surface, which can be read back anywhere.
|
|
76
|
+
*
|
|
77
|
+
* This is a still picture, not a frame of the loop: animators are not ticked and the frame
|
|
78
|
+
* counters do not move, so taking one never changes what the next real frame shows.
|
|
79
|
+
*/
|
|
80
|
+
TakeScreenShot(): Uint8Array | null;
|
|
69
81
|
Dispose(): void;
|
|
70
82
|
private gestures;
|
|
71
83
|
get Gestures(): GesturesMode;
|
|
@@ -76,6 +88,19 @@ export declare class Canvas {
|
|
|
76
88
|
private readonly pendingGestures;
|
|
77
89
|
private readonly onPointer;
|
|
78
90
|
private readonly preventTouch;
|
|
91
|
+
/** Called when no control handled a context-menu request; return true to suppress the browser's canvas menu. */
|
|
92
|
+
ContextMenu?: (sender: Canvas, e: ContextMenuEventArgs) => boolean | void;
|
|
93
|
+
/** DOM contextmenu (right click / long press / Menu key) -> ContextMenuEventArgs routed through the tree like a tap. */
|
|
94
|
+
private readonly onContextMenu;
|
|
95
|
+
private cursorPointer;
|
|
96
|
+
/**
|
|
97
|
+
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
98
|
+
* is pointer-events:none, so the mouse position is tested against the accessibility snapshot (the accessible
|
|
99
|
+
* controls' rects in points, already sorted and rate-limited), each hit control answering WantsPointerCursor
|
|
100
|
+
* (itself tappable, or a tappable span of a label), and the canvas element's cursor is switched only when the
|
|
101
|
+
* answer changes. Mouse moves only, no work without a mouse and none in the frame loop.
|
|
102
|
+
*/
|
|
103
|
+
private UpdateCursor;
|
|
79
104
|
/** Mouse wheel -> TouchActionResult.Wheel (page scroll suppressed while gestures are enabled). */
|
|
80
105
|
private readonly onWheel;
|
|
81
106
|
private AttachInput;
|
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, 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.
|
|
@@ -179,6 +179,50 @@ export class Canvas {
|
|
|
179
179
|
FrameIndex = 0;
|
|
180
180
|
/** On-screen surface (SkiaBackdrop snapshots it). */
|
|
181
181
|
get Surface() { return this.surface; }
|
|
182
|
+
/**
|
|
183
|
+
* A picture of what is on screen right now, as PNG bytes (C# DrawnView.TakeScreenShot).
|
|
184
|
+
*
|
|
185
|
+
* The on-screen canvas cannot simply be read: an accelerated surface lives in a WebGL drawing
|
|
186
|
+
* buffer the browser clears after compositing, so both `canvas.toDataURL()` and a snapshot of
|
|
187
|
+
* the live surface come back blank. The content is therefore drawn once more into an offscreen
|
|
188
|
+
* RASTER surface, which can be read back anywhere.
|
|
189
|
+
*
|
|
190
|
+
* This is a still picture, not a frame of the loop: animators are not ticked and the frame
|
|
191
|
+
* counters do not move, so taking one never changes what the next real frame shows.
|
|
192
|
+
*/
|
|
193
|
+
TakeScreenShot() {
|
|
194
|
+
const CK = Super.CK;
|
|
195
|
+
const w = this.Element.width, h = this.Element.height;
|
|
196
|
+
if (!CK || !w || !h)
|
|
197
|
+
return null;
|
|
198
|
+
const surface = CK.MakeSurface(w, h);
|
|
199
|
+
if (!surface)
|
|
200
|
+
return null;
|
|
201
|
+
try {
|
|
202
|
+
const canvas = surface.getCanvas();
|
|
203
|
+
canvas.clear(Super.ParseColor(this.BackgroundColor));
|
|
204
|
+
const root = this.content;
|
|
205
|
+
if (root) {
|
|
206
|
+
const scale = this.RenderingScale;
|
|
207
|
+
root.Measure(w, h, scale);
|
|
208
|
+
root.Arrange(new SKRect(0, 0, w, h), root.WidthRequest, root.HeightRequest, scale);
|
|
209
|
+
root.Render({ Context: { Canvas: canvas, Surface: surface }, Destination: new SKRect(0, 0, w, h), Scale: scale });
|
|
210
|
+
}
|
|
211
|
+
surface.flush();
|
|
212
|
+
const image = surface.makeImageSnapshot();
|
|
213
|
+
if (!image)
|
|
214
|
+
return null;
|
|
215
|
+
try {
|
|
216
|
+
return image.encodeToBytes();
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
image.delete();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
surface.delete();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
182
226
|
Dispose() {
|
|
183
227
|
this.disposed = true;
|
|
184
228
|
this.Gestures = "Disabled";
|
|
@@ -216,8 +260,17 @@ export class Canvas {
|
|
|
216
260
|
e.type === "pointercancel" ? "Cancelled" : undefined;
|
|
217
261
|
if (!type)
|
|
218
262
|
return;
|
|
219
|
-
|
|
220
|
-
|
|
263
|
+
// only the primary mouse button is a touch (DrawnUi.Blazor): right / middle buttons never start a gesture, and
|
|
264
|
+
// their Up / Cancel (pointer never pressed here) is ignored too, so a right click cannot end as a Tapped
|
|
265
|
+
if (type === "Pressed" && e.pointerType === "mouse" && e.button !== 0)
|
|
266
|
+
return;
|
|
267
|
+
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
|
|
268
|
+
if (e.pointerType === "mouse")
|
|
269
|
+
this.UpdateCursor(e.offsetX, e.offsetY);
|
|
270
|
+
return;
|
|
271
|
+
} // hover not ported (TouchActionResult.Pointer)
|
|
272
|
+
if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
|
|
273
|
+
return;
|
|
221
274
|
// Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
|
|
222
275
|
if (type === "Pressed") {
|
|
223
276
|
try {
|
|
@@ -234,6 +287,48 @@ export class Canvas {
|
|
|
234
287
|
this.OnTouchAction(args);
|
|
235
288
|
};
|
|
236
289
|
preventTouch = (e) => e.preventDefault();
|
|
290
|
+
/** Called when no control handled a context-menu request; return true to suppress the browser's canvas menu. */
|
|
291
|
+
ContextMenu;
|
|
292
|
+
/** DOM contextmenu (right click / long press / Menu key) -> ContextMenuEventArgs routed through the tree like a tap. */
|
|
293
|
+
onContextMenu = (e) => {
|
|
294
|
+
const rect = this.Element.getBoundingClientRect();
|
|
295
|
+
const scale = this.RenderingScale;
|
|
296
|
+
const x = e.clientX - rect.left, y = e.clientY - rect.top;
|
|
297
|
+
const pointerType = e.pointerType;
|
|
298
|
+
const source = pointerType === "touch" || pointerType === "pen" ? "touch" : pointerType === "mouse" || e.button === 2 ? "mouse" : "keyboard";
|
|
299
|
+
const args = new ContextMenuEventArgs(new SKPoint(x, y), new SKPoint(x * scale, y * scale), source, e);
|
|
300
|
+
let handled = this.content?.ProcessContextMenu(args.Pixels, args) ?? false;
|
|
301
|
+
if (!handled && this.ContextMenu)
|
|
302
|
+
handled = this.ContextMenu(this, args) === true;
|
|
303
|
+
if (handled)
|
|
304
|
+
e.preventDefault();
|
|
305
|
+
};
|
|
306
|
+
cursorPointer = false;
|
|
307
|
+
/**
|
|
308
|
+
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
309
|
+
* is pointer-events:none, so the mouse position is tested against the accessibility snapshot (the accessible
|
|
310
|
+
* controls' rects in points, already sorted and rate-limited), each hit control answering WantsPointerCursor
|
|
311
|
+
* (itself tappable, or a tappable span of a label), and the canvas element's cursor is switched only when the
|
|
312
|
+
* answer changes. Mouse moves only, no work without a mouse and none in the frame loop.
|
|
313
|
+
*/
|
|
314
|
+
UpdateCursor(x, y) {
|
|
315
|
+
let hit = false;
|
|
316
|
+
const scale = this.RenderingScale;
|
|
317
|
+
for (const n of this.AccessibilityManager.Snapshot) {
|
|
318
|
+
if (x < n.Rect.Left || x >= n.Rect.Right || y < n.Rect.Top || y >= n.Rect.Bottom)
|
|
319
|
+
continue;
|
|
320
|
+
// the control decides (whole control, or a tappable span of a label); point in pixels relative to the control's
|
|
321
|
+
// origin, taken from the snapshot rect (already carries the scroll / cache offset; a rotated control gets its bbox)
|
|
322
|
+
if (n.Source.WantsPointerCursor((x - n.Rect.Left) * scale, (y - n.Rect.Top) * scale)) {
|
|
323
|
+
hit = true;
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (hit !== this.cursorPointer) {
|
|
328
|
+
this.cursorPointer = hit;
|
|
329
|
+
this.Element.style.cursor = hit ? "pointer" : "";
|
|
330
|
+
}
|
|
331
|
+
}
|
|
237
332
|
/** Mouse wheel -> TouchActionResult.Wheel (page scroll suppressed while gestures are enabled). */
|
|
238
333
|
onWheel = (e) => {
|
|
239
334
|
e.preventDefault();
|
|
@@ -253,6 +348,7 @@ export class Canvas {
|
|
|
253
348
|
el.style.userSelect = "none";
|
|
254
349
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
255
350
|
el.addEventListener(t, this.onPointer);
|
|
351
|
+
el.addEventListener("contextmenu", this.onContextMenu);
|
|
256
352
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
257
353
|
if (this.gestures === "Lock")
|
|
258
354
|
el.addEventListener("touchmove", this.preventTouch, { passive: false });
|
|
@@ -263,6 +359,7 @@ export class Canvas {
|
|
|
263
359
|
el.style.userSelect = "";
|
|
264
360
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
265
361
|
el.removeEventListener(t, this.onPointer);
|
|
362
|
+
el.removeEventListener("contextmenu", this.onContextMenu);
|
|
266
363
|
el.removeEventListener("wheel", this.onWheel);
|
|
267
364
|
el.removeEventListener("touchmove", this.preventTouch);
|
|
268
365
|
this.activeTouchIds.clear();
|
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";
|
|
@@ -66,6 +66,33 @@ export declare class SkiaGesturesInfo {
|
|
|
66
66
|
constructor(Args: SkiaGesturesParameters, Info: GestureEventProcessingInfo);
|
|
67
67
|
}
|
|
68
68
|
/** DrawnUi ControlTappedEventArgs. */
|
|
69
|
+
/** Where a context-menu request came from: right click, long press (Android fires contextmenu), or the keyboard Menu key. */
|
|
70
|
+
export type ContextMenuSource = "mouse" | "touch" | "keyboard";
|
|
71
|
+
/**
|
|
72
|
+
* Arguments of SkiaControl.ContextMenu / Canvas.ContextMenu: a browser `contextmenu` request on the canvas (right
|
|
73
|
+
* click, long press on touch, the Menu key). Handlers return true to take it: the browser's own menu ("Save image")
|
|
74
|
+
* is then suppressed; otherwise it shows as usual.
|
|
75
|
+
*/
|
|
76
|
+
export declare class ContextMenuEventArgs {
|
|
77
|
+
/** Point on the canvas, in points (CSS px). */
|
|
78
|
+
Location: SKPoint;
|
|
79
|
+
/** Same point in pixels (canvas space). */
|
|
80
|
+
Pixels: SKPoint;
|
|
81
|
+
Source: ContextMenuSource;
|
|
82
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
83
|
+
Native: MouseEvent;
|
|
84
|
+
/** Deepest control under the point that had a ContextMenu handler (set while routing). */
|
|
85
|
+
Control?: SkiaControl;
|
|
86
|
+
/** Point inside Control, in pixels relative to its DrawingRect origin (set while routing). */
|
|
87
|
+
Local: SKPoint;
|
|
88
|
+
constructor(
|
|
89
|
+
/** Point on the canvas, in points (CSS px). */
|
|
90
|
+
Location: SKPoint,
|
|
91
|
+
/** Same point in pixels (canvas space). */
|
|
92
|
+
Pixels: SKPoint, Source: ContextMenuSource,
|
|
93
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
94
|
+
Native: MouseEvent);
|
|
95
|
+
}
|
|
69
96
|
export declare class ControlTappedEventArgs {
|
|
70
97
|
Control: SkiaControl;
|
|
71
98
|
Parameters: SkiaGesturesParameters;
|
package/dist/core/Gestures.js
CHANGED
|
@@ -91,7 +91,33 @@ export class SkiaGesturesInfo {
|
|
|
91
91
|
this.Info = Info;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Arguments of SkiaControl.ContextMenu / Canvas.ContextMenu: a browser `contextmenu` request on the canvas (right
|
|
96
|
+
* click, long press on touch, the Menu key). Handlers return true to take it: the browser's own menu ("Save image")
|
|
97
|
+
* is then suppressed; otherwise it shows as usual.
|
|
98
|
+
*/
|
|
99
|
+
export class ContextMenuEventArgs {
|
|
100
|
+
Location;
|
|
101
|
+
Pixels;
|
|
102
|
+
Source;
|
|
103
|
+
Native;
|
|
104
|
+
/** Deepest control under the point that had a ContextMenu handler (set while routing). */
|
|
105
|
+
Control;
|
|
106
|
+
/** Point inside Control, in pixels relative to its DrawingRect origin (set while routing). */
|
|
107
|
+
Local = SKPoint.Empty;
|
|
108
|
+
constructor(
|
|
109
|
+
/** Point on the canvas, in points (CSS px). */
|
|
110
|
+
Location,
|
|
111
|
+
/** Same point in pixels (canvas space). */
|
|
112
|
+
Pixels, Source,
|
|
113
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
114
|
+
Native) {
|
|
115
|
+
this.Location = Location;
|
|
116
|
+
this.Pixels = Pixels;
|
|
117
|
+
this.Source = Source;
|
|
118
|
+
this.Native = Native;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
95
121
|
export class ControlTappedEventArgs {
|
|
96
122
|
Control;
|
|
97
123
|
Parameters;
|
|
@@ -3,7 +3,7 @@ import { type Color, type LayoutOptions, SKRect, ScaledSize, type SkiaCacheType,
|
|
|
3
3
|
import { type IOverlayEffect } from "./Animators";
|
|
4
4
|
import { Easing } from "./Easing";
|
|
5
5
|
import type { Canvas } from "./Canvas";
|
|
6
|
-
import { ControlTappedEventArgs, GestureEventProcessingInfo, type LockTouch, SKPoint, SkiaGesturesInfo, SkiaGesturesParameters } from "./Gestures";
|
|
6
|
+
import { ContextMenuEventArgs, ControlTappedEventArgs, GestureEventProcessingInfo, type LockTouch, SKPoint, SkiaGesturesInfo, SkiaGesturesParameters } from "./Gestures";
|
|
7
7
|
import { type CachedTexture, type IPostRendererEffect, type SkiaEffect } from "./SkiaEffect";
|
|
8
8
|
/** Mirrors DrawnUi DrawingContext: ctx.Context.Canvas / Surface, ctx.Destination (pixels), ctx.Scale. */
|
|
9
9
|
export interface DrawingContext {
|
|
@@ -180,6 +180,12 @@ export declare class SkiaControl {
|
|
|
180
180
|
/** Clip overlay effects to the control's shape (CreateClip). */
|
|
181
181
|
ClipEffects: boolean;
|
|
182
182
|
Tapped?: (sender: SkiaControl, e: ControlTappedEventArgs) => void;
|
|
183
|
+
/**
|
|
184
|
+
* Context-menu request over this control (right click, long press on touch, Menu key). Return true to handle it:
|
|
185
|
+
* the browser's own canvas menu is suppressed. Routed deepest child first, then parents, then Canvas.ContextMenu.
|
|
186
|
+
* Not in DrawnUi.Net (a web concept).
|
|
187
|
+
*/
|
|
188
|
+
ContextMenu?: (sender: SkiaControl, e: ContextMenuEventArgs) => boolean | void;
|
|
183
189
|
ChildTapped?: (sender: SkiaControl, e: ControlTappedEventArgs) => void;
|
|
184
190
|
/** Raw gesture hook: set e.Consumed = true to stop propagation (not for Up). */
|
|
185
191
|
ConsumeGestures?: (sender: SkiaControl, e: SkiaGesturesInfo) => void;
|
|
@@ -235,6 +241,11 @@ export declare class SkiaControl {
|
|
|
235
241
|
get AccessibilityCanInteract(): boolean;
|
|
236
242
|
set AccessibilityCanInteract(v: boolean);
|
|
237
243
|
protected DefaultAccessibilityCanInteract(): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Whether the mouse at this point (pixels, relative to DrawingRect's origin) is over something tappable, for the
|
|
246
|
+
* host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
|
|
247
|
+
*/
|
|
248
|
+
WantsPointerCursor(_x: number, _y: number): boolean;
|
|
238
249
|
/** aria-pressed for toggles; undefined = not a toggle. */
|
|
239
250
|
get AccessibilityIsPressed(): boolean | undefined;
|
|
240
251
|
set AccessibilityIsPressed(v: boolean | undefined);
|
|
@@ -370,6 +381,11 @@ export declare class SkiaControl {
|
|
|
370
381
|
get HitBoxAuto(): SKRect;
|
|
371
382
|
HitIsInside(x: number, y: number): boolean;
|
|
372
383
|
IsGestureForChild(child: SkiaControl, point: SKPoint): boolean;
|
|
384
|
+
/**
|
|
385
|
+
* Routes a context-menu request like a tap: visible, non-transparent children under the point first (top-most
|
|
386
|
+
* first, each in its own transformed space), then this control's ContextMenu handler. True = handled.
|
|
387
|
+
*/
|
|
388
|
+
ProcessContextMenu(point: SKPoint, e: ContextMenuEventArgs): boolean;
|
|
373
389
|
/** Children that may receive gestures, top-most LAST (layouts return their Views). */
|
|
374
390
|
protected GetGestureListeners(): readonly SkiaControl[];
|
|
375
391
|
private static readonly NoListeners;
|
package/dist/core/SkiaControl.js
CHANGED
|
@@ -238,6 +238,12 @@ export class SkiaControl {
|
|
|
238
238
|
ClipEffects = true;
|
|
239
239
|
// ---- gesture events (single handler each; C# events map to one callback prop) ----
|
|
240
240
|
Tapped;
|
|
241
|
+
/**
|
|
242
|
+
* Context-menu request over this control (right click, long press on touch, Menu key). Return true to handle it:
|
|
243
|
+
* the browser's own canvas menu is suppressed. Routed deepest child first, then parents, then Canvas.ContextMenu.
|
|
244
|
+
* Not in DrawnUi.Net (a web concept).
|
|
245
|
+
*/
|
|
246
|
+
ContextMenu;
|
|
241
247
|
ChildTapped;
|
|
242
248
|
/** Raw gesture hook: set e.Consumed = true to stop propagation (not for Up). */
|
|
243
249
|
ConsumeGestures;
|
|
@@ -388,6 +394,11 @@ export class SkiaControl {
|
|
|
388
394
|
this.AccessibilityChanged();
|
|
389
395
|
} }
|
|
390
396
|
DefaultAccessibilityCanInteract() { return !!this.Tapped; }
|
|
397
|
+
/**
|
|
398
|
+
* Whether the mouse at this point (pixels, relative to DrawingRect's origin) is over something tappable, for the
|
|
399
|
+
* host's pointer cursor. Default: the control as a whole (AccessibilityCanInteract); SkiaLabel adds its tappable spans.
|
|
400
|
+
*/
|
|
401
|
+
WantsPointerCursor(_x, _y) { return this.AccessibilityCanInteract; }
|
|
391
402
|
/** aria-pressed for toggles; undefined = not a toggle. */
|
|
392
403
|
get AccessibilityIsPressed() { return this.accessibilityIsPressed; }
|
|
393
404
|
set AccessibilityIsPressed(v) { if (this.accessibilityIsPressed !== v) {
|
|
@@ -1079,6 +1090,25 @@ export class SkiaControl {
|
|
|
1079
1090
|
const local = child.TransformPointToLocalSpace(point);
|
|
1080
1091
|
return child.HitIsInside(local.X, local.Y);
|
|
1081
1092
|
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Routes a context-menu request like a tap: visible, non-transparent children under the point first (top-most
|
|
1095
|
+
* first, each in its own transformed space), then this control's ContextMenu handler. True = handled.
|
|
1096
|
+
*/
|
|
1097
|
+
ProcessContextMenu(point, e) {
|
|
1098
|
+
const listeners = this.GetGestureListeners();
|
|
1099
|
+
for (let i = listeners.length - 1; i >= 0; i--) {
|
|
1100
|
+
const listener = listeners[i];
|
|
1101
|
+
if (!listener.IsVisible || listener.InputTransparent || !this.IsGestureForChild(listener, point))
|
|
1102
|
+
continue;
|
|
1103
|
+
if (listener.ProcessContextMenu(listener.TransformPointToLocalSpace(point), e))
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
if (!this.ContextMenu)
|
|
1107
|
+
return false;
|
|
1108
|
+
e.Control = this;
|
|
1109
|
+
e.Local = new SKPoint(point.X - this.DrawingRect.Left, point.Y - this.DrawingRect.Top);
|
|
1110
|
+
return this.ContextMenu(this, e) === true;
|
|
1111
|
+
}
|
|
1082
1112
|
/** Children that may receive gestures, top-most LAST (layouts return their Views). */
|
|
1083
1113
|
GetGestureListeners() { return SkiaControl.NoListeners; }
|
|
1084
1114
|
static NoListeners = [];
|
package/dist/core/Types.d.ts
CHANGED
|
@@ -134,9 +134,14 @@ export type DrawImageAlignment = "Start" | "Center" | "End";
|
|
|
134
134
|
/** MAUI/DrawnUi ScrollOrientation. */
|
|
135
135
|
export type ScrollOrientation = "Vertical" | "Horizontal" | "Both" | "Neither";
|
|
136
136
|
/**
|
|
137
|
-
* DrawnUi SkiaCacheType. Operations = recorded draw commands (SkPicture) replayed each frame
|
|
137
|
+
* DrawnUi SkiaCacheType. Operations / OperationsFull = recorded draw commands (SkPicture) replayed each frame.
|
|
138
138
|
* Image = offscreen surface snapshot blitted each frame (GPU-backed when the canvas is WebGL).
|
|
139
|
-
*
|
|
139
|
+
* ImageDoubleBuffered = an Image cache that keeps the previous one and draws it — or DrawPlaceholder when there is
|
|
140
|
+
* none — while a new one cannot be produced yet.
|
|
141
|
+
* ImageComposite / ImageCompositeGPU = an Image cache whose offscreen surface survives between records: when only
|
|
142
|
+
* children changed, their old and new bounds plus the siblings those overlap are erased and only those children are
|
|
143
|
+
* painted again.
|
|
144
|
+
* GPU resolves to Image and ImageCompositeGPU to ImageComposite; the browser has no separate GPU-only path.
|
|
140
145
|
*/
|
|
141
146
|
export type SkiaCacheType = "None" | "Operations" | "OperationsFull" | "Image" | "ImageDoubleBuffered" | "ImageComposite" | "ImageCompositeGPU" | "GPU";
|
|
142
147
|
/** DrawnUi GradientType (Conical accepted, drawn as Circular). */
|