drawnui-react 0.1.0-preview.1 → 0.1.0-preview.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -29
- package/SKIPPED.md +20 -15
- package/dist/controls/SkiaButton.d.ts +4 -1
- package/dist/controls/SkiaButton.js +20 -7
- package/dist/controls/SkiaCarousel.d.ts +6 -0
- package/dist/controls/SkiaCarousel.js +7 -1
- package/dist/controls/SkiaCheckbox.d.ts +0 -1
- package/dist/controls/SkiaCheckbox.js +0 -6
- package/dist/controls/SkiaEditor.js +8 -0
- package/dist/controls/SkiaLabel.d.ts +2 -0
- package/dist/controls/SkiaLabel.js +9 -0
- package/dist/controls/SkiaLayout.d.ts +10 -0
- package/dist/controls/SkiaLayout.js +64 -1
- package/dist/controls/SkiaRadioButton.js +1 -2
- package/dist/controls/SkiaScroll.d.ts +9 -0
- package/dist/controls/SkiaScroll.js +120 -13
- package/dist/controls/SkiaScrollBar.d.ts +27 -0
- package/dist/controls/SkiaScrollBar.js +51 -2
- package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
- package/dist/controls/SkiaShaderCarousel.js +5 -0
- package/dist/controls/SkiaSwitch.d.ts +0 -1
- package/dist/controls/SkiaSwitch.js +0 -6
- package/dist/controls/SkiaToggle.d.ts +18 -4
- package/dist/controls/SkiaToggle.js +45 -3
- package/dist/core/Canvas.d.ts +82 -4
- package/dist/core/Canvas.js +255 -9
- package/dist/core/Gestures.d.ts +47 -1
- package/dist/core/Gestures.js +43 -1
- package/dist/core/SkiaControl.d.ts +30 -1
- package/dist/core/SkiaControl.js +59 -3
- package/dist/core/Styles.d.ts +51 -0
- package/dist/core/Styles.js +92 -0
- package/dist/core/Super.d.ts +10 -1
- package/dist/core/Super.js +20 -3
- package/dist/core/Types.d.ts +7 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/react/SkiaShell.js +5 -2
- package/dist/react/index.d.ts +22 -4
- package/dist/react/index.js +25 -4
- package/dist/react/reconciler.js +2 -0
- package/dist/vite/index.d.ts +74 -0
- package/dist/vite/index.js +204 -0
- package/package.json +35 -4
- package/PARITY.md +0 -287
package/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. The control that consumes a gesture keeps receiving it until it lets go (DrawnUi `Canvas.HadInput`, one pointer here instead of C#'s set): without it a drag handle, a slider thumb or a held button stops getting `Panning` the moment the finger leaves its box, and a control that stops consuming hands routing straight back to the tree (a button whose press turns into a scroll pan). 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. |
|
|
@@ -18,7 +18,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
18
18
|
| `SkiaBackdrop` | ported | `Blur` (5), `Brightness` (gamma), `UseContext`, `BackgroundColor` tint, `HasEffects`, `GetImage`; children drawn first and blurred with the snapshot (C# order); `UseContext` snapshots the surface the pixels end up on (on-screen, or the nearest Image cache surface through its `Origin`), also while an Operations picture is being recorded, since caches record inline; after each paint the ancestors' caches are staled for the next frame (no frame requested) so content that changes beneath without invalidating this branch (an image loading later) is picked up by the next frame. Used by the shell popups/modals (`ShellDefaults.PopupsBackgroundBlur`). Not ported: `CacheSource`, the ghost mode. |
|
|
19
19
|
| Disposal (`Dispose` / `OnDisposing`) | ported | The React renderer calls `Dispose()` when an element leaves the tree (`detachDeletedInstance`): caches, gradient shaders, running `*ToAsync` animations, overlay effects, the accessibility node, then the children (static and templated pool); `SkiaScroll`/`SnappingLayout` stop their animators, `SkiaSvg` drops its raster, `SkiaLottie`/`SkiaGif` free the Skottie animation / frames; `Canvas.Dispose` disposes the content. Not ported: `IsDisposing` staged disposal, `Disposing` event, `OnWillDisposeWithChildren`. |
|
|
20
20
|
| `KeyboardManager` | ported | Window-level keydown / keyup capture listeners like `drawnui-keyboard.js`: `KeyDown` / `KeyUp` with the DOM `event.code` ("KeyA", "ArrowLeft"; C# maps the same codes to `InputKey`), `KeyChar` for printable keys without Ctrl / Alt / Meta, `IsShiftPressed` / `IsControlPressed` / `IsAltPressed`, `AttachToKeyboard` (idempotent, pointerdown pulls window focus inside iframes), `BlurExternalTextInput`; `Subscribe` / `Unsubscribe` instead of C# events. Not ported: `InputKey` enum (codes are strings). |
|
|
21
|
-
| `SkiaEditor` | partial | SkiaShape host with padding, content `SkiaLabel
|
|
21
|
+
| `SkiaEditor` | partial | SkiaShape host with padding, content `SkiaLabel` (single-line: `HorizontalTextAlignment` Center / End get the field as the label's minimum width, so short text centers and long text still scrolls), placeholder label, caret (blinking 1 s, `CursorColor`, `CanShowCursor`) and selection overlay (`SelectionColor`) painted by the editor, single line (`MaxLines` 1, scrolls horizontally to the caret) / multiline (`MaxLines` != 1, `AutoHeight`) with own scroll offsets instead of the C# inner `SkiaScroll`; `Text` (line breaks normalized, spaces for single line), `PlaceholderText/Color/HorizontalAlignment`, `FontSize` (12) / `FontFamily` / `FontWeight` / `TextColor` / `TextGradient` / `LineHeight` / `Horizontal|VerticalTextAlignment`, `IsPassword` (bullets), `ReturnType` (Send submits multiline), `KeyboardType` / `IsSpellCheckEnabled` / `UseMarkdown` / `UseUnicode` accepted, `CursorPosition`, `SelectionLength`, `IsFocused` (one focused editor at a time), `ControlStyle` (C# `ApplyControlStyleVisuals` palettes over unset values), events `TextChanged` / `FocusChanged` / `CursorMoved` / `TextSubmitted`, `Submit` / `SelectAll` / `SetSelection` / `SelectWord` / `GetSelectedText` / `DeleteSelection` / `InsertAtCursor` / `CutSelection` / `CopySelection` / `PasteFromClipboard` (navigator.clipboard), the Blazor head stubs `StubTypeText` / `StubPressEnter` / `StubBackspace` / `StubDelete` (grapheme clusters via Intl.Segmenter) / `StubMoveCursor` / `StubSelectRange` / `StubSelectAll` / `HandleVerticalArrow`, Ctrl+A / C / X / V, Home / End, Escape blurs, tap places the caret, drag selects, long press selects a word, shift + tap extends. Keys come from `KeyboardManager` like the Blazor / Wasm heads; on top of that (not in DrawnUi.Blazor) a focused editor is mirrored by a hidden DOM textarea (`TextInputProxy`, `inputmode` from `KeyboardType`, `enterkeyhint` from `ReturnType`) so IME composition, the mobile soft keyboard, autocorrect and native paste / cut / undo reach the editor: every input event is diffed against the editor text and replayed through `StubSelectRange` + `StubTypeText` / `StubBackspace` / `StubPressEnter`; while the proxy holds DOM focus, Backspace / Delete / Enter / Ctrl+C / X / V key events are left to it. iOS may need a second tap for the keyboard (focus() runs on the next frame, outside the touch handler). Not ported: `SkiaCursor` control + `CursorGradient`, `SkiaEditorSelection` handles, `Superview.FocusedChild`, `CommandOn*`, `SkiaEditorDocument` (rich formatting), single-line Enter clearing focus (Blazor keeps it). |
|
|
22
22
|
| `SkiaSprite` / `SkiaSpriteSet` | ported | Sprite: `Source` (sheets cached per source, `CachedSpriteSheets`), `Columns` / `Rows` / `MaxFrames`, `FramesPerSecond` (24), `FrameSequence` / `AnimationName` + `CreateAnimationSequence` / `RegisteredAnimations`, `CurrentFrame`, `TotalFrames` / `DurationMs` / `FrameDurationMs` / `FrameWidth` / `FrameHeight`, `ApplyPlacementConfig` (units-per-pixel, size, anchor, offset = C# SpriteFrameImage ResolveFrameMetrics), transparent frame borders trimmed per frame (pixels read once), nearest sampling, AspectFit of the frame, `DisplayRect`, `Success` / `Error`, the AnimatedFramesRenderer playback. Set: `State`, `Define(state, source, columns, rows, fps, repeat, autoPlay, placement)`, `CurrentSprite`, `OnChangeState` hook, hit box from the drawn frame. Not ported: `Display` as a separate SkiaImage child (`SpriteFrameImage`) and the standalone `SkiaSprite(display)` ctor, `RescalingQuality` other than None, `InflateAmount`, `ClearCache` / `RemoveFromCache` (SkiaImageManager owns the images), `SkiaSpriteSet.InvalidateInternal`. Code-behind subclasses (FastRepro `WarriorSprite`) mount through a ref + `AddSubView` and are disposed by the mounting effect. |
|
|
23
23
|
| `IsClippedToBounds`, `ClipEffects` | ported | Clip to `DrawingRect` around content, children and post-animators; `ClipEffects=false` grows the clip by the aggregated effects margin so a child's shadow/glow can escape. Layouts aggregate their children's effects margins (C# `AggregatedEffectsMarginPixels`), cached per control and reset by `InvalidateMeasure`. Not ported: `Clipping` callback, `ClipWith`, `ClipFrom`, platform clips; C# currently ignores `ClipEffects` in its render path (always expands). |
|
|
24
24
|
| `Padding` on base `SkiaControl` | skipped | Only `SkiaLayout.Padding` exists. |
|
|
@@ -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 | |
|
|
@@ -47,10 +47,13 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
47
47
|
| Area | Status | Notes |
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `RenderingMode` | partial | `Accelerated` (WebGL) with automatic fallback to software; read once at first surface creation. |
|
|
50
|
-
| `Gestures` param | ported |
|
|
50
|
+
| `Gestures` param | ported | Touch: `Lock` applies `touch-action:none`; `Enabled` behaves like MAUI's Enabled inside a native scroll view: `touch-action` is the axes the page (or a scrolling ancestor) can scroll (`pan-y` / `pan-x` / both, `none` when it cannot; inside an iframe the vertical axis always counts, because the embedding page cannot be inspected cross-origin), recomputed on attach, on window / html / body resize and after every touch, so a finger pan along a page axis scrolls the page and the canvas pointer is cancelled (a cancelled pointer never flings a `SkiaScroll`), taps and the other axis stay on the canvas, and a full-page app keeps every touch. `user-select:none` in both. Claimed touches: in `Enabled` a touch or pen Down is processed immediately (queued gestures first) and, when a control used it (same rule as the wheel: marked `Handled`, or a consumer that is not a `BlockGesturesBelow` layer), the touch is claimed and its `touchmove` events are default-prevented until it ends, so the page cannot take that drag mid-gesture (iOS Safari ignores `touch-action` for off-axis drags, w3c/pointerevents#303); unclaimed touches share the page as before. Controls that use a Down: `ConsumeGestures` handlers that set `Consumed`, `SkiaCarousel` (always), `SkiaSlider`, `SkiaEditor`, `SkiaButton` when `OnDown` accepts; `SkiaScroll` and plain `Tapped` controls do not. Mouse wheel: `Enabled` shares it with the page like Blazor's `TouchHandlingStyle.Manual` (the wheel is processed in the DOM handler and the browser default is prevented only when a control used it: a scroll that moved, a `ConsumeGestures` handler that consumed, or any other consumer that is not a `BlockGesturesBelow` layer merely blocking the controls below, so an embedded canvas lets the page scroll over anything that does not scroll, including a shell page, and a scroll at its edge hands the wheel to the page; `TouchActionEventArgs.Handled` is the AppoMobi marker for that use); `Lock` prevents every wheel and also blocks `touchmove` default. Not ported: Blazor's pointer capture policy for `Enabled` (capture only while locked, release when a pan is not consumed); React captures every press. |
|
|
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
|
-
|
|
|
53
|
-
|
|
|
54
|
+
| Canvas draw events | ported | `WillFirstTimeDraw` (once, right before the first frame drawn on a ready surface, with that frame's `{ Canvas, Surface }`), `WasDrawn` (after every frame, no context, like the C# `null`), `WasRendered` (set after the first frame completes, so the first `WasDrawn` still sees `false`, C# order), `CanRender` (surface ready). JSX: `<Canvas WillFirstTimeDraw WasDrawn>`; the engine draws frame 1 in its constructor, so the component hands both callbacks to the constructor's `init`. Not ported: `WillDraw`. |
|
|
55
|
+
| 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`. |
|
|
56
|
+
| 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
57
|
|
|
55
58
|
## Controls
|
|
56
59
|
|
|
@@ -59,7 +62,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
59
62
|
| `SkiaLayout` Absolute / Column / Row (+ `SkiaStack`, `SkiaRow`, `SkiaLayer`) | ported | `Spacing`, `Padding`, `Children`/`Views`, `AddSubView`/`RemoveSubView`/`InsertSubView`. |
|
|
60
63
|
| `SkiaLayout` templated (Column only): `ItemsSource` (array), `ItemTemplate` (factory `() => SkiaControl`), `RecyclingTemplate` Enabled/Disabled, `MeasureItemsStrategy` `MeasureFirst` (uniform, O(1) for 100k items) / `MeasureAll`, `ChildrenFactory` (ViewsAdapter pool: `GetOrCreateViewForIndex`, `ReleaseViewAt`), `FirstVisibleIndex`/`LastVisibleIndex`, `DebugString`, `ApplyItemsSource`, `VirtualisationInflated`, `BindingContext`/`ContextIndex` on every control, `SkiaDynamicDrawnCell.SetContent` | ported | Cells realized/bound/arranged/drawn only for the visible viewport (+inflation) each frame. |
|
|
61
64
|
| `LoadMoreCommand` / `LoadMoreTopCommand` / `LoadMoreOffset` / `LoadMoreTopOffset` | ported | Callbacks instead of ICommand. Bottom fires once per content extent when within the offset of the end (or when the content underfills the viewport), re-arms when the content grows or the user moves away (>offset+100 pt, >2 s) — C# CheckNeedToLoadMore. Top arms only after the offset left the top edge, fires when it comes back. Not ported: opposite-direction cooldown (`IsOppositeLoadMoreBlocked`), `IInsideViewport.ShouldTriggerLoadMore`. |
|
|
62
|
-
| Structure-preserving `ItemsSource` updates | ported (array diff) | C# listens to `ObservableCollection` Add/Insert; React apps replace the array, so the setter detects **append** (old is a prefix: measured heights kept, tail measured lazily) and **prepend** (old is a suffix: view indices shifted without rebinding, new head measured synchronously up to 200 items, `ItemsInsertedAtStart(px)` lets the `SkiaScroll` move its offset so visible rows stay put — C# head-insert rebase). Any other change rebuilds the structure (heights re-measured, offset kept). Not ported: removals
|
|
65
|
+
| Structure-preserving `ItemsSource` updates | ported (array diff) | C# listens to `ObservableCollection` Add/Insert; React apps replace the array, so the setter detects **append** (old is a prefix: measured heights kept, tail measured lazily) and **prepend** (old is a suffix: view indices shifted without rebinding, new head measured synchronously up to 200 items, `ItemsInsertedAtStart(px)` lets the `SkiaScroll` move its offset so visible rows stay put — C# head-insert rebase). and **reorder** (same items permuted, the C# `ObservableCollection.Move` case: contexts rebound, the index-keyed measured heights permuted with their items, nothing re-measured, extent and scroll offset unchanged). Any other change rebuilds the structure (heights re-measured, offset kept). Not ported: removals in place, `Replace`. |
|
|
63
66
|
| Templated Row/Grid/Wrap, `Split`, `ItemTemplateSelector`, `ItemTemplatePoolSize`/`ReserveTemplates`, `UsePreparedViews`, `SkiaCachedStack`, measure memo | skipped | |
|
|
64
67
|
| `ItemsSourceWindow` / `WindowSourceThreshold` (built-in window over huge sources) | not needed | The C# window bounds pipeline work (structures, pools, measures) for `MeasureFirst`/`MeasureAll` over big collections. React `MeasureVisible` measures on demand and keeps only visible cells realized, so the pipeline is already O(visible); memory of the array itself belongs to the app (paginate with `LoadMore*`). Revisit only if `MeasureAll` over 100k+ items becomes a requirement. |
|
|
65
68
|
| `MeasureItemsStrategy="MeasureVisible"` + background measurement | ported | Initial pass measures enough items to fill the viewport (≥ `BackgroundMeasurementBatchSize`), content extent = exact prefix + average × remaining (C# MeasureList estimate), visible cells measured on demand and laid out with their real heights, the prefix grows in `requestIdleCallback` slices (time-budgeted, then one `InvalidateMeasure`), `LastMeasuredIndex`, `DebugString` shows `measured n/N`. Not ported: cancellation/`_pendingStructureChanges` interplay, `UsePreparedViews`, windowed sources, anchor correction when an estimate above the viewport changes (content may shift by the delta). |
|
|
@@ -72,7 +75,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
72
75
|
| `SkiaImageManager` | partial | `Instance.LoadImageAsync(url, signal)` (direct), `LoadImageManagedAsync(url, signal, priority)` through the priority queue (High > Normal > Low, `MaxParallelLoads` 5 in flight like the C# semaphore, one fetch per url serves every requester), `PreloadImage` / `PreloadImages(urls, priority)`, `CancelAll`, `GetFromCache`, `CanReload` callback on failure, `QueuedCount` / `RunningCount`, `ReuseBitmaps`, `Clear()`; in-memory cache of decoded images only. No `CacheLongevitySecs` eviction, no platform loaders, no `LoadImageOnlineAsync` retry policy, no `ImageSource` types (urls only). |
|
|
73
76
|
| `SkiaImageTiles` | ported | `TileWidth` / `TileHeight` (points, nothing drawn until set), `TileAspect` (AspectCover), `TileOffsetX` / `TileOffsetY` (wrap), `TileCacheType` (Image) for the inner tile `SkiaImage` sharing the decoded source, clipped to bounds. |
|
|
74
77
|
| `SkiaSvg` | partial | Inline `SvgString` without `xmlns` gets it injected (browsers refuse to decode an `<img>` SVG without it, SkiaSharp does not care) — the C# check-mark markup relies on that. `Source` (URL) / `SvgString`, `Aspect` (default `AspectFitFill`, uses the shared `RescaleAspect` math, not SkiaSvg's own matrix path), alignments, `TintColor` (SrcIn), `Success`/`Error`, `IsLoading`, `DisplayRect`. Rendering: browser decodes the SVG, rasterized at the displayed pixel size and cached per size (CanvasKit npm has no SVG module). Not ported: shadows, FontAwesome duotone colors, `FillGradient`, `Zoom*`/offsets/`InflateAmount`, `IconFilePath`/embedded resources, `UseCache=Operations` default (no caching yet). |
|
|
75
|
-
| `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both, single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize`, `AutoScrollingSpeedMs`, notches accumulate onto the running target), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1, thumb squashed on overscroll), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
|
|
78
|
+
| `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both/Neither (Neither: pans never move the viewport, the wheel is not consumed so a parent scroll may take it; programmatic `ScrollTo` still works), single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize`, `AutoScrollingSpeedMs`, notches accumulate onto the running target), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1 + `HideDurationSecs` 0.25, thumb squashed on overscroll, `IsDraggable` + `GrabPadding` 8: the scroll takes a gesture that starts on the bar from down to up, thumb drag keeps the grab point, a track press jumps the thumb under the pointer, the content gets neither the pan nor the tap; a custom `IScrollBar` opts in by implementing `IsDraggable` / `BeginDrag` / `GetDragProgress`; bars are parent-independent (C# `IsParentIndependent`): a thumb resize invalidates the bar alone and the scroll re-measures it on draw, nothing above re-measures), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
|
|
76
79
|
| `SkiaShape` (+ `SkiaFrame`) | partial | `BevelType` Bevel / Emboss + `Bevel` (`Depth`, `LightColor`, `ShadowColor`, `Opacity`; C# `PaintBevelEffect` edge paths for Rectangle, rounded Rectangle, Circle/Ellipse, Polygon, Path via ContourMeasure halves). `Shadows` (`SkiaShadow` X/Y/Blur/Opacity/Color/ShadowOnly, plain literals accepted): fill painted once per shadow with a DropShadow image filter (C# `PaintWithShadowsInternal`), hollow shapes clip their outline away so only the outside shadow remains; caches record the effects margin (`3·Blur·scale ± offset`, C# `MergeShadowMargin`) via `ComputeEffectsMargin`/`ExpandedCacheRect`. Not ported: `PlatformShadow` (MAUI `Shadow` brush), `ExpandDirtyRegion`, bevel/emboss. `Type` Rectangle/Circle/Ellipse/Arc (`Value1` start, `Value2` sweep)/Polygon+Line (`Points` ratios)/Path (`PathData`, fitted + centered), `CornerRadius` (uniform or per-corner), `StrokeWidth` (points, negative = px) drawn inside the bounds, `StrokeColor`, `StrokeCap`, `ClipBackgroundColor`, children laid out inside the stroke and clipped to the shape, `CreateClip` = shape, `FillGradient` fill, `UseCache=Operations` default. Not ported: `Squricle`, `Custom`, `SmoothPoints`, `StrokePath` (dashes), `StrokeGradient`, `StrokeBlendMode`, sub-1px stroke compensation, `MeasuredStrokeAware*` exposure, `LayoutChildren` override hook. |
|
|
77
80
|
| `SkiaButton` | partial | Now a `SkiaLayout` hosting a `SkiaShape` frame (`Tag="BtnShape"`) + `SkiaLabel` (`Tag="BtnText"`), sized from the label + Padding; `CornerRadius` (default 8), `StrokeColor`/`StrokeWidth`, `BackgroundColor`, text props, `ApplyEffect`, `Tapped`/`Down`/`Up`. Still no `ButtonStyle` looks, icons, `TextCase`, elevation, shimmer, custom templating through tags. |
|
|
78
81
|
| `SnappingLayout` | ported | `Animated`, `RespondsToGestures`, `IgnoreWrongDirection`, `Bounces`, `RubberDamping`, `RubberEffect`, `AutoVelocityMultiplyPts`, `SnapDistanceRatio`, `SnapPoints`, `CurrentPosition`, `CurrentSnap`, `ContentOffsetBounds`, `IsUserPanning`/`IsUserFocused`, `InTransition`, `Scrolled`/`TransitionChanged`/`Stopped`, `ClampOffset` (rubber), `FindNearestAnchor`, `SelectNextAnchor` (dot product), `ScrollToNearestAnchor`, `ScrollToOffset` (spring `Spring(1+RubberDamping, 200, 0.5·(1+RubberDamping))` when `Bounces`, else 0.1–0.8 s from velocity). The settled state is reported when the snap animation stops (C# relies on `ApplyPosition` ticks). Not ported: `AnimatorSpring` reuse, `SnapDistanceRatio` for non-carousel layouts, hover. |
|
|
@@ -80,7 +83,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
80
83
|
| `SkiaCarousel` | ported | Static `Children` or recycled `ItemsSource` + `ItemTemplate` cells (only visible + neighbour slides realized, `ReleaseExcept` for the looped non-contiguous set); `IsVertical`, `SidesOffset`, `Spacing`, `IsLooped` (virtual anchors -1/-2, wrap by `GoNext`/`GoPrev`/swipe at the borders, unbounded offset along the axis, edge slides drawn on the far side, `FixIndex` teleport after the wrap, `InterruptSnapping` strip shift), `PreloadNeighboors` (default true), `DynamicSize` (auto-sized axis follows the selected slide, re-measured on index change), `SwipeSpeed` (velocity ×SwipeSpeed/2, spring stiffness 200·SwipeSpeed, linear cap 0.25/speedK s, `SinOut`), `LinearSpeedMs` (one slide = LinearSpeedMs), `SelectedIndex` + `SelectedIndexChanged`, `LastIndex`, `MaxIndex`, `ChildrenTotal`/`ChildrenCount`, `IsAtStart`/`IsAtEnd`, `ScrollProgress`, `ScrollAmount`, `TransitionProgress`, `ItemAppearing`/`ItemDisappearing`, `Stopped`, `GoNext`/`GoPrev`/`ScrollTo(index, animate)`, C# gesture rules (velocity dead zone 100, clamp 500, snap-if-no-pan on Up, only the slides drawn on screen receive gestures). `IsRightToLeft` accepted (C# TODO). Not ported: `TransitionDirection`, `SafeIndex`, `ScrolledCommand`/`SelectedIndexChangedCommand`, `SelectedItem`, `CheckConstraints` exception, `OnTemplatesAvailable` background init, hover, pinch. |
|
|
81
84
|
| Templated `Row` / `Wrap` / `Grid`, `Split`, `SkiaDecoratedGrid` | ported | `ItemsSource` + `ItemTemplate` on any layout type: Column stays the virtualized list; Row / Wrap / Grid realize every item (no virtualization, like the C# non-list layouts) through the same `ViewsAdapter`. `Split` (column count for Wrap and for a Column, which then lays out like a wrap with fixed slots; a templated Grid places item i at (i % Split, i / Split), `Invert` column-major), `SplitAlign` (true: fixed slot width), `DynamicColumns` (a short last row spreads over the width). `SkiaDecoratedGrid`: `HorizontalLine` / `VerticalLine` gradients painted in `RowSpacing` / `ColumnSpacing` under the children (C# defaults). Not ported: `SplitSpace` (C# TODO), `SplitMax`, `MeasureItemsStrategy` for non-Column templated layouts, C# `CellsToRelease` recycling for Wrap. |
|
|
82
85
|
| `AccessibilityTextSelectable` (React extension) | ported | Opt-in per control, off by default: a `SkiaLabel` with it renders its laid-out lines as real, transparent DOM text in the accessibility overlay (one span per line at the drawn position, the registered font aliases are also installed as CSS `FontFace`s so glyphs line up), `pointer-events` on, so the browser selects, copies and reads it like HTML; the wheel over such text is re-dispatched to the canvas. Pointer input over the text never reaches the drawn control, hence opt-in. Not in DrawnUi.Net. |
|
|
83
|
-
| `SkiaShaderCarousel` | ported | `TransitionShader` (url), `TransitionShaderCode`, `TransitionTemplate`, `InterruptedTransitionMs` (50), `TransitionEffect` (`CreateTransitionEffect` factory), `TransitionFromIndex` / `TransitionToIndex`, `FromToChanged`; slides never move (`SlideOffset` hook), looped wrap rules, one gesture = at most one slide, interrupted transition wrapped up first, `RecyclingTemplate` Disabled; the carousel is cached as Image by default (React only, so the overlapping slides are never blitted). Slides MUST use `UseCache="Image"`. |
|
|
86
|
+
| `SkiaShaderCarousel` | ported | Only the selected slide takes gestures (C# `IsSlideHitTestable`): every slide sits on the same rect, so the topmost on-screen one used to catch every tap. `TransitionShader` (url), `TransitionShaderCode`, `TransitionTemplate`, `InterruptedTransitionMs` (50), `TransitionEffect` (`CreateTransitionEffect` factory), `TransitionFromIndex` / `TransitionToIndex`, `FromToChanged`; slides never move (`SlideOffset` hook), looped wrap rules, one gesture = at most one slide, interrupted transition wrapped up first, `RecyclingTemplate` Disabled; the carousel is cached as Image by default (React only, so the overlapping slides are never blitted). Slides MUST use `UseCache="Image"`. |
|
|
84
87
|
| `SkiaDrawer` | ported | `Direction` (FromBottom/Top/Left/Right), `HeaderSize`, `AmplitudeSize`, `AutoClose`, `IsOpen` + `IsOpenChanged`, `StateTransitionComplete`, `Open()`/`Close()`; moves itself with `TranslationX/Y` like C#, so it must be edge-aligned by the parent (`VerticalOptions="End"` for FromBottom); snap points `[open, hidden]` rebuilt on size change; gestures ported (children first, then vertical/horizontal drag, lock-bounce at the open edge, velocity clamp 3000, touches outside the box ignored / `AutoClose`). Not ported: `IsOpenCommand`, hover, the C# +1 px header quirk. |
|
|
85
88
|
| Everything else (`SkiaSwitch`, `SkiaCheckbox`, `SkiaSlider`, `SkiaProgress`, `SkiaCamera`, `SkiaMauiElement`, ...) | skipped | |
|
|
86
89
|
|
|
@@ -90,18 +93,20 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
90
93
|
|---|---|---|
|
|
91
94
|
| `Super.UseDrawnUi().ConfigureFonts(f => f.AddFont(src, alias)).BuildAsync()` | ported | Same shape as DrawnUi.Net/OpenTK. |
|
|
92
95
|
| `AddFont(src, alias, weight)`, font fallback, emoji | skipped | |
|
|
93
|
-
| `PreloadAssets
|
|
96
|
+
| `PreloadAssets` | skipped | `ConfigureStyles` is ported, see the Engine table. |
|
|
94
97
|
|
|
95
98
|
## React layer
|
|
96
99
|
|
|
97
100
|
| Area | Status | Notes |
|
|
98
101
|
|---|---|---|
|
|
99
102
|
| Custom `react-reconciler` renderer, PascalCase props mapped 1:1 to control properties | ported | |
|
|
100
|
-
| `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. |
|
|
103
|
+
| `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 (changing it after the first measure rebuilds the content and re-pins only the sizes the style set, C# `RebuildDefaultContent`) (`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. |
|
|
104
|
+
| `SkiaControl.ExecuteAfterCreated` / fluent `.Initialize(...)` | skipped | C# runs it once when the control gets its parent so code-behind can reach `.Assign`'d refs; in React the element tree is declarative and `ref` / `useEffect` cover it. |
|
|
101
105
|
| `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
|
|
106
|
+
| `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
107
|
| `SkiaButton.FontFamilyFallback` | React extension | Pass-through to the inner label so icon-glyph buttons render with `AddSymbols()` faces. |
|
|
104
108
|
| 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. |
|
|
109
|
+
| 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
110
|
| React context bridging across the `<Canvas>` boundary | skipped | Contexts from the DOM tree are not visible inside the drawn tree. |
|
|
106
111
|
| Refs to engine controls from JSX | skipped | `getPublicInstance` returns the control, `ref` not wired/typed. |
|
|
107
112
|
| 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;
|
|
@@ -127,6 +127,12 @@ export declare class SkiaCarousel extends SnappingLayout {
|
|
|
127
127
|
/** C# RenderViewsList: position every slide, draw the visible ones (+ neighbours when PreloadNeighboors), release the rest. */
|
|
128
128
|
protected Paint(ctx: DrawingContext): void;
|
|
129
129
|
private SlideRect;
|
|
130
|
+
/**
|
|
131
|
+
* Whether the slide at `index` takes part in hit-testing for the frame being rendered (C# IsSlideHitTestable).
|
|
132
|
+
* Every slide drawn on screen does by default; a carousel whose slides share one rect (SkiaShaderCarousel) narrows
|
|
133
|
+
* this to the slide the user actually sees.
|
|
134
|
+
*/
|
|
135
|
+
protected IsSlideHitTestable(_index: number): boolean;
|
|
130
136
|
protected GetGestureListeners(): readonly SkiaControl[];
|
|
131
137
|
ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
|
|
132
138
|
protected DefaultAccessibilityLabel(): string | undefined;
|
|
@@ -504,7 +504,7 @@ export class SkiaCarousel extends SnappingLayout {
|
|
|
504
504
|
view.Measure(inner.Width, inner.Height, scale); // recycled cells carry new content
|
|
505
505
|
view.Arrange(this.SlideRect(inner, this.SlideOffset(cell.offset), scale), view.WidthRequest, view.HeightRequest, scale);
|
|
506
506
|
view.Render(ctx);
|
|
507
|
-
if (cell.visible)
|
|
507
|
+
if (cell.visible && this.IsSlideHitTestable(cell.index))
|
|
508
508
|
visible.push(view);
|
|
509
509
|
}
|
|
510
510
|
this.visibleViews = visible;
|
|
@@ -514,6 +514,12 @@ export class SkiaCarousel extends SnappingLayout {
|
|
|
514
514
|
? SKRect.Create(inner.Left, inner.Top + offset.Y * scale, inner.Width, inner.Height)
|
|
515
515
|
: SKRect.Create(inner.Left + offset.X * scale, inner.Top, inner.Width, inner.Height);
|
|
516
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Whether the slide at `index` takes part in hit-testing for the frame being rendered (C# IsSlideHitTestable).
|
|
519
|
+
* Every slide drawn on screen does by default; a carousel whose slides share one rect (SkiaShaderCarousel) narrows
|
|
520
|
+
* this to the slide the user actually sees.
|
|
521
|
+
*/
|
|
522
|
+
IsSlideHitTestable(_index) { return true; }
|
|
517
523
|
GetGestureListeners() { return this.visibleViews; }
|
|
518
524
|
// ---- gestures (port of C# SkiaCarousel.ProcessGestures) ----
|
|
519
525
|
ProcessGestures(args, apply) {
|
|
@@ -15,7 +15,6 @@ export declare class SkiaCheckbox extends SkiaToggle {
|
|
|
15
15
|
get ColorCheckOn(): Color;
|
|
16
16
|
set ColorCheckOn(v: Color);
|
|
17
17
|
protected StyleDefault(name: "ColorThumbOn" | "ColorFrameOn" | "ColorThumbOff" | "ColorFrameOff"): Color | undefined;
|
|
18
|
-
private SetContentSize;
|
|
19
18
|
protected CreateDefaultContent(): void;
|
|
20
19
|
ApplyProperties(): void;
|
|
21
20
|
}
|
|
@@ -27,12 +27,6 @@ export class SkiaCheckbox extends SkiaToggle {
|
|
|
27
27
|
};
|
|
28
28
|
return table[this.UsingControlStyle]?.[name];
|
|
29
29
|
}
|
|
30
|
-
SetContentSize(w, h) {
|
|
31
|
-
if (this.WidthRequest < 0)
|
|
32
|
-
this.WidthRequest = w;
|
|
33
|
-
if (this.HeightRequest < 0)
|
|
34
|
-
this.HeightRequest = h;
|
|
35
|
-
}
|
|
36
30
|
CreateDefaultContent() {
|
|
37
31
|
const off = new SkiaShape();
|
|
38
32
|
off.Tag = "FrameOff";
|
|
@@ -305,6 +305,14 @@ export class SkiaEditor extends SkiaShape {
|
|
|
305
305
|
MeasureAbsolute(w, h, scale) {
|
|
306
306
|
const px = this.Padding.HorizontalThickness * scale, py = this.Padding.VerticalThickness * scale;
|
|
307
307
|
const innerW = isFinite(w) ? Math.max(0, w - px) : Infinity;
|
|
308
|
+
// Single-line: the label is Start-aligned and measures to its content, so Center / End alignment had nothing to
|
|
309
|
+
// align in. Give it the field as a MINIMUM: short text centers in the field, longer text still grows past it and
|
|
310
|
+
// scrolls (C# 2e334ddd).
|
|
311
|
+
const minWidth = !this.IsMultiline && this.horizontalTextAlignment !== "Start" && isFinite(innerW) && innerW > 0 ? innerW / scale : -1;
|
|
312
|
+
if (this.Label.MinimumWidthRequest !== minWidth) {
|
|
313
|
+
this.Label.MinimumWidthRequest = minWidth;
|
|
314
|
+
this.Label.NeedMeasure = true;
|
|
315
|
+
}
|
|
308
316
|
const labelSize = this.Label.Measure(this.IsMultiline ? innerW : Infinity, Infinity, scale);
|
|
309
317
|
if (this.placeholder.IsVisible)
|
|
310
318
|
this.placeholder.Measure(innerW, Infinity, scale);
|
|
@@ -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));
|
|
@@ -70,6 +70,16 @@ export declare class SkiaLayout extends SkiaControl {
|
|
|
70
70
|
* anything else rebuilds. Returns false when a full rebuild is needed.
|
|
71
71
|
*/
|
|
72
72
|
private ApplyIncrementalChange;
|
|
73
|
+
/**
|
|
74
|
+
* Same items in another order — a drag-to-reorder, or any code writing a permuted array. DrawnUi handles the
|
|
75
|
+
* matching `ObservableCollection.Move` by keeping the structure and only rebinding the contexts, because its
|
|
76
|
+
* arrange re-flows each row from its bound view (`HandleStructurePreservingMove` / `ApplyMoveChange`, 2026-09-08).
|
|
77
|
+
* The measured heights here are index-keyed and authoritative, so they travel with their items instead: nothing is
|
|
78
|
+
* remeasured, the extent cannot change, and the scroll keeps its offset. Rebuilding instead would reset
|
|
79
|
+
* `MeasureVisible` to "exact for the first screen, estimated after" and throw a scrolled list back towards the top.
|
|
80
|
+
* Returns false when the two arrays are not a permutation of each other, so a real content change still rebuilds.
|
|
81
|
+
*/
|
|
82
|
+
private ApplyReorderChange;
|
|
73
83
|
/** Factory creating one cell (DrawnUi DataTemplate). Cells receive the item as BindingContext. */
|
|
74
84
|
get ItemTemplate(): (() => SkiaControl) | undefined;
|
|
75
85
|
set ItemTemplate(value: (() => SkiaControl) | undefined);
|