drawnui-react 0.1.0-preview.1 → 0.1.0-preview.3
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 +12 -9
- package/dist/controls/SkiaButton.d.ts +4 -1
- package/dist/controls/SkiaButton.js +20 -7
- 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 +104 -3
- package/dist/core/Gestures.d.ts +42 -1
- package/dist/core/Gestures.js +38 -1
- package/dist/core/SkiaControl.d.ts +24 -1
- package/dist/core/SkiaControl.js +44 -0
- 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 +13 -3
- package/dist/react/index.js +16 -1
- 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 +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,25 @@ 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…"). Every mouse button goes
|
|
71
|
+
through the gesture pipeline as in DrawnUi.Net (`e.Parameters.Event.Pointer.Button` says which), so a right click is
|
|
72
|
+
a `Tapped` with `Button` `"Right"` for games and custom controls; a handler that should act on the primary button only
|
|
73
|
+
checks it. To take the browser menu request, handle `ContextMenu` on any control, exactly like `Tapped`:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<SkiaShape Tapped={(s, e) => { if ((e.Parameters.Event.Pointer?.Button ?? "Left") === "Left") open(); }} />
|
|
77
|
+
<SkiaShape ContextMenu={(sender, e) => { openMyMenu(e.Location); return true; }} />
|
|
78
|
+
<Canvas ContextMenu={(canvas, e) => true /* nothing hit: swallow the browser menu everywhere */} />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The request is routed like a tap: deepest visible control under the point first (through transforms), then its
|
|
82
|
+
parents, then the `Canvas` prop. The first handler returning `true` takes it and the browser menu is suppressed;
|
|
83
|
+
with no handler the browser menu shows as before. `e` carries `Location` (points on the canvas), `Pixels`, `Local`
|
|
84
|
+
(pixels inside the handling control), `Source` (`"mouse"`, `"touch"` for a long press, `"keyboard"` for the Menu
|
|
85
|
+
key) and `Native` (the DOM event, for modifiers). Web-only: DrawnUi.Net has no mouse buttons.
|
|
86
|
+
|
|
52
87
|
## Accessibility
|
|
53
88
|
|
|
54
89
|
Same model as DrawnUi.Blazor: the `<canvas>` is `aria-hidden`, an invisible DOM overlay mirrors every
|
|
@@ -68,25 +103,48 @@ label readable and every button focusable without touching each control.
|
|
|
68
103
|
The overlay has `pointer-events: none`, so hover and all pointer gestures still reach the canvas — the
|
|
69
104
|
Blazor "accessible control loses hover" limitation does not apply.
|
|
70
105
|
|
|
71
|
-
|
|
106
|
+
Text selection (React extension, opt-in): `AccessibilityTextSelectable` on a `SkiaLabel` renders its laid-out lines
|
|
107
|
+
as real, invisible DOM text in the overlay — one span per drawn line, in the same font (registered fonts are also
|
|
108
|
+
installed as CSS `FontFace`s) and stretched to the drawn line width — with pointer events on. The browser then
|
|
109
|
+
selects, copies (Ctrl+C / context menu) and reads it like an HTML paragraph, and the wheel over it still scrolls the
|
|
110
|
+
drawn content. It is off by default and must stay off on anything gesture-driven (buttons, carousels, drawers): the
|
|
111
|
+
selectable text owns the pointer, so taps and pans under it never reach the drawn control. Everything else in the
|
|
112
|
+
overlay is `user-select: none`, so select-all only highlights opted-in text.
|
|
72
113
|
|
|
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
|
-
```
|
|
114
|
+
## Crawlers and AI agents: static HTML from the accessibility tree
|
|
79
115
|
|
|
80
|
-
|
|
116
|
+
A drawn app serves `<div id="root"></div>`; a crawler that does not run JavaScript sees nothing. The
|
|
117
|
+
`drawnUiStatic` Vite plugin fixes that at build time, from the app itself:
|
|
81
118
|
|
|
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`. |
|
|
119
|
+
```ts
|
|
120
|
+
// vite.config.ts
|
|
121
|
+
import { drawnUiStatic } from "drawnui-react/vite";
|
|
122
|
+
export default defineConfig({ plugins: [react(), drawnUiStatic()] });
|
|
123
|
+
```
|
|
91
124
|
|
|
92
|
-
|
|
125
|
+
After `vite build` it serves the build, opens it in headless Chrome (`playwright-core`, your dev dependency; GitHub's
|
|
126
|
+
ubuntu runners ship a Chrome), lets the engine draw, reads the accessibility snapshot of the root page and of every
|
|
127
|
+
page a root button opens (or the `routes` you list), and writes ordinary visible HTML into `dist/index.html`: a
|
|
128
|
+
heading is a heading, a button is a link to the page it opened, a label is a paragraph. The overlay's own markup
|
|
129
|
+
(transparent text over the canvas) is never copied — in a static file it would read as hidden text. Nothing is
|
|
130
|
+
hand-written, so it cannot drift from what the canvas draws.
|
|
131
|
+
|
|
132
|
+
Where it lands: right after the mount element (`#root`), or wherever you put `<!-- drawnui-static -->` in
|
|
133
|
+
`index.html`. With the mount element filling the viewport that is below the fold: a visitor sees the app area
|
|
134
|
+
loading, the text is a scroll away, genuinely visible, nothing collapsed or hidden. The first `<Canvas>` that mounts
|
|
135
|
+
removes it once its engine has drawn frame 1 (one `querySelectorAll`, nothing in the frame loop, nothing to pay for
|
|
136
|
+
an app built without the plugin). If the app never boots it stays. The mount element keeps its size, so nothing
|
|
137
|
+
above the fold moves; a page that scrolled only because of the block loses its scrollbar (classic scrollbars give
|
|
138
|
+
the canvas those ~15 px back — `html { scrollbar-gutter: stable }` keeps the gutter if you prefer). If the mount
|
|
139
|
+
element is a box inside a longer page, put the marker at the end of the body so your own content does not move.
|
|
140
|
+
Look: a default stylesheet ships with the block (inherits your `body` font and color, translucent surfaces); override
|
|
141
|
+
by styling `.drawnui-static` or pass `render: { style: false }`.
|
|
142
|
+
|
|
143
|
+
Two things to know. Check it with `curl` or view-source, not DevTools after boot. And crawlers that do not run
|
|
144
|
+
JavaScript (GPTBot, ClaudeBot, CCBot, link previews) read this HTML, while Googlebot renders JavaScript and sees the
|
|
145
|
+
booted page, i.e. the accessibility overlay; both come from the same tree, and a control without an
|
|
146
|
+
`AccessibilityRole` is invisible to both.
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
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. Every mouse button is delivered (`Event.Pointer`: `Button`, `ButtonNumber`, `DeviceType`, `PressedButtons`, the AppoMobi.Gestures PointerData): a right click is Down / Up / Tapped with `Button` "Right" plus a `ContextMenu`; handlers that must be primary-only check `e.Parameters.Event.Pointer?.Button`. |
|
|
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,8 +29,8 @@ 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. |
|
|
33
|
-
| Styles / `ConfigureStyles` |
|
|
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
|
+
| Styles / `ConfigureStyles` | ported | `Super.UseDrawnUi().ConfigureStyles(styles => styles.AddStyle({ TargetType, ApplyToDerivedTypes, Setters }))`, the C# `Style` shape with the `Setter` list written as a plain object (`Setters: { FontFamily: "FontText" }`) because controls hold plain properties instead of BindableProperty. Per-class resolution cached, base-type styles applied furthest-first so the nearest wins, exact type last (C# `ResolveStylesForType`). Applied by the reconciler between `new` and the JSX props, so props override; a control built in code-behind is styled on its first `Measure` and keeps values that differ from its class defaults (the React stand-in for C# `ExplicitPropertiesSet`, which needs BindableProperty). Not ported: `BasedOn`, `Class`, `Triggers`, `Behaviors`, the per-control `Style` property, `ConditionalStyle`. |
|
|
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. |
|
|
36
36
|
| `Shimmer` touch animation, `ShimmerAnimator`, `ClippedEffectsWith`, `TransformView`, `DelayCallbackMs`, `removePrevious`, `Pause`/`Resume`, `UseInterpolator`, spring/deceleration timing, `AnimateExtensions` (`FadeToAsync`, `TranslateToAsync`, ...) | skipped | |
|
|
@@ -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
|
|
|
@@ -90,7 +92,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
90
92
|
|---|---|---|
|
|
91
93
|
| `Super.UseDrawnUi().ConfigureFonts(f => f.AddFont(src, alias)).BuildAsync()` | ported | Same shape as DrawnUi.Net/OpenTK. |
|
|
92
94
|
| `AddFont(src, alias, weight)`, font fallback, emoji | skipped | |
|
|
93
|
-
| `PreloadAssets
|
|
95
|
+
| `PreloadAssets` | skipped | `ConfigureStyles` is ported, see the Engine table. |
|
|
94
96
|
|
|
95
97
|
## React layer
|
|
96
98
|
|
|
@@ -99,9 +101,10 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
99
101
|
| Custom `react-reconciler` renderer, PascalCase props mapped 1:1 to control properties | ported | |
|
|
100
102
|
| `SkiaToggle` / `SkiaSwitch` / `SkiaCheckbox` / `SkiaRadioButton` | ported | Same names: `IsToggled`, `Toggled`, `DefaultValue`, `IsAnimated`, `RespondsToGestures`, `ColorThumbOn/Off`, `ColorFrameOn/Off` (+ `ColorCheckOn`), `ControlStyle` with the Default / Cupertino / Material / Material3 / Windows looks and geometry of the C# style builders (`Frame`/`Thumb`, `FrameOff`/`FrameOn`/`ViewCheckOn`, `On` ring + `Text`), thumb animated with `TranslateToAsync` (`AnimationSpeed` 200 ms), radio groups by `GroupName` or parent. Not ported: `CommandToggled`/`CommandTapped`, hover (`CheckHovered`), `TransformView` hotspot (the toggle itself takes the tap), `Platform` style resolves from the browser UA. |
|
|
101
103
|
| `SkiaProgress` / `SkiaSlider` | ported (painted, not composed) | `Value`/`Min`/`Max`, `TrackColor`/`ProgressColor`; slider `Min`/`Max`/`Step`/`Start`/`End`/`EnableRange`/`RangeMin`/`SliderHeight`/`ThumbColor`/`TrackColor`/`TrackSelectedColor`/`ClickOnTrailEnabled`/`StartChanged`/`EndChanged`, `StartThumbX`/`EndThumbX`, the C# position<->value math (`SliderHeight` = thumb box, `Step` rounding, `RangeMin` clamps), drag + click-on-trail + per-style looks (track heights, thumb sizes, palettes, Material3 gap + stop dot). Track/trail/thumbs are painted directly instead of child shapes (`SliderTrail`/`SliderThumb` not exposed). Not ported: `Orientation` Vertical, `Invert`, `ValueStringFormat`/`MinMaxStringFormat` + `StartDesc`/`EndDesc`, `IgnoreWrongDirection`, `AvailableWidthAdjustment`, `Trail*Offset`, `Cupertino*` sizing props, hover. |
|
|
102
|
-
| `SkiaButton.ControlStyle` | ported | Cupertino / Material / Material3 / Windows accent, corner radius, font size/weight and minimum
|
|
104
|
+
| `SkiaButton.ControlStyle` | ported | Cupertino / Material / Material3 / Windows accent, corner radius, font size/weight and minimum content size from the C# style builders, applied only when the user left `BackgroundColor`/`CornerRadius`(8)/`FontSize`(15)/`MinimumWidthRequest`/`MinimumHeightRequest` unset; default look is now the C# Crimson accent. `SetDefaultMinimumContentSize` is mirrored for BOTH axes (every C# style passes width 100, heights 41/36/40/40/32) and for the default `Unset` style too, skipped on a `Fill` axis or when a `WidthRequest`/`HeightRequest` is set, so short captions clamp to 100pt as in C#; the C# value adds `Margins` because it compares against a margin-inclusive size, here Measure applies it margin-exclusive and adds the margin after. 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. |
|
|
@@ -44,7 +44,10 @@ export declare class SkiaButton extends SkiaLayout {
|
|
|
44
44
|
/** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
|
|
45
45
|
protected PaintBackground(): void;
|
|
46
46
|
get UsingControlStyle(): ResolvedControlStyle;
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size.
|
|
49
|
+
* Every C# style calls SetDefaultMinimumContentSize(100, minH), so the width floor is 100 everywhere.
|
|
50
|
+
*/
|
|
48
51
|
private static Look;
|
|
49
52
|
protected MeasureAbsolute(w: number, h: number, scale: number): ScaledSize;
|
|
50
53
|
/** The frame sits under the padding box: arrange children in the full rect, the label centered inside padding. */
|
|
@@ -49,6 +49,11 @@ export class SkiaButton extends SkiaLayout {
|
|
|
49
49
|
this.label.VerticalOptions = "Center";
|
|
50
50
|
this.AddSubView(this.frame);
|
|
51
51
|
this.AddSubView(this.label);
|
|
52
|
+
// C# ButtonLabel is styled when the button builds its content, and ApplyProperties then overwrites FontFamily with
|
|
53
|
+
// the button's own (empty by default) value, which the style no longer touches. Same order here: style the caption
|
|
54
|
+
// now, MeasureAbsolute below pushes SkiaButton.FontFamily over it every measure.
|
|
55
|
+
this.frame.ApplyInitialStyles(true);
|
|
56
|
+
this.label.ApplyInitialStyles(true);
|
|
52
57
|
}
|
|
53
58
|
/** Set to `Aria.RoleButton` to expose every button (React extension). */
|
|
54
59
|
static DefaultAccessibilityRole;
|
|
@@ -57,14 +62,17 @@ export class SkiaButton extends SkiaLayout {
|
|
|
57
62
|
/** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
|
|
58
63
|
PaintBackground() { }
|
|
59
64
|
get UsingControlStyle() { return ResolveControlStyle(this.ControlStyle); }
|
|
60
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* C# Create*StyleContent defaults: accent, corner radius, font size/weight, minimum content size.
|
|
67
|
+
* Every C# style calls SetDefaultMinimumContentSize(100, minH), so the width floor is 100 everywhere.
|
|
68
|
+
*/
|
|
61
69
|
static Look(s) {
|
|
62
70
|
switch (s) {
|
|
63
|
-
case "Cupertino": return { bg: "#007AFF", radius: 8, font: 17, weight: 600, minH: 36, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.2, Color: Colors.Black }) };
|
|
64
|
-
case "Material": return { bg: "#2196F3", radius: 4, font: 14, weight: 0, minH: 40, shadow: new SkiaShadow({ X: 0, Y: 2, Blur: 4, Opacity: 0.3, Color: Colors.Black }) };
|
|
65
|
-
case "Material3": return { bg: "#6750A4", radius: 20, font: 14, weight: 0, minH: 40 };
|
|
66
|
-
case "Windows": return { bg: "#0078D7", radius: 4, font: 15, weight: 500, minH: 32, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 1, Opacity: 0.2, Color: Colors.Black }) };
|
|
67
|
-
default: return { bg: "#DC143C", radius: 8, font: 15, weight: 0, minH: 41 };
|
|
71
|
+
case "Cupertino": return { bg: "#007AFF", radius: 8, font: 17, weight: 600, minW: 100, minH: 36, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.2, Color: Colors.Black }) };
|
|
72
|
+
case "Material": return { bg: "#2196F3", radius: 4, font: 14, weight: 0, minW: 100, minH: 40, shadow: new SkiaShadow({ X: 0, Y: 2, Blur: 4, Opacity: 0.3, Color: Colors.Black }) };
|
|
73
|
+
case "Material3": return { bg: "#6750A4", radius: 20, font: 14, weight: 0, minW: 100, minH: 40 };
|
|
74
|
+
case "Windows": return { bg: "#0078D7", radius: 4, font: 15, weight: 500, minW: 100, minH: 32, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 1, Opacity: 0.2, Color: Colors.Black }) };
|
|
75
|
+
default: return { bg: "#DC143C", radius: 8, font: 15, weight: 0, minW: 100, minH: 41 };
|
|
68
76
|
}
|
|
69
77
|
}
|
|
70
78
|
MeasureAbsolute(w, h, scale) {
|
|
@@ -75,7 +83,12 @@ export class SkiaButton extends SkiaLayout {
|
|
|
75
83
|
this.frame.StrokeWidth = this.StrokeWidth;
|
|
76
84
|
if (this.frame.Shadows[0] !== look.shadow)
|
|
77
85
|
this.frame.Shadows = look.shadow ? [look.shadow] : [];
|
|
78
|
-
|
|
86
|
+
// C# SetDefaultMinimumContentSize(100, minH) from the style builder: a style default only where the app left the
|
|
87
|
+
// size unset, skipped on a Fill axis. C# adds Margins because it compares the minimum against a margin-inclusive
|
|
88
|
+
// size; here Measure applies it to the margin-excluded size and adds the margin after, so the plain value is used.
|
|
89
|
+
if (this.WidthRequest < 0 && this.MinimumWidthRequest < 0 && this.HorizontalOptions !== "Fill")
|
|
90
|
+
this.MinimumWidthRequest = look.minW;
|
|
91
|
+
if (this.HeightRequest < 0 && this.MinimumHeightRequest < 0 && this.VerticalOptions !== "Fill")
|
|
79
92
|
this.MinimumHeightRequest = look.minH;
|
|
80
93
|
this.label.Text = this.Text;
|
|
81
94
|
this.label.TextColor = this.TextColor;
|
|
@@ -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, 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.
|
|
@@ -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,13 @@ export class Canvas {
|
|
|
216
260
|
e.type === "pointercancel" ? "Cancelled" : undefined;
|
|
217
261
|
if (!type)
|
|
218
262
|
return;
|
|
219
|
-
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId))
|
|
220
|
-
|
|
263
|
+
if (type === "Moved" && !this.activeTouchIds.has(e.pointerId)) {
|
|
264
|
+
if (e.pointerType === "mouse")
|
|
265
|
+
this.UpdateCursor(e.offsetX, e.offsetY);
|
|
266
|
+
return;
|
|
267
|
+
} // hover not ported (TouchActionResult.Pointer)
|
|
268
|
+
if ((type === "Released" || type === "Cancelled") && !this.activeTouchIds.has(e.pointerId))
|
|
269
|
+
return; // Up of a pointer that never pressed here
|
|
221
270
|
// Capture so Up outside the element still arrives; throws for synthetic events (tests) — harmless.
|
|
222
271
|
if (type === "Pressed") {
|
|
223
272
|
try {
|
|
@@ -231,9 +280,59 @@ export class Canvas {
|
|
|
231
280
|
args.Type = type;
|
|
232
281
|
args.Scale = this.RenderingScale;
|
|
233
282
|
args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
|
|
283
|
+
// every mouse button is delivered with its PointerData (AppoMobi.Gestures): a right click is a Down / Up / Tapped
|
|
284
|
+
// with Button "Right" for games and custom controls, and a ContextMenu on top
|
|
285
|
+
const pd = new PointerData();
|
|
286
|
+
pd.Button = e.button === 0 ? "Left" : e.button === 1 ? "Middle" : e.button === 2 ? "Right" : e.button === 3 ? "XButton1" : e.button === 4 ? "XButton2" : "Extended";
|
|
287
|
+
pd.ButtonNumber = e.button === 1 ? 3 : e.button === 2 ? 2 : e.button + 1;
|
|
288
|
+
pd.DeviceType = e.pointerType === "touch" ? "Touch" : e.pointerType === "pen" ? "Pen" : "Mouse";
|
|
289
|
+
pd.PressedButtons = e.buttons;
|
|
290
|
+
args.Pointer = pd;
|
|
234
291
|
this.OnTouchAction(args);
|
|
235
292
|
};
|
|
236
293
|
preventTouch = (e) => e.preventDefault();
|
|
294
|
+
/** Called when no control handled a context-menu request; return true to suppress the browser's canvas menu. */
|
|
295
|
+
ContextMenu;
|
|
296
|
+
/** DOM contextmenu (right click / long press / Menu key) -> ContextMenuEventArgs routed through the tree like a tap. */
|
|
297
|
+
onContextMenu = (e) => {
|
|
298
|
+
const rect = this.Element.getBoundingClientRect();
|
|
299
|
+
const scale = this.RenderingScale;
|
|
300
|
+
const x = e.clientX - rect.left, y = e.clientY - rect.top;
|
|
301
|
+
const pointerType = e.pointerType;
|
|
302
|
+
const source = pointerType === "touch" || pointerType === "pen" ? "touch" : pointerType === "mouse" || e.button === 2 ? "mouse" : "keyboard";
|
|
303
|
+
const args = new ContextMenuEventArgs(new SKPoint(x, y), new SKPoint(x * scale, y * scale), source, e);
|
|
304
|
+
let handled = this.content?.ProcessContextMenu(args.Pixels, args) ?? false;
|
|
305
|
+
if (!handled && this.ContextMenu)
|
|
306
|
+
handled = this.ContextMenu(this, args) === true;
|
|
307
|
+
if (handled)
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
};
|
|
310
|
+
cursorPointer = false;
|
|
311
|
+
/**
|
|
312
|
+
* DrawnUi.Blazor shows `cursor: pointer` over interactive controls through its overlay elements; here the overlay
|
|
313
|
+
* is pointer-events:none, so the mouse position is tested against the accessibility snapshot (the accessible
|
|
314
|
+
* controls' rects in points, already sorted and rate-limited), each hit control answering WantsPointerCursor
|
|
315
|
+
* (itself tappable, or a tappable span of a label), and the canvas element's cursor is switched only when the
|
|
316
|
+
* answer changes. Mouse moves only, no work without a mouse and none in the frame loop.
|
|
317
|
+
*/
|
|
318
|
+
UpdateCursor(x, y) {
|
|
319
|
+
let hit = false;
|
|
320
|
+
const scale = this.RenderingScale;
|
|
321
|
+
for (const n of this.AccessibilityManager.Snapshot) {
|
|
322
|
+
if (x < n.Rect.Left || x >= n.Rect.Right || y < n.Rect.Top || y >= n.Rect.Bottom)
|
|
323
|
+
continue;
|
|
324
|
+
// the control decides (whole control, or a tappable span of a label); point in pixels relative to the control's
|
|
325
|
+
// origin, taken from the snapshot rect (already carries the scroll / cache offset; a rotated control gets its bbox)
|
|
326
|
+
if (n.Source.WantsPointerCursor((x - n.Rect.Left) * scale, (y - n.Rect.Top) * scale)) {
|
|
327
|
+
hit = true;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (hit !== this.cursorPointer) {
|
|
332
|
+
this.cursorPointer = hit;
|
|
333
|
+
this.Element.style.cursor = hit ? "pointer" : "";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
237
336
|
/** Mouse wheel -> TouchActionResult.Wheel (page scroll suppressed while gestures are enabled). */
|
|
238
337
|
onWheel = (e) => {
|
|
239
338
|
e.preventDefault();
|
|
@@ -253,6 +352,7 @@ export class Canvas {
|
|
|
253
352
|
el.style.userSelect = "none";
|
|
254
353
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
255
354
|
el.addEventListener(t, this.onPointer);
|
|
355
|
+
el.addEventListener("contextmenu", this.onContextMenu);
|
|
256
356
|
el.addEventListener("wheel", this.onWheel, { passive: false });
|
|
257
357
|
if (this.gestures === "Lock")
|
|
258
358
|
el.addEventListener("touchmove", this.preventTouch, { passive: false });
|
|
@@ -263,6 +363,7 @@ export class Canvas {
|
|
|
263
363
|
el.style.userSelect = "";
|
|
264
364
|
for (const t of ["pointerdown", "pointermove", "pointerup", "pointercancel"])
|
|
265
365
|
el.removeEventListener(t, this.onPointer);
|
|
366
|
+
el.removeEventListener("contextmenu", this.onContextMenu);
|
|
266
367
|
el.removeEventListener("wheel", this.onWheel);
|
|
267
368
|
el.removeEventListener("touchmove", this.preventTouch);
|
|
268
369
|
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";
|
|
@@ -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;
|
|
@@ -66,6 +80,33 @@ export declare class SkiaGesturesInfo {
|
|
|
66
80
|
constructor(Args: SkiaGesturesParameters, Info: GestureEventProcessingInfo);
|
|
67
81
|
}
|
|
68
82
|
/** DrawnUi ControlTappedEventArgs. */
|
|
83
|
+
/** Where a context-menu request came from: right click, long press (Android fires contextmenu), or the keyboard Menu key. */
|
|
84
|
+
export type ContextMenuSource = "mouse" | "touch" | "keyboard";
|
|
85
|
+
/**
|
|
86
|
+
* Arguments of SkiaControl.ContextMenu / Canvas.ContextMenu: a browser `contextmenu` request on the canvas (right
|
|
87
|
+
* click, long press on touch, the Menu key). Handlers return true to take it: the browser's own menu ("Save image")
|
|
88
|
+
* is then suppressed; otherwise it shows as usual.
|
|
89
|
+
*/
|
|
90
|
+
export declare class ContextMenuEventArgs {
|
|
91
|
+
/** Point on the canvas, in points (CSS px). */
|
|
92
|
+
Location: SKPoint;
|
|
93
|
+
/** Same point in pixels (canvas space). */
|
|
94
|
+
Pixels: SKPoint;
|
|
95
|
+
Source: ContextMenuSource;
|
|
96
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
97
|
+
Native: MouseEvent;
|
|
98
|
+
/** Deepest control under the point that had a ContextMenu handler (set while routing). */
|
|
99
|
+
Control?: SkiaControl;
|
|
100
|
+
/** Point inside Control, in pixels relative to its DrawingRect origin (set while routing). */
|
|
101
|
+
Local: SKPoint;
|
|
102
|
+
constructor(
|
|
103
|
+
/** Point on the canvas, in points (CSS px). */
|
|
104
|
+
Location: SKPoint,
|
|
105
|
+
/** Same point in pixels (canvas space). */
|
|
106
|
+
Pixels: SKPoint, Source: ContextMenuSource,
|
|
107
|
+
/** The DOM event: modifiers, target, preventDefault if you need it yourself. */
|
|
108
|
+
Native: MouseEvent);
|
|
109
|
+
}
|
|
69
110
|
export declare class ControlTappedEventArgs {
|
|
70
111
|
Control: SkiaControl;
|
|
71
112
|
Parameters: SkiaGesturesParameters;
|