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

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
  }
@@ -47,6 +47,13 @@ export declare class Canvas {
47
47
  private frameId;
48
48
  private disposed;
49
49
  private readonly observer;
50
+ /** The WebGL context is lost: no frames until the browser restores it. */
51
+ private contextLost;
52
+ /**
53
+ * Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
54
+ * replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
55
+ */
56
+ GpuEpoch: number;
50
57
  /** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
51
58
  constructor(Element: HTMLCanvasElement, init?: (canvas: Canvas) => void);
52
59
  /** Request a redraw (full measure + arrange + render) on the next animation frame. */
@@ -61,6 +68,17 @@ export declare class Canvas {
61
68
  * redrawn content lands in the same paint and no blank frame is ever shown while dragging.
62
69
  */
63
70
  private OnResized;
71
+ /** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
72
+ private readonly onContextLost;
73
+ /**
74
+ * WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
75
+ * gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
76
+ * any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
77
+ * `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
78
+ * GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
79
+ * context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
80
+ */
81
+ private readonly onContextRestored;
64
82
  /** Draws one frame immediately (outside the rAF loop) and presents it. */
65
83
  private DrawNow;
66
84
  private Draw;
@@ -83,6 +101,8 @@ export declare class Canvas {
83
101
  * fingers moved. 0 = units unknown: every event scrolls one line.
84
102
  */
85
103
  WheelDeltaPerNotch: number;
104
+ /** Vsync time of the last frame the animators ran on (nanoseconds, the performance.now() timeline). */
105
+ FrameTimeNanos: number;
86
106
  /** On-screen surface (SkiaBackdrop snapshots it). */
87
107
  get Surface(): Surface | undefined;
88
108
  /**
@@ -54,19 +54,28 @@ export class Canvas {
54
54
  frameId = 0;
55
55
  disposed = false;
56
56
  observer;
57
+ /** The WebGL context is lost: no frames until the browser restores it. */
58
+ contextLost = false;
59
+ /**
60
+ * Bumped when the WebGL context was restored: every GPU object made before (Image caches, picture caches that may
61
+ * replay them, kept composite surfaces, effect textures) is remade at its next draw (DrawnUi.Rust Gpu::epoch).
62
+ */
63
+ GpuEpoch = 0;
57
64
  /** `init` runs before the first frame is drawn: the place to attach `WillFirstTimeDraw` / `WasDrawn` for that frame. */
58
65
  constructor(Element, init) {
59
66
  this.Element = Element;
60
67
  if (!Super.CK)
61
68
  throw new Error("DrawnUi: call Super.UseDrawnUi()...BuildAsync() before creating a Canvas");
62
69
  init?.(this);
70
+ Element.addEventListener("webglcontextlost", this.onContextLost);
71
+ Element.addEventListener("webglcontextrestored", this.onContextRestored);
63
72
  this.observer = new ResizeObserver(() => this.OnResized());
64
73
  this.observer.observe(Element);
65
74
  this.OnResized();
66
75
  }
67
76
  /** Request a redraw (full measure + arrange + render) on the next animation frame. */
68
77
  Update() {
69
- if (this.frameId || !this.surface || this.disposed)
78
+ if (this.frameId || !this.surface || this.disposed || this.contextLost)
70
79
  return;
71
80
  const surface = this.surface;
72
81
  this.frameId = surface.requestAnimationFrame((c) => {
@@ -117,16 +126,53 @@ export class Canvas {
117
126
  this.RenderingScale = dpr;
118
127
  this.Element.width = w;
119
128
  this.Element.height = h;
129
+ if (this.contextLost)
130
+ return; // the restore makes the surface at this size
120
131
  this.ReleaseSurface();
121
132
  this.surface = this.CreateSurface(w, h);
122
133
  if (!this.surface)
123
134
  throw new Error("DrawnUi: cannot create surface");
124
135
  this.DrawNow();
125
136
  }
137
+ /** WebGL context lost: keep the browser restoring it (preventDefault) and stop the frames. */
138
+ onContextLost = (e) => {
139
+ e.preventDefault();
140
+ this.contextLost = true;
141
+ if (this.frameId) {
142
+ cancelAnimationFrame(this.frameId);
143
+ this.frameId = 0;
144
+ }
145
+ };
146
+ /**
147
+ * WebGL context restored (as DrawnUi.Rust and the C# Wasm / Blazor heads): every GL object of the lost generation is
148
+ * gone. The old Skia context is abandoned first, while its own GL handle is current and before the new context makes
149
+ * any object, so its GL calls reach only lost-generation objects. CanvasKit exposes no `abandonContext`;
150
+ * `releaseResourcesAndAbandonContext` is its only way to abandon, and after it freeing an old image or surface makes no
151
+ * GL call. Then the same WebGL object is registered again (the old handle deleted first, which clears the canvas'
152
+ * context object) with a new GrContext and surface; GpuEpoch makes the old generation's GPU objects remake themselves.
153
+ */
154
+ onContextRestored = () => {
155
+ if (this.disposed)
156
+ return;
157
+ if (this.grContext)
158
+ this.grContext.releaseResourcesAndAbandonContext();
159
+ this.surface?.delete();
160
+ this.surface = undefined;
161
+ this.grContext?.delete();
162
+ this.grContext = undefined;
163
+ if (this.glHandle) {
164
+ Super.CK.deleteContext(this.glHandle);
165
+ this.glHandle = undefined;
166
+ }
167
+ this.contextLost = false;
168
+ this.GpuEpoch++;
169
+ this.surface = this.CreateSurface(this.Element.width, this.Element.height);
170
+ this.DrawNow();
171
+ };
126
172
  /** Draws one frame immediately (outside the rAF loop) and presents it. */
127
173
  DrawNow() {
128
174
  const surface = this.surface;
129
- if (!surface || this.disposed)
175
+ if (!surface || this.disposed || this.contextLost)
130
176
  return;
131
177
  this.Draw(surface.getCanvas());
132
178
  surface.flush();
@@ -140,6 +186,7 @@ export class Canvas {
140
186
  // (measured 12..23 ms between frames shown 16.7 ms apart), and a game loop moving by speed * delta then
141
187
  // jumps unevenly on screen.
142
188
  const frameTime = Number(document.timeline?.currentTime ?? started);
189
+ this.FrameTimeNanos = Math.round(frameTime * 1_000_000);
143
190
  const executed = this.ExecuteAnimators(Math.round(frameTime * 1_000_000));
144
191
  const canRender = this.CanRender;
145
192
  if (canRender && !this.WasRendered)
@@ -209,6 +256,8 @@ export class Canvas {
209
256
  * fingers moved. 0 = units unknown: every event scrolls one line.
210
257
  */
211
258
  WheelDeltaPerNotch = 100;
259
+ /** Vsync time of the last frame the animators ran on (nanoseconds, the performance.now() timeline). */
260
+ FrameTimeNanos = 0;
212
261
  /** On-screen surface (SkiaBackdrop snapshots it). */
213
262
  get Surface() { return this.surface; }
214
263
  /**
@@ -257,6 +306,8 @@ export class Canvas {
257
306
  }
258
307
  Dispose() {
259
308
  this.disposed = true;
309
+ this.Element.removeEventListener("webglcontextlost", this.onContextLost);
310
+ this.Element.removeEventListener("webglcontextrestored", this.onContextRestored);
260
311
  this.Gestures = "Disabled";
261
312
  this.observer.disconnect();
262
313
  this.content?.Dispose();
@@ -389,9 +440,11 @@ export class Canvas {
389
440
  args.Scale = this.RenderingScale;
390
441
  args.Location = new SKPoint((e.clientX - rect.left) * this.RenderingScale, (e.clientY - rect.top) * this.RenderingScale);
391
442
  args.StartingLocation = args.Location;
392
- // lines and pages to pixels, then the dominant axis (the .NET browser heads, fc8980c1)
443
+ // lines and pages to pixels, then the dominant axis (the .NET browser heads, fc8980c1); a vertical scroll leaves
444
+ // horizontal events alone (C# ba03cb20)
393
445
  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 };
446
+ const horizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY);
447
+ args.Wheel = { Delta: (horizontal ? e.deltaX : e.deltaY) * factor, IsHorizontal: horizontal };
395
448
  if (this.gestures === "Lock") {
396
449
  e.preventDefault();
397
450
  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
@@ -36,6 +36,8 @@ export declare class CachedObject {
36
36
  constructor(Type: SkiaCacheType,
37
37
  /** Rect the content was recorded for, canvas pixels at recording time. */
38
38
  Bounds: SKRect, Scale: number, Picture?: SkPicture | undefined, Image?: Image | undefined);
39
+ /** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
40
+ Epoch: number;
39
41
  /** Replays the cache with its top-left moved to (left, top). */
40
42
  Draw(canvas: SkCanvas, left: number, top: number): void;
41
43
  Dispose(): void;
@@ -181,7 +183,12 @@ export declare class SkiaControl {
181
183
  protected OnVisualEffectsChanged(): void;
182
184
  /** Post renderers that can render this frame (compiled shaders); an unready effect leaves the control drawn plainly. */
183
185
  private ActivePostRenderers;
184
- /** C# CachedImage: the cached texture and the canvas rect it was rasterized over (Image caches only). */
186
+ /**
187
+ * C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
188
+ * re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
189
+ * at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
190
+ * there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
191
+ */
185
192
  get CachedImage(): CachedTexture | undefined;
186
193
  /** Clip overlay effects to the control's shape (CreateClip). */
187
194
  ClipEffects: boolean;
@@ -22,6 +22,8 @@ export class CachedObject {
22
22
  this.Picture = Picture;
23
23
  this.Image = Image;
24
24
  }
25
+ /** Canvas.GpuEpoch it was made in: after a WebGL context restore it is remade (a picture may replay old GPU images). */
26
+ Epoch = 0;
25
27
  /** Replays the cache with its top-left moved to (left, top). */
26
28
  Draw(canvas, left, top) {
27
29
  const CK = Super.CK;
@@ -235,10 +237,18 @@ export class SkiaControl {
235
237
  return [];
236
238
  return this.EffectPostRenderers.filter((e) => e.NeedApply);
237
239
  }
238
- /** C# CachedImage: the cached texture and the canvas rect it was rasterized over (Image caches only). */
240
+ /**
241
+ * C# CachedImage: the cached texture (Image caches only) and the canvas rect it covers NOW. Scrolled content is
242
+ * re-arranged here, not drawn through a translated canvas, so a cache recorded before its control moved is blitted
243
+ * at the control's current aligned rect (RenderContent) and its record-time Bounds are stale: an effect sampling it
244
+ * there (the slides of a SkiaShaderCarousel after the page scrolled) was off by the scroll amount.
245
+ */
239
246
  get CachedImage() {
240
247
  const c = this.RenderObject;
241
- return c?.Image ? { Image: c.Image, Bounds: c.Bounds } : undefined;
248
+ if (!c?.Image)
249
+ return undefined;
250
+ const r = this.AlignedCacheRect(c.Scale);
251
+ return { Image: c.Image, Bounds: SKRect.Create(r.Left, r.Top, c.Bounds.Width, c.Bounds.Height) };
242
252
  }
243
253
  /** Clip overlay effects to the control's shape (CreateClip). */
244
254
  ClipEffects = true;
@@ -683,7 +693,10 @@ export class SkiaControl {
683
693
  }
684
694
  else {
685
695
  const r = cacheType === "Operations" ? this.ExpandedCacheRect(ctx.Scale) : this.AlignedCacheRect(ctx.Scale);
686
- const stale = this.cacheDirty || !this.RenderObject || this.RenderObject.Type !== cacheType
696
+ const epoch = this.Superview?.GpuEpoch ?? 0;
697
+ if (this.RenderObjectPrevious && this.RenderObjectPrevious.Epoch !== epoch)
698
+ this.DisposePrevious(); // made on a lost GPU context
699
+ const stale = this.cacheDirty || !this.RenderObject || this.RenderObject.Type !== cacheType || this.RenderObject.Epoch !== epoch
687
700
  || this.RenderObject.Scale !== ctx.Scale
688
701
  || Math.round(this.RenderObject.Bounds.Width) !== Math.round(r.Width) || Math.round(this.RenderObject.Bounds.Height) !== Math.round(r.Height);
689
702
  if (stale)
@@ -742,6 +755,7 @@ export class SkiaControl {
742
755
  const picture = recorder.finishRecordingAsPicture();
743
756
  recorder.delete();
744
757
  this.RenderObject = new CachedObject("Operations", r, ctx.Scale, picture);
758
+ this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
745
759
  }
746
760
  else if (cacheType === "ImageComposite") {
747
761
  this.CreateCompositeRenderingObject(ctx, r, w, h);
@@ -765,6 +779,7 @@ export class SkiaControl {
765
779
  const image = offscreen.makeImageSnapshot();
766
780
  offscreen.delete();
767
781
  this.RenderObject = new CachedObject(cacheType, r, ctx.Scale, undefined, image);
782
+ this.RenderObject.Epoch = this.Superview?.GpuEpoch ?? 0;
768
783
  }
769
784
  this.cacheDirty = false;
770
785
  }
@@ -783,7 +798,9 @@ export class SkiaControl {
783
798
  }
784
799
  let surface = this.compositeSurface;
785
800
  const prev = this.RenderObject;
786
- const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale
801
+ const epoch = this.Superview?.GpuEpoch ?? 0;
802
+ // the kept surface belongs to the GPU context it was made on: after a context restore it is made again
803
+ const sameGeometry = !!surface && !!prev && prev.Type === "ImageComposite" && prev.Scale === ctx.Scale && prev.Epoch === epoch
787
804
  && Math.round(prev.Bounds.Width) === w && Math.round(prev.Bounds.Height) === h;
788
805
  if (!sameGeometry) {
789
806
  surface?.delete();
@@ -863,6 +880,7 @@ export class SkiaControl {
863
880
  const image = surface.makeImageSnapshot();
864
881
  const old = this.RenderObject;
865
882
  this.RenderObject = new CachedObject("ImageComposite", r, ctx.Scale, undefined, image);
883
+ this.RenderObject.Epoch = epoch;
866
884
  if (old) {
867
885
  const sv = this.Superview;
868
886
  if (sv)
@@ -125,6 +125,13 @@ 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
+ private frozenEpoch;
129
+ /** Canvas.GpuEpoch: GPU textures made before a WebGL context restore are made again. */
130
+ protected GpuEpoch(): number;
131
+ /** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
132
+ private frozenAt?;
133
+ private ParentOrigin;
134
+ private FollowParent;
128
135
  /** C# GetPrimaryTexture: the parent's cache, a frozen snapshot (Once) or nothing (Never). */
129
136
  protected GetPrimaryTexture(_ctx: DrawingContext, _destination: SKRect): CachedTexture | undefined;
130
137
  /** 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,12 +159,27 @@ export class SkiaShaderEffect extends SkiaEffect {
158
159
  return undefined;
159
160
  return { Image: image, Bounds: destination, Origin: { X: ox, Y: oy } };
160
161
  }
162
+ frozenEpoch = 0;
163
+ /** Canvas.GpuEpoch: GPU textures made before a WebGL context restore are made again. */
164
+ GpuEpoch() { return this.Parent?.Superview?.GpuEpoch ?? 0; }
165
+ /** Where the parent was when the texture was frozen (Once): the texture moves with it, as its cache does. */
166
+ frozenAt;
167
+ ParentOrigin() { const r = this.Parent?.DrawingRect; return { X: r?.Left ?? 0, Y: r?.Top ?? 0 }; }
168
+ FollowParent(texture) {
169
+ if (!texture || !this.frozenAt)
170
+ return texture;
171
+ const now = this.ParentOrigin(), dx = now.X - this.frozenAt.X, dy = now.Y - this.frozenAt.Y;
172
+ if (dx === 0 && dy === 0)
173
+ return texture;
174
+ const b = texture.Bounds;
175
+ 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 } };
176
+ }
161
177
  /** C# GetPrimaryTexture: the parent's cache, a frozen snapshot (Once) or nothing (Never). */
162
178
  GetPrimaryTexture(_ctx, _destination) {
163
179
  switch (this.UseBackground) {
164
180
  case "Never": return undefined;
165
181
  case "Once":
166
- if (!this.AquiredBackground || !this.frozen) {
182
+ if (!this.AquiredBackground || !this.frozen || this.frozenEpoch !== this.GpuEpoch()) { // a frozen texture of a lost GPU context is taken again
167
183
  this.ReleaseFrozenSnapshot();
168
184
  let snapshot = this.Parent?.CachedImage;
169
185
  let owned = false;
@@ -175,9 +191,11 @@ export class SkiaShaderEffect extends SkiaEffect {
175
191
  this.frozen = snapshot;
176
192
  this.frozenOwned = owned;
177
193
  this.AquiredBackground = true;
194
+ this.frozenAt = this.ParentOrigin();
195
+ this.frozenEpoch = this.GpuEpoch();
178
196
  }
179
197
  }
180
- return this.frozen;
198
+ return this.FollowParent(this.frozen);
181
199
  default:
182
200
  return this.Parent?.CachedImage;
183
201
  }
@@ -420,7 +438,8 @@ export class ShaderDoubleTexturesEffect extends SkiaShaderEffect {
420
438
  return undefined;
421
439
  const w = Math.round(d.Width), h = Math.round(d.Height);
422
440
  const cur = slot === "p" ? this.resizedPrimary : this.resizedSecondary;
423
- if (cur && cur.source === source && cur.w === w && cur.h === h)
441
+ const epoch = this.GpuEpoch();
442
+ if (cur && cur.source === source && cur.w === w && cur.h === h && cur.epoch === epoch)
424
443
  return cur.image;
425
444
  const main = ctx.Context.Surface;
426
445
  if (!main)
@@ -435,7 +454,7 @@ export class ShaderDoubleTexturesEffect extends SkiaShaderEffect {
435
454
  const image = surface.makeImageSnapshot();
436
455
  surface.delete();
437
456
  cur?.image.delete();
438
- const entry = { source, w, h, image };
457
+ const entry = { source, w, h, epoch, image };
439
458
  if (slot === "p")
440
459
  this.resizedPrimary = entry;
441
460
  else
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.15",
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",