drawnui-react 0.1.0-preview.1

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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/PARITY.md +287 -0
  3. package/README.md +92 -0
  4. package/SKIPPED.md +107 -0
  5. package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
  6. package/dist/controls/AnimatedFramesRenderer.js +133 -0
  7. package/dist/controls/GridStructure.d.ts +79 -0
  8. package/dist/controls/GridStructure.js +403 -0
  9. package/dist/controls/RefreshIndicator.d.ts +35 -0
  10. package/dist/controls/RefreshIndicator.js +82 -0
  11. package/dist/controls/SkiaBackdrop.d.ts +25 -0
  12. package/dist/controls/SkiaBackdrop.js +80 -0
  13. package/dist/controls/SkiaButton.d.ts +58 -0
  14. package/dist/controls/SkiaButton.js +148 -0
  15. package/dist/controls/SkiaCarousel.d.ts +133 -0
  16. package/dist/controls/SkiaCarousel.js +603 -0
  17. package/dist/controls/SkiaCheckbox.d.ts +21 -0
  18. package/dist/controls/SkiaCheckbox.js +118 -0
  19. package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
  20. package/dist/controls/SkiaDecoratedGrid.js +46 -0
  21. package/dist/controls/SkiaDrawer.d.ts +46 -0
  22. package/dist/controls/SkiaDrawer.js +212 -0
  23. package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
  24. package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
  25. package/dist/controls/SkiaEditor.d.ts +163 -0
  26. package/dist/controls/SkiaEditor.js +803 -0
  27. package/dist/controls/SkiaGif.d.ts +54 -0
  28. package/dist/controls/SkiaGif.js +206 -0
  29. package/dist/controls/SkiaHotspot.d.ts +13 -0
  30. package/dist/controls/SkiaHotspot.js +36 -0
  31. package/dist/controls/SkiaImage.d.ts +75 -0
  32. package/dist/controls/SkiaImage.js +222 -0
  33. package/dist/controls/SkiaImageTiles.d.ts +27 -0
  34. package/dist/controls/SkiaImageTiles.js +76 -0
  35. package/dist/controls/SkiaLabel.d.ts +160 -0
  36. package/dist/controls/SkiaLabel.js +580 -0
  37. package/dist/controls/SkiaLayout.d.ts +163 -0
  38. package/dist/controls/SkiaLayout.js +723 -0
  39. package/dist/controls/SkiaLottie.d.ts +75 -0
  40. package/dist/controls/SkiaLottie.js +290 -0
  41. package/dist/controls/SkiaProgress.d.ts +40 -0
  42. package/dist/controls/SkiaProgress.js +80 -0
  43. package/dist/controls/SkiaRadioButton.d.ts +32 -0
  44. package/dist/controls/SkiaRadioButton.js +157 -0
  45. package/dist/controls/SkiaRichLabel.d.ts +77 -0
  46. package/dist/controls/SkiaRichLabel.js +274 -0
  47. package/dist/controls/SkiaScroll.d.ts +231 -0
  48. package/dist/controls/SkiaScroll.js +958 -0
  49. package/dist/controls/SkiaScrollBar.d.ts +56 -0
  50. package/dist/controls/SkiaScrollBar.js +158 -0
  51. package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
  52. package/dist/controls/SkiaShaderCarousel.js +246 -0
  53. package/dist/controls/SkiaShape.d.ts +63 -0
  54. package/dist/controls/SkiaShape.js +317 -0
  55. package/dist/controls/SkiaSlider.d.ts +74 -0
  56. package/dist/controls/SkiaSlider.js +243 -0
  57. package/dist/controls/SkiaSprite.d.ts +99 -0
  58. package/dist/controls/SkiaSprite.js +324 -0
  59. package/dist/controls/SkiaSpriteSet.d.ts +27 -0
  60. package/dist/controls/SkiaSpriteSet.js +75 -0
  61. package/dist/controls/SkiaSvg.d.ts +43 -0
  62. package/dist/controls/SkiaSvg.js +181 -0
  63. package/dist/controls/SkiaSwitch.d.ts +26 -0
  64. package/dist/controls/SkiaSwitch.js +147 -0
  65. package/dist/controls/SkiaToggle.d.ts +50 -0
  66. package/dist/controls/SkiaToggle.js +71 -0
  67. package/dist/controls/SnappingLayout.d.ts +78 -0
  68. package/dist/controls/SnappingLayout.js +217 -0
  69. package/dist/controls/TextSpan.d.ts +70 -0
  70. package/dist/controls/TextSpan.js +76 -0
  71. package/dist/core/Accessibility.d.ts +104 -0
  72. package/dist/core/Accessibility.js +150 -0
  73. package/dist/core/Animators.d.ts +82 -0
  74. package/dist/core/Animators.js +247 -0
  75. package/dist/core/Canvas.d.ts +89 -0
  76. package/dist/core/Canvas.js +328 -0
  77. package/dist/core/ControlStyle.d.ts +6 -0
  78. package/dist/core/ControlStyle.js +11 -0
  79. package/dist/core/Easing.d.ts +13 -0
  80. package/dist/core/Easing.js +15 -0
  81. package/dist/core/Gestures.d.ts +74 -0
  82. package/dist/core/Gestures.js +104 -0
  83. package/dist/core/ImageEffects.d.ts +29 -0
  84. package/dist/core/ImageEffects.js +55 -0
  85. package/dist/core/KeyboardManager.d.ts +25 -0
  86. package/dist/core/KeyboardManager.js +54 -0
  87. package/dist/core/ScrollAnimators.d.ts +90 -0
  88. package/dist/core/ScrollAnimators.js +244 -0
  89. package/dist/core/SkiaControl.d.ts +401 -0
  90. package/dist/core/SkiaControl.js +1239 -0
  91. package/dist/core/SkiaEffect.d.ts +216 -0
  92. package/dist/core/SkiaEffect.js +571 -0
  93. package/dist/core/SkiaImageManager.d.ts +43 -0
  94. package/dist/core/SkiaImageManager.js +122 -0
  95. package/dist/core/Super.d.ts +61 -0
  96. package/dist/core/Super.js +178 -0
  97. package/dist/core/TextInputProxy.d.ts +31 -0
  98. package/dist/core/TextInputProxy.js +144 -0
  99. package/dist/core/Types.d.ts +177 -0
  100. package/dist/core/Types.js +122 -0
  101. package/dist/core/ViewsAdapter.d.ts +41 -0
  102. package/dist/core/ViewsAdapter.js +113 -0
  103. package/dist/index.d.ts +48 -0
  104. package/dist/index.js +48 -0
  105. package/dist/react/SkiaShell.d.ts +144 -0
  106. package/dist/react/SkiaShell.js +490 -0
  107. package/dist/react/index.d.ts +110 -0
  108. package/dist/react/index.js +158 -0
  109. package/dist/react/reconciler.d.ts +14 -0
  110. package/dist/react/reconciler.js +160 -0
  111. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 - Present day Nick Kovalsky aka AppoMobi and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PARITY.md ADDED
@@ -0,0 +1,287 @@
1
+ # Parity notes: DrawnUi.React vs DrawnUi (.NET)
2
+
3
+ Behavioural differences between the two implementations that are NOT plain omissions (those live in
4
+ [SKIPPED.md](SKIPPED.md)). Each entry says what differs, why, and whether the .NET side should adopt it.
5
+ Updated whenever the port deliberately diverges or finds something worth back-porting.
6
+
7
+ ## Scrolling
8
+
9
+ ### Viewport offset snapped to device pixels while moving
10
+ - **React**: `SkiaScroll.ArrangeContent` rounds `offset × scale` every frame (drag, fling, bounce, wheel).
11
+ - **.NET**: `SkiaScroll.PositionViewport` rounds `offsetPixels` only when `!IsUserPanning && !IsScrolling`
12
+ (and once after init). While moving, content sits at fractional pixels; cached cells look stable only because
13
+ `CachedObject.Draw` blits the bitmap with nearest sampling, which snaps as a side effect. Uncached text shimmers
14
+ while scrolling (each glyph re-rasterized at a new sub-pixel phase per frame).
15
+ - **Opinion — adopt in .NET: yes.** Two lines in `PositionViewport` (round unconditionally). Benefits: uncached
16
+ or `Operations`-cached content becomes as stable as `Image`-cached cells; the round happens where the code already
17
+ rounds at rest, so anchor/offset math downstream is unchanged. Cost: on low-DPR screens the slow fling tail steps
18
+ by whole device pixels — exactly what `ScrollFlingAnimator`'s pixel-aware finish (`FinishStepPixels`, gated by
19
+ `PixelAwareFlingFinishBelowScale`) already smooths; on high-DPR the quantum is sub-visual. Verify with the fling
20
+ harness (paint cadence + applied px offsets) before shipping; watch `OffsetVisibleAnchorY` paths, which pass
21
+ fractional points and must not fight the round.
22
+
23
+ ### Mouse-wheel notches accumulate onto the running target
24
+ - **React**: `ApplyWheelScroll` starts from the running animator's `Parameters.Destination` when one is active, so N
25
+ notches inside one frame travel N × `WheelLineSize`.
26
+ - **.NET**: `ApplyWheelScroll` starts from `ViewportOffsetY`, which has barely moved when notches arrive faster than
27
+ frames; each new `ScrollTo` stops the previous one — a fast spin collapses to roughly one step.
28
+ - **Opinion — adopt in .NET: yes**, same shape: `var from = _animatorFlingY.IsRunning ? _animatorFlingY.Parameters.Destination : ViewportOffsetY`.
29
+
30
+ ## Caching
31
+
32
+ ### Text properties invalidate like bindable properties
33
+ - **Both**: `SkiaLabel.Text/FontSize/TextColor/FontFamily` are accessors that call `Update()` (C#: BindableProperty
34
+ changed callbacks). Plain public fields on other controls do NOT invalidate when assigned directly — React props go
35
+ through `applyProps` which calls `Update()`, but engine-level code must call `Update()` itself after mutating a field.
36
+ Converting the remaining hot properties to accessors is pending.
37
+
38
+ ## Text
39
+
40
+ ### Font weights are registered per alias
41
+ - **Both**: `ConfigureFonts(f => f.AddFont(source, alias, weight))`; `FontWeight`/`FontAttributes=Bold` resolve to the nearest
42
+ registered weight of the alias (400 = default). React adds: italic without an italic face = synthetic skew (-0.25), the C#
43
+ side has no synthetic italic. Defaults adopted from C#: `FontSize` 12, `TextColor` GreenYellow (unstyled text stays visible).
44
+ - **`FontFamilyFallback` chain (React extension)**: C# takes ONE fallback alias; React accepts a comma-separated chain
45
+ (`"FontSymbols,FontSymbols2"`) tried in order per codepoint, and spaces are never moved to a fallback run (keeps word gaps
46
+ at the main font's width). Opinion: worth back-porting to .NET — a single fallback cannot cover arrows (Math) and
47
+ ♥/★ (Symbols 2) at once, which is exactly the split `AddSymbols()` ships.
48
+
49
+ ### Markdown parser
50
+ - C# `SkiaRichLabel` parses with CommonMark.NET; React ships a small hand-written parser (headings, lists, fenced
51
+ code, inline emphasis/code/links, escapes). Same span output rules (`SpanWithAttributes`), same style properties.
52
+ Code blocks: C# paints `ParagraphColor` across the full line width, React paints the span background only.
53
+ Not worth a dependency for the demo; swap in a CommonMark library if edge cases matter.
54
+
55
+ ### Span decorations use estimated metrics
56
+ - C# reads `UnderlinePosition` / `StrikeoutPosition` / `XHeight` from the SKFont metrics and falls back to
57
+ 1 px / half x-height when a face lacks them. CanvasKit exposes none of the three, so React always uses the
58
+ C# fallbacks: underline at `baseline + 1 scaled px`, strikeout at `baseline - 0.26 * fontSize` (x-height ≈ 0.52 em).
59
+ Visually identical for OpenSans; faces with unusual x-height may sit the strike a pixel off.
60
+ - Spaces: a span fragment starting/ending with a space contributes a break opportunity but the space itself is not
61
+ painted with the span's `BackgroundColor` (C# paints it). Cosmetic; not worth changing on either side.
62
+
63
+ ## Layout
64
+
65
+ ### Grid attached properties are plain child props
66
+ - C#: `draw:SkiaLayout.Column="1"` attached bindable properties. React: `Column={1}` / `Row` / `ColumnSpan` / `RowSpan`
67
+ props on any control (fields on `SkiaControl`, read by the grid only). Same defaults (0 / 0 / 1 / 1), no behaviour change.
68
+
69
+ ### `MaximumWidthRequest` / `MaximumHeightRequest`
70
+ - Same as C# (1.10.5.18+): caps the measured size AND the arranged Fill box; alignment uses the parent's full box. Used by
71
+ the demo for responsive pages (`SkiaStack MaximumWidthRequest={720} HorizontalOptions="Center"`) — fluid below the cap,
72
+ fixed above it, no media queries.
73
+
74
+ ## Lists
75
+
76
+ ### ItemsSource changes are diffed, not observed
77
+ - **C#**: `ObservableCollection` events say exactly what changed.
78
+ - **React**: state is immutable arrays; the layout compares old/new (first/middle/last element identity) to recognise
79
+ append and prepend and keeps its structure; anything else rebuilds. Same user-visible result for the paging and
80
+ chat-history cases; an in-place removal costs a rebuild here. Opinion: nothing to back-port, .NET has the events.
81
+
82
+ ### MeasureVisible measures visible cells on demand, not only in the background
83
+ - **C#**: initial measured batch + background batches; a cell entering the viewport before its batch arrives uses
84
+ the estimate until measured.
85
+ - **React**: a cell entering the viewport is measured right there (it is being bound anyway) and laid out with its
86
+ real height, the estimate is used only for the anchor offset of the first visible item; the idle pass then
87
+ extends the exact prefix. Result: no visible resize of on-screen cells, only the far-away offsets refine.
88
+ - Opinion: back-portable and cheap on .NET too (measure at bind time in `DrawStack` when the height is unknown).
89
+
90
+ ### Recycled cells contract
91
+ - Same in both: the templated `SkiaLayout` (ItemsSource + ItemTemplate) is the `SkiaScroll`'s ONLY content; anything
92
+ above the list goes above the scroll or into the scroll `Header` (not ported yet). Nesting the templated layout inside a
93
+ static stack makes it a BindableLayout, not a CollectionView, and `ScrollToIndex` requires Content to be the layout.
94
+ - **React-only rule**: `ItemTemplate` must be a stable function reference (module-level or `useCallback`). A new arrow on
95
+ every render is a new template → the pool is rebuilt each render (C# XAML sets `DataTemplate` once, so it never hits this).
96
+
97
+ ## Colors
98
+
99
+ ### Hex alpha position
100
+ - **Both**: 8-digit hex is `#AARRGGBB` (MAUI `Color.FromArgb`), 4-digit is `#ARGB`. All color strings go through
101
+ `Super.ParseColor`; CanvasKit's own `parseColorString` (CSS `#RRGGBBAA`) is used only for `rgb()/rgba()` strings.
102
+ Web developers used to CSS must be told — `"#22FFFFFF"` is 13% white here, not opaque cyan.
103
+
104
+ ## Transforms
105
+
106
+ ### Transform / Opacity changes stale ancestor caches, not the control's own
107
+ - **C#**: transform properties call `RedrawCanvas`; whether a cached parent re-records depends on the invalidation path.
108
+ - **React**: `RepaintComposition()` marks every ancestor cache dirty (their pictures contain this control's composited
109
+ output) and keeps the control's own cache (content unchanged); the reconciler routes transform/opacity prop changes
110
+ there instead of `Update()`, so animating a child never remeasures. Found the hard way: the animated logo lived
111
+ inside an `Operations`-cached `SkiaShape` and did not move until the parent was re-recorded.
112
+ - Opinion: matches what DrawnUi does at the top cached container; nothing to back-port beyond making sure a child's
113
+ transform change invalidates the parent's cache on every path.
114
+
115
+ ### Cancellation and skew
116
+ - C# `*ToAsync` take a `CancellationTokenSource`; React takes an `AbortSignal` and rejects with `AbortError`.
117
+ - C# ignores negative `SkewX/SkewY` (`> 0` check); React applies both signs. Opinion: C# check looks accidental.
118
+
119
+ ## Effects
120
+
121
+ ### ClipEffects is honoured
122
+ - C# `WillClipEffects` exists but the render path always expands the clip by the effects margin. React: with
123
+ `IsClippedToBounds`, `ClipEffects=true` (default) clips to the exact box, `false` expands by the aggregated
124
+ effects margin. Opinion: wire `WillClipEffects` into `DrawWithClipAndTransforms` on .NET, it is a one-line gate.
125
+
126
+ ### Color matrix units
127
+ - SkiaSharp `CreateColorMatrix` translations are 0..255, CanvasKit `MakeMatrix` 0..1; React divides the C# constants
128
+ so `Darken=5` looks the same on both. Gamma has no table filter in CanvasKit — linear approximation.
129
+
130
+ ## Accessibility
131
+
132
+ ### Overlay does not capture pointer events
133
+ - **C# (Blazor)**: the ARIA overlay elements sit above the canvas and receive clicks, so a control with
134
+ accessibility metadata stops getting `Pointer` (hover) gestures — documented limitation.
135
+ - **React**: overlay elements have `pointer-events: none`; real pointers always reach the canvas, keyboard and
136
+ screen-reader activation arrive as DOM `click`/`keydown` on the focused element and are routed as a `Tapped`.
137
+ ATs that simulate a physical click at coordinates hit the canvas directly and work as well.
138
+ - Opinion: back-port to Blazor — one CSS rule on `.xaml-a11y-element` (`pointer-events: none`) plus keeping
139
+ `tabindex`/`@onclick`/`@onkeydown` as they are; removes the hover limitation with no other change.
140
+
141
+ ### Default roles per class
142
+ - **C#**: opt-in per control (`AccessibilityRole` null by default), `SkiaLabel` only syncs `AccessibilityLabel`.
143
+ - **React**: same opt-in, plus `SkiaLabel.DefaultAccessibilityRole` / `SkiaButton.DefaultAccessibilityRole` statics
144
+ (unset by default) so an app can make every label/button accessible in two lines; `AccessibilityLabel` falls
145
+ back to the control text, `AccessibilityCanInteract` to "has a Tapped handler".
146
+ - Opinion: worth back-porting as static defaults on `SkiaLabel`/`SkiaButton` — keeps the opt-in contract and gives
147
+ "readable labels" without touching every control.
148
+
149
+ ### Nodes pruned instead of unregistered
150
+ - **C#**: controls unregister on detach/dispose/visibility change.
151
+ - **React**: the snapshot rebuild drops nodes without a `Superview`, invisible, or farther than one canvas size
152
+ outside it; rects are re-read from `DrawingRect`, so they follow scrolling. Behavioural difference: a removed
153
+ node can linger up to `MinUpdateIntervalMs` in the DOM. Pooled recycled cells get `Parent = undefined` on release.
154
+
155
+ ### Focus scrolls the drawn content into view
156
+ - **React-only**: when keyboard focus lands on an overlay node that is outside its `SkiaScroll` viewport,
157
+ `SkiaScroll.EnsureVisible(control)` animates every scroll ancestor so the control is visible (browser
158
+ behaviour for DOM pages). Opinion: back-port — Blazor users tabbing through a drawn list get the same
159
+ experience as a native page; needs a `ScrollToView`-like helper plus the overlay `focus` callback.
160
+
161
+ ## Rendering
162
+
163
+ ### Redraw synchronously inside the resize callback
164
+ - **React**: the `ResizeObserver` callback recreates the surface and draws immediately (RO runs after layout,
165
+ before paint), so a live window drag never presents a blank frame; the GL context/GrContext live for the Canvas
166
+ lifetime, only the surface is recreated.
167
+ - **.NET**: platform views handle resize natively (SkiaSharp views recreate surfaces on size change and request a
168
+ paint); Windows `DrawnSwapChainPanel` already owns surface recreation.
169
+ - **Opinion**: no action; noted so the web behaviour is understood as intentional.
170
+
171
+ ### SVG rendering
172
+ - **React**: no SVG module in CanvasKit's npm build → browser decodes, raster per displayed size, `TintColor` via
173
+ `SrcIn`. Effects that operate on the SVG picture (`FillGradient`, FontAwesome duotone) are not reproducible this way.
174
+ - **.NET**: `Svg.Skia` picture, vector at any scale.
175
+ - **Opinion**: web-only constraint; nothing to back-port.
176
+
177
+ ## SkiaCarousel
178
+
179
+ ### Wrong-direction check follows the carousel axis
180
+ - **React**: the first pan compares the movement along the carousel axis with the movement across it (`IsVertical` aware), using the total movement since Down.
181
+ - **.NET**: `movex < RenderingScale * 2 || movey > movex` on the per-event delta, regardless of `IsVertical` — a vertical carousel rejects its own vertical swipes.
182
+ - **Opinion**: back-port; pick the axis from `IsVertical`.
183
+
184
+ ### LinearSpeedMs ratio in points
185
+ - **React**: `ratio = |end - start| / CellSize.Units`, so `LinearSpeedMs` is the time of exactly one slide.
186
+ - **.NET**: divides the unit displacement by `CellSize.Pixels.Width`, so one slide takes `LinearSpeedMs / RenderingScale` (350 ms becomes 175 ms on a 2x screen), which contradicts the doc comment.
187
+ - **Opinion**: back-port; use `CellSize.Units`.
188
+
189
+ ### Programmatic SelectedIndex interrupts a running snap
190
+ - **React**: setting `SelectedIndex` (or `ScrollTo`) while a snap animates calls `InterruptSnapping` first, like `GoNext`/`GoPrev`, so the new target is honoured.
191
+ - **.NET**: only `GoNext`/`GoPrev` interrupt; a plain `SelectedIndex` set during `_isSnapping` is ignored by `OnSelectedIndexChanged` and the carousel ends on the old target while the property says otherwise.
192
+ - **Opinion**: back-port; call `InterruptSnapping()` from the `SelectedIndex` property changed handler.
193
+
194
+ ## Animated frames
195
+
196
+ ### Animator initialized on the first layout only
197
+ - **React**: `AnimatedFramesRenderer.OnLayoutChanged` runs on every frame (Arrange is per frame here), so the animator is created / auto-started only on the first layout; later `SetAnimation` calls initialize explicitly.
198
+ - **.NET**: `OnLayoutChanged` fires only on a real layout change, so `InitializeAnimator` + `Start` on every call is harmless.
199
+ - **Opinion**: no action for .NET; note for anyone porting the control to a per-frame-arrange engine.
200
+
201
+ ### Overlay animators stale the ancestors' caches while running
202
+ - **React**: `RenderingAnimator.TickFrame` marks every ancestor cache dirty on each tick, so a ripple on a button inside a `UseCache=Image` card is drawn (the card re-records for the ~500 ms of the effect).
203
+ - **.NET**: the effect invalidates the parent through the regular `Update`/`Repaint` path.
204
+ - **Opinion**: same outcome; noted because the React cache model had to add it explicitly.
205
+
206
+ ## SkiaBackdrop
207
+
208
+ ### Ancestor caches staled after every backdrop paint
209
+ - **React**: a backdrop recorded into a cached parent (the demo card is a SkiaShape with the default Operations cache) kept a snapshot taken before the baboon image had loaded: the image's invalidation climbs its own branch and never reaches the sibling shape. After each paint the backdrop marks its ancestors' caches stale (microtask, no frame requested), so the next frame for any reason re-records it.
210
+ - **.NET**: the same tree in `MainPageBackdrop` works because the sandbox content loads before the first record or the page redraws for other reasons; a late-loading sibling would leave the same stale snapshot.
211
+ - **Opinion**: consider the same "stale ancestors after paint" in `SkiaBackdrop.Paint`; it costs nothing while the canvas is idle.
212
+
213
+ ## Visual effects
214
+
215
+ ### Post renderers wait for the shader to compile
216
+ - **React**: `SkiaControl.EffectPostRenderers` is filtered by `NeedApply` at render time; `SkiaShaderEffect.NeedApply` fetches the source (async, once) and compiles (sync) when needed, so a control with an effect whose `.sksl` is still loading is blitted plainly and takes the shader on the next frame.
217
+ - **.NET**: `DrawRenderObject` skips the cache blit whenever `EffectPostRenderers` is non-empty and the effect logs "failed to create shader" until compiled, so the control is invisible while the shader is missing.
218
+ - **Opinion**: filtering by `NeedApply` there too avoids the blank control on a slow resource / compile error.
219
+
220
+ ### Texture texel origin
221
+ - **React**: `CachedTexture.Origin` records where texel (0,0) of the texture sits in canvas space; a cache image gets no local matrix (shaders sample `fragCoord - iOffset` in texel space, as in C#), a whole-surface snapshot gets a translation so texel (0,0) is the destination's top-left (a bounded `makeImageSnapshot` of a GPU surface is not origin-safe in CanvasKit).
222
+ - **.NET**: `CreateSnapshot` maps the destination through `TotalMatrix` and snapshots that sub-rect.
223
+ - **Opinion**: same outcome.
224
+
225
+ ### Effects' `Update()` re-records the parent
226
+ - **React**: `SkiaEffect.Update` invalidates the parent's cache and stales the ancestors (`RepaintComposition`), the C# `Parent.Update()` semantics; the parent's own re-record is what lets `SkiaShaderCarousel` realize new slides while the transition progresses.
227
+
228
+ ## SkiaShaderCarousel
229
+
230
+ ### Cached as Image by default
231
+ - **React**: the constructor sets `UseCache="Image"` so the overlapping slides (all arranged at offset 0) are recorded into a cache that is never blitted (the post renderer replaces the blit) instead of being painted on screen under the effect.
232
+ - **.NET**: the user sets the cache type; with `UseCache=None` `DrawRenderObject` is never used and the transition effect never runs.
233
+ - **Opinion**: forcing an Image cache in the C# constructor would make the control work out of the box.
234
+
235
+ ### `OnChildrenInitialized` before the first position
236
+ - **React**: `InitializeChildren` raises `OnChildrenInitialized` before `ApplyIndex(true)`; the shader carousel resets its from/to state there, and the first `OnScrollProgressChanged` (from the instant `ApplyPosition`) then sets them up. The other order left the first transition pair at -1 until the first swipe.
237
+
238
+ ## SkiaEditor
239
+
240
+ ### Hidden DOM textarea for IME / soft keyboards
241
+ - **React**: `TextInputProxy` mirrors the focused editor into a hidden textarea and replays its input events through the stub methods (diff of the value). DrawnUi.Blazor has no DOM input at all (physical keyboard only); this is a deliberate addition so mobile browsers can type.
242
+ - **.NET**: `SkiaEditor.Blazor.cs` subscribes to `KeyboardManager` only.
243
+ - **Opinion**: the same proxy would give the Blazor / Wasm heads mobile input; the diff approach avoids per-inputType handling and keeps IME composition intact.
244
+
245
+ ## SkiaImageManager
246
+
247
+ ### Queue in the browser
248
+ - **React**: `LoadImageManagedAsync` orders by priority and caps concurrent fetches at 5 like the C# semaphore; `SkiaImage.Source` goes through it (`LoadPriority`). Browsers already limit connections per host, so the cap mostly keeps decode work paced.
249
+
250
+ ## SkiaScroll
251
+
252
+ ### Scroll inside a cached parent stales it
253
+ - **React**: every offset change calls `RepaintComposition` (ancestor caches staled, own cache kept) instead of a plain `Repaint`; before, a `SkiaScroll` inside an Operations-cached card moved its arranged rects but never repainted (the card's picture was replayed).
254
+ - **.NET**: `Update()` invalidates up the tree.
255
+
256
+ ### Wheel goes to the innermost scroll first
257
+ - **React**: a nested scroll under the pointer takes the wheel; when it sits at its edge in that direction it declines and the outer scroll moves. C# has no wheel routing rule for nested scrolls.
258
+
259
+ ### Refresh indicator position
260
+ - **React**: `RefreshIndicator.SetDragRatio` slides the view in linearly with the overscroll and parks it at `RefreshShowDistance` (centered in the gap when the gap is taller than the view); the C# curve (`getPosition(k)`) depends on the sign convention of `InternalViewportOffset` and produced off-screen positions with this port's positive top overscroll.
261
+
262
+ ### Snap uses the last paint's geometry
263
+ - **React**: `Snap` computes the target offset from the child's position relative to the content start as arranged at the last paint, so a fling that stopped a tick after that paint still lands the child exactly; `ScrollTo` applies its exact destination when the deceleration curve finishes (`LandScrollTo`).
264
+
265
+ ## Layouts
266
+
267
+ ### Templated Row / Wrap / Grid are not virtualized
268
+ - **React**: every item is realized through the `ViewsAdapter` (the C# non-list layouts also measure and draw all cells); only the templated single-column Column is the virtualized list.
269
+
270
+ ### `OnChildrenInitialized` order in SkiaCarousel
271
+ - see SkiaShaderCarousel above.
272
+
273
+ ## Caching
274
+
275
+ ### ImageComposite dirty tracking
276
+ - **React**: `RepaintComposition` is the only dirty source (transform / own cache invalidation of a child); a remeasure anywhere below marks the composite for a full record. C# tracks `DirtyChildrenTracker` from `InvalidateByChild` too; both erase the union of old + new transformed bounds and pull intersecting siblings in.
277
+ - **React-only gotcha**: React props that are new objects on every render (`Margin={new Thickness(...)}`) remeasure the child each render and force full records; memoize them.
278
+
279
+ ### Image caches on whole pixels
280
+ - **React**: `Image` / `ImageComposite` / `ImageDoubleBuffered` caches record the expanded rect snapped outward to integer device pixels; the blit is 1:1 and a shader effect sampling `fragCoord - iOffset` hits texel centers (a fractional `DrawingRect.Left` made `blit.sksl` bilinear-blur the image by a sub-pixel amount). Picture caches keep the exact rect.
281
+ - **.NET**: `CachedObject.Bounds` / recording areas are already integer pixels.
282
+
283
+ ## Accessibility
284
+
285
+ ### Selectable text is opt-in
286
+ - **React**: `AccessibilityTextSelectable` (default false) puts a label's lines into the overlay as real text with pointer events; the text then owns the pointer (selection), so it is never enabled implicitly — custom controls would lose taps and pans under their labels. ARIA roles / labels stay unaffected.
287
+ - **.NET**: no selectable labels (only `SkiaEditor` selects).
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # DrawnUi.React
2
+
3
+ Prototype of the [DrawnUi](https://drawnui.net) engine rewritten in TypeScript on top of
4
+ [CanvasKit](https://skia.org/docs/user/modules/canvaskit/) (Skia for the browser), composed with React
5
+ through a custom `react-reconciler` renderer.
6
+
7
+ Goal: the same API surface and semantics as DrawnUi (.NET) — same control names, same PascalCase
8
+ property names, same measure/arrange/paint contract — so knowledge and docs transfer 1:1.
9
+
10
+ ```tsx
11
+ await Super.UseDrawnUi()
12
+ .ConfigureFonts((fonts) => fonts.AddFont("fonts/OpenSans-Regular.ttf", "FontText"))
13
+ .BuildAsync();
14
+
15
+ <Canvas BackgroundColor={Colors.DarkSlateBlue} RenderingMode="Accelerated" Gestures="Enabled">
16
+ <SkiaStack Spacing={8} Padding={new Thickness(16)} VerticalOptions="Center">
17
+ <SkiaLabel Text="Hello World" FontSize={32} TextColor={Colors.White} HorizontalOptions="Center" />
18
+ <SkiaButton Text="Tap me" ApplyEffect="Ripple" HorizontalOptions="Center" Tapped={() => setCount((c) => c + 1)} />
19
+ </SkiaStack>
20
+ </Canvas>
21
+ ```
22
+
23
+ ## Layout
24
+
25
+ - `src/` — the library, imported by samples as `drawnui-react` (React tags + all engine types) or `drawnui-react/core` (engine only).
26
+ - `src/core` — `Super` (startup, CanvasKit, fonts), `SkiaControl` (measure/arrange/render/gestures), `Canvas` (host, surface, frame loop, input), animators, value types.
27
+ - `src/controls` — `SkiaLayout` (+ `SkiaStack`/`SkiaRow`/`SkiaLayer`), `SkiaLabel`, `SkiaHotspot`, `SkiaButton`.
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.
32
+
33
+ What is intentionally missing: see [SKIPPED.md](SKIPPED.md).
34
+
35
+ ## Where React ends and DrawnUi begins
36
+
37
+ React never touches the canvas. The engine (`src/core`, `src/controls`) is plain TypeScript: `SkiaControl` trees
38
+ that measure, arrange and paint themselves on a CanvasKit surface, exactly like the .NET `SkiaControl` trees — it can
39
+ be driven from any framework, or from no framework at all (`new SkiaLabel()`, `AddSubView`, `canvas.Content = ...`).
40
+
41
+ `react-reconciler` is React's own renderer-building package: the same core that powers `react-dom` and
42
+ `react-native`, minus the DOM. You hand it a "host config" — how to create an instance for a JSX tag, how to append
43
+ / remove / reorder children, how to apply changed props — and React does the rest: diffing, hooks, state, effects,
44
+ keys, Suspense. Our host config (`src/react/reconciler.ts`) maps every tag to an engine class (`<SkiaLabel>` →
45
+ `new SkiaLabel()`), `appendChild` to `AddSubView`, and a changed prop to a plain property assignment on the control
46
+ (`Text`, `FontSize`, `Tapped`…), after which the control invalidates itself the way it would from C#. So the JSX is
47
+ just a declarative way to build and mutate the same control tree; the render loop, caching, gestures, animators and
48
+ accessibility all live in the engine and would work identically under Vue, Svelte, Blazor-JS interop or a game loop.
49
+ That is also why the demo pages describe DrawnUi features, not React ones: the same pages are meant to be reused as
50
+ the showcase for other frameworks on this engine.
51
+
52
+ ## Accessibility
53
+
54
+ Same model as DrawnUi.Blazor: the `<canvas>` is `aria-hidden`, an invisible DOM overlay mirrors every
55
+ accessible drawn control (`role`, `aria-label`, `title` hint, `aria-pressed`, `aria-live`, `tabindex`), rebuilt
56
+ at most once per second from the arranged rects. Keyboard (Tab / Enter / Space) and screen-reader activation
57
+ are routed back into the gesture pipeline as a `Tapped` on the control.
58
+
59
+ Per control (C# names): `AccessibilityRole` (enables the node; use `Aria.*`), `AccessibilityLabel`
60
+ (defaults to the control's text), `AccessibilityHint`, `AccessibilityCanInteract` (defaults to "has a
61
+ `Tapped` handler"), `AccessibilityIsPressed`, `AccessibilityLive`. `Aria.RolePresentation` hides a control that
62
+ would otherwise get a default role.
63
+
64
+ App-wide opt-in (React extension): `SkiaLabel.DefaultAccessibilityRole = Aria.RoleText` and
65
+ `SkiaButton.DefaultAccessibilityRole = Aria.RoleButton` (import the classes from `drawnui-react/core`) make every
66
+ label readable and every button focusable without touching each control.
67
+
68
+ The overlay has `pointer-events: none`, so hover and all pointer gestures still reach the canvas — the
69
+ Blazor "accessible control loses hover" limitation does not apply.
70
+
71
+ ## Run
72
+
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
+ ```
79
+
80
+ ## Publishing
81
+
82
+ Every push to `master` runs `.github/workflows/deploy.yml`: build `samples/demo`, deploy `dist/demo`
83
+ to the Cloudflare Pages project `helloreact-drawnui` → **https://helloreact.drawnui.net**.
84
+
85
+ Repository secrets used by the workflow:
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`. |
91
+
92
+ Adding another published sample = one more `wrangler pages deploy dist/<name> --project-name <project>` step and a Pages project + custom domain for it.
package/SKIPPED.md ADDED
@@ -0,0 +1,107 @@
1
+ # Skipped vs DrawnUi (.NET)
2
+
3
+ Ledger of what the prototype deliberately does not port yet. Everything that IS ported keeps the
4
+ DrawnUi name and semantics; nothing here is a redesign, only an omission.
5
+
6
+ ## Engine
7
+
8
+ | Area | Status | Notes |
9
+ |---|---|---|
10
+ | `UseCache` | partial | `None`, `Operations` (SkPicture replay), `Image` (offscreen surface snapshot, GPU-backed on WebGL, nearest-sampled blit). `ImageDoubleBuffered` keeps `RenderObjectPrevious` and draws it when a new cache cannot be produced yet, `DrawPlaceholder(ctx)` hook when nothing exists (recording itself is synchronous — no background thread, same as DrawnUi.Blazor with `CanUseCacheDoubleBuffering = false`). `GPU`, `ImageComposite`, `ImageCompositeGPU` accepted but resolve to `Image` (no per-child composite, no dedicated GPU 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`, `RenderObjectPrevious` fallback, offscreen bake threads, `DrawPlaceholder`, cache validity by GRContext. |
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, velocity, `SoftLock`, `AddGestures` attached props, `OnGestures` delegate, transform-aware mapping (`HasTransform`, cache offsets `TranslateInputCoords`) | skipped | Enum members exist for parity, never produced. `ChildOffset` is always zero (no caches/transforms yet). |
14
+ | C# multi-subscriber `event`s | changed shape | One callback per event prop (`Tapped={fn}`), same names. `Command*` (ICommand) variants not ported. |
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
+ | `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. |
17
+ | `AnimatedFramesRenderer` / `SkiaLottie` / `SkiaGif` | ported | `AutoPlay`, `Repeat`, `SpeedRatio`, `DefaultFrame` (-1 = last), `Start(delay)`/`Stop`/`Seek`, `IsPlaying`, `Started`/`Finished`, `PlayWhenAvailable`, `GetFrameAt`. Lottie on CanvasKit Skottie (needs the `full` build, +0.9 MB raw wasm): `Source` (URL / app path, JSON cached per source, in-flight dedup), `ColorTint` / `Colors` (C# `ApplyTint` JSON rewrite), `IsOn` + `DefaultFrameWhenOn` + `ApplyIsOnWhenNotPlaying`, `StopAtCurrentFrame`, `ProcessJson`, `GoToStart`/`GoToEnd`, `SetAnimation`, `LoadSource`, `CreateAnimation`, `TotalFrames`; `UseCache=ImageDoubleBuffered` re-recorded per frame. GIF on CanvasKit AnimatedImage: every frame decoded once (`GifAnimation`: `SeekFrame`, `GetFrameNumber`, `DurationMs`, `TotalFrames`), `Aspect` (AspectFitFill), alignment, `Success`/`Error`. Not ported: `SkiaGif.Display` inner SkiaImage (frames are drawn directly), custom-control ctor `SkiaGif(display)`, `SkiaLottie` Skottie property/slot editing, sound, `ReloadSource` sync file paths, `Dispose` wiring on unmount (no engine hook from the reconciler yet). |
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
+ | 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
+ | `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`, 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
+ | `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
+ | `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
+ | `Padding` on base `SkiaControl` | skipped | Only `SkiaLayout.Padding` exists. |
25
+ | `SkiaRichLabel` (markdown) | ported | Own lightweight parser (C# uses CommonMark.NET): `#`–`###+` headings (+9/+4/+2 pt, bold, `HeadingTextColor`), paragraphs (soft breaks kept as "
26
+ "), bullet / numbered lists (`PrefixBullet`, `PrefixNumbered`), fenced code blocks (`CodeTextColor`, `CodeBlockBackgroundColor` as span background — C# paints it as `ParagraphColor` full-width), inline code, `**bold**`/`__bold__`, `*italic*`/`_italic_`, `~~strike~~` (`StrikeoutColor`), `[text](url)` (`LinkColor`, `UnderlineLink`, `UnderlineWidth`, `LinkTapped`, `OnLinkTapped`), backslash escapes, `MarkdownEnabled=false` = literal. Not ported, with the path to do it later (all inside `SkiaRichLabel.ts` / `SkiaLabel.ts`, no architecture change): **block quotes, nested lists, setext headings** = parser only (indent levels, `>` prefix, spans with prefix/left margin); **tables** = parser + render as a generated `SkiaGrid` of labels (a label cannot do columns; same limit as C#); **images `![alt](src)`** = a span kind that reserves a box and draws a `SkiaImage`, the run layout already carries per-run widths (C# has no images either); **inline HTML** = out, C# neither; **`CommandLinkTapped`** = MAUI ICommand, `LinkTapped` is the React shape, nothing to port; **per-run typeface detection (`BuildSpanData`)** = run segmentation already exists (`FontFamilyFallback` chain per codepoint), only the automatic system-font `MatchCharacter` is impossible in CanvasKit — fallbacks stay explicit; **code-block full-width background** = add `ParagraphColor` to `TextSpan` and paint a line-wide rect in `SkiaLabel.Paint`; **parser edge cases** = swap `RenderDocument` for `markdown-it`/`micromark` on demand, spans unchanged. |
27
+ | `MinimumWidthRequest`/`MaximumWidthRequest` (+Height) | ported | Same semantics as C#: `-1` = unset; `WidthRequest` wins over `MaximumWidthRequest`; the maximum caps both the measured size and the arranged Fill box, alignment still uses the parent's full box (a centered `Fill` child with `MaximumWidthRequest` stays centered). |
28
+ | `LockRatio` | ported | Same as C#: a single set `WidthRequest`/`HeightRequest` drives both sides (`CalculateSizeRequest`), otherwise the constraints are locked with `SmartMax`/`SmartMin` × \|ratio\| (sign decides larger/smaller side, infinite side loses). |
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
+ | `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
+ | `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` | skipped | |
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
+ | 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
+ | `Shimmer` touch animation, `ShimmerAnimator`, `ClippedEffectsWith`, `TransformView`, `DelayCallbackMs`, `removePrevious`, `Pause`/`Resume`, `UseInterpolator`, spring/deceleration timing, `AnimateExtensions` (`FadeToAsync`, `TranslateToAsync`, ...) | skipped | |
37
+ | `VisualEffects` + `SkiaEffect` / `IPostRendererEffect` / `ISkiaGestureProcessor` | partial | `VisualEffects` array on every control (assign a new array; effects attach / detach, dispose with the control, `DisableEffects`, `GetEffectMargin` aggregated into the effects margin); post renderers run after the direct paint (C# `DrawDirectInternal`) or instead of the Image cache blit (C# `DrawRenderObject`, the effect samples `CachedImage`); effects with `ProcessGestures` see the parent's gestures first (C# `EffectsGestureProcessors`). Not ported: `IRenderEffect` chain (`ChainEffectResult`, `Draw(ctx, drawControl)`), `IColorEffect` / `IImageEffect` on VisualEffects (`BlurEffect`, `DropShadowEffect`, `OuterGlowEffect`, `ColorPresetEffect`, `AdjustRGBEffect`, `ChainAdjust*`, `Chain*`), `IStateEffect.UpdateState`, `Tag` lookup. |
38
+ | `SkiaShaderEffect` / `ShaderDoubleTexturesEffect` / `ShaderTransitionEffect` / `AnimatedShaderEffect` | ported | CanvasKit `RuntimeEffect` (SkSL): `ShaderSource` (fetched once per url), `ShaderCode`, `ShaderTemplate` (`//script-goes-here`), compiled once per source text, Shadertoy uniforms `iResolution` / `iImageResolution` / `iTime` / `iOffset` / `iMouse` (`TimeSeconds`, `MouseCurrent`, `MouseInitial`), `Uniforms` + `SetUniform` (arrays sized by the next uniform slot, undeclared names skipped like C#), `UseBackground` Always / Once (`AquiredBackground`, `ReleaseFrozenSnapshot`) / Never, `AutoCreateInputTexture`, `UseContext`, `BlendMode`, `FilterMode` / `MipmapMode` / `TileMode`, `OnCompilationError` (logged when unset), `LoadedCode`, `IsCompiled`; texture children by declaration order (`uniform shader` names parsed, missing ones sample transparent); double textures: `ControlFrom` / `ControlTo` (their Image caches), `PrimarySource` / `SecondarySource` (resized to the parent box); transition: `Progress`, `ratio`, embedded gl-transitions `DefaultTemplate`; animated: `Play` / `Stop` / `Completed`, `DurationMs`, `Center` -> `iCenter`. `iOrigin` is not written (C# neither). Not ported: `SkiaShader` engine class as a standalone (`DrawRect` / `DrawImage` helpers), `CompareTo` / equality. |
39
+ | Multithreading / offscreen rendering | skipped | Browser main thread only. |
40
+ | Hot reload hook | skipped | Vite HMR reloads the page. |
41
+ | Second measure pass / fill-in-auto re-measure rules | skipped | Column/Row give children an infinite main axis; cross axis = available. |
42
+ | `ScaledSize` as class with `IsEmpty` etc. | partial | Only `Pixels`/`Units`. |
43
+ | `Colors` | partial | Small subset; any CSS hex/rgb() string works. Named CSS colors ("red") do not. |
44
+
45
+ ## Canvas / host
46
+
47
+ | Area | Status | Notes |
48
+ |---|---|---|
49
+ | `RenderingMode` | partial | `Accelerated` (WebGL) with automatic fallback to software; read once at first surface creation. |
50
+ | `Gestures` param | ported | Enabled applies `touch-action:none; user-select:none`; Lock also blocks `touchmove` default. |
51
+ | Keyboard, focus (`FocusedChild`) | skipped | |
52
+ | FPS / rendering stats (`Super.EnableRenderingStats`, `SkiaLabelFps`) | skipped | |
53
+ | Insets / safe areas | skipped | N/A in browser for now. |
54
+
55
+ ## Controls
56
+
57
+ | Control | Status | Notes |
58
+ |---|---|---|
59
+ | `SkiaLayout` Absolute / Column / Row (+ `SkiaStack`, `SkiaRow`, `SkiaLayer`) | ported | `Spacing`, `Padding`, `Children`/`Views`, `AddSubView`/`RemoveSubView`/`InsertSubView`. |
60
+ | `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
+ | `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/moves in place, `Replace`. |
63
+ | Templated Row/Grid/Wrap, `Split`, `ItemTemplateSelector`, `ItemTemplatePoolSize`/`ReserveTemplates`, `UsePreparedViews`, `SkiaCachedStack`, measure memo | skipped | |
64
+ | `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
+ | `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). |
66
+ | `SkiaLayout` Wrap (static children) + `SkiaWrap` alias | ported | Left-to-right flow, `Spacing` between items and rows, row height = tallest item. Not ported: `Split`, main-axis `Fill` children sharing a row (1.9.7.4 flex-fill), `UseDynamicColumns`, RTL. |
67
+ | `SkiaLayout` Grid (static children) + `SkiaGrid` alias | ported | Port of `SkiaGridStructure` (MAUI grid manager): `ColumnDefinitions`/`RowDefinitions` as `"*, 2*, Auto, 100"` or arrays, `DefaultColumnDefinition`/`DefaultRowDefinition` (Auto, like C#), `ColumnSpacing`/`RowSpacing`, child `Column`/`Row`/`ColumnSpan`/`RowSpan` (plain props instead of attached properties), implicit tracks, unknown/known measure passes, span resolution, star compression, Fill-child minimums, last-track stretch when the grid fills, remeasure at final cells. Not ported: `Split`/`Invert` (templated grids), `UseDynamicColumns`, `SkiaDecoratedGrid`, `GetOrderedSubviews` ZIndex ordering. |
68
+ | `SkiaLabel` | partial | Word wrap (+ character break for long words), `MaxLines` with tail ellipsis, `LineBreakMode` (NoWrap / wraps / Tail; Head/Middle behave as Tail), `HorizontalTextAlignment` Start/Center/End (Fill* accepted, align Start), `VerticalTextAlignment`, `LineSpacing`, `LineHeight`, `FontWeight` + `FontAttributes` via faces registered per weight (`AddFont(src, alias, weight)`, nearest weight wins, synthetic bold (`setEmbolden`) when the nearest face is lighter than 600 — C# `Font.Embolden`; italic = synthetic skew when no italic face; empty `FontFamily` = the first registered family, weights included), `TextTransform`, `Padding`, `LinesCount`; defaults as C# (`FontSize` 12, `TextColor` GreenYellow). `FontFamilyFallback` ported and extended: comma-separated chain (`"FontSymbols,FontSymbols2"`), per-codepoint run segmentation, spaces always stay on the main font; `fonts.AddSymbols()` (`FontSymbols` = Noto Sans Math subset, `FontSymbols2` = Noto Sans Symbols 2 subset) and `fonts.AddEmojis()` (`FontEmoji` = Noto Color Emoji faces+hands subset) ship the same subsets as DrawnUi.Blazor. `Spans` ported as `<TextSpan>` children: `Text`, `TextColor`/`FontSize`/`FontFamily` (unset = inherit from the label, like C# HasSetColor/HasSetSize/HasSetFont), `FontWeight`, `IsBold`, `IsItalic`, `Underline`/`UnderlineWidth`, `Strikeout`/`StrikeoutWidth`/`StrikeoutColor` (Red default), `BackgroundColor`, `Tapped` + `ForceCaptureInput` (hit-tested through per-fragment `Rects`, ripple on the label, `OnSpanTapped` overridable), mixed sizes on one line share a baseline. TextSpan not ported: `ParagraphColor`, `CommandTapped`, per-span `LineSpacing`/`LineHeight`, `AutoFindFont`, `DrawingOffset`, `Shape`/glyph shaping. Not ported: `AutoFont`, `CharacterSpacing`, `AutoSize`/`AutoSizeText`, `DropShadow*`, text `StrokeColor`/`StrokeWidth`/`StrokeGradient`, `GradientByLines`, `MonoForDigits`, `ParagraphSpacing`, `KeepSpacesOnLineBreaks`, `Format`, RTL/bidi, hyphenation, `SkiaRichLabel`/markdown. |
69
+ | `SkiaHotspot` | ported | Fill/Fill, `Tapped`, `Down`, `Up`, `LockPanning`, `TouchDown`; consumes only Tapped like the C# one. No `AnimationTapped`/ripple/shimmer. |
70
+ | `SkiaButton` | partial | Default look only: rounded frame radius 8 (hardcoded like the C# default content), centered label, `Text`/`TextColor`/`FontSize`/`FontFamily`/`BackgroundColor`/`IsPressed`/`IsDisabled`/`LockPanning`, `Tapped`/`Down`/`Up`. `IsPressed` is tracked but has no visual; press feedback = `ApplyEffect="Ripple"`. No `ButtonStyle` platform looks, icons, `TextCase`, elevation, shimmer, `BtnText`/`BtnShape` templating. |
71
+ | `SkiaImage` | partial | Effects ported: `AddEffect` (BlackAndWhite/Grayscale, Pastel, Sepia, InvertColors, Tint + `ColorTint`/`EffectBlendMode`, Darken, Lighten, Contrast, Saturation, Brightness, TSL, Gamma ≈ linear fit — CanvasKit has no table color filter), `Blur` (image filter, mirror tile), `ZoomX/ZoomY`, `HorizontalOffset/VerticalOffset`; C# color matrices use 0..255 translations, CanvasKit 0..1 — converted. `HSL` (hue from `Gamma`, `Saturation`, `Brightness`, tint blend), `Custom` = your own `PaintColorFilter` (C# public fields `PaintColorFilter` / `PaintImageFilter`, the latter replaces `Blur`), `ImageBitmap` (decoded image shared, not owned), `LoadPriority` of the queued `Source` load. Not ported: `UseGradient`/`StartColor`/`EndColor`, sprites, `InflateAmount`, `EraseChangedContent`, `RescaleSource`/`RescalingQuality`. `Source` (URL), `Aspect` (all `TransformAspect` values; `Tile` is a no-op like C#, tiling is `SkiaImageTiles`, same `RescaleAspect` math), `HorizontalAlignment`/`VerticalAlignment`, `Success`/`Error`, `IsLoading`, `LoadedSource`, `DisplayRect`, `AspectScale`; overflow clipped to the box. Not ported: `LoadSourceOnFirstDraw`, `PreviewBase64`, `ImageBitmap`/`LoadedImageSource`, `RescaleSource`/`CacheRescaledSource`/`RescalingQuality`, all adjustments (`Brightness`…`Blur`, `ColorTint`, gradient, `Zoom*`, offsets), sprites, `DrawWhenEmpty`, `EraseChangedContent`, `UseAssembly`. Measure: bounded box taken as is, unbounded axis from source aspect; no `NeedAutoWidth/Height` from Start alignment. |
72
+ | `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
+ | `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
+ | `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. |
76
+ | `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
+ | `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
+ | `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. |
79
+ | `UseCache="ImageComposite"` / `ImageCompositeGPU`, `Left` / `Top` | ported | ImageComposite keeps its offscreen surface across records; a child that calls `RepaintComposition` (transform / own repaint) is tracked as dirty by its composite ancestors (`TrackChildAsDirty`, `DirtyChildrenInternal`), the next record erases the dirty children's old + new transformed bounds plus the siblings they overlap and paints only those (`IsRenderingWithComposition`, layouts skip the rest); own content, a remeasure, a size / scale change record fully (`LastCompositeRecord` diagnostics). `Left` / `Top` on a cached control blit the cache at the offset without a matrix or save/restore (the translation is still set as `RenderTransformMatrix` so gestures / accessibility map through it); uncached controls keep the matrix path. Not ported: `ExpandDirtyRegion`, C# `CachedObject` surface reuse for plain `Image`. |
80
+ | `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
+ | 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
+ | `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"`. |
84
+ | `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
+ | Everything else (`SkiaSwitch`, `SkiaCheckbox`, `SkiaSlider`, `SkiaProgress`, `SkiaCamera`, `SkiaMauiElement`, ...) | skipped | |
86
+
87
+ ## Startup
88
+
89
+ | Area | Status | Notes |
90
+ |---|---|---|
91
+ | `Super.UseDrawnUi().ConfigureFonts(f => f.AddFont(src, alias)).BuildAsync()` | ported | Same shape as DrawnUi.Net/OpenTK. |
92
+ | `AddFont(src, alias, weight)`, font fallback, emoji | skipped | |
93
+ | `PreloadAssets`, `ConfigureStyles` | skipped | |
94
+
95
+ ## React layer
96
+
97
+ | Area | Status | Notes |
98
+ |---|---|---|
99
+ | 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. |
101
+ | `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 height from the C# style builders, applied only when the user left `BackgroundColor`/`CornerRadius`(8)/`FontSize`(15)/`MinimumHeightRequest` unset; default look is now the C# Crimson accent. Not ported: `Background` brushes, pressed-state shadow changes. |
103
+ | `SkiaButton.FontFamilyFallback` | React extension | Pass-through to the inner label so icon-glyph buttons render with `AddSymbols()` faces. |
104
+ | 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. |
105
+ | React context bridging across the `<Canvas>` boundary | skipped | Contexts from the DOM tree are not visible inside the drawn tree. |
106
+ | Refs to engine controls from JSX | skipped | `getPublicInstance` returns the control, `ref` not wired/typed. |
107
+ | Fluent code-behind API (`.Assign`, `.OnTapped`, `.ObserveProperty`) | skipped | Engine classes are plain TS classes; React is the composition layer. |