drawnui-react 0.1.0-preview.16 → 0.1.0-preview.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SKIPPED.md +7 -7
- package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
- package/dist/controls/AnimatedFramesRenderer.js +16 -1
- package/dist/controls/DrawnGame.d.ts +4 -1
- package/dist/controls/DrawnGame.js +7 -3
- package/dist/controls/GridStructure.d.ts +13 -1
- package/dist/controls/GridStructure.js +38 -2
- package/dist/controls/SkiaButton.d.ts +12 -2
- package/dist/controls/SkiaButton.js +19 -6
- package/dist/controls/SkiaCarousel.d.ts +1 -1
- package/dist/controls/SkiaCarousel.js +3 -2
- package/dist/controls/SkiaEditor.d.ts +5 -1
- package/dist/controls/SkiaEditor.js +19 -17
- package/dist/controls/SkiaGif.d.ts +3 -0
- package/dist/controls/SkiaGif.js +11 -1
- package/dist/controls/SkiaImage.js +15 -10
- package/dist/controls/SkiaLabel.d.ts +52 -3
- package/dist/controls/SkiaLabel.js +222 -50
- package/dist/controls/SkiaLayout.d.ts +5 -0
- package/dist/controls/SkiaLayout.js +7 -4
- package/dist/controls/SkiaLottie.js +3 -1
- package/dist/controls/SkiaProgress.d.ts +2 -1
- package/dist/controls/SkiaProgress.js +4 -1
- package/dist/controls/SkiaRadioButton.js +3 -1
- package/dist/controls/SkiaScroll.d.ts +17 -1
- package/dist/controls/SkiaScroll.js +84 -9
- package/dist/controls/SkiaShape.d.ts +13 -2
- package/dist/controls/SkiaShape.js +50 -10
- package/dist/controls/SkiaSlider.d.ts +10 -2
- package/dist/controls/SkiaSlider.js +15 -2
- package/dist/controls/SkiaSprite.d.ts +1 -0
- package/dist/controls/SkiaSprite.js +4 -0
- package/dist/controls/SkiaToggle.d.ts +5 -1
- package/dist/controls/SkiaToggle.js +11 -2
- package/dist/controls/SnappingLayout.d.ts +10 -1
- package/dist/controls/SnappingLayout.js +16 -1
- package/dist/core/Accessibility.d.ts +13 -0
- package/dist/core/Accessibility.js +10 -4
- package/dist/core/Animators.d.ts +28 -1
- package/dist/core/Animators.js +76 -8
- package/dist/core/Canvas.d.ts +21 -4
- package/dist/core/Canvas.js +122 -23
- package/dist/core/Easing.d.ts +12 -1
- package/dist/core/Easing.js +25 -1
- package/dist/core/Gestures.d.ts +24 -1
- package/dist/core/Gestures.js +75 -2
- package/dist/core/Hover.d.ts +14 -4
- package/dist/core/Hover.js +38 -22
- package/dist/core/ScrollAnimators.js +9 -0
- package/dist/core/SkiaControl.d.ts +27 -6
- package/dist/core/SkiaControl.js +81 -38
- package/dist/react/SkiaShell.js +33 -14
- package/dist/react/index.js +29 -2
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ await Super.UseDrawnUi()
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
npm i drawnui-react
|
|
30
|
+
npm i drawnui-react react react-dom
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
`drawnui-react` = React tags + every engine type, `drawnui-react/core` = the engine only. Ships ES modules + `.d.ts`;
|
package/SKIPPED.md
CHANGED
|
@@ -10,11 +10,11 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
10
10
|
| `UseCache` | partial | `None`, `Operations` / `OperationsFull` (SkPicture replay), `Image` (offscreen surface snapshot, GPU-backed on WebGL, nearest-sampled blit), `ImageDoubleBuffered` (`RenderObjectPrevious` kept and drawn when a new cache cannot be produced yet, `DrawPlaceholder(ctx)` hook when there is none; recording itself is synchronous — no background thread, same as DrawnUi.Blazor with `CanUseCacheDoubleBuffering = false`), `ImageComposite` / `ImageCompositeGPU` (per-child composite, see the `UseCache="ImageComposite"` row below). `GPU` resolves to `Image` and `ImageCompositeGPU` to `ImageComposite`: no separate GPU-only path. `Super.CacheEnabled`, `RenderObject`/`CachedObject`, `InvalidateCache`, `DestroyRenderingObject`, `Canvas.DisposeObject` (deleted after flush). Defaults per C#: `SkiaLabel`/`SkiaSvg` Operations, everything else None. Not ported: `CacheSharing`, `AllowCaching`, offscreen bake threads, cache validity by GRContext. |
|
|
11
11
|
| Incremental invalidation | partial | `Measure` returns the previous size when `!NeedMeasure` and constraints+scale are unchanged; `Update()`/`InvalidateMeasure()` bubble up and stale every ancestor cache; `Repaint()` keeps caches. Arrange still runs per frame for the whole tree; no dirty regions, no `DirtyChildrenTracker`. Post-animators on a control nested INSIDE a cached ancestor are drawn only when that ancestor re-records. |
|
|
12
12
|
| Gestures core: `Canvas.Gestures` (Disabled/Enabled/Lock), `ProcessGestures(args, apply)`, `ConsumeGestures`, `Tapped`/`ChildTapped`, `InputTransparent`, `BlockGesturesBelow`, `LockChildrenGestures`, `HitBoxAuto`/`HitIsInside`/`IsGestureForChild`, `SkiaGesturesParameters`/`GestureEventProcessingInfo`/`SkiaGesturesInfo`/`ControlTappedEventArgs` | ported | Pointer events -> per-pointer `OnTouchAction` state machine (port of DrawnUi.Blazor) -> Down/Panning/Tapped/Up, queued and processed at frame start. Tap slop = 16pt like AppoMobi TouchEffect. The control that consumes a gesture keeps receiving it until it lets go (DrawnUi `Canvas.HadInput`, one pointer here instead of C#'s set): without it a drag handle, a slider thumb or a held button stops getting `Panning` the moment the finger leaves its box, and a control that stops consuming hands routing straight back to the tree (a button whose press turns into a scroll pan). Every mouse button is delivered (`Event.Pointer`: `Button`, `ButtonNumber`, `DeviceType`, `PressedButtons`, the AppoMobi.Gestures PointerData): a right click is Down / Up / Tapped with `Button` "Right" plus a `ContextMenu`; handlers that must be primary-only check `e.Parameters.Event.Pointer?.Button`. |
|
|
13
|
-
| Gestures: `
|
|
13
|
+
| Gestures: `Pointer` (routed as a gesture; hover itself is ported, see `ReceivesHover`; `LongPressing` is produced since 2026-10-10: `Canvas.LongPressTimeMs` 1500 held still, no Tapped after it), multi-touch pinch, `SoftLock`, `AddGestures` attached props, `OnGestures` delegate | skipped | The enum members exist for parity and are never produced; there is no pinch member at all. Ported instead of skipped: velocity (`Gestures.ts` fills `Distance.Velocity` per move, the scroll / snapping physics run on it) and transform-aware mapping — a child receives the point through the inverse `RenderTransformMatrix` (`TransformPointToLocalSpace`), so rotated / scaled / translated controls hit-test correctly. `ChildOffset` stays zero because that inverse matrix replaces the C# cache-offset math (`TranslateInputCoords`), not because transforms are missing. `Wheel` IS produced (`Canvas.onWheel`). |
|
|
14
14
|
| C# multi-subscriber `event`s | changed shape | One callback per event prop (`Tapped={fn}`), same names. `Command*` (ICommand) variants not ported. |
|
|
15
15
|
| `Opacity`, `TranslationX/Y`, `Rotation`, `ScaleX/Y` (+`Scale`), `SkewX/Y`, `AnchorX/Y`, `HasTransform`, `RenderTransformMatrix`, `TransformPointToLocalSpace` | ported | Same matrix order as C# `ApplyTransforms` (pivot → rotation → scale/skew → translation), opacity = `saveLayer` alpha over the subtree, gestures mapped through the inverse matrix (`IsGestureForChild`, child receives the local point), accessibility rect = transformed bounds. Animations: `AnimateAsync`, `AnimateRangeAsync`, `FadeToAsync`, `ScaleToAsync`, `TranslateToAsync`, `RotateToAsync` as Promises with `AbortSignal` (one running per kind, like the C# per-property cancellation). Not ported: `RotationX/Y/Z`, `TranslationZ`, `Perspective1/2`, `CameraAngle`, `LinkTransforms`, `AddTranslationX/Y`, `UseTranslation*` overrides, `CustomizeLayerPaint`, `SkipRenderingOutOfBounds`. |
|
|
16
16
|
| `Left` / `Top` | ported (as a translate) | Same paint-time offset in points, but folded into the render matrix (a `canvas.translate`, hit-testing follows) for every control, cached or not; C# applies them only when blitting a cache. |
|
|
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`; `
|
|
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`; default cache `Operations` (C# ImageDoubleBuffered, see PARITY "Lottie and GIF default caches"). GIF on CanvasKit AnimatedImage: every frame decoded once (`GifAnimation`: `SeekFrame`, `GetFrameNumber`, `DurationMs`, `TotalFrames`), `Aspect` (AspectFitFill), alignment, `Success`/`Error`, default cache `None` (C# ImageDoubleBuffered). 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
18
|
| `SkiaBackdrop` | ported | `Blur` (5), `Brightness` (gamma), `UseContext`, `BackgroundColor` tint, `HasEffects`, `GetImage`; children drawn first and blurred with the snapshot (C# order); `UseContext` snapshots the surface the pixels end up on (on-screen, or the nearest Image cache surface through its `Origin`), also while an Operations picture is being recorded, since caches record inline; after each paint the ancestors' caches are staled for the next frame (no frame requested) so content that changes beneath without invalidating this branch (an image loading later) is picked up by the next frame. Used by the shell popups/modals (`ShellDefaults.PopupsBackgroundBlur`). Not ported: `CacheSource`, the ghost mode. |
|
|
19
19
|
| Disposal (`Dispose` / `OnDisposing`) | ported | The React renderer calls `Dispose()` when an element leaves the tree (`detachDeletedInstance`): caches, gradient shaders, running `*ToAsync` animations, overlay effects, the accessibility node, then the children (static and templated pool); `SkiaScroll`/`SnappingLayout` stop their animators, `SkiaSvg` drops its raster, `SkiaLottie`/`SkiaGif` free the Skottie animation / frames; `Canvas.Dispose` disposes the content. Not ported: `IsDisposing` staged disposal, `Disposing` event, `OnWillDisposeWithChildren`. |
|
|
20
20
|
| Hover: `ReceivesHover` (opt-in; on by default in SkiaButton, SkiaSlider, the toggles, SkiaRadioButton, SkiaCarousel, SkiaDrawer), `IsHovered`, `HoverChanged` (turns `ReceivesHover` on, as C# `OnHovered`) | ported | drawnui-cross 6m: every opted-in control under the mouse is hovered; nothing changes while a scroll, carousel or drawer moves its content, one check at the last pointer position when it stops and after an add / remove, a rebind, a hidden hovered control or an instant scroll jump; pointerleave clears at once; touch never hovers (`Canvas.Hover`, a `HoverManager`). Not ported: C# `SetHover`, `CheckHovered` / `OnHover` / `IsPointerOver` (no Pointer gestures are routed), SkiaSpinner / wheel picker (no such controls). |
|
|
@@ -71,14 +71,14 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
71
71
|
| `MeasureItemsStrategy="MeasureVisible"` + background measurement | ported | Initial pass measures enough items to fill the viewport (≥ `BackgroundMeasurementBatchSize`), content extent = exact prefix + average × remaining (C# MeasureList estimate), visible cells measured on demand and laid out with their real heights, the prefix grows in `requestIdleCallback` slices (time-budgeted, then one `InvalidateMeasure`), `LastMeasuredIndex`, `DebugString` shows `measured n/N`. Not ported: cancellation/`_pendingStructureChanges` interplay, `UsePreparedViews`, windowed sources, anchor correction when an estimate above the viewport changes (content may shift by the delta). |
|
|
72
72
|
| `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. |
|
|
73
73
|
| `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. |
|
|
74
|
-
| `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`, `
|
|
74
|
+
| `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. `CharacterSpacing` (C#: (value - 1) points after every glyph, measured, wrapped and cut at the drawn width), Chinese / Japanese line breaks between characters with kinsoku (C# FitAtBreak, `CanBreakInsideWord`). Not ported: `AutoFont`, `AutoSize`/`AutoSizeText`, `DropShadow*`, text `StrokeColor`/`StrokeWidth`/`StrokeGradient`, `GradientByLines`, `MonoForDigits`, `KeepSpacesOnLineBreaks`, `Format`, RTL/bidi, hyphenation, `SkiaRichLabel`/markdown. |
|
|
75
75
|
| `SkiaHotspot` | ported | Fill/Fill, `Tapped`, `Down`, `Up`, `LockPanning`, `TouchDown`; consumes only Tapped like the C# one. No `AnimationTapped`/ripple/shimmer. |
|
|
76
|
-
| `SkiaButton` | partial | Default look only: rounded frame radius 8 (hardcoded like the C# default content), centered label, `Text`/`TextColor`/`FontSize`/`FontFamily`/`BackgroundColor`/`IsPressed`/`IsDisabled
|
|
76
|
+
| `SkiaButton` | partial | Default look only: rounded frame radius 8 (hardcoded like the C# default content), centered label, `Text`/`TextColor`/`FontSize`/`FontFamily`/`BackgroundColor`/`IsPressed`/`RespondsToGestures` (`IsDisabled` = its obsolete opposite)/`LockPanning`, `Tapped`/`Down`/`Up`. `RespondsToGestures = false`: no press, no `Tapped`/`Down`/`Up`, a press or tap on it stops there, keyboard skips it, reported disabled (drawnui-cross 6w). `IsPressed` is tracked but has no visual; press feedback = `ApplyEffect="Ripple"`. No `ButtonStyle` platform looks, icons, `TextCase`, elevation, shimmer, `BtnText`/`BtnShape` templating. |
|
|
77
77
|
| `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. |
|
|
78
78
|
| `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). |
|
|
79
79
|
| `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. |
|
|
80
80
|
| `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). |
|
|
81
|
-
| `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`.
|
|
81
|
+
| `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`. `ScrollToIndex` Start / Center / End as an open order (C# OrderedScrollToIndex): a row not measured yet is gone to by the estimate and aimed again every frame until its place is measured and the viewport stands on it; a press, the wheel or another scroll drops it; LoadMore waits while it is open. Controls outside the clip are skipped with their subtree (C# stack Virtualisation, for every control; see PARITY). Not 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. |
|
|
82
82
|
| `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. |
|
|
83
83
|
| `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. |
|
|
84
84
|
| `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. |
|
|
@@ -103,9 +103,9 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
|
|
|
103
103
|
| Area | Status | Notes |
|
|
104
104
|
|---|---|---|
|
|
105
105
|
| Custom `react-reconciler` renderer, PascalCase props mapped 1:1 to control properties | ported | |
|
|
106
|
-
| `SkiaToggle` / `SkiaSwitch` / `SkiaCheckbox` / `SkiaRadioButton` | ported | Same names: `IsToggled`, `Toggled`, `DefaultValue`, `IsAnimated`, `RespondsToGestures
|
|
106
|
+
| `SkiaToggle` / `SkiaSwitch` / `SkiaCheckbox` / `SkiaRadioButton` | ported | Same names: `IsToggled`, `Toggled`, `DefaultValue`, `IsAnimated`, `RespondsToGestures` (false: a tap does not toggle it and stops there, keyboard skips it, reported disabled), `ColorThumbOn/Off`, `ColorFrameOn/Off` (+ `ColorCheckOn`), `ControlStyle` with the Default / Cupertino / Material / Material3 / Windows looks and geometry of the C# style builders (changing it after the first measure rebuilds the content and re-pins only the sizes the style set, C# `RebuildDefaultContent`) (`Frame`/`Thumb`, `FrameOff`/`FrameOn`/`ViewCheckOn`, `On` ring + `Text`), thumb animated with `TranslateToAsync` (`AnimationSpeed` 200 ms), radio groups by `GroupName` or parent. Not ported: `CommandToggled`/`CommandTapped`, hover (`CheckHovered`), `TransformView` hotspot (the toggle itself takes the tap), `Platform` style resolves from the browser UA. |
|
|
107
107
|
| `SkiaControl.ExecuteAfterCreated` / fluent `.Initialize(...)` | skipped | C# runs it once when the control gets its parent so code-behind can reach `.Assign`'d refs; in React the element tree is declarative and `ref` / `useEffect` cover it. |
|
|
108
|
-
| `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
|
+
| `SkiaProgress` / `SkiaSlider` | ported (painted, not composed) | Screen readers: `AccessibilityValue` (aria-valuenow / min / max, valuetext "65%" or "20 – 80", a slider horizontal), the name only from `AccessibilityLabel` (drawnui-cross 6c). `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. |
|
|
109
109
|
| `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. |
|
|
110
110
|
| `SkiaButton.FontFamilyFallback` | React extension | Pass-through to the inner label so icon-glyph buttons render with `AddSymbols()` faces. |
|
|
111
111
|
| 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. |
|
|
@@ -38,6 +38,13 @@ export declare class AnimatedFramesRenderer extends SkiaControl {
|
|
|
38
38
|
protected OnLayoutChanged(): void;
|
|
39
39
|
PlayIfNeeded(): void;
|
|
40
40
|
InitializeAnimator(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Animation time (ms, the animator's range) of the next change of what is shown after `value`, or undefined when it
|
|
43
|
+
* may change every frame (Lottie). A GIF or a sprite returns its next frame's start.
|
|
44
|
+
*/
|
|
45
|
+
protected NextChangeMs(_value: number): number | undefined;
|
|
46
|
+
/** The animator sleeps until the next change: no ticks and no frames in between (DrawnUi.Rust sleeping frame animators). */
|
|
47
|
+
private SleepUntilNextChange;
|
|
41
48
|
/** Configure the animator range/speed once the frames source is known. */
|
|
42
49
|
protected OnAnimatorInitializing(): void;
|
|
43
50
|
/** The animator produced a new frame value. */
|
|
@@ -69,7 +69,7 @@ export class AnimatedFramesRenderer extends SkiaControl {
|
|
|
69
69
|
InitializeAnimator() {
|
|
70
70
|
if (!this.Animator) {
|
|
71
71
|
const a = new SkiaValueAnimator(this);
|
|
72
|
-
a.OnUpdated = (v) => this.OnAnimatorUpdated(v);
|
|
72
|
+
a.OnUpdated = (v) => { this.OnAnimatorUpdated(v); this.SleepUntilNextChange(a, v); };
|
|
73
73
|
a.OnStart = () => { this.WasStarted = true; this.OnStarted(); };
|
|
74
74
|
a.OnStop = () => { if (this.WasStarted)
|
|
75
75
|
this.OnFinished(); this.WasStarted = false; };
|
|
@@ -82,6 +82,21 @@ export class AnimatedFramesRenderer extends SkiaControl {
|
|
|
82
82
|
this.Start();
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Animation time (ms, the animator's range) of the next change of what is shown after `value`, or undefined when it
|
|
87
|
+
* may change every frame (Lottie). A GIF or a sprite returns its next frame's start.
|
|
88
|
+
*/
|
|
89
|
+
NextChangeMs(_value) { return undefined; }
|
|
90
|
+
/** The animator sleeps until the next change: no ticks and no frames in between (DrawnUi.Rust sleeping frame animators). */
|
|
91
|
+
SleepUntilNextChange(a, value) {
|
|
92
|
+
const next = this.NextChangeMs(value), range = a.mMaxValue - a.mMinValue;
|
|
93
|
+
if (next === undefined || !(range > 0) || !(a.Speed > 0) || next <= value) {
|
|
94
|
+
a.SleepUntilNanos = 0;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const wallMs = (Math.min(next, a.mMaxValue) - value) * (a.Speed / range);
|
|
98
|
+
a.SleepUntilNanos = a.LastFrameTimeNanos + wallMs * 1_000_000;
|
|
99
|
+
}
|
|
85
100
|
/** Configure the animator range/speed once the frames source is known. */
|
|
86
101
|
OnAnimatorInitializing() { }
|
|
87
102
|
/** The animator produced a new frame value. */
|
|
@@ -24,7 +24,10 @@ export declare class DrawnGame extends SkiaLayout {
|
|
|
24
24
|
StopLoop(): void;
|
|
25
25
|
/** Starts the game loop. */
|
|
26
26
|
StartLoop(delayMs?: number): void;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Internal, override `GameLoop` for your game. Frame time is in nanoseconds. The first frame after StartLoop or
|
|
29
|
+
* Resume gets a delta of 0, not the time since the clock's origin or since the stop (DrawnUi.Rust game_loop).
|
|
30
|
+
*/
|
|
28
31
|
protected GameTick(frameTimeNanos: number): void;
|
|
29
32
|
get IsPaused(): boolean;
|
|
30
33
|
set IsPaused(value: boolean);
|
|
@@ -34,11 +34,15 @@ export class DrawnGame extends SkiaLayout {
|
|
|
34
34
|
/** Starts the game loop. */
|
|
35
35
|
StartLoop(delayMs = 0) {
|
|
36
36
|
this.appLoop ??= new ActionOnTickAnimator(this, (t) => this.GameTick(t));
|
|
37
|
+
this.LastFrameTimeNanos = 0; // the first frame of a (re)started loop has no previous one: its delta is 0
|
|
37
38
|
this.appLoop.Start(delayMs);
|
|
38
39
|
}
|
|
39
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Internal, override `GameLoop` for your game. Frame time is in nanoseconds. The first frame after StartLoop or
|
|
42
|
+
* Resume gets a delta of 0, not the time since the clock's origin or since the stop (DrawnUi.Rust game_loop).
|
|
43
|
+
*/
|
|
40
44
|
GameTick(frameTimeNanos) {
|
|
41
|
-
const deltaSeconds = (frameTimeNanos - this.LastFrameTimeNanos) / 1_000_000_000;
|
|
45
|
+
const deltaSeconds = this.LastFrameTimeNanos === 0 ? 0 : (frameTimeNanos - this.LastFrameTimeNanos) / 1_000_000_000;
|
|
42
46
|
this.LastFrameTimeNanos = frameTimeNanos;
|
|
43
47
|
this.GameLoop(deltaSeconds);
|
|
44
48
|
}
|
|
@@ -49,7 +53,7 @@ export class DrawnGame extends SkiaLayout {
|
|
|
49
53
|
this.OnPaused();
|
|
50
54
|
}
|
|
51
55
|
Resume() {
|
|
52
|
-
this.LastFrameTimeNanos =
|
|
56
|
+
this.LastFrameTimeNanos = 0; // the frame clock restarts: the next delta is 0
|
|
53
57
|
this.IsPaused = false;
|
|
54
58
|
this.OnResumed();
|
|
55
59
|
}
|
|
@@ -17,7 +17,7 @@ export declare class DefinitionInfo {
|
|
|
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
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">;
|
|
20
|
+
export type GridChild = Pick<SkiaControl, "IsVisible" | "Row" | "Column" | "RowSpan" | "ColumnSpan" | "HorizontalOptions" | "VerticalOptions" | "MinimumWidthRequest" | "MinimumHeightRequest" | "HeightRequest" | "Margin" | "MeasuredSize" | "Measure">;
|
|
21
21
|
/**
|
|
22
22
|
* Port of DrawnUi SkiaGridStructure (itself adapted from the MAUI Grid manager). Works in POINTS: constraints come in
|
|
23
23
|
* already reduced by the grid's padding, cells are returned relative to the padded box. Children are measured in
|
|
@@ -65,6 +65,18 @@ export declare class SkiaGridStructure {
|
|
|
65
65
|
private ResolveStars;
|
|
66
66
|
private ResolveStarColumns;
|
|
67
67
|
private ResolveStarRows;
|
|
68
|
+
/**
|
|
69
|
+
* C# SizesAutoRow: the child decides the height of its single Auto row, so it is measured with the height that row
|
|
70
|
+
* could still grow to, not with the row's current size (drawnui-cross 6p).
|
|
71
|
+
*/
|
|
72
|
+
private SizesAutoRow;
|
|
73
|
+
private static NeedFillY;
|
|
74
|
+
/**
|
|
75
|
+
* C# HeightOnAutoRow: the height the single Auto row could still grow to, unbounded for a vertical Fill child (which
|
|
76
|
+
* would take the whole constraint). The row's size so far came from a first pass at the grid's whole width, so a
|
|
77
|
+
* label wrapping in a star column was measured against its one-line height.
|
|
78
|
+
*/
|
|
79
|
+
private HeightOnAutoRow;
|
|
68
80
|
private MeasureKnownCells;
|
|
69
81
|
private AvailableWidth;
|
|
70
82
|
private AvailableHeight;
|
|
@@ -294,7 +294,20 @@ export class SkiaGridStructure {
|
|
|
294
294
|
ResolveStarRows(heightConstraint) {
|
|
295
295
|
this.ResolveStars(this.Rows, heightConstraint - this.GridHeight(), (c) => c.IsRowSpanStar, (v) => v.MeasuredSize.Units.Height);
|
|
296
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* C# SizesAutoRow: the child decides the height of its single Auto row, so it is measured with the height that row
|
|
299
|
+
* could still grow to, not with the row's current size (drawnui-cross 6p).
|
|
300
|
+
*/
|
|
301
|
+
SizesAutoRow(cell) { return cell.RowSpan === 1 && this.Rows[cell.Row].IsAuto; }
|
|
302
|
+
static NeedFillY(child) { return child.VerticalOptions === "Fill" && child.HeightRequest < 0; }
|
|
303
|
+
/**
|
|
304
|
+
* C# HeightOnAutoRow: the height the single Auto row could still grow to, unbounded for a vertical Fill child (which
|
|
305
|
+
* would take the whole constraint). The row's size so far came from a first pass at the grid's whole width, so a
|
|
306
|
+
* label wrapping in a star column was measured against its one-line height.
|
|
307
|
+
*/
|
|
308
|
+
HeightOnAutoRow(cell, child) { return SkiaGridStructure.NeedFillY(child) ? Infinity : this.AvailableHeight(cell); }
|
|
297
309
|
MeasureKnownCells() {
|
|
310
|
+
let autoRowGrew = false;
|
|
298
311
|
for (const cell of this.cells) {
|
|
299
312
|
if (!cell.NeedsKnownMeasurePass)
|
|
300
313
|
continue;
|
|
@@ -307,12 +320,30 @@ export class SkiaGridStructure {
|
|
|
307
320
|
continue;
|
|
308
321
|
const child = this.children[cell.ViewIndex];
|
|
309
322
|
const rect = this.GetCellBoundsFor(child);
|
|
310
|
-
|
|
323
|
+
if (child.IsVisible && this.SizesAutoRow(cell)) {
|
|
324
|
+
// the row grows to the child at its real width, within the room the grid has left
|
|
325
|
+
let grown = this.MeasurePx(child, rect.Width, this.HeightOnAutoRow(cell, child)).Height;
|
|
326
|
+
const available = this.AvailableHeight(cell);
|
|
327
|
+
if (isFinite(available) && grown > available)
|
|
328
|
+
grown = available;
|
|
329
|
+
if (grown > this.Rows[cell.Row].Size) {
|
|
330
|
+
this.Rows[cell.Row].Update(grown);
|
|
331
|
+
autoRowGrew = true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else
|
|
335
|
+
this.MeasurePx(child, rect.Width, rect.Height);
|
|
311
336
|
if (cell.IsColumnSpanStar && cell.ColumnSpan > 1)
|
|
312
337
|
this.TrackSpan({ Key: `c${cell.Column}:${cell.ColumnSpan}`, Start: cell.Column, Length: cell.ColumnSpan, IsColumn: true, Requested: rect.Width });
|
|
313
338
|
if (cell.IsRowSpanStar && cell.RowSpan > 1)
|
|
314
339
|
this.TrackSpan({ Key: `r${cell.Row}:${cell.RowSpan}`, Start: cell.Row, Length: cell.RowSpan, IsColumn: false, Requested: rect.Height });
|
|
315
340
|
}
|
|
341
|
+
if (autoRowGrew) {
|
|
342
|
+
// star rows were resolved against the smaller Auto rows: they get what is left now, nothing when Auto took it all
|
|
343
|
+
SkiaGridStructure.ZeroOutStars(this.Rows);
|
|
344
|
+
if (this.heightConstraint - this.GridHeight() > 0)
|
|
345
|
+
this.ResolveStarRows(this.heightConstraint);
|
|
346
|
+
}
|
|
316
347
|
}
|
|
317
348
|
AvailableWidth(cell) {
|
|
318
349
|
let w = 0, absolute = true;
|
|
@@ -390,10 +421,15 @@ export class SkiaGridStructure {
|
|
|
390
421
|
RemeasureChildrenAtFinalCells() {
|
|
391
422
|
for (const cell of this.cells) {
|
|
392
423
|
const child = this.children[cell.ViewIndex];
|
|
424
|
+
if (!child.IsVisible)
|
|
425
|
+
continue;
|
|
393
426
|
const rect = this.GetCellBoundsFor(child);
|
|
394
427
|
if (rect.Width <= 0 || rect.Height <= 0)
|
|
395
428
|
continue;
|
|
396
|
-
|
|
429
|
+
// on an Auto row a child that is not Fill vertically keeps the height the row can grow to, as in
|
|
430
|
+
// MeasureKnownCells: the same request returns at once, and wrapped text is not cut to the row
|
|
431
|
+
const height = this.SizesAutoRow(cell) && !SkiaGridStructure.NeedFillY(child) ? Math.max(rect.Height, this.AvailableHeight(cell)) : rect.Height;
|
|
432
|
+
const m = this.MeasurePx(child, rect.Width, height);
|
|
397
433
|
if (cell.ColumnSpan === 1 && this.Columns[cell.Column].IsAuto && m.Width > this.Columns[cell.Column].Size)
|
|
398
434
|
this.Columns[cell.Column].Update(m.Width);
|
|
399
435
|
if (cell.RowSpan === 1 && this.Rows[cell.Row].IsAuto && m.Height > this.Rows[cell.Row].Size)
|
|
@@ -26,7 +26,17 @@ export declare class SkiaButton extends SkiaLayout {
|
|
|
26
26
|
StrokeColor: Color;
|
|
27
27
|
StrokeWidth: number;
|
|
28
28
|
IsPressed: boolean;
|
|
29
|
-
|
|
29
|
+
private respondsToGestures;
|
|
30
|
+
/**
|
|
31
|
+
* False switches the button off for the user: taps, presses and keyboard activation do nothing (no Tapped, Down, Up),
|
|
32
|
+
* a tap on it does not reach controls under it, and screen readers report it disabled. Code can still change it.
|
|
33
|
+
* Same property as on toggles, sliders, scrolls and drawers (drawnui-cross 6w). Default true.
|
|
34
|
+
*/
|
|
35
|
+
get RespondsToGestures(): boolean;
|
|
36
|
+
set RespondsToGestures(v: boolean);
|
|
37
|
+
/** @deprecated The opposite of RespondsToGestures, which every control uses: set `RespondsToGestures = false`. */
|
|
38
|
+
get IsDisabled(): boolean;
|
|
39
|
+
set IsDisabled(v: boolean);
|
|
30
40
|
LockPanning: boolean;
|
|
31
41
|
/** Touch feedback played on Down (DrawnUi SkiaButton.ApplyEffect). */
|
|
32
42
|
ApplyEffect: SkiaTouchAnimation;
|
|
@@ -43,7 +53,7 @@ export declare class SkiaButton extends SkiaLayout {
|
|
|
43
53
|
static DefaultAccessibilityRole?: string;
|
|
44
54
|
protected DefaultAccessibilityLabel(): string | undefined;
|
|
45
55
|
protected DefaultAccessibilityCanInteract(): boolean;
|
|
46
|
-
/**
|
|
56
|
+
/** With RespondsToGestures = false the button ignores taps, keyboard navigation skips it and it is reported disabled. */
|
|
47
57
|
protected AcceptsInput(): boolean;
|
|
48
58
|
/** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
|
|
49
59
|
protected PaintBackground(): void;
|
|
@@ -26,7 +26,20 @@ export class SkiaButton extends SkiaLayout {
|
|
|
26
26
|
StrokeColor = Colors.Transparent;
|
|
27
27
|
StrokeWidth = 0;
|
|
28
28
|
IsPressed = false;
|
|
29
|
-
|
|
29
|
+
respondsToGestures = true;
|
|
30
|
+
/**
|
|
31
|
+
* False switches the button off for the user: taps, presses and keyboard activation do nothing (no Tapped, Down, Up),
|
|
32
|
+
* a tap on it does not reach controls under it, and screen readers report it disabled. Code can still change it.
|
|
33
|
+
* Same property as on toggles, sliders, scrolls and drawers (drawnui-cross 6w). Default true.
|
|
34
|
+
*/
|
|
35
|
+
get RespondsToGestures() { return this.respondsToGestures; }
|
|
36
|
+
set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
|
|
37
|
+
this.respondsToGestures = v;
|
|
38
|
+
this.NotifyAccessibility();
|
|
39
|
+
} }
|
|
40
|
+
/** @deprecated The opposite of RespondsToGestures, which every control uses: set `RespondsToGestures = false`. */
|
|
41
|
+
get IsDisabled() { return !this.respondsToGestures; }
|
|
42
|
+
set IsDisabled(v) { this.RespondsToGestures = !v; }
|
|
30
43
|
LockPanning = false;
|
|
31
44
|
/** Touch feedback played on Down (DrawnUi SkiaButton.ApplyEffect). */
|
|
32
45
|
ApplyEffect = "Ripple"; // C# default
|
|
@@ -60,9 +73,9 @@ export class SkiaButton extends SkiaLayout {
|
|
|
60
73
|
/** Set to `Aria.RoleButton` to expose every button (React extension). */
|
|
61
74
|
static DefaultAccessibilityRole;
|
|
62
75
|
DefaultAccessibilityLabel() { return this.Text || undefined; }
|
|
63
|
-
DefaultAccessibilityCanInteract() { return
|
|
64
|
-
/**
|
|
65
|
-
AcceptsInput() { return
|
|
76
|
+
DefaultAccessibilityCanInteract() { return this.respondsToGestures; }
|
|
77
|
+
/** With RespondsToGestures = false the button ignores taps, keyboard navigation skips it and it is reported disabled. */
|
|
78
|
+
AcceptsInput() { return this.respondsToGestures; }
|
|
66
79
|
/** The button's own BackgroundColor/CornerRadius/Stroke are the frame's; the button itself paints nothing. */
|
|
67
80
|
PaintBackground() { }
|
|
68
81
|
get UsingControlStyle() { return ResolveControlStyle(this.ControlStyle); }
|
|
@@ -130,8 +143,8 @@ export class SkiaButton extends SkiaLayout {
|
|
|
130
143
|
this.Repaint();
|
|
131
144
|
}
|
|
132
145
|
ProcessGestures(args, apply) {
|
|
133
|
-
if (this.
|
|
134
|
-
return
|
|
146
|
+
if (!this.respondsToGestures)
|
|
147
|
+
return this.BlockedGestureResult(args); // no press, no Down / Up / Tapped; a tap on it stops here
|
|
135
148
|
const point = args.Event.Location;
|
|
136
149
|
if (args.Type === "Down") {
|
|
137
150
|
this.IsPressed = true;
|
|
@@ -101,7 +101,7 @@ export declare class SkiaCarousel extends SnappingLayout {
|
|
|
101
101
|
private GetVirtualSnapPoints;
|
|
102
102
|
protected GetVirtualAnchor(current: SKPoint): SnapPoint;
|
|
103
103
|
protected FindNearestAnchorInternal(current: SKPoint, velocity: SKPoint): SKPoint;
|
|
104
|
-
/** C# SkiaCarousel.ScrollToNearestAnchor: velocity below
|
|
104
|
+
/** C# SkiaCarousel.ScrollToNearestAnchor: velocity below SnapVelocityThreshold counts as none; looped snaps around virtual anchors. */
|
|
105
105
|
ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
|
|
106
106
|
/** C# SkiaCarousel.SelectNextAnchor: base choice, plus a wrap to the virtual anchor at the strip borders when looped. */
|
|
107
107
|
SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
|
|
@@ -232,9 +232,10 @@ export class SkiaCarousel extends SnappingLayout {
|
|
|
232
232
|
}
|
|
233
233
|
return super.FindNearestAnchorInternal(current, velocity);
|
|
234
234
|
}
|
|
235
|
-
/** C# SkiaCarousel.ScrollToNearestAnchor: velocity below
|
|
235
|
+
/** C# SkiaCarousel.ScrollToNearestAnchor: velocity below SnapVelocityThreshold counts as none; looped snaps around virtual anchors. */
|
|
236
236
|
ScrollToNearestAnchor(location, velocity) {
|
|
237
|
-
|
|
237
|
+
const t = this.SnapVelocityThreshold;
|
|
238
|
+
velocity = new SKPoint(Math.abs(velocity.X) < t ? 0 : velocity.X, Math.abs(velocity.Y) < t ? 0 : velocity.Y);
|
|
238
239
|
if (this.ApplyLoopedLogic) {
|
|
239
240
|
const origin = this.FindNearestAnchorInternal(location, velocity);
|
|
240
241
|
const target = this.SelectNextAnchor(origin, velocity);
|
|
@@ -58,7 +58,8 @@ export declare class SkiaEditor extends SkiaShape {
|
|
|
58
58
|
private readonly placeholder;
|
|
59
59
|
private scrollX;
|
|
60
60
|
private scrollY;
|
|
61
|
-
|
|
61
|
+
/** The caret blinks on a timer, asleep between blinks: no frame runs while it waits (DrawnUi.Rust editor_rules). */
|
|
62
|
+
private blinkTimer;
|
|
62
63
|
private caretVisible;
|
|
63
64
|
private subscribed;
|
|
64
65
|
private stubSelectionStop;
|
|
@@ -102,6 +103,9 @@ export declare class SkiaEditor extends SkiaShape {
|
|
|
102
103
|
set CursorPosition(v: number);
|
|
103
104
|
get SelectionLength(): number;
|
|
104
105
|
set SelectionLength(v: number);
|
|
106
|
+
/** Shows the caret for a full half second, then blinks every 500 ms. */
|
|
107
|
+
private StartBlink;
|
|
108
|
+
private StopBlink;
|
|
105
109
|
private OnCursorMoved;
|
|
106
110
|
get IsFocused(): boolean;
|
|
107
111
|
set IsFocused(v: boolean);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SKPoint } from "../core/Gestures";
|
|
2
2
|
import { Super } from "../core/Super";
|
|
3
|
-
import { SkiaValueAnimator } from "../core/Animators";
|
|
4
3
|
import { ResolveControlStyle } from "../core/ControlStyle";
|
|
5
4
|
import { KeyboardManager } from "../core/KeyboardManager";
|
|
6
5
|
import { TextInputProxy } from "../core/TextInputProxy";
|
|
@@ -67,7 +66,8 @@ export class SkiaEditor extends SkiaShape {
|
|
|
67
66
|
placeholder = new SkiaLabel();
|
|
68
67
|
scrollX = 0;
|
|
69
68
|
scrollY = 0;
|
|
70
|
-
|
|
69
|
+
/** The caret blinks on a timer, asleep between blinks: no frame runs while it waits (DrawnUi.Rust editor_rules). */
|
|
70
|
+
blinkTimer = 0;
|
|
71
71
|
caretVisible = true;
|
|
72
72
|
subscribed = false;
|
|
73
73
|
stubSelectionStop = -1;
|
|
@@ -98,15 +98,6 @@ export class SkiaEditor extends SkiaShape {
|
|
|
98
98
|
this.placeholder.IsVisible = false;
|
|
99
99
|
this.AddSubView(this.placeholder);
|
|
100
100
|
this.AddSubView(this.Label);
|
|
101
|
-
this.blink.mMinValue = 0;
|
|
102
|
-
this.blink.mMaxValue = 1;
|
|
103
|
-
this.blink.Speed = 1000;
|
|
104
|
-
this.blink.Repeat = -1;
|
|
105
|
-
this.blink.OnUpdated = (v) => { const visible = v < 0.5; if (visible !== this.caretVisible) {
|
|
106
|
-
this.caretVisible = visible;
|
|
107
|
-
this.InvalidateCache();
|
|
108
|
-
this.RepaintComposition();
|
|
109
|
-
} };
|
|
110
101
|
this.ApplyControlStyleVisuals();
|
|
111
102
|
this.UpdateLabel();
|
|
112
103
|
}
|
|
@@ -197,9 +188,22 @@ export class SkiaEditor extends SkiaShape {
|
|
|
197
188
|
this.selectionLength = l;
|
|
198
189
|
this.OnCursorMoved();
|
|
199
190
|
}
|
|
191
|
+
/** Shows the caret for a full half second, then blinks every 500 ms. */
|
|
192
|
+
StartBlink() {
|
|
193
|
+
this.StopBlink();
|
|
194
|
+
this.caretVisible = true;
|
|
195
|
+
this.blinkTimer = window.setInterval(() => { this.caretVisible = !this.caretVisible; this.InvalidateCache(); this.RepaintComposition(); }, 500);
|
|
196
|
+
}
|
|
197
|
+
StopBlink() { if (this.blinkTimer) {
|
|
198
|
+
clearInterval(this.blinkTimer);
|
|
199
|
+
this.blinkTimer = 0;
|
|
200
|
+
} }
|
|
200
201
|
OnCursorMoved() {
|
|
201
202
|
this.ScrollToCaret();
|
|
202
|
-
this.
|
|
203
|
+
if (this.isFocused)
|
|
204
|
+
this.StartBlink();
|
|
205
|
+
else
|
|
206
|
+
this.caretVisible = true;
|
|
203
207
|
TextInputProxy.Sync(this);
|
|
204
208
|
this.CursorMoved?.(this);
|
|
205
209
|
this.InvalidateCache();
|
|
@@ -215,8 +219,7 @@ export class SkiaEditor extends SkiaShape {
|
|
|
215
219
|
SkiaEditor.Focused.IsFocused = false;
|
|
216
220
|
SkiaEditor.Focused = this;
|
|
217
221
|
this.SetFocusNative(true);
|
|
218
|
-
this.
|
|
219
|
-
this.blink.Start();
|
|
222
|
+
this.StartBlink();
|
|
220
223
|
}
|
|
221
224
|
else {
|
|
222
225
|
if (SkiaEditor.Focused === this)
|
|
@@ -225,7 +228,7 @@ export class SkiaEditor extends SkiaShape {
|
|
|
225
228
|
this.dragSelecting = false;
|
|
226
229
|
this.stubSelectionStop = -1;
|
|
227
230
|
this.selectionMovingEdge = -1;
|
|
228
|
-
this.
|
|
231
|
+
this.StopBlink();
|
|
229
232
|
this.SetFocusNative(false);
|
|
230
233
|
}
|
|
231
234
|
this.FocusChanged?.(this, v);
|
|
@@ -819,8 +822,7 @@ export class SkiaEditor extends SkiaShape {
|
|
|
819
822
|
return out;
|
|
820
823
|
}
|
|
821
824
|
OnDisposing() {
|
|
822
|
-
this.
|
|
823
|
-
this.blink.Dispose();
|
|
825
|
+
this.StopBlink();
|
|
824
826
|
if (SkiaEditor.Focused === this)
|
|
825
827
|
SkiaEditor.Focused = undefined;
|
|
826
828
|
this.SetFocusNative(false);
|
|
@@ -14,6 +14,8 @@ export declare class GifAnimation {
|
|
|
14
14
|
private positionsMs;
|
|
15
15
|
/** Negative = the last frame. */
|
|
16
16
|
SeekFrame(frame: number): void;
|
|
17
|
+
/** Start (ms) of the frame after the one at `msTime`; DurationMs after the last one. */
|
|
18
|
+
NextFrameStartMs(msTime: number): number;
|
|
17
19
|
GetFrameNumber(msTime: number): number;
|
|
18
20
|
/** Decodes every frame of an encoded GIF (CanvasKit AnimatedImage). */
|
|
19
21
|
LoadFromBytes(bytes: ArrayBuffer): void;
|
|
@@ -37,6 +39,7 @@ export declare class SkiaGif extends AnimatedFramesRenderer {
|
|
|
37
39
|
set Source(v: string);
|
|
38
40
|
Start(delayMs?: number): void;
|
|
39
41
|
protected OnAnimatorUpdated(value: number): void;
|
|
42
|
+
protected NextChangeMs(value: number): number | undefined;
|
|
40
43
|
/** Seeks by time in ms (the animator range is 0..DurationMs). */
|
|
41
44
|
protected OnAnimatorSeeking(time: number): void;
|
|
42
45
|
protected OnAnimatorInitializing(): void;
|
package/dist/controls/SkiaGif.js
CHANGED
|
@@ -21,6 +21,13 @@ export class GifAnimation {
|
|
|
21
21
|
if (frame >= 0 && frame <= this.TotalFrames - 1)
|
|
22
22
|
this.Frame = this.Frames[frame];
|
|
23
23
|
}
|
|
24
|
+
/** Start (ms) of the frame after the one at `msTime`; DurationMs after the last one. */
|
|
25
|
+
NextFrameStartMs(msTime) {
|
|
26
|
+
for (let i = 0; i < this.positionsMs.length; i++)
|
|
27
|
+
if (msTime < this.positionsMs[i])
|
|
28
|
+
return this.positionsMs[i];
|
|
29
|
+
return this.DurationMs;
|
|
30
|
+
}
|
|
24
31
|
GetFrameNumber(msTime) {
|
|
25
32
|
if (this.positionsMs.length === 0 || this.DurationMs <= 0)
|
|
26
33
|
return 0;
|
|
@@ -87,7 +94,9 @@ export class SkiaGif extends AnimatedFramesRenderer {
|
|
|
87
94
|
loadGeneration = 0;
|
|
88
95
|
constructor() {
|
|
89
96
|
super();
|
|
90
|
-
|
|
97
|
+
// C# ImageDoubleBuffered pays off where a thread bakes the bitmap; on the web it is one offscreen pass per frame
|
|
98
|
+
// change, so the web engines draw a GIF frame with no cache (drawnui-cross rule 3)
|
|
99
|
+
this.UseCache = "None";
|
|
91
100
|
}
|
|
92
101
|
get Source() { return this.source; }
|
|
93
102
|
set Source(v) { if (this.source !== v) {
|
|
@@ -97,6 +106,7 @@ export class SkiaGif extends AnimatedFramesRenderer {
|
|
|
97
106
|
Start(delayMs = 0) { if ((this.Animation?.TotalFrames ?? 0) > 0)
|
|
98
107
|
super.Start(delayMs); }
|
|
99
108
|
OnAnimatorUpdated(value) { this.Seek(value); }
|
|
109
|
+
NextChangeMs(value) { return this.Animation && this.Animation.DurationMs > 0 ? this.Animation.NextFrameStartMs(value) : undefined; }
|
|
100
110
|
/** Seeks by time in ms (the animator range is 0..DurationMs). */
|
|
101
111
|
OnAnimatorSeeking(time) {
|
|
102
112
|
const a = this.Animation;
|
|
@@ -164,20 +164,25 @@ export class SkiaImage extends SkiaControl {
|
|
|
164
164
|
MeasureAbsolute(widthConstraint, heightConstraint, scale) {
|
|
165
165
|
const img = this.LoadedSource;
|
|
166
166
|
let w = widthConstraint, h = heightConstraint;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
else if (!isFinite(h) && isFinite(w))
|
|
172
|
-
h = w / aspect;
|
|
167
|
+
// nothing loaded: the box it is offered (C# OnMeasuring), an unbounded side 0
|
|
168
|
+
if (!img) {
|
|
169
|
+
this.AspectScale = { X: 0, Y: 0 };
|
|
170
|
+
return ScaledSize.FromPixels(isFinite(w) ? w : 0, isFinite(h) ? h : 0, scale);
|
|
173
171
|
}
|
|
174
|
-
|
|
172
|
+
const iw = img.width(), ih = img.height();
|
|
173
|
+
if (!isFinite(w) && isFinite(h))
|
|
174
|
+
w = h * iw / ih;
|
|
175
|
+
else if (!isFinite(h) && isFinite(w))
|
|
176
|
+
h = w * ih / iw;
|
|
177
|
+
if (!isFinite(w) || !isFinite(h) || iw <= 0 || ih <= 0) {
|
|
175
178
|
this.AspectScale = { X: 0, Y: 0 };
|
|
176
179
|
return ScaledSize.FromPixels(0, 0, scale);
|
|
177
180
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
// C# OnMeasuring / DrawnUi.Rust: an auto-sized side is the bitmap's pixels scaled by the aspect for this box, never
|
|
182
|
+
// more than the box (a Fill or requested side still takes its box in Measure)
|
|
183
|
+
const s = SkiaImage.RescaleAspect(iw, ih, SKRect.Create(0, 0, w, h), this.Aspect);
|
|
184
|
+
this.AspectScale = s;
|
|
185
|
+
return ScaledSize.FromPixels(Math.min(iw * s.X, w), Math.min(ih * s.Y, h), scale);
|
|
181
186
|
}
|
|
182
187
|
Paint(ctx) {
|
|
183
188
|
const img = this.LoadedSource;
|