drawnui-react 0.1.0-preview.1 → 0.1.0-preview.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PARITY.md DELETED
@@ -1,287 +0,0 @@
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).