drawnui-react 0.1.0-preview.12 → 0.1.0-preview.14

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 (42) hide show
  1. package/SKIPPED.md +6 -5
  2. package/dist/controls/GridStructure.d.ts +4 -2
  3. package/dist/controls/GridStructure.js +2 -2
  4. package/dist/controls/SkiaButton.d.ts +2 -0
  5. package/dist/controls/SkiaButton.js +2 -0
  6. package/dist/controls/SkiaCarousel.js +4 -0
  7. package/dist/controls/SkiaEditor.d.ts +7 -0
  8. package/dist/controls/SkiaEditor.js +9 -1
  9. package/dist/controls/SkiaGif.js +1 -1
  10. package/dist/controls/SkiaLabelFps.d.ts +16 -0
  11. package/dist/controls/SkiaLabelFps.js +41 -0
  12. package/dist/controls/SkiaLayout.d.ts +32 -6
  13. package/dist/controls/SkiaLayout.js +171 -10
  14. package/dist/controls/SkiaLottie.js +1 -1
  15. package/dist/controls/SkiaScroll.d.ts +5 -4
  16. package/dist/controls/SkiaScroll.js +27 -8
  17. package/dist/controls/SkiaSlider.d.ts +8 -0
  18. package/dist/controls/SkiaSlider.js +39 -0
  19. package/dist/controls/SkiaSvg.js +1 -1
  20. package/dist/core/Accessibility.d.ts +59 -0
  21. package/dist/core/Accessibility.js +303 -3
  22. package/dist/core/Canvas.d.ts +8 -0
  23. package/dist/core/Canvas.js +14 -1
  24. package/dist/core/Gestures.d.ts +5 -1
  25. package/dist/core/Gestures.js +4 -1
  26. package/dist/core/SkiaControl.d.ts +27 -2
  27. package/dist/core/SkiaControl.js +38 -4
  28. package/dist/core/SkiaEffect.d.ts +4 -0
  29. package/dist/core/SkiaEffect.js +17 -3
  30. package/dist/core/SkiaImageManager.js +1 -1
  31. package/dist/core/Super.d.ts +7 -0
  32. package/dist/core/Super.js +15 -1
  33. package/dist/core/ViewsAdapter.d.ts +7 -2
  34. package/dist/core/ViewsAdapter.js +10 -3
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/react/index.d.ts +4 -1
  38. package/dist/react/index.js +63 -9
  39. package/dist/react/reconciler.js +2 -1
  40. package/dist/vite/index.d.ts +9 -0
  41. package/dist/vite/index.js +36 -3
  42. package/package.json +4 -1
package/SKIPPED.md CHANGED
@@ -52,9 +52,9 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
52
52
  | `Gestures` param | ported | Touch: `Lock` applies `touch-action:none`; `Enabled` behaves like MAUI's Enabled inside a native scroll view: `touch-action` is the axes the page (or a scrolling ancestor) can scroll (`pan-y` / `pan-x` / both, `none` when it cannot; inside an iframe the vertical axis always counts, because the embedding page cannot be inspected cross-origin), recomputed on attach, on window / html / body resize and after every touch, so a finger pan along a page axis scrolls the page and the canvas pointer is cancelled (a cancelled pointer never flings a `SkiaScroll`), taps and the other axis stay on the canvas, and a full-page app keeps every touch. `user-select:none` in both. Claimed touches: in `Enabled` a touch or pen Down is processed immediately (queued gestures first) and, when a control used it (same rule as the wheel: marked `Handled`, or a consumer that is not a `BlockGesturesBelow` layer), the touch is claimed and its `touchmove` events are default-prevented until it ends, so the page cannot take that drag mid-gesture (iOS Safari ignores `touch-action` for off-axis drags, w3c/pointerevents#303); unclaimed touches share the page as before. Controls that use a Down: `ConsumeGestures` handlers that set `Consumed`, `SkiaCarousel` (always), `SkiaSlider`, `SkiaEditor`, `SkiaButton` when `OnDown` accepts; `SkiaScroll` and plain `Tapped` controls do not. Mouse wheel: `Enabled` shares it with the page like Blazor's `TouchHandlingStyle.Manual` (the wheel is processed in the DOM handler and the browser default is prevented only when a control used it: a scroll that moved, a `ConsumeGestures` handler that consumed, or any other consumer that is not a `BlockGesturesBelow` layer merely blocking the controls below, so an embedded canvas lets the page scroll over anything that does not scroll, including a shell page, and a scroll at its edge hands the wheel to the page; `TouchActionEventArgs.Handled` is the AppoMobi marker for that use); `Lock` prevents every wheel and also blocks `touchmove` default. Not ported: Blazor's pointer capture policy for `Enabled` (capture only while locked, release when a pan is not consumed); React captures every press. |
53
53
  | Mouse cursor over interactive controls (Blazor `Canvas.razor` `.xaml-a11y-interactive { cursor: pointer }`) | ported (differently) | The overlay here is `pointer-events: none`, so `Canvas.UpdateCursor` hit-tests mouse moves with no active pointer against the accessibility snapshot rects (`CanInteract` = has `Tapped`, buttons, links) and asks each hit control `WantsPointerCursor(x, y)` (default `AccessibilityCanInteract`; `SkiaLabel` also answers for its tappable `TextSpan`s), setting the canvas element's `cursor` only when the answer changes. Touch pointers do nothing. Engine hover (`IsHovered` / `OnHover`, `TouchActionResult.Pointer`) stays skipped, see Gestures. |
54
54
  | `ContextMenu` (right click / long press / Menu key, React extension) | ported (web-only) | `SkiaControl.ContextMenu` / `Canvas.ContextMenu` handlers with `ContextMenuEventArgs` (`Location` points, `Pixels`, `Local` px in the control, `Source`, `Native`); the DOM `contextmenu` event is routed like a tap (`ProcessContextMenu`: visible non-transparent children top-most first through `TransformPointToLocalSpace`, then the control, then the canvas). `true` = handled, browser menu suppressed; nothing handled = browser menu. Not in DrawnUi.Net. |
55
- | Keyboard, focus (`FocusedChild`) | skipped | |
55
+ | Keyboard navigation (drawnui-cross 6c) | ported | Through the accessibility overlay, the DrawnUi.Blazor way: Tab / Shift+Tab walk the interactive nodes in reading order, Enter / Space activate (`SkiaAccessibilityManager.Activate`), ArrowLeft / Right / Up / Down, Home / End, PageUp / PageDown go to the focused control first (`OnAccessibilityKey`, `SkiaAccessibilityManager.Key`: `SkiaSlider` steps, `SkiaEditor` keeps them while editing), then to its arrow-key group (`MoveInGroup`). A container with a composite role (`Aria.IsCompositeRole`: list, listbox, grid, toolbar, radiogroup, tablist, menu, menubar) is one Tab stop (roving tabindex, `IsTabStop`, entered on the item focused last); the arrows move by item index (Column: Up / Down, Row: Left / Right, Wrap / Grid / Split: all four, Up / Down by a row), no wrap, items not realized are scrolled in with `ScrollToIndex` and focused once drawn (`OnKeyboardFocusRequested`). `SkiaEditor` is a Tab stop: Tab in takes the caret, Tab / Shift+Tab leave it for the next node (also after a click into it). Focus ring = the overlay element's `:focus-visible` outline; focus scrolls the node into view in every enclosing scroll. Not ported: `FocusedChild` / canvas `KeyboardFocusNode`, Escape leaving the drawn nodes, a canvas-drawn focus ring. |
56
56
  | Canvas draw events | ported | `WillFirstTimeDraw` (once, right before the first frame drawn on a ready surface, with that frame's `{ Canvas, Surface }`), `WasDrawn` (after every frame, no context, like the C# `null`), `WasRendered` (set after the first frame completes, so the first `WasDrawn` still sees `false`, C# order), `CanRender` (surface ready). JSX: `<Canvas WillFirstTimeDraw WasDrawn>`; the engine draws frame 1 in its constructor, so the component hands both callbacks to the constructor's `init`. Not ported: `WillDraw`. |
57
- | Rendering stats | partial | `Canvas.FPS` (frames drawn over the last second) and `Canvas.FrameTime` (ms of the last measure + arrange + render, GPU flush excluded) are computed every frame and reachable from React through the `<Canvas>` ref. Not ported: the `SkiaLabelFps` control, `Super.EnableRenderingStats`. |
57
+ | Rendering stats | partial | `Canvas.FPS` (frames drawn over the last second) and `Canvas.FrameTime` (ms of the last measure + arrange + render, GPU flush excluded) are computed every frame and reachable from React through the `<Canvas>` ref. `SkiaLabelFps` ported (label on top, input-transparent, parent-independent, re-measures itself on draw; its value changes only on frames that running animators request, see PARITY). Not ported: `SkiaLabelFps.ForceRefresh` / `Format` / `MonoForDigits`, `Super.EnableRenderingStats`. |
58
58
  | Insets / safe areas | ported | `Super.Insets` measures `env(safe-area-inset-*)` off a probe element (once, and again on resize / orientation), `Super.OnInsetsChanged` notifies; `SkiaShell` pads its nav and tab bars with them. Not ported: the C# per-page `IgnoreSafeArea` / MAUI inset plumbing. |
59
59
 
60
60
  ## Controls
@@ -77,13 +77,13 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
77
77
  | `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). |
78
78
  | `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. |
79
79
  | `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). |
80
- | `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both/Neither (Neither: pans never move the viewport, the wheel is not consumed so a parent scroll may take it; programmatic `ScrollTo` still works), single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize`, `AutoScrollingSpeedMs`, notches accumulate onto the running target), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1 + `HideDurationSecs` 0.25, thumb squashed on overscroll, `IsDraggable` + `GrabPadding` 8: the scroll takes a gesture that starts on the bar from down to up, thumb drag keeps the grab point, a track press jumps the thumb under the pointer, the content gets neither the pan nor the tap; a custom `IScrollBar` opts in by implementing `IsDraggable` / `BeginDrag` / `GetDragProgress`; bars are parent-independent (C# `IsParentIndependent`): a thumb resize invalidates the bar alone and the scroll re-measures it on draw, nothing above re-measures), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
80
+ | `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both/Neither (Neither: pans never move the viewport, the wheel is not consumed so a parent scroll may take it; programmatic `ScrollTo` still works), single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize` per notch, an event moving by its share of a notch: `Canvas.WheelDeltaPerNotch` 100 CSS px after `deltaMode` lines x 40 / pages x 800, dominant axis, as the C# browser heads; `AutoScrollingSpeedMs` glide for a notch, an event under half a notch moves at once (touchpads, see PARITY); events accumulate onto the running move's exact target, and a restarted glide starts at the event so a fast swipe moves on every frame; `Wheel.IsHorizontal`: a vertical scroll leaves horizontal events, a horizontal scroll takes both, C# ba03cb20), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1 + `HideDurationSecs` 0.25, thumb squashed on overscroll, `IsDraggable` + `GrabPadding` 8: the scroll takes a gesture that starts on the bar from down to up, thumb drag keeps the grab point, a track press jumps the thumb under the pointer, the content gets neither the pan nor the tap; a custom `IScrollBar` opts in by implementing `IsDraggable` / `BeginDrag` / `GetDragProgress`; bars are parent-independent (C# `IsParentIndependent`): a thumb resize invalidates the bar alone and the scroll re-measures it on draw, nothing above re-measures), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
81
81
  | `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. |
82
82
  | `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. |
83
83
  | `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. |
84
84
  | `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`. |
85
85
  | `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. |
86
- | 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. |
86
+ | Templated `Row` / `Wrap` / `Grid`, `Split`, `SkiaDecoratedGrid` | ported | `ItemsSource` + `ItemTemplate` on any layout type: Column stays the virtualized list. Row / Wrap / Grid / a split Column follow `RecyclingTemplate` (drawnui-cross 6b): Enabled measures every item on pooled views (C# measures the structure on one template instance) and binds views only to the slots drawn, their views go back to the pool when the slot leaves the viewport (`VirtualisationInflated` widens it), every slot draws under a cache (C# `GetOnScreenVisibleArea`); Disabled keeps one view per item, never rebound. Changing `RecyclingTemplate` rebuilds the cells. `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 (every item is measured once per structure). |
87
87
  | `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. |
88
88
  | `SkiaShaderCarousel` | ported | Only the selected slide takes gestures (C# `IsSlideHitTestable`): every slide sits on the same rect, so the topmost on-screen one used to catch every tap. `TransitionShader` (url), `TransitionShaderCode`, `TransitionTemplate`, `InterruptedTransitionMs` (50), `TransitionEffect` (`CreateTransitionEffect` factory), `TransitionFromIndex` / `TransitionToIndex`, `FromToChanged`; slides never move (`SlideOffset` hook), looped wrap rules, one gesture = at most one slide, interrupted transition wrapped up first, `RecyclingTemplate` Disabled; the carousel is cached as Image by default (React only, so the overlapping slides are never blitted). Slides MUST use `UseCache="Image"`. |
89
89
  | `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. |
@@ -107,8 +107,9 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
107
107
  | `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. |
108
108
  | `SkiaButton.ControlStyle` | ported | Cupertino / Material / Material3 / Windows accent, corner radius, font size/weight and minimum content size from the C# style builders, applied only when the user left `BackgroundColor`/`CornerRadius`(8)/`FontSize`(15)/`MinimumWidthRequest`/`MinimumHeightRequest` unset; default look is now the C# Crimson accent. `SetDefaultMinimumContentSize` is mirrored for BOTH axes (every C# style passes width 100, heights 41/36/40/40/32) and for the default `Unset` style too, skipped on a `Fill` axis or when a `WidthRequest`/`HeightRequest` is set, so short captions clamp to 100pt as in C#; the C# value adds `Margins` because it compares against a margin-inclusive size, here Measure applies it margin-exclusive and adds the margin after. Not ported: `Background` brushes, pressed-state shadow changes. |
109
109
  | `SkiaButton.FontFamilyFallback` | React extension | Pass-through to the inner label so icon-glyph buttons render with `AddSymbols()` faces. |
110
- | 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. |
110
+ | 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`, reading order by rows (tops within half the smaller height form one row, read left to right), `AccessibilityCanInteract` only while a tap could reach the control (`CanReceiveGesture`: the control and its ancestors visible, none `InputTransparent`, no ancestor `LockChildrenGestures` holding the gesture, `AcceptsInput`, false for a disabled `SkiaButton`), `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. |
111
111
  | Static HTML for crawlers (`drawnui-react/vite` `drawnUiStatic()`, React extension) | ported | Build-time only: after `vite build` the plugin serves the build, opens it in headless Chrome (`playwright-core`, optional peer), reads the accessibility snapshot of the root page and of every page a root button opens (or `routes`), and writes visible semantic HTML (heading → heading, button → `<a href="#/route">`, text → paragraph, other controls → list items; `img` and `presentation` dropped) after the mount element or at `<!-- drawnui-static -->`, with `DEFAULT_STATIC_STYLE` (`render.style` to override / disable). Runtime: the first `<Canvas>` mount removes every `[data-drawnui-static]` element right after its engine drew frame 1 (`removeStaticContent`, one querySelectorAll, nothing in the frame loop). Not in DrawnUi.Net (Blazor sites hand-write static SEO content, see the drawnui-blazor notes). Not done: per-route documents (pages live in the hash, one URL), `<title>` / meta per page, sitemap entries. |
112
+ | Versioned public files (`drawnui-react/vite` `drawnUiAssetStamps()` + `Super.ResolveAssetUrl`, React extension) | ported | Build-time plugin: hashes every file of the public folder and writes the map into index.html as `globalThis.DrawnUiAssetStamps`, stamps the page's own `href` / `src` links to public files. Every engine loader (images and sprites through `SkiaImageManager`, fonts in `BuildAsync`, `SkiaSvg`, `SkiaGif`, `SkiaLottie`, shader sources) fetches through `Super.ResolveAssetUrl`, which appends `?v=<hash>` (`&v=` after an existing query) to a listed file; caches stay keyed by the source. Dev server untouched. Replaceable by the app. No C# counterpart. |
112
113
  | React context bridging across the `<Canvas>` boundary | skipped | Contexts from the DOM tree are not visible inside the drawn tree. |
113
114
  | Refs to engine controls from JSX | skipped | `getPublicInstance` returns the control, `ref` not wired/typed. |
114
115
  | Fluent code-behind API (`.Assign`, `.OnTapped`, `.ObserveProperty`) | skipped | Engine classes are plain TS classes; React is the composition layer. |
@@ -16,6 +16,8 @@ export declare class DefinitionInfo {
16
16
  }
17
17
  /** Parses "*, 2*, Auto, 100" (or an array of the same values) into tracks. */
18
18
  export declare function ParseDefinitions(defs: string | GridLength[] | undefined): DefinitionInfo[];
19
+ /** What the structure reads from a child: a control, or the slot of a recycled templated item (measured on a pooled view). */
20
+ export type GridChild = Pick<SkiaControl, "IsVisible" | "Row" | "Column" | "RowSpan" | "ColumnSpan" | "HorizontalOptions" | "VerticalOptions" | "MinimumWidthRequest" | "MinimumHeightRequest" | "Margin" | "MeasuredSize" | "Measure">;
19
21
  /**
20
22
  * Port of DrawnUi SkiaGridStructure (itself adapted from the MAUI Grid manager). Works in POINTS: constraints come in
21
23
  * already reduced by the grid's padding, cells are returned relative to the padded box. Children are measured in
@@ -35,7 +37,7 @@ export declare class SkiaGridStructure {
35
37
  private readonly spans;
36
38
  private readonly explicitWidth;
37
39
  private readonly explicitHeight;
38
- constructor(grid: SkiaLayout, widthConstraint: number, heightConstraint: number, scale: number);
40
+ constructor(grid: SkiaLayout, widthConstraint: number, heightConstraint: number, scale: number, children?: readonly GridChild[]);
39
41
  private InitializeTracks;
40
42
  private static CreateDefinitionIfMissing;
41
43
  private static ToLengthType;
@@ -46,7 +48,7 @@ export declare class SkiaGridStructure {
46
48
  LeftEdgeOfColumn(column: number): number;
47
49
  TopEdgeOfRow(row: number): number;
48
50
  /** Cell rectangle in points relative to the padded box (+ offsets). */
49
- GetCellBoundsFor(view: SkiaControl, xOffset?: number, yOffset?: number): {
51
+ GetCellBoundsFor(view: GridChild, xOffset?: number, yOffset?: number): {
50
52
  Left: number;
51
53
  Top: number;
52
54
  Width: number;
@@ -88,7 +88,7 @@ export class SkiaGridStructure {
88
88
  spans = new Map();
89
89
  explicitWidth;
90
90
  explicitHeight;
91
- constructor(grid, widthConstraint, heightConstraint, scale) {
91
+ constructor(grid, widthConstraint, heightConstraint, scale, children = grid.Views) {
92
92
  this.grid = grid;
93
93
  this.widthConstraint = widthConstraint;
94
94
  this.heightConstraint = heightConstraint;
@@ -99,7 +99,7 @@ export class SkiaGridStructure {
99
99
  this.explicitHeight = grid.HeightRequest;
100
100
  this.Rows = this.InitializeTracks(grid.RowDefinitions, grid.DefaultRowDefinition);
101
101
  this.Columns = this.InitializeTracks(grid.ColumnDefinitions, grid.DefaultColumnDefinition);
102
- this.children = grid.Views.filter((v) => v.IsVisible);
102
+ this.children = children.filter((v) => v.IsVisible);
103
103
  this.InitializeCells();
104
104
  this.MeasureCells();
105
105
  }
@@ -41,6 +41,8 @@ export declare class SkiaButton extends SkiaLayout {
41
41
  static DefaultAccessibilityRole?: string;
42
42
  protected DefaultAccessibilityLabel(): string | undefined;
43
43
  protected DefaultAccessibilityCanInteract(): boolean;
44
+ /** A disabled button ignores taps, so keyboard navigation skips it too. */
45
+ protected AcceptsInput(): boolean;
44
46
  /** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
45
47
  protected PaintBackground(): void;
46
48
  get UsingControlStyle(): ResolvedControlStyle;
@@ -59,6 +59,8 @@ export class SkiaButton extends SkiaLayout {
59
59
  static DefaultAccessibilityRole;
60
60
  DefaultAccessibilityLabel() { return this.Text || undefined; }
61
61
  DefaultAccessibilityCanInteract() { return !this.IsDisabled; }
62
+ /** A disabled button ignores taps, so keyboard navigation skips it too. */
63
+ AcceptsInput() { return !this.IsDisabled; }
62
64
  /** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
63
65
  PaintBackground() { }
64
66
  get UsingControlStyle() { return ResolveControlStyle(this.ControlStyle); }
@@ -561,6 +561,10 @@ export class SkiaCarousel extends SnappingLayout {
561
561
  if (!this.IsUserPanning) {
562
562
  const movex = Math.abs(e.Distance.Total.X), movey = Math.abs(e.Distance.Total.Y);
563
563
  const along = this.IsVertical ? movey : movex, across = this.IsVertical ? movex : movey;
564
+ // a first move too short to tell the direction decides nothing yet (a 1 CSS px move at devicePixelRatio 2):
565
+ // marking it wrong kept the whole drag dead until the next press (DrawnUi.Rust c6b22e3; C# decides on every move)
566
+ if (along < scale * 2 && across < scale * 2)
567
+ return consumedDefault;
564
568
  if (along < scale * 2 || across > along) {
565
569
  this.wrongDirection = true;
566
570
  return consumedDefault;
@@ -128,7 +128,14 @@ export declare class SkiaEditor extends SkiaShape {
128
128
  private LabelPoint;
129
129
  protected Paint(ctx: DrawingContext): void;
130
130
  ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
131
+ /** A text field is a Tab stop by default, like a native text box, with no Tapped handler needed. */
132
+ protected DefaultAccessibilityCanInteract(): boolean;
133
+ /** Keyboard navigation reached the field (Tab) or left it: take the caret and the input sink like a native text box, or release them. */
134
+ OnAccessibilityFocused(focused: boolean): void;
135
+ /** Enter / Space on the field while it has no caret start editing; while editing they are text. */
131
136
  OnAccessibilityActivated(): void;
137
+ /** While editing, arrows, Home / End and PageUp / PageDown belong to the text: they never move a surrounding group. */
138
+ OnAccessibilityKey(_key: string): boolean;
132
139
  protected DefaultAccessibilityLabel(): string | undefined;
133
140
  Submit(): void;
134
141
  private ExecuteSubmit;
@@ -508,7 +508,15 @@ export class SkiaEditor extends SkiaShape {
508
508
  }
509
509
  return this;
510
510
  }
511
- OnAccessibilityActivated() { this.IsFocused = true; }
511
+ /** A text field is a Tab stop by default, like a native text box, with no Tapped handler needed. */
512
+ DefaultAccessibilityCanInteract() { return true; }
513
+ /** Keyboard navigation reached the field (Tab) or left it: take the caret and the input sink like a native text box, or release them. */
514
+ OnAccessibilityFocused(focused) { this.IsFocused = focused; }
515
+ /** Enter / Space on the field while it has no caret start editing; while editing they are text. */
516
+ OnAccessibilityActivated() { if (!this.IsFocused)
517
+ this.OnAccessibilityFocused(true); }
518
+ /** While editing, arrows, Home / End and PageUp / PageDown belong to the text: they never move a surrounding group. */
519
+ OnAccessibilityKey(_key) { return this.IsFocused; }
512
520
  DefaultAccessibilityLabel() { return this.text || this.placeholderText || undefined; }
513
521
  // ---- editing API (C# names) ----
514
522
  Submit() {
@@ -158,7 +158,7 @@ export class SkiaGif extends AnimatedFramesRenderer {
158
158
  async LoadSource(source) {
159
159
  if (!source)
160
160
  return undefined;
161
- const r = await fetch(source);
161
+ const r = await fetch(Super.ResolveAssetUrl(source));
162
162
  if (!r.ok)
163
163
  throw new Error(`${r.status} ${source}`);
164
164
  const animation = new GifAnimation();
@@ -0,0 +1,16 @@
1
+ import type { DrawingContext } from "../core/SkiaControl";
2
+ import type { SKRect } from "../core/Types";
3
+ import { SkiaLabel } from "./SkiaLabel";
4
+ /**
5
+ * Mirrors DrawnUi SkiaLabelFps: a label showing the canvas FPS, drawn above its siblings, input-transparent, never
6
+ * re-laying out its parent. Divergence: C# sets Text on every draw, and each new value asks for another frame, so
7
+ * the counter keeps an idle canvas redrawing. Here the value changes only on frames that running animators already
8
+ * request (scroll inertia, transitions, Lottie, game loops): the counter never asks for a frame itself, and an idle
9
+ * canvas keeps the last value measured. Not ported: ForceRefresh, Format, MonoForDigits.
10
+ */
11
+ export declare class SkiaLabelFps extends SkiaLabel {
12
+ constructor();
13
+ private arrangedIn?;
14
+ Arrange(destination: SKRect, widthRequest: number, heightRequest: number, scale: number): void;
15
+ Render(ctx: DrawingContext): void;
16
+ }
@@ -0,0 +1,41 @@
1
+ import { Aria } from "../core/Accessibility";
2
+ import { SkiaLabel } from "./SkiaLabel";
3
+ /**
4
+ * Mirrors DrawnUi SkiaLabelFps: a label showing the canvas FPS, drawn above its siblings, input-transparent, never
5
+ * re-laying out its parent. Divergence: C# sets Text on every draw, and each new value asks for another frame, so
6
+ * the counter keeps an idle canvas redrawing. Here the value changes only on frames that running animators already
7
+ * request (scroll inertia, transitions, Lottie, game loops): the counter never asks for a frame itself, and an idle
8
+ * canvas keeps the last value measured. Not ported: ForceRefresh, Format, MonoForDigits.
9
+ */
10
+ export class SkiaLabelFps extends SkiaLabel {
11
+ constructor() {
12
+ super();
13
+ this.IsParentIndependent = true;
14
+ this.AccessibilityRole = Aria.RolePresentation;
15
+ this.Tag = "FPS";
16
+ this.Text = "FPS --";
17
+ this.MaxLines = 1;
18
+ this.BackgroundColor = "#000000";
19
+ this.TextColor = "#32CD32"; // LimeGreen
20
+ this.InputTransparent = true;
21
+ this.ZIndex = Number.MAX_SAFE_INTEGER;
22
+ }
23
+ arrangedIn;
24
+ Arrange(destination, widthRequest, heightRequest, scale) {
25
+ this.arrangedIn = destination;
26
+ super.Arrange(destination, widthRequest, heightRequest, scale);
27
+ }
28
+ Render(ctx) {
29
+ const view = this.Superview;
30
+ // the frame this asks for is requested anyway by the running animators
31
+ if (view && view.AnimatingControls.size > 0)
32
+ this.Text = `FPS ${view.FPS.toFixed(1).padStart(4, "0")}`;
33
+ // parent-independent: the parent does not re-lay out for it, so it re-measures itself here (as SkiaScroll does for its bars)
34
+ const d = this.arrangedIn;
35
+ if (this.NeedMeasure && d) {
36
+ this.Measure(d.Width, d.Height, ctx.Scale);
37
+ this.Arrange(d, this.WidthRequest, this.HeightRequest, ctx.Scale);
38
+ }
39
+ super.Render(ctx);
40
+ }
41
+ }
@@ -6,9 +6,11 @@ import { SkiaGridStructure } from "./GridStructure";
6
6
  * Mirrors DrawnUi SkiaLayout (Absolute / Column / Row / Wrap / Grid).
7
7
  * Column/Row give children an infinite main axis (MAUI stack semantics: Fill on the main axis = auto-sized).
8
8
  *
9
- * Templated mode (ItemsSource + ItemTemplate) is a Column only: cells are created through the ViewsAdapter for
10
- * the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
11
- * MeasureAll (default) measures every item once, MeasureFirst measures one cell and assumes uniform size.
9
+ * Templated mode (ItemsSource + ItemTemplate): a single-column Column is the virtualized list, cells are created through
10
+ * the ViewsAdapter for the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
11
+ * MeasureAll (default) measures every item once, MeasureFirst measures one cell and assumes uniform size. A templated
12
+ * Row / Wrap / Grid / split Column follows RecyclingTemplate: Enabled measures every item on pooled views and binds views
13
+ * only to the slots drawn, Disabled keeps one view per item.
12
14
  */
13
15
  export declare class SkiaLayout extends SkiaControl {
14
16
  /** Layout type. SkiaShape redeclares it as ShapeType (any shape value lays out as Absolute), like the C# hidden Type. */
@@ -24,7 +26,10 @@ export declare class SkiaLayout extends SkiaControl {
24
26
  RowSpacing: number;
25
27
  /** Structure computed by the last measure; cells are arranged from it. */
26
28
  GridStructure?: SkiaGridStructure;
27
- RecyclingTemplate: RecyclingTemplate;
29
+ private recyclingTemplate;
30
+ /** Enabled: views come from a pool and are bound to the items being drawn; Disabled: one view per item, never rebound. */
31
+ get RecyclingTemplate(): RecyclingTemplate;
32
+ set RecyclingTemplate(value: RecyclingTemplate);
28
33
  /** Column count for Column / Wrap content and a templated Grid (0 = free flow); C# Split. */
29
34
  Split: number;
30
35
  /** Split > 0: every slot keeps the same width (true) or the cell keeps its measured width (C# SplitAlign). */
@@ -85,9 +90,16 @@ export declare class SkiaLayout extends SkiaControl {
85
90
  get ItemTemplate(): (() => SkiaControl) | undefined;
86
91
  set ItemTemplate(value: (() => SkiaControl) | undefined);
87
92
  get IsTemplated(): boolean;
88
- /** The virtualized list case: a templated single-column Column. Templated Row / Wrap / Grid realize every item. */
93
+ /** The virtualized list case: a templated single-column Column. */
89
94
  private get IsTemplatedList();
90
- /** Views taking part in the layout pass: static children, or every item of a templated Row / Wrap / Grid (C# non-list layouts are not virtualized). */
95
+ /** A templated Row / Wrap / Grid / split Column with RecyclingTemplate Enabled: slots measured on pooled views, views bound to the slots drawn. */
96
+ private get IsRecycledLayout();
97
+ /** Slots of a recycled layout, one per item. */
98
+ private slots?;
99
+ /**
100
+ * What the layout pass measures and arranges: static children; the slots of a recycled templated layout; or, with
101
+ * RecyclingTemplate Disabled, one realized view per item.
102
+ */
91
103
  private LayoutViews;
92
104
  /** Drops realized cells and rebuilds the structure (DrawnUi ApplyItemsSource). */
93
105
  ApplyItemsSource(): void;
@@ -142,8 +154,22 @@ export declare class SkiaLayout extends SkiaControl {
142
154
  GetItemOffsetPixels(index: number): number;
143
155
  private ItemHeightPixels;
144
156
  protected OnLayoutChanged(): void;
157
+ /** Static children and realized views are arranged; the slots of a recycled layout record where their item goes. */
158
+ private ArrangeChildren;
145
159
  protected Paint(ctx: DrawingContext): void;
146
160
  protected GetCompositeChildren(): readonly SkiaControl[];
161
+ private readonly drawnSlots;
162
+ /**
163
+ * Recycled templated layout (C# DrawStack): a view from the pool is bound to each slot that can be seen and drawn
164
+ * there; the views of the other slots go back to the pool.
165
+ */
166
+ private PaintSlots;
167
+ /**
168
+ * Where slots are drawn (C# GetOnScreenVisibleArea): what can be seen, inflated by VirtualisationInflated. Under a
169
+ * cache (this layout's or an ancestor's, below the nearest scroll) every slot, because the cache is blitted later at
170
+ * other offsets.
171
+ */
172
+ private SlotsArea;
147
173
  /** Realizes, binds, arranges and draws only the cells intersecting the visible viewport (+ inflation). */
148
174
  private PaintTemplated;
149
175
  /**
@@ -2,13 +2,81 @@ import { SkiaControl } from "../core/SkiaControl";
2
2
  import { ViewsAdapter } from "../core/ViewsAdapter";
3
3
  import { SKRect, ScaledSize, Thickness } from "../core/Types";
4
4
  import { SkiaGridStructure } from "./GridStructure";
5
+ /**
6
+ * The place of one item in a recycled templated Row / Wrap / Grid / split Column. C# measures the structure on one
7
+ * template instance bound to each item in turn: Measure here binds a pooled view to the item, keeps its size, the
8
+ * constraints and what the template decided for that item, then gives the view back unless the item is on screen.
9
+ * The same item measured again with the same constraints keeps its size without a rebind (as the list keeps its item
10
+ * heights: a new ItemsSource array is how content changes). Arrange only records the slot; the drawing pass binds a
11
+ * view to each slot it draws.
12
+ */
13
+ class TemplatedSlot {
14
+ Index;
15
+ factory;
16
+ IsVisible = true;
17
+ Row = 0;
18
+ Column = 0;
19
+ RowSpan = 1;
20
+ ColumnSpan = 1;
21
+ HorizontalOptions = "Start";
22
+ VerticalOptions = "Start";
23
+ Margin = Thickness.Zero;
24
+ WidthRequest = -1;
25
+ HeightRequest = -1;
26
+ MinimumWidthRequest = -1;
27
+ MinimumHeightRequest = -1;
28
+ MeasuredSize = ScaledSize.Default;
29
+ WidthConstraint = Infinity;
30
+ HeightConstraint = Infinity;
31
+ /** The rect the layout arranged this slot in (pixels). */
32
+ Destination = SKRect.Empty;
33
+ /** The view last arranged in this slot and the Destination it got, so an unchanged view is not arranged twice. */
34
+ ArrangedView;
35
+ ArrangedFor;
36
+ measuredItem;
37
+ measuredScale = 0;
38
+ constructor(Index, factory) {
39
+ this.Index = Index;
40
+ this.factory = factory;
41
+ }
42
+ Measure(widthConstraint, heightConstraint, scale) {
43
+ const shown = this.factory.GetViewForIndex(this.Index), item = this.factory.Items[this.Index];
44
+ if (!shown && item === this.measuredItem && scale === this.measuredScale
45
+ && Object.is(widthConstraint, this.WidthConstraint) && Object.is(heightConstraint, this.HeightConstraint))
46
+ return this.MeasuredSize;
47
+ const view = shown ?? this.factory.GetOrCreateViewForIndex(this.Index, true);
48
+ if (!view)
49
+ return (this.MeasuredSize = ScaledSize.Default);
50
+ this.MeasuredSize = view.Measure(widthConstraint, heightConstraint, scale);
51
+ this.measuredItem = item;
52
+ this.measuredScale = scale;
53
+ this.WidthConstraint = widthConstraint;
54
+ this.HeightConstraint = heightConstraint;
55
+ this.IsVisible = view.IsVisible;
56
+ this.RowSpan = view.RowSpan;
57
+ this.ColumnSpan = view.ColumnSpan;
58
+ this.HorizontalOptions = view.HorizontalOptions;
59
+ this.VerticalOptions = view.VerticalOptions;
60
+ this.Margin = view.Margin;
61
+ this.WidthRequest = view.WidthRequest;
62
+ this.HeightRequest = view.HeightRequest;
63
+ this.MinimumWidthRequest = view.MinimumWidthRequest;
64
+ this.MinimumHeightRequest = view.MinimumHeightRequest;
65
+ if (!shown)
66
+ this.factory.ReleaseViewAt(this.Index);
67
+ return this.MeasuredSize;
68
+ }
69
+ Arrange(destination, _widthRequest, _heightRequest, _scale) { this.Destination = destination; }
70
+ }
5
71
  /**
6
72
  * Mirrors DrawnUi SkiaLayout (Absolute / Column / Row / Wrap / Grid).
7
73
  * Column/Row give children an infinite main axis (MAUI stack semantics: Fill on the main axis = auto-sized).
8
74
  *
9
- * Templated mode (ItemsSource + ItemTemplate) is a Column only: cells are created through the ViewsAdapter for
10
- * the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
11
- * MeasureAll (default) measures every item once, MeasureFirst measures one cell and assumes uniform size.
75
+ * Templated mode (ItemsSource + ItemTemplate): a single-column Column is the virtualized list, cells are created through
76
+ * the ViewsAdapter for the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
77
+ * MeasureAll (default) measures every item once, MeasureFirst measures one cell and assumes uniform size. A templated
78
+ * Row / Wrap / Grid / split Column follows RecyclingTemplate: Enabled measures every item on pooled views and binds views
79
+ * only to the slots drawn, Disabled keeps one view per item.
12
80
  */
13
81
  export class SkiaLayout extends SkiaControl {
14
82
  /** Layout type. SkiaShape redeclares it as ShapeType (any shape value lays out as Absolute), like the C# hidden Type. */
@@ -26,7 +94,16 @@ export class SkiaLayout extends SkiaControl {
26
94
  /** Structure computed by the last measure; cells are arranged from it. */
27
95
  GridStructure;
28
96
  // ---- templated children (same names as DrawnUi) ----
29
- RecyclingTemplate = "Enabled";
97
+ recyclingTemplate = "Enabled";
98
+ /** Enabled: views come from a pool and are bound to the items being drawn; Disabled: one view per item, never rebound. */
99
+ get RecyclingTemplate() { return this.recyclingTemplate; }
100
+ set RecyclingTemplate(value) {
101
+ if (this.recyclingTemplate === value)
102
+ return;
103
+ this.recyclingTemplate = value;
104
+ if (this.itemTemplate)
105
+ this.ApplyItemsSource(); // the pool follows the mode
106
+ }
30
107
  /** Column count for Column / Wrap content and a templated Grid (0 = free flow); C# Split. */
31
108
  Split = 0;
32
109
  /** Split > 0: every slot keeps the same width (true) or the cell keeps its measured width (C# SplitAlign). */
@@ -216,13 +293,26 @@ export class SkiaLayout extends SkiaControl {
216
293
  this.ApplyItemsSource();
217
294
  }
218
295
  get IsTemplated() { return !!this.itemTemplate && !!this.itemsSource; }
219
- /** The virtualized list case: a templated single-column Column. Templated Row / Wrap / Grid realize every item. */
296
+ /** The virtualized list case: a templated single-column Column. */
220
297
  get IsTemplatedList() { return this.IsTemplated && this.Type === "Column" && this.Split <= 1; }
221
- /** Views taking part in the layout pass: static children, or every item of a templated Row / Wrap / Grid (C# non-list layouts are not virtualized). */
298
+ /** A templated Row / Wrap / Grid / split Column with RecyclingTemplate Enabled: slots measured on pooled views, views bound to the slots drawn. */
299
+ get IsRecycledLayout() { return this.IsTemplated && !this.IsTemplatedList && this.recyclingTemplate === "Enabled"; }
300
+ /** Slots of a recycled layout, one per item. */
301
+ slots;
302
+ /**
303
+ * What the layout pass measures and arranges: static children; the slots of a recycled templated layout; or, with
304
+ * RecyclingTemplate Disabled, one realized view per item.
305
+ */
222
306
  LayoutViews() {
223
307
  if (!this.IsTemplated)
224
308
  return this.views;
225
- const n = this.itemsSource.length, out = [];
309
+ const n = this.itemsSource.length;
310
+ if (this.IsRecycledLayout) {
311
+ if (this.slots?.length !== n)
312
+ this.slots = Array.from({ length: n }, (_, i) => new TemplatedSlot(i, this.ChildrenFactory));
313
+ return this.slots;
314
+ }
315
+ const out = [];
226
316
  for (let i = 0; i < n; i++) {
227
317
  const v = this.ChildrenFactory.GetOrCreateViewForIndex(i);
228
318
  if (v)
@@ -232,7 +322,8 @@ export class SkiaLayout extends SkiaControl {
232
322
  }
233
323
  /** Drops realized cells and rebuilds the structure (DrawnUi ApplyItemsSource). */
234
324
  ApplyItemsSource() {
235
- this.ChildrenFactory.Initialize(this.itemTemplate, this.itemsSource ?? [], this.RecyclingTemplate);
325
+ this.ChildrenFactory.Initialize(this.itemTemplate, this.itemsSource ?? [], this.recyclingTemplate);
326
+ this.slots = undefined;
236
327
  this.structureDirty = true;
237
328
  this.InvalidateMeasure();
238
329
  }
@@ -435,7 +526,7 @@ export class SkiaLayout extends SkiaControl {
435
526
  }
436
527
  });
437
528
  }
438
- const g = new SkiaGridStructure(this, wPts, hPts, scale);
529
+ const g = new SkiaGridStructure(this, wPts, hPts, scale, this.LayoutViews());
439
530
  g.DecompressStars(wPts, hPts);
440
531
  const needAutoWidth = this.WidthRequest < 0 && this.HorizontalOptions !== "Fill";
441
532
  const needAutoHeight = this.HeightRequest < 0 && this.VerticalOptions !== "Fill";
@@ -600,6 +691,22 @@ export class SkiaLayout extends SkiaControl {
600
691
  OnLayoutChanged() {
601
692
  if (this.IsTemplatedList)
602
693
  return; // cells are arranged per frame for the visible range only
694
+ this.ArrangeChildren();
695
+ // views on screen follow their slots also when a cache above skips the drawing pass (gestures, accessibility)
696
+ if (this.IsRecycledLayout) {
697
+ const scale = this.RenderingScale;
698
+ for (const v of this.ChildrenFactory.GetViewsInUse()) {
699
+ const s = this.slots?.[v.ContextIndex];
700
+ if (s) {
701
+ v.Arrange(s.Destination, v.WidthRequest, v.HeightRequest, scale);
702
+ s.ArrangedView = v;
703
+ s.ArrangedFor = s.Destination;
704
+ }
705
+ }
706
+ }
707
+ }
708
+ /** Static children and realized views are arranged; the slots of a recycled layout record where their item goes. */
709
+ ArrangeChildren() {
603
710
  const scale = this.RenderingScale;
604
711
  const p = this.Padding;
605
712
  const r = this.DrawingRect;
@@ -646,12 +753,66 @@ export class SkiaLayout extends SkiaControl {
646
753
  this.PaintTemplated(ctx);
647
754
  return;
648
755
  }
756
+ if (this.IsRecycledLayout) {
757
+ this.PaintSlots(ctx);
758
+ return;
759
+ }
649
760
  const composing = this.IsRenderingWithComposition;
650
761
  for (const v of this.GetOrderedSubviews())
651
762
  if (!composing || this.DirtyChildrenInternal.has(v))
652
763
  v.Render(ctx);
653
764
  }
654
- GetCompositeChildren() { return this.IsTemplatedList ? [] : this.GetOrderedSubviews(); }
765
+ GetCompositeChildren() { return this.IsTemplatedList || this.IsRecycledLayout ? [] : this.GetOrderedSubviews(); }
766
+ drawnSlots = new Set();
767
+ /**
768
+ * Recycled templated layout (C# DrawStack): a view from the pool is bound to each slot that can be seen and drawn
769
+ * there; the views of the other slots go back to the pool.
770
+ */
771
+ PaintSlots(ctx) {
772
+ const slots = this.slots ?? [], f = this.ChildrenFactory, scale = this.RenderingScale, area = this.SlotsArea();
773
+ const drawn = this.drawnSlots;
774
+ drawn.clear();
775
+ for (const s of slots) {
776
+ const d = s.Destination;
777
+ if (s.IsVisible && d.Right > area.Left && d.Left < area.Right && d.Bottom > area.Top && d.Top < area.Bottom)
778
+ drawn.add(s.Index);
779
+ }
780
+ f.ReleaseExcept(drawn);
781
+ this.FirstVisibleIndex = this.LastVisibleIndex = -1;
782
+ for (const i of drawn) {
783
+ const s = slots[i], view = f.GetOrCreateViewForIndex(i, true);
784
+ if (!view)
785
+ continue;
786
+ // a view last measured for another item is measured for this one; the whole size is compared because a Wrap has
787
+ // no main-axis size key (C# 3d7bd78f)
788
+ const has = view.MeasuredSize.Pixels, want = s.MeasuredSize.Pixels;
789
+ if (Math.abs(has.Width - want.Width) > 1 || Math.abs(has.Height - want.Height) > 1)
790
+ view.NeedMeasure = true;
791
+ const size = view.MeasuredSize;
792
+ view.Measure(s.WidthConstraint, s.HeightConstraint, scale);
793
+ if (s.ArrangedView !== view || s.ArrangedFor !== s.Destination || view.MeasuredSize !== size) {
794
+ view.Arrange(s.Destination, view.WidthRequest, view.HeightRequest, scale);
795
+ s.ArrangedView = view;
796
+ s.ArrangedFor = s.Destination;
797
+ }
798
+ view.Render(ctx);
799
+ if (this.FirstVisibleIndex < 0)
800
+ this.FirstVisibleIndex = i;
801
+ this.LastVisibleIndex = i;
802
+ }
803
+ }
804
+ /**
805
+ * Where slots are drawn (C# GetOnScreenVisibleArea): what can be seen, inflated by VirtualisationInflated. Under a
806
+ * cache (this layout's or an ancestor's, below the nearest scroll) every slot, because the cache is blitted later at
807
+ * other offsets.
808
+ */
809
+ SlotsArea() {
810
+ for (let c = this; c && !("ViewportOffsetY" in c); c = c.Parent)
811
+ if (c.UsingCacheType !== "None")
812
+ return this.DrawingRect;
813
+ const v = this.GetVisibleViewport(), inflate = this.VirtualisationInflated * this.RenderingScale;
814
+ return new SKRect(v.Left - inflate, v.Top - inflate, v.Right + inflate, v.Bottom + inflate);
815
+ }
655
816
  /** Realizes, binds, arranges and draws only the cells intersecting the visible viewport (+ inflation). */
656
817
  PaintTemplated(ctx) {
657
818
  const items = this.itemsSource;
@@ -174,7 +174,7 @@ export class SkiaLottie extends AnimatedFramesRenderer {
174
174
  return Promise.resolve(cached);
175
175
  let p = SkiaLottie.inFlight.get(source);
176
176
  if (!p) {
177
- p = fetch(source).then(async (r) => { if (!r.ok)
177
+ p = fetch(Super.ResolveAssetUrl(source)).then(async (r) => { if (!r.ok)
178
178
  throw new Error(`${r.status} ${source}`); const json = await r.text(); SkiaLottie.CachedAnimations.set(source, json); return json; })
179
179
  .finally(() => SkiaLottie.inFlight.delete(source));
180
180
  SkiaLottie.inFlight.set(source, p);
@@ -12,7 +12,7 @@ export interface ScaledPoint {
12
12
  * Mirrors DrawnUi SkiaScroll (plain content, no header/footer/refresh/virtualization yet).
13
13
  * Offsets are in POINTS and <= 0 while inside bounds (content moves up/left as you scroll).
14
14
  * Physics ported 1:1: deceleration fling (rate = 1 - FrictionScrolled/100) cut at the content edge,
15
- * rubber-band overscroll while dragging, spring bounce back on release, wheel = WheelLineSize per notch.
15
+ * rubber-band overscroll while dragging, spring bounce back on release, wheel = WheelLineSize per notch (an event moves by its share of a notch).
16
16
  */
17
17
  export declare class SkiaScroll extends SkiaControl {
18
18
  static WheelLineSize: number;
@@ -232,9 +232,10 @@ export declare class SkiaScroll extends SkiaControl {
232
232
  /** DrawnUi StartToFlingFrom + PrepareToFlingAfterInitialized: cut the curve so it stops exactly at the edge. */
233
233
  private StartToFlingFrom;
234
234
  /**
235
- * One wheel notch = WheelLineSize points, hard-clamped, animated over AutoScrollingSpeedMs.
236
- * Notches arriving while the previous one is still animating add onto its target, so a fast wheel
237
- * spin travels N steps instead of restarting from the barely-moved current offset.
235
+ * One wheel notch = WheelLineSize points, hard-clamped, animated over AutoScrollingSpeedMs. An event moves by its share
236
+ * of a notch (Canvas.WheelDeltaPerNotch), so a touchpad's many small events scroll as far as the fingers moved (C#
237
+ * aaa871c3); an event under half a notch moves the content at once (no glide). Events arriving while the previous one is still animating add onto its target, so a fast wheel spin
238
+ * travels N steps instead of restarting from the barely-moved current offset.
238
239
  */
239
240
  private ApplyWheelScroll;
240
241
  }