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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKIPPED.md CHANGED
@@ -77,7 +77,7 @@ DrawnUi name and semantics; nothing here is a redesign, only an omission.
77
77
  | `SkiaImageManager` | partial | `Instance.LoadImageAsync(url, signal)` (direct), `LoadImageManagedAsync(url, signal, priority)` through the priority queue (High > Normal > Low, `MaxParallelLoads` 5 in flight like the C# semaphore, one fetch per url serves every requester), `PreloadImage` / `PreloadImages(urls, priority)`, `CancelAll`, `GetFromCache`, `CanReload` callback on failure, `QueuedCount` / `RunningCount`, `ReuseBitmaps`, `Clear()`; in-memory cache of decoded images only. No `CacheLongevitySecs` eviction, no platform loaders, no `LoadImageOnlineAsync` retry policy, no `ImageSource` types (urls only). |
78
78
  | `SkiaImageTiles` | ported | `TileWidth` / `TileHeight` (points, nothing drawn until set), `TileAspect` (AspectCover), `TileOffsetX` / `TileOffsetY` (wrap), `TileCacheType` (Image) for the inner tile `SkiaImage` sharing the decoded source, clipped to bounds. |
79
79
  | `SkiaSvg` | partial | Inline `SvgString` without `xmlns` gets it injected (browsers refuse to decode an `<img>` SVG without it, SkiaSharp does not care) — the C# check-mark markup relies on that. `Source` (URL) / `SvgString`, `Aspect` (default `AspectFitFill`, uses the shared `RescaleAspect` math, not SkiaSvg's own matrix path), alignments, `TintColor` (SrcIn), `Success`/`Error`, `IsLoading`, `DisplayRect`. Rendering: browser decodes the SVG, rasterized at the displayed pixel size and cached per size (CanvasKit npm has no SVG module). Not ported: shadows, FontAwesome duotone colors, `FillGradient`, `Zoom*`/offsets/`InflateAmount`, `IconFilePath`/embedded resources, `UseCache=Operations` default (no caching yet). |
80
- | `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both/Neither (Neither: pans never move the viewport, the wheel is not consumed so a parent scroll may take it; programmatic `ScrollTo` still works), single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize` 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), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1 + `HideDurationSecs` 0.25, thumb squashed on overscroll, `IsDraggable` + `GrabPadding` 8: the scroll takes a gesture that starts on the bar from down to up, thumb drag keeps the grab point, a track press jumps the thumb under the pointer, the content gets neither the pan nor the tap; a custom `IScrollBar` opts in by implementing `IsDraggable` / `BeginDrag` / `GetDragProgress`; bars are parent-independent (C# `IsParentIndependent`): a thumb resize invalidates the bar alone and the scroll re-measures it on draw, nothing above re-measures), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
80
+ | `SkiaScroll` | partial | `Orientation` Vertical/Horizontal/Both/Neither (Neither: pans never move the viewport, the wheel is not consumed so a parent scroll may take it; programmatic `ScrollTo` still works), single `Content`, `ViewportOffsetX/Y` (points, <= 0), pan with the C# 0.85 delta interpolation, fling on the DrawnUi deceleration curve (`FrictionScrolled`, `ChangeVelocityScrolled`, `MaxVelocity`, edge-cut duration), rubber-band overscroll (`Bounces`, `RubberEffect`) + spring bounce (`RubberDamping`, `MaxBounceVelocity`), mouse wheel (`WheelLineSize` per notch, an event moving by its share of a notch: `Canvas.WheelDeltaPerNotch` 100 CSS px after `deltaMode` lines x 40 / pages x 800, dominant axis, as the C# browser heads; `AutoScrollingSpeedMs` glide for a notch, an event under half a notch moves at once (touchpads, see PARITY); events accumulate onto the running move's exact target, and a restarted glide starts at the event so a fast swipe moves on every frame; `Wheel.IsHorizontal`: a vertical scroll leaves horizontal events, a horizontal scroll takes both, C# ba03cb20), `ScrollTo`/`ScrollToTop`/`ScrollToBottom`/`StopScrolling`, `Scrolled`, `IgnoreWrongDirection`, `RespondsToGestures`, `IsUserPanning`/`IsScrolling`, `ContentSize`, `ContentOffsetBounds`, `OverScrolled`. `Header` / `Footer` (JSX children with `Tag="Header"` / `Tag="Footer"`; the header scrolls in the flow, `HeaderSticky` pins it at the viewport start drawn over the content, `HeaderBehind` draws it under the content, `HeaderParallaxRatio` (1 = with the content, 0.5 = half speed), `ParallaxOverscrollEnabled`, `ContentOffset` between a behind / sticky header and the content; both add to the scroll extent), scroll bars (`ScrollBarsVisibility` None / Vertical / Horizontal / Both creating default `SkiaScrollBar`s, `ScrollBar` / `ScrollBarHorizontal` custom `IScrollBar` controls via `Tag="ScrollBar"` / `"ScrollBarHorizontal"`, `ScrollBarThumbColor` / `ScrollBarTrackColor`; `SkiaScrollBar`: `Dock`, `ThumbColor`, `TrackColor`, `Thickness` 4, `EdgeMargin` 2, `MinThumbSize` 32, `AutoHide` + `HideDelaySecs` 1 + `HideDurationSecs` 0.25, thumb squashed on overscroll, `IsDraggable` + `GrabPadding` 8: the scroll takes a gesture that starts on the bar from down to up, thumb drag keeps the grab point, a track press jumps the thumb under the pointer, the content gets neither the pan nor the tap; a custom `IScrollBar` opts in by implementing `IsDraggable` / `BeginDrag` / `GetDragProgress`; bars are parent-independent (C# `IsParentIndependent`): a thumb resize invalidates the bar alone and the scroll re-measures it on draw, nothing above re-measures), pull to refresh (`RefreshEnabled`, `RefreshCommand` callback, `IsRefreshing` two-way, `RefreshDistanceLimit` 150, `RefreshShowDistance` 50, `RefreshIndicator` `IRefreshIndicator` control via `Tag="RefreshIndicator"`; `RefreshIndicator` base: `Orientation`, `SetDragRatio` slides the view in with the pull and parks it at RefreshShowDistance, `IsRunning` + `OnIsRunningChanged`, `VisibleRatio`), `SnapToChildren` Center / Side after a fling or a release (`Snap`, `CheckNeedToSnap`), `TrackIndexPosition` Start / Center / End + `TrackIndexPositionOffset` reporting `CurrentIndex` / `CurrentIndexChanged`. Not ported: `ScrollToIndex` with `Center`/deferred ordered scroll for unmeasured indexes (Start/End on measured structures ported), zoom (`ViewportZoom`, `ZoomLocked`), `ReverseGestures` (inverted chat), keyboard adaptation, `ResetScrollPositionOnContentSizeChanged`, `ScrollType`, `ScrollingEnded` event, `LottieRefreshIndicator`, virtualization hooks (`Virtualisation`, `UseVirtual`, windowed content bounds); the C# refresh-indicator position curve is replaced by a linear slide-in. |
81
81
  | `SkiaShape` (+ `SkiaFrame`) | partial | `BevelType` Bevel / Emboss + `Bevel` (`Depth`, `LightColor`, `ShadowColor`, `Opacity`; C# `PaintBevelEffect` edge paths for Rectangle, rounded Rectangle, Circle/Ellipse, Polygon, Path via ContourMeasure halves). `Shadows` (`SkiaShadow` X/Y/Blur/Opacity/Color/ShadowOnly, plain literals accepted): fill painted once per shadow with a DropShadow image filter (C# `PaintWithShadowsInternal`), hollow shapes clip their outline away so only the outside shadow remains; caches record the effects margin (`3·Blur·scale ± offset`, C# `MergeShadowMargin`) via `ComputeEffectsMargin`/`ExpandedCacheRect`. Not ported: `PlatformShadow` (MAUI `Shadow` brush), `ExpandDirtyRegion`, bevel/emboss. `Type` Rectangle/Circle/Ellipse/Arc (`Value1` start, `Value2` sweep)/Polygon+Line (`Points` ratios)/Path (`PathData`, fitted + centered), `CornerRadius` (uniform or per-corner), `StrokeWidth` (points, negative = px) drawn inside the bounds, `StrokeColor`, `StrokeCap`, `ClipBackgroundColor`, children laid out inside the stroke and clipped to the shape, `CreateClip` = shape, `FillGradient` fill, `UseCache=Operations` default. Not ported: `Squricle`, `Custom`, `SmoothPoints`, `StrokePath` (dashes), `StrokeGradient`, `StrokeBlendMode`, sub-1px stroke compensation, `MeasuredStrokeAware*` exposure, `LayoutChildren` override hook. |
82
82
  | `SkiaButton` | partial | Now a `SkiaLayout` hosting a `SkiaShape` frame (`Tag="BtnShape"`) + `SkiaLabel` (`Tag="BtnText"`), sized from the label + Padding; `CornerRadius` (default 8), `StrokeColor`/`StrokeWidth`, `BackgroundColor`, text props, `ApplyEffect`, `Tapped`/`Down`/`Up`. Still no `ButtonStyle` looks, icons, `TextCase`, elevation, shimmer, custom templating through tags. |
83
83
  | `SnappingLayout` | ported | `Animated`, `RespondsToGestures`, `IgnoreWrongDirection`, `Bounces`, `RubberDamping`, `RubberEffect`, `AutoVelocityMultiplyPts`, `SnapDistanceRatio`, `SnapPoints`, `CurrentPosition`, `CurrentSnap`, `ContentOffsetBounds`, `IsUserPanning`/`IsUserFocused`, `InTransition`, `Scrolled`/`TransitionChanged`/`Stopped`, `ClampOffset` (rubber), `FindNearestAnchor`, `SelectNextAnchor` (dot product), `ScrollToNearestAnchor`, `ScrollToOffset` (spring `Spring(1+RubberDamping, 200, 0.5·(1+RubberDamping))` when `Bounces`, else 0.1–0.8 s from velocity). The settled state is reported when the snap animation stops (C# relies on `ApplyPosition` ticks). Not ported: `AnimatorSpring` reuse, `SnapDistanceRatio` for non-carousel layouts, hover. |
@@ -561,6 +561,10 @@ export class SkiaCarousel extends SnappingLayout {
561
561
  if (!this.IsUserPanning) {
562
562
  const movex = Math.abs(e.Distance.Total.X), movey = Math.abs(e.Distance.Total.Y);
563
563
  const along = this.IsVertical ? movey : movex, across = this.IsVertical ? movex : movey;
564
+ // a first move too short to tell the direction decides nothing yet (a 1 CSS px move at devicePixelRatio 2):
565
+ // marking it wrong kept the whole drag dead until the next press (DrawnUi.Rust c6b22e3; C# decides on every move)
566
+ if (along < scale * 2 && across < scale * 2)
567
+ return consumedDefault;
564
568
  if (along < scale * 2 || across > along) {
565
569
  this.wrongDirection = true;
566
570
  return consumedDefault;
@@ -901,6 +901,9 @@ export class SkiaScroll extends SkiaControl {
901
901
  return child;
902
902
  if (!this.RespondsToGestures || this.Orientation === "Neither")
903
903
  return child ?? consumedDefault; // Neither: not used, a parent scroll may take it
904
+ // a vertical scroll leaves horizontal events (the sideways part of a diagonal swipe) to whatever is around it
905
+ if (e.Wheel.IsHorizontal && this.Orientation === "Vertical")
906
+ return child ?? consumedDefault;
904
907
  if (!this.ApplyWheelScroll(e.Wheel.Delta))
905
908
  return consumedDefault; // at its edge: not used, the page may take it
906
909
  e.Handled = true;
@@ -1069,6 +1072,13 @@ export class SkiaScroll extends SkiaControl {
1069
1072
  // content behind the fingers, a visible delay before it started to move
1070
1073
  const glide = perNotch <= 0 || Math.abs(delta) >= perNotch / 2;
1071
1074
  this.ScrollTo(x, y, glide ? this.AutoScrollingSpeedMs / 1000 : 0, true);
1075
+ // a restarted glide starts at the event, not at its first frame: a fast trackpad swipe sends a notch-sized event
1076
+ // before every frame, and a glide drawn at progress 0 on each of them left the content standing still until the
1077
+ // events stopped, then it jumped (DrawnUi.Rust dfe36fe). Never before the last frame, at most one frame back.
1078
+ if (glide && running.IsRunning) {
1079
+ const event = performance.now() * 1_000_000;
1080
+ running.StartFrameTimeNanos = running.LastFrameTimeNanos = Math.max(this.Superview?.FrameTimeNanos ?? 0, event - 16_666_667);
1081
+ }
1072
1082
  return true;
1073
1083
  }
1074
1084
  }
@@ -83,6 +83,8 @@ export declare class Canvas {
83
83
  * fingers moved. 0 = units unknown: every event scrolls one line.
84
84
  */
85
85
  WheelDeltaPerNotch: number;
86
+ /** Vsync time of the last frame the animators ran on (nanoseconds, the performance.now() timeline). */
87
+ FrameTimeNanos: number;
86
88
  /** On-screen surface (SkiaBackdrop snapshots it). */
87
89
  get Surface(): Surface | undefined;
88
90
  /**
@@ -140,6 +140,7 @@ export class Canvas {
140
140
  // (measured 12..23 ms between frames shown 16.7 ms apart), and a game loop moving by speed * delta then
141
141
  // jumps unevenly on screen.
142
142
  const frameTime = Number(document.timeline?.currentTime ?? started);
143
+ this.FrameTimeNanos = Math.round(frameTime * 1_000_000);
143
144
  const executed = this.ExecuteAnimators(Math.round(frameTime * 1_000_000));
144
145
  const canRender = this.CanRender;
145
146
  if (canRender && !this.WasRendered)
@@ -209,6 +210,8 @@ export class Canvas {
209
210
  * fingers moved. 0 = units unknown: every event scrolls one line.
210
211
  */
211
212
  WheelDeltaPerNotch = 100;
213
+ /** Vsync time of the last frame the animators ran on (nanoseconds, the performance.now() timeline). */
214
+ FrameTimeNanos = 0;
212
215
  /** On-screen surface (SkiaBackdrop snapshots it). */
213
216
  get Surface() { return this.surface; }
214
217
  /**
@@ -389,9 +392,11 @@ export class Canvas {
389
392
  args.Scale = this.RenderingScale;
390
393
  args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
391
394
  args.StartingLocation = args.Location;
392
- // lines and pages to pixels, then the dominant axis (the .NET browser heads, fc8980c1)
395
+ // lines and pages to pixels, then the dominant axis (the .NET browser heads, fc8980c1); a vertical scroll leaves
396
+ // horizontal events alone (C# ba03cb20)
393
397
  const factor = e.deltaMode === 1 ? 40 : e.deltaMode === 2 ? 800 : 1;
394
- args.Wheel = { Delta: (Math.abs(e.deltaY) >= Math.abs(e.deltaX) ? e.deltaY : e.deltaX) * factor };
398
+ const horizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY);
399
+ args.Wheel = { Delta: (horizontal ? e.deltaX : e.deltaY) * factor, IsHorizontal: horizontal };
395
400
  if (this.gestures === "Lock") {
396
401
  e.preventDefault();
397
402
  this.OnGestureEvent(args, "Wheel");
@@ -49,9 +49,13 @@ export declare class TouchActionEventArgs {
49
49
  Timestamp: number;
50
50
  /** ms since the previous event of the same pointer. */
51
51
  DeltaTimeMs: number;
52
- /** Mouse wheel: Delta > 0 = wheel down (browser deltaY sign). */
52
+ /**
53
+ * Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
54
+ * wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
55
+ */
53
56
  Wheel: {
54
57
  Delta: number;
58
+ IsHorizontal?: boolean;
55
59
  };
56
60
  /**
57
61
  * AppoMobi TouchActionEventArgs.Handled: set by a control that actually used the event. The canvas prevents the
@@ -43,7 +43,10 @@ export class TouchActionEventArgs {
43
43
  Timestamp = performance.now();
44
44
  /** ms since the previous event of the same pointer. */
45
45
  DeltaTimeMs = 0;
46
- /** Mouse wheel: Delta > 0 = wheel down (browser deltaY sign). */
46
+ /**
47
+ * Mouse wheel: Delta > 0 = wheel down / right (browser sign). IsHorizontal: the event is along the X axis (a tilting
48
+ * wheel, the sideways part of a two-finger touchpad swipe), as C# WheelEventArgs.IsHorizontal.
49
+ */
47
50
  Wheel = { Delta: 0 };
48
51
  /**
49
52
  * AppoMobi TouchActionEventArgs.Handled: set by a control that actually used the event. The canvas prevents the
@@ -181,7 +181,12 @@ export declare class SkiaControl {
181
181
  protected OnVisualEffectsChanged(): void;
182
182
  /** Post renderers that can render this frame (compiled shaders); an unready effect leaves the control drawn plainly. */
183
183
  private ActivePostRenderers;
184
- /** C# CachedImage: the cached texture and the canvas rect it was rasterized over (Image caches only). */
184
+ /**
185
+ * C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
186
+ * re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
187
+ * at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
188
+ * there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
189
+ */
185
190
  get CachedImage(): CachedTexture | undefined;
186
191
  /** Clip overlay effects to the control's shape (CreateClip). */
187
192
  ClipEffects: boolean;
@@ -235,10 +235,18 @@ export class SkiaControl {
235
235
  return [];
236
236
  return this.EffectPostRenderers.filter((e) => e.NeedApply);
237
237
  }
238
- /** C# CachedImage: the cached texture and the canvas rect it was rasterized over (Image caches only). */
238
+ /**
239
+ * C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
240
+ * re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
241
+ * at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
242
+ * there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
243
+ */
239
244
  get CachedImage() {
240
245
  const c = this.RenderObject;
241
- return c?.Image ? { Image: c.Image, Bounds: c.Bounds } : undefined;
246
+ if (!c?.Image)
247
+ return undefined;
248
+ const r = this.AlignedCacheRect(c.Scale);
249
+ return { Image: c.Image, Bounds: SKRect.Create(r.Left, r.Top, c.Bounds.Width, c.Bounds.Height) };
242
250
  }
243
251
  /** Clip overlay effects to the control's shape (CreateClip). */
244
252
  ClipEffects = true;
@@ -125,6 +125,10 @@ export declare class SkiaShaderEffect extends SkiaEffect implements IPostRendere
125
125
  protected ApplyShaderSource(): void;
126
126
  /** Snapshot of what the parent painted (C# CreateSnapshot): the surface the pixels are on, mapped through its origin. */
127
127
  protected CreateSnapshot(ctx: DrawingContext, destination: SKRect): CachedTexture | undefined;
128
+ /** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
129
+ private frozenAt?;
130
+ private ParentOrigin;
131
+ private FollowParent;
128
132
  /** C# GetPrimaryTexture: the parent's cache, a frozen snapshot (Once) or nothing (Never). */
129
133
  protected GetPrimaryTexture(_ctx: DrawingContext, _destination: SKRect): CachedTexture | undefined;
130
134
  /** Image → shader with the effect's sampling, translated so texel (0,0) = bounds top-left; cached per image. */
@@ -2,7 +2,7 @@ import { SkiaValueAnimator } from "./Animators";
2
2
  import { Easing } from "./Easing";
3
3
  import { SkiaImageManager } from "./SkiaImageManager";
4
4
  import { Super } from "./Super";
5
- import { Thickness } from "./Types";
5
+ import { SKRect, Thickness } from "./Types";
6
6
  /**
7
7
  * Mirrors DrawnUi SkiaEffect: something attached to a control through `VisualEffects`. `Update()` re-renders the
8
8
  * parent (C# invalidates the parent's cache and redraws), `GetEffectMargin` reports what it paints outside the box.
@@ -129,6 +129,7 @@ export class SkiaShaderEffect extends SkiaEffect {
129
129
  this.DisposeImage(this.frozen.Image);
130
130
  this.frozen = undefined;
131
131
  this.frozenOwned = false;
132
+ this.frozenAt = undefined;
132
133
  }
133
134
  DisposeImage(image) {
134
135
  const sv = this.Parent?.Superview;
@@ -158,6 +159,18 @@ export class SkiaShaderEffect extends SkiaEffect {
158
159
  return undefined;
159
160
  return { Image: image, Bounds: destination, Origin: { X: ox, Y: oy } };
160
161
  }
162
+ /** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
163
+ frozenAt;
164
+ ParentOrigin() { const r = this.Parent?.DrawingRect; return { X: r?.Left ?? 0, Y: r?.Top ?? 0 }; }
165
+ FollowParent(texture) {
166
+ if (!texture || !this.frozenAt)
167
+ return texture;
168
+ const now = this.ParentOrigin(), dx = now.X - this.frozenAt.X, dy = now.Y - this.frozenAt.Y;
169
+ if (dx === 0 && dy === 0)
170
+ return texture;
171
+ const b = texture.Bounds;
172
+ return { Image: texture.Image, Bounds: new SKRect(b.Left + dx, b.Top + dy, b.Right + dx, b.Bottom + dy), Origin: texture.Origin && { X: texture.Origin.X + dx, Y: texture.Origin.Y + dy } };
173
+ }
161
174
  /** C# GetPrimaryTexture: the parent's cache, a frozen snapshot (Once) or nothing (Never). */
162
175
  GetPrimaryTexture(_ctx, _destination) {
163
176
  switch (this.UseBackground) {
@@ -175,9 +188,10 @@ export class SkiaShaderEffect extends SkiaEffect {
175
188
  this.frozen = snapshot;
176
189
  this.frozenOwned = owned;
177
190
  this.AquiredBackground = true;
191
+ this.frozenAt = this.ParentOrigin();
178
192
  }
179
193
  }
180
- return this.frozen;
194
+ return this.FollowParent(this.frozen);
181
195
  default:
182
196
  return this.Parent?.CachedImage;
183
197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drawnui-react",
3
- "version": "0.1.0-preview.13",
3
+ "version": "0.1.0-preview.14",
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)",
@@ -55,6 +55,7 @@
55
55
  "sync:skills": "node dev/sync-skills.mjs",
56
56
  "check:recycling": "node dev/check.mjs dev/check-recycling.ts",
57
57
  "check:keyboard": "node dev/check.mjs dev/check-keyboard.ts",
58
+ "check:wheel": "node dev/check.mjs dev/check-wheel.ts",
58
59
  "site:extras": "node dev/site-extras.mjs dist/demo",
59
60
  "build:lib": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
60
61
  "prepublishOnly": "npm run build:lib",