drawnui-react 0.1.0-preview.17 → 0.1.0-preview.19

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/SKIPPED.md CHANGED
@@ -12,7 +12,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
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
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
- | `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`. |
15
+ | `Opacity`, `TranslationX/Y`, `Rotation`, `ScaleX/Y` (+`Scale`), `SkewX/Y`, `RotationX/Y/Z`, `TranslationZ`, `AnchorX/Y`, `HasTransform`, `RenderTransformMatrix`, `TransformPointToLocalSpace` | ported | Same matrix order as C# `ApplyTransforms` (pivot → rotation → scale/skew → 3D → translation; 3D = C# `Create3dMatrix`, camera 576 px, TranslationZ as depth), image caches drawn smooth once the final matrix distorts them (C# `WillDrawDistorted`, a picture holding image blits records again when its playback turns distorted or straight), 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: `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
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. |
@@ -73,7 +73,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
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
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`/`LockPanning`, `Tapped`/`Down`/`Up`. `IsPressed` is tracked but has no visual; press feedback = `ApplyEffect="Ripple"`. No `ButtonStyle` platform looks, icons, `TextCase`, elevation, shimmer, `BtnText`/`BtnShape` templating. |
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. |
@@ -103,7 +103,7 @@ 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`, `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. |
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
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. |
@@ -26,7 +26,17 @@ export declare class SkiaButton extends SkiaLayout {
26
26
  StrokeColor: Color;
27
27
  StrokeWidth: number;
28
28
  IsPressed: boolean;
29
- IsDisabled: boolean;
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
- /** A disabled button ignores taps, so keyboard navigation skips it too. */
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
- IsDisabled = false;
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 !this.IsDisabled; }
64
- /** A disabled button ignores taps, so keyboard navigation skips it too. */
65
- AcceptsInput() { return !this.IsDisabled; }
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.IsDisabled)
134
- return null;
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;
@@ -144,8 +144,10 @@ export class SkiaRadioButton extends SkiaToggle {
144
144
  super.OnToggledChanged();
145
145
  }
146
146
  ProcessGestures(args, apply) {
147
+ if (!this.RespondsToGestures)
148
+ return this.BlockedGestureResult(args); // a tap does not select it, nor reach controls under it
147
149
  if (args.Type === "Tapped") {
148
- if (!this.IsToggled && this.RespondsToGestures) {
150
+ if (!this.IsToggled) {
149
151
  this.IsToggled = true;
150
152
  return this;
151
153
  }
@@ -31,7 +31,11 @@ export declare class SkiaScroll extends SkiaControl {
31
31
  AutoScrollingSpeedMs: number;
32
32
  ScrollingSpeedMs: number;
33
33
  IgnoreWrongDirection: boolean;
34
- RespondsToGestures: boolean;
34
+ private respondsToGestures;
35
+ /** False: neither a drag, the wheel nor the keyboard scrolls it (code still can), and it is reported disabled; its content keeps its own taps (drawnui-cross 6w). */
36
+ get RespondsToGestures(): boolean;
37
+ set RespondsToGestures(v: boolean);
38
+ protected AcceptsInput(): boolean;
35
39
  private header?;
36
40
  private footer?;
37
41
  /** A view before the content along the scroll axis; `Tag="Header"` on a JSX child sets it. */
@@ -28,7 +28,14 @@ export class SkiaScroll extends SkiaControl {
28
28
  AutoScrollingSpeedMs = 600;
29
29
  ScrollingSpeedMs = 400;
30
30
  IgnoreWrongDirection = false;
31
- RespondsToGestures = true;
31
+ respondsToGestures = true;
32
+ /** False: neither a drag, the wheel nor the keyboard scrolls it (code still can), and it is reported disabled; its content keeps its own taps (drawnui-cross 6w). */
33
+ get RespondsToGestures() { return this.respondsToGestures; }
34
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
35
+ this.respondsToGestures = v;
36
+ this.NotifyAccessibility();
37
+ } }
38
+ AcceptsInput() { return this.respondsToGestures; }
32
39
  // ---- header / footer (C# SkiaScroll.Header / Footer) ----
33
40
  header;
34
41
  footer;
@@ -19,7 +19,11 @@ export declare class SkiaSlider extends SkiaControl {
19
19
  /** Minimum distance between the two thumbs, in value units. */
20
20
  RangeMin: number;
21
21
  ClickOnTrailEnabled: boolean;
22
- RespondsToGestures: boolean;
22
+ private respondsToGestures;
23
+ /** False: neither a drag nor the arrow keys move it (code still can), and it is reported disabled (drawnui-cross 6w). */
24
+ get RespondsToGestures(): boolean;
25
+ set RespondsToGestures(v: boolean);
26
+ protected AcceptsInput(): boolean;
23
27
  IsPressed: boolean;
24
28
  IsUserPanning: boolean;
25
29
  /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
@@ -20,7 +20,14 @@ export class SkiaSlider extends SkiaControl {
20
20
  /** Minimum distance between the two thumbs, in value units. */
21
21
  RangeMin = 0;
22
22
  ClickOnTrailEnabled = true;
23
- RespondsToGestures = true;
23
+ respondsToGestures = true;
24
+ /** False: neither a drag nor the arrow keys move it (code still can), and it is reported disabled (drawnui-cross 6w). */
25
+ get RespondsToGestures() { return this.respondsToGestures; }
26
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
27
+ this.respondsToGestures = v;
28
+ this.NotifyAccessibility();
29
+ } }
30
+ AcceptsInput() { return this.respondsToGestures; }
24
31
  IsPressed = false;
25
32
  IsUserPanning = false;
26
33
  /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
@@ -19,7 +19,11 @@ export declare abstract class SkiaToggle extends SkiaLayout {
19
19
  Toggled?: (sender: SkiaToggle, value: boolean) => void;
20
20
  DefaultValue: boolean;
21
21
  IsAnimated: boolean;
22
- RespondsToGestures: boolean;
22
+ private respondsToGestures;
23
+ /** False: a tap or the keyboard does not toggle it (code still can), a tap on it stops there, and it is reported disabled (drawnui-cross 6w). */
24
+ get RespondsToGestures(): boolean;
25
+ set RespondsToGestures(v: boolean);
26
+ protected AcceptsInput(): boolean;
23
27
  private isToggled;
24
28
  private colorThumbOn?;
25
29
  private colorFrameOn?;
@@ -22,7 +22,14 @@ export class SkiaToggle extends SkiaLayout {
22
22
  Toggled;
23
23
  DefaultValue = false;
24
24
  IsAnimated = true;
25
- RespondsToGestures = true;
25
+ respondsToGestures = true;
26
+ /** False: a tap or the keyboard does not toggle it (code still can), a tap on it stops there, and it is reported disabled (drawnui-cross 6w). */
27
+ get RespondsToGestures() { return this.respondsToGestures; }
28
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
29
+ this.respondsToGestures = v;
30
+ this.NotifyAccessibility();
31
+ } }
32
+ AcceptsInput() { return this.respondsToGestures; }
26
33
  isToggled = false;
27
34
  colorThumbOn;
28
35
  colorFrameOn;
@@ -106,7 +113,9 @@ export class SkiaToggle extends SkiaLayout {
106
113
  get AccessibilityIsPressed() { return this.isToggled; }
107
114
  set AccessibilityIsPressed(_v) { }
108
115
  ProcessGestures(args, apply) {
109
- if (args.Type === "Tapped" && this.RespondsToGestures) {
116
+ if (!this.respondsToGestures)
117
+ return this.BlockedGestureResult(args); // a tap does not toggle it, nor reach controls under it
118
+ if (args.Type === "Tapped") {
110
119
  this.IsToggled = !this.IsToggled;
111
120
  return this;
112
121
  }
@@ -11,7 +11,11 @@ import { SkiaLayout } from "./SkiaLayout";
11
11
  */
12
12
  export declare abstract class SnappingLayout extends SkiaLayout {
13
13
  Animated: boolean;
14
- RespondsToGestures: boolean;
14
+ private respondsToGestures;
15
+ /** False: neither a swipe nor the keyboard moves it (code still can), and it is reported disabled; its content keeps its own taps (drawnui-cross 6w). */
16
+ get RespondsToGestures(): boolean;
17
+ set RespondsToGestures(v: boolean);
18
+ protected AcceptsInput(): boolean;
15
19
  IgnoreWrongDirection: boolean;
16
20
  Bounces: boolean;
17
21
  RubberDamping: number;
@@ -11,7 +11,14 @@ import { SkiaLayout } from "./SkiaLayout";
11
11
  */
12
12
  export class SnappingLayout extends SkiaLayout {
13
13
  Animated = true;
14
- RespondsToGestures = true;
14
+ respondsToGestures = true;
15
+ /** False: neither a swipe nor the keyboard moves it (code still can), and it is reported disabled; its content keeps its own taps (drawnui-cross 6w). */
16
+ get RespondsToGestures() { return this.respondsToGestures; }
17
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
18
+ this.respondsToGestures = v;
19
+ this.NotifyAccessibility();
20
+ } }
21
+ AcceptsInput() { return this.respondsToGestures; }
15
22
  IgnoreWrongDirection = false;
16
23
  Bounces = false;
17
24
  RubberDamping = 0.7;
@@ -21,6 +21,10 @@ export interface DrawingContext {
21
21
  Y: number;
22
22
  };
23
23
  Recording?: boolean;
24
+ /** The picture being recorded is drawn scaled, turned, skewed or projected: images blitted into it sample smoothly (C#). */
25
+ PlaybackDistorted?: boolean;
26
+ /** An image cache (or a picture holding one) was blitted here (C#): the picture is recorded again when its playback distortion changes. */
27
+ HasImageBlits?: boolean;
24
28
  };
25
29
  Destination: SKRect;
26
30
  Scale: number;
@@ -38,8 +42,12 @@ export declare class CachedObject {
38
42
  Bounds: SKRect, Scale: number, Picture?: SkPicture | undefined, Image?: Image | undefined);
39
43
  /** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
40
44
  Epoch: number;
41
- /** Replays the cache with its top-left moved to (left, top). */
42
- Draw(canvas: SkCanvas, left: number, top: number): void;
45
+ /** A picture recorded to be drawn distorted: the images in it sample smoothly (C# CachedObject.PlaybackDistorted). */
46
+ PlaybackDistorted: boolean;
47
+ /** A picture holding image blits, whose sampling was fixed when it was recorded (C# CachedObject.HasImageBlits). */
48
+ HasImageBlits: boolean;
49
+ /** Replays the cache with its top-left moved to (left, top); `smooth` = an image drawn distorted (linear + mipmaps). */
50
+ Draw(canvas: SkCanvas, left: number, top: number, smooth?: boolean): void;
43
51
  Dispose(): void;
44
52
  }
45
53
  /**
@@ -86,6 +94,14 @@ export declare class SkiaControl {
86
94
  /** Degrees. */
87
95
  SkewX: number;
88
96
  SkewY: number;
97
+ /** Degrees, a 3D turn around the horizontal axis through the pivot, seen with perspective (C# RotationX). */
98
+ RotationX: number;
99
+ /** Degrees, a 3D turn around the vertical axis through the pivot (C# RotationY). */
100
+ RotationY: number;
101
+ /** Degrees, a 3D turn around the axis facing the viewer, after RotationX and RotationY (C# RotationZ). */
102
+ RotationZ: number;
103
+ /** Depth: toward the viewer (+, bigger) or away (−, smaller), turned or flat; draws at 576 / (576 − TranslationZ) (C# TranslationZ). */
104
+ TranslationZ: number;
89
105
  /** Pivot for Rotation/Scale/Skew as a fraction of the box (MAUI default 0.5). */
90
106
  AnchorX: number;
91
107
  AnchorY: number;
@@ -109,6 +125,7 @@ export declare class SkiaControl {
109
125
  get Scale(): number;
110
126
  set Scale(v: number);
111
127
  get HasTransform(): boolean;
128
+ private get Has3d();
112
129
  /** Matrix applied at the last render (canvas space), undefined when none; gestures map through its inverse. */
113
130
  RenderTransformMatrix?: number[];
114
131
  /** Generic numeric parameters (DrawnUi Value1/Value2): SkiaShape Arc uses start angle / sweep angle in degrees. */
@@ -293,6 +310,12 @@ export declare class SkiaControl {
293
310
  protected MeasureAbsolute(_widthConstraint: number, _heightConstraint: number, scale: number): ScaledSize;
294
311
  /** Places MeasuredSize inside destination per Margin + Horizontal/VerticalOptions -> DrawingRect. */
295
312
  Arrange(destination: SKRect, _widthRequest: number, _heightRequest: number, scale: number): void;
313
+ /**
314
+ * A cached ancestor replays its picture where a re-arrange moved it (a scroll) without rendering this control again,
315
+ * so the matrix made at its last Render moves with its rect: taps, hover and the accessibility rect stay on what is
316
+ * drawn (C# offsets the cached render tree instead). A control that renders again gets a fresh matrix there.
317
+ */
318
+ private FollowMatrix;
296
319
  /** C# SuperViewChanged: starts the animators queued before this control was attached. */
297
320
  private StartPendingAnimators;
298
321
  private static nextAccessibilityId;
@@ -375,8 +398,17 @@ export declare class SkiaControl {
375
398
  * for the pointer. Keyboard navigation uses it: Tapped for Tab and Enter / Space, Panning for the arrow keys.
376
399
  */
377
400
  CanReceiveGesture(gesture: SkiaGesturesParameters["Type"]): boolean;
378
- /** False while the control ignores gestures itself, e.g. a disabled button. Default true. */
401
+ /**
402
+ * False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
403
+ * Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
404
+ */
379
405
  protected AcceptsInput(): boolean;
406
+ /**
407
+ * What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
408
+ * press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
409
+ * it (a scroll behind it still scrolls).
410
+ */
411
+ protected BlockedGestureResult(args: SkiaGesturesParameters): SkiaControl | null;
380
412
  NotifyAccessibilityFocused(focused: boolean): void;
381
413
  /** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
382
414
  protected static SmartMax(a: number, b: number): number;
@@ -399,8 +431,22 @@ export declare class SkiaControl {
399
431
  /** DrawingRect grown by the effects margin, in pixels — the rect a cache is recorded for. */
400
432
  protected ExpandedCacheRect(scale: number): SKRect;
401
433
  Render(ctx: DrawingContext): void;
402
- /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · T(pivot) · translation, in canvas pixels. */
434
+ /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · 3D · T(pivot) · translation, in canvas pixels. */
403
435
  protected CreateRenderTransformMatrix(destination: SKRect, scale: number): number[];
436
+ /** C# camera distance of the 3D transforms (Sk3dView.CameraDistance): 8 inches at 72 dpi, in canvas pixels. */
437
+ static readonly CameraDistance = 576;
438
+ /**
439
+ * C# Create3dMatrix (Sk3dView): RotationX, then RotationY, then RotationZ (negated) around the pivot, projected from
440
+ * the camera; TranslationZ is depth, a flat control at depth z draws at camera / (camera − z).
441
+ */
442
+ static Create3dMatrix(rotationX: number, rotationY: number, rotationZ: number, translationZ: number): number[];
443
+ /** C# SkiaSamplingOptions.IsMoveOnly: the matrix only moves (or mirrors), so a bitmap keeps its pixels 1:1. */
444
+ static IsMoveOnly(m: ArrayLike<number>): boolean;
445
+ /**
446
+ * C# WillDrawDistorted: what this control draws here ends up scaled, turned, skewed or projected, by its own transforms
447
+ * or a live ancestor's (both in the canvas matrix) or by the picture it is recorded into. Images then sample smoothly.
448
+ */
449
+ protected WillDrawDistorted(ctx: DrawingContext): boolean;
404
450
  /** Maps a point given in the parent's space into this control's untransformed space (DrawnUi TransformPointToLocalSpace). */
405
451
  TransformPointToLocalSpace(point: SKPoint): SKPoint;
406
452
  /**
@@ -410,6 +456,8 @@ export declare class SkiaControl {
410
456
  private AlignedCacheRect;
411
457
  /** Cache blit or live paint, then post animators — the part a transform/opacity layer wraps. */
412
458
  private RenderContent;
459
+ /** Blits a cache: an image samples smoothly when drawn distorted; a picture being recorded learns it holds image blits. */
460
+ private DrawCache;
413
461
  /** Background + Paint(): the part of the control that a cache captures. */
414
462
  protected PaintContent(ctx: DrawingContext): void;
415
463
  /** Subclasses whose PaintBackground has something to draw without a BackgroundColor (shape shadows). */
@@ -24,12 +24,19 @@ export class CachedObject {
24
24
  }
25
25
  /** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
26
26
  Epoch = 0;
27
- /** Replays the cache with its top-left moved to (left, top). */
28
- Draw(canvas, left, top) {
27
+ /** A picture recorded to be drawn distorted: the images in it sample smoothly (C# CachedObject.PlaybackDistorted). */
28
+ PlaybackDistorted = false;
29
+ /** A picture holding image blits, whose sampling was fixed when it was recorded (C# CachedObject.HasImageBlits). */
30
+ HasImageBlits = false;
31
+ /** Replays the cache with its top-left moved to (left, top); `smooth` = an image drawn distorted (linear + mipmaps). */
32
+ Draw(canvas, left, top, smooth = false) {
29
33
  const CK = Super.CK;
30
34
  if (this.Image) {
31
- // Nearest sampling: a cached bitmap is blitted 1:1, never resampled.
32
- canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Nearest, CK.MipmapMode.None, null);
35
+ // nearest keeps a bitmap blitted 1:1 sharp; once scaled, turned or projected it needs smooth sampling (C# 1e4610f6)
36
+ if (smooth)
37
+ canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Linear, CK.MipmapMode.Linear, null);
38
+ else
39
+ canvas.drawImageOptions(this.Image, left, top, CK.FilterMode.Nearest, CK.MipmapMode.None, null);
33
40
  }
34
41
  else if (this.Picture) {
35
42
  const saved = canvas.save();
@@ -89,6 +96,14 @@ export class SkiaControl {
89
96
  /** Degrees. */
90
97
  SkewX = 0;
91
98
  SkewY = 0;
99
+ /** Degrees, a 3D turn around the horizontal axis through the pivot, seen with perspective (C# RotationX). */
100
+ RotationX = 0;
101
+ /** Degrees, a 3D turn around the vertical axis through the pivot (C# RotationY). */
102
+ RotationY = 0;
103
+ /** Degrees, a 3D turn around the axis facing the viewer, after RotationX and RotationY (C# RotationZ). */
104
+ RotationZ = 0;
105
+ /** Depth: toward the viewer (+, bigger) or away (−, smaller), turned or flat; draws at 576 / (576 − TranslationZ) (C# TranslationZ). */
106
+ TranslationZ = 0;
92
107
  /** Pivot for Rotation/Scale/Skew as a fraction of the box (MAUI default 0.5). */
93
108
  AnchorX = 0.5;
94
109
  AnchorY = 0.5;
@@ -116,8 +131,9 @@ export class SkiaControl {
116
131
  get Scale() { return this.ScaleX; }
117
132
  set Scale(v) { this.ScaleX = v; this.ScaleY = v; }
118
133
  get HasTransform() {
119
- return this.TranslationX !== 0 || this.TranslationY !== 0 || this.Left !== 0 || this.Top !== 0 || this.Rotation !== 0 || this.ScaleX !== 1 || this.ScaleY !== 1 || this.SkewX !== 0 || this.SkewY !== 0;
134
+ return this.TranslationX !== 0 || this.TranslationY !== 0 || this.Left !== 0 || this.Top !== 0 || this.Rotation !== 0 || this.ScaleX !== 1 || this.ScaleY !== 1 || this.SkewX !== 0 || this.SkewY !== 0 || this.Has3d;
120
135
  }
136
+ get Has3d() { return this.RotationX !== 0 || this.RotationY !== 0 || this.RotationZ !== 0 || this.TranslationZ !== 0; }
121
137
  /** Matrix applied at the last render (canvas space), undefined when none; gestures map through its inverse. */
122
138
  RenderTransformMatrix;
123
139
  /** Generic numeric parameters (DrawnUi Value1/Value2): SkiaShape Arc uses start angle / sweep angle in degrees. */
@@ -470,7 +486,9 @@ export class SkiaControl {
470
486
  const fullW = destination.Width - m.HorizontalThickness * scale, fullH = destination.Height - m.VerticalThickness * scale;
471
487
  const x = availL + SkiaControl.Align(this.HorizontalOptions, fullW, w);
472
488
  const y = availT + SkiaControl.Align(this.VerticalOptions, fullH, h);
489
+ const old = this.DrawingRect;
473
490
  this.DrawingRect = SKRect.Create(x, y, w, h);
491
+ this.FollowMatrix(old);
474
492
  this.OnLayoutChanged();
475
493
  if (this.PendingUnattachedAnimators)
476
494
  this.StartPendingAnimators();
@@ -478,6 +496,18 @@ export class SkiaControl {
478
496
  if (!this.registeredWithAccessibility && this.IsAccessibilityElement)
479
497
  this.NotifyAccessibility();
480
498
  }
499
+ /**
500
+ * A cached ancestor replays its picture where a re-arrange moved it (a scroll) without rendering this control again,
501
+ * so the matrix made at its last Render moves with its rect: taps, hover and the accessibility rect stay on what is
502
+ * drawn (C# offsets the cached render tree instead). A control that renders again gets a fresh matrix there.
503
+ */
504
+ FollowMatrix(old) {
505
+ const m = this.RenderTransformMatrix, r = this.DrawingRect;
506
+ if (!m || (old.Left === r.Left && old.Top === r.Top))
507
+ return;
508
+ const M = Super.CK.Matrix, dx = r.Left - old.Left, dy = r.Top - old.Top;
509
+ this.RenderTransformMatrix = M.multiply(M.translated(dx, dy), m, M.translated(-dx, -dy));
510
+ }
481
511
  /** C# SuperViewChanged: starts the animators queued before this control was attached. */
482
512
  StartPendingAnimators() {
483
513
  if (!this.Superview)
@@ -633,8 +663,19 @@ export class SkiaControl {
633
663
  return false;
634
664
  return true;
635
665
  }
636
- /** False while the control ignores gestures itself, e.g. a disabled button. Default true. */
666
+ /**
667
+ * False while the control ignores input itself: RespondsToGestures = false on a button, toggle, slider, scroll...
668
+ * Taps, keyboard and the accessibility state (reported disabled) all follow it (drawnui-cross 6w). Default true.
669
+ */
637
670
  AcceptsInput() { return true; }
671
+ /**
672
+ * What a control that takes no input returns for a gesture that reached it (C# BlockedGestureResult): it keeps a
673
+ * press, tap or long press, so nothing under it reacts to them, and lets moves and wheel go on to the controls around
674
+ * it (a scroll behind it still scrolls).
675
+ */
676
+ BlockedGestureResult(args) {
677
+ return args.Type === "Down" || args.Type === "Tapped" || args.Type === "LongPressing" ? this : null;
678
+ }
638
679
  NotifyAccessibilityFocused(focused) { this.Superview?.AccessibilityManager.NotifyFocused(focused ? this : undefined); }
639
680
  /** DrawnUi SmartMax: larger of two sizes, an infinite one loses. */
640
681
  static SmartMax(a, b) {
@@ -700,7 +741,7 @@ export class SkiaControl {
700
741
  const applyOpacity = this.Opacity < 1;
701
742
  // C# Left/Top: a cached control is blitted at an offset, no matrix and no save/restore around the subtree
702
743
  const offsetOnly = (this.Left !== 0 || this.Top !== 0) && this.UsingCacheType !== "None" && this.TranslationX === 0 && this.TranslationY === 0
703
- && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0;
744
+ && this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !this.Has3d;
704
745
  const needTransform = this.HasTransform && !offsetOnly;
705
746
  let dx = 0, dy = 0;
706
747
  if (needTransform) {
@@ -746,11 +787,12 @@ export class SkiaControl {
746
787
  if (saved)
747
788
  canvas.restore();
748
789
  }
749
- /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · T(pivot) · translation, in canvas pixels. */
790
+ /** Port of DrawnUi ApplyTransforms: T(-pivot) · rotation · scale/skew · 3D · T(pivot) · translation, in canvas pixels. */
750
791
  CreateRenderTransformMatrix(destination, scale) {
751
792
  const M = Super.CK.Matrix;
752
793
  const moveX = (this.TranslationX + this.Left) * scale, moveY = (this.TranslationY + this.Top) * scale;
753
- if (this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0)
794
+ const has3d = this.Has3d;
795
+ if (this.Rotation === 0 && this.ScaleX === 1 && this.ScaleY === 1 && this.SkewX === 0 && this.SkewY === 0 && !has3d)
754
796
  return M.translated(moveX, moveY);
755
797
  const px = destination.Left + destination.Width * this.AnchorX;
756
798
  const py = destination.Top + destination.Height * this.AnchorY;
@@ -759,7 +801,36 @@ export class SkiaControl {
759
801
  const scaleSkew = [this.ScaleX, kx, 0, ky, this.ScaleY, 0, 0, 0, 1];
760
802
  const rotation = this.Rotation !== 0 ? M.rotated((Math.PI * this.Rotation) / 180) : M.identity();
761
803
  // multiply(a, b, c) = a·b·c, the right-most applies first
762
- return M.multiply(M.translated(px + moveX, py + moveY), scaleSkew, rotation, M.translated(-px, -py));
804
+ if (!has3d)
805
+ return M.multiply(M.translated(px + moveX, py + moveY), scaleSkew, rotation, M.translated(-px, -py));
806
+ return M.multiply(M.translated(px + moveX, py + moveY), SkiaControl.Create3dMatrix(this.RotationX, this.RotationY, this.RotationZ, this.TranslationZ), scaleSkew, rotation, M.translated(-px, -py));
807
+ }
808
+ /** C# camera distance of the 3D transforms (Sk3dView.CameraDistance): 8 inches at 72 dpi, in canvas pixels. */
809
+ static CameraDistance = 576;
810
+ /**
811
+ * C# Create3dMatrix (Sk3dView): RotationX, then RotationY, then RotationZ (negated) around the pivot, projected from
812
+ * the camera; TranslationZ is depth, a flat control at depth z draws at camera / (camera − z).
813
+ */
814
+ static Create3dMatrix(rotationX, rotationY, rotationZ, translationZ) {
815
+ const d = Math.PI / 180, camera = SkiaControl.CameraDistance;
816
+ const sx = Math.sin(rotationX * d), cx = Math.cos(rotationX * d), sy = Math.sin(rotationY * d), cy = Math.cos(rotationY * d);
817
+ const sz = Math.sin(-rotationZ * d), cz = Math.cos(-rotationZ * d);
818
+ return [
819
+ cy * cz, cy * sz, 0,
820
+ sx * sy * cz - cx * sz, sx * sy * sz + cx * cz, 0,
821
+ (cx * sy * cz + sx * sz) / camera, (cx * sy * sz - sx * cz) / camera, Math.max(camera - translationZ, 1) / camera,
822
+ ];
823
+ }
824
+ /** C# SkiaSamplingOptions.IsMoveOnly: the matrix only moves (or mirrors), so a bitmap keeps its pixels 1:1. */
825
+ static IsMoveOnly(m) {
826
+ return m[1] === 0 && m[3] === 0 && m[6] === 0 && m[7] === 0 && m[8] === 1 && Math.abs(m[0]) === 1 && Math.abs(m[4]) === 1;
827
+ }
828
+ /**
829
+ * C# WillDrawDistorted: what this control draws here ends up scaled, turned, skewed or projected, by its own transforms
830
+ * or a live ancestor's (both in the canvas matrix) or by the picture it is recorded into. Images then sample smoothly.
831
+ */
832
+ WillDrawDistorted(ctx) {
833
+ return !!ctx.Context.PlaybackDistorted || !SkiaControl.IsMoveOnly(ctx.Context.Canvas.getTotalMatrix());
763
834
  }
764
835
  /** Maps a point given in the parent's space into this control's untransformed space (DrawnUi TransformPointToLocalSpace). */
765
836
  TransformPointToLocalSpace(point) {
@@ -798,23 +869,32 @@ export class SkiaControl {
798
869
  const epoch = this.Superview?.GpuEpoch ?? 0;
799
870
  if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
800
871
  this.DisposePrevious(); // made on a lost GPU context
801
- if (this.CacheStale(ctx.Scale))
872
+ // a picture replays its image blits with the sampling chosen when it was recorded: recorded again once its
873
+ // playback turns distorted or straight (C# SamplingMismatch), else a cached child of a turning card stays jagged
874
+ const o = this.RenderObject;
875
+ if (this.CacheStale(ctx.Scale) || (o?.Picture && o.HasImageBlits && o.PlaybackDistorted !== this.WillDrawDistorted(ctx)))
802
876
  this.CreateRenderingObject({ ...ctx, Destination: this.DrawingRect }, cacheType);
803
877
  if (this.RenderObject) {
804
878
  // C# DrawRenderObject: with post renderers an Image cache is not blitted, the effects sample it (CachedImage)
805
879
  // and paint the result; a picture cache has no texture, so it is replayed first and snapshotted by the effect
806
880
  if (post.length === 0 || !this.RenderObject.Image)
807
- this.RenderObject.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy);
881
+ this.DrawCache(this.RenderObject, ctx, r.Left + dx, r.Top + dy);
808
882
  for (const e of post)
809
883
  e.Render(own);
810
884
  }
811
885
  else if (this.RenderObjectPrevious)
812
- this.RenderObjectPrevious.Draw(ctx.Context.Canvas, r.Left + dx, r.Top + dy); // double buffer: last good frame
886
+ this.DrawCache(this.RenderObjectPrevious, ctx, r.Left + dx, r.Top + dy); // double buffer: last good frame
813
887
  else
814
888
  this.DrawPlaceholder(own);
815
889
  }
816
890
  this.ExecutePostAnimators(own);
817
891
  }
892
+ /** Blits a cache: an image samples smoothly when drawn distorted; a picture being recorded learns it holds image blits. */
893
+ DrawCache(cache, ctx, left, top) {
894
+ if (cache.Image || cache.HasImageBlits)
895
+ ctx.Context.HasImageBlits = true;
896
+ cache.Draw(ctx.Context.Canvas, left, top, !!cache.Image && this.WillDrawDistorted(ctx));
897
+ }
818
898
  /** Background + Paint(): the part of the control that a cache captures. */
819
899
  PaintContent(ctx) {
820
900
  if (this.BackgroundColor || (this.FillGradient && this.FillGradientPaintsBackground()) || this.PaintsBackgroundWithoutColor())
@@ -849,12 +929,16 @@ export class SkiaControl {
849
929
  }
850
930
  if (cacheType === "Operations") {
851
931
  const recorder = new CK.PictureRecorder();
932
+ const playbackDistorted = this.WillDrawDistorted(ctx);
852
933
  const canvas = recorder.beginRecording(CK.LTRBRect(r.Left, r.Top, r.Right, r.Bottom));
853
- this.PaintContent({ ...ctx, Context: { ...ctx.Context, Canvas: canvas, Recording: true } });
934
+ const recording = { ...ctx.Context, Canvas: canvas, Recording: true, PlaybackDistorted: playbackDistorted, HasImageBlits: false };
935
+ this.PaintContent({ ...ctx, Context: recording });
854
936
  const picture = recorder.finishRecordingAsPicture();
855
937
  recorder.delete();
856
938
  this.RenderObject = new CachedObject("Operations", r, ctx.Scale, picture);
857
939
  this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
940
+ this.RenderObject.PlaybackDistorted = playbackDistorted;
941
+ this.RenderObject.HasImageBlits = recording.HasImageBlits;
858
942
  }
859
943
  else if (cacheType === "ImageComposite") {
860
944
  this.CreateCompositeRenderingObject(ctx, r, w, h);
@@ -38,7 +38,7 @@ export const Registry = {
38
38
  };
39
39
  const SKIP = new Set(["children", "key", "ref"]);
40
40
  /** Props that change how the subtree is composited, not what it contains: repaint, keep caches (DrawnUi RedrawCanvas). */
41
- const REPAINT_ONLY = new Set(["TranslationX", "TranslationY", "Rotation", "ScaleX", "ScaleY", "Scale", "SkewX", "SkewY", "AnchorX", "AnchorY", "Opacity"]);
41
+ const REPAINT_ONLY = new Set(["TranslationX", "TranslationY", "TranslationZ", "Rotation", "RotationX", "RotationY", "RotationZ", "ScaleX", "ScaleY", "Scale", "SkewX", "SkewY", "AnchorX", "AnchorY", "Opacity"]);
42
42
  /** Props that change only how a control looks, never its size (C# NeedDraw): an ImageComposite above redraws only its area. */
43
43
  const isDrawOnly = (k) => k.endsWith("Color") || k === "FillGradient" || k === "StrokeGradient";
44
44
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawnui-react",
3
- "version": "0.1.0-preview.17",
3
+ "version": "0.1.0-preview.19",
4
4
  "description": "DrawnUi for React: the DrawnUi (.NET) drawn-UI engine in TypeScript on CanvasKit (Skia WASM). Same controls, same PascalCase API as DrawnUi.Net.",
5
5
  "license": "MIT",
6
6
  "author": "Nick Kovalsky (AppoMobi)",
@@ -73,6 +73,8 @@
73
73
  "check:velocity": "node dev/check.mjs dev/check-velocity.ts",
74
74
  "check:scroll-index": "node dev/check.mjs dev/check-scroll-index.ts",
75
75
  "check:culling": "node dev/check.mjs dev/check-culling.ts",
76
+ "check:responds": "node dev/check.mjs dev/check-responds.ts",
77
+ "check:3d": "node dev/check.mjs dev/check-3d.ts",
76
78
  "site:extras": "node dev/site-extras.mjs dist/demo",
77
79
  "build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
78
80
  "prepublishOnly": "npm run build:lib",