drawnui-react 0.1.0-preview.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/PARITY.md +287 -0
  3. package/README.md +92 -0
  4. package/SKIPPED.md +107 -0
  5. package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
  6. package/dist/controls/AnimatedFramesRenderer.js +133 -0
  7. package/dist/controls/GridStructure.d.ts +79 -0
  8. package/dist/controls/GridStructure.js +403 -0
  9. package/dist/controls/RefreshIndicator.d.ts +35 -0
  10. package/dist/controls/RefreshIndicator.js +82 -0
  11. package/dist/controls/SkiaBackdrop.d.ts +25 -0
  12. package/dist/controls/SkiaBackdrop.js +80 -0
  13. package/dist/controls/SkiaButton.d.ts +58 -0
  14. package/dist/controls/SkiaButton.js +148 -0
  15. package/dist/controls/SkiaCarousel.d.ts +133 -0
  16. package/dist/controls/SkiaCarousel.js +603 -0
  17. package/dist/controls/SkiaCheckbox.d.ts +21 -0
  18. package/dist/controls/SkiaCheckbox.js +118 -0
  19. package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
  20. package/dist/controls/SkiaDecoratedGrid.js +46 -0
  21. package/dist/controls/SkiaDrawer.d.ts +46 -0
  22. package/dist/controls/SkiaDrawer.js +212 -0
  23. package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
  24. package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
  25. package/dist/controls/SkiaEditor.d.ts +163 -0
  26. package/dist/controls/SkiaEditor.js +803 -0
  27. package/dist/controls/SkiaGif.d.ts +54 -0
  28. package/dist/controls/SkiaGif.js +206 -0
  29. package/dist/controls/SkiaHotspot.d.ts +13 -0
  30. package/dist/controls/SkiaHotspot.js +36 -0
  31. package/dist/controls/SkiaImage.d.ts +75 -0
  32. package/dist/controls/SkiaImage.js +222 -0
  33. package/dist/controls/SkiaImageTiles.d.ts +27 -0
  34. package/dist/controls/SkiaImageTiles.js +76 -0
  35. package/dist/controls/SkiaLabel.d.ts +160 -0
  36. package/dist/controls/SkiaLabel.js +580 -0
  37. package/dist/controls/SkiaLayout.d.ts +163 -0
  38. package/dist/controls/SkiaLayout.js +723 -0
  39. package/dist/controls/SkiaLottie.d.ts +75 -0
  40. package/dist/controls/SkiaLottie.js +290 -0
  41. package/dist/controls/SkiaProgress.d.ts +40 -0
  42. package/dist/controls/SkiaProgress.js +80 -0
  43. package/dist/controls/SkiaRadioButton.d.ts +32 -0
  44. package/dist/controls/SkiaRadioButton.js +157 -0
  45. package/dist/controls/SkiaRichLabel.d.ts +77 -0
  46. package/dist/controls/SkiaRichLabel.js +274 -0
  47. package/dist/controls/SkiaScroll.d.ts +231 -0
  48. package/dist/controls/SkiaScroll.js +958 -0
  49. package/dist/controls/SkiaScrollBar.d.ts +56 -0
  50. package/dist/controls/SkiaScrollBar.js +158 -0
  51. package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
  52. package/dist/controls/SkiaShaderCarousel.js +246 -0
  53. package/dist/controls/SkiaShape.d.ts +63 -0
  54. package/dist/controls/SkiaShape.js +317 -0
  55. package/dist/controls/SkiaSlider.d.ts +74 -0
  56. package/dist/controls/SkiaSlider.js +243 -0
  57. package/dist/controls/SkiaSprite.d.ts +99 -0
  58. package/dist/controls/SkiaSprite.js +324 -0
  59. package/dist/controls/SkiaSpriteSet.d.ts +27 -0
  60. package/dist/controls/SkiaSpriteSet.js +75 -0
  61. package/dist/controls/SkiaSvg.d.ts +43 -0
  62. package/dist/controls/SkiaSvg.js +181 -0
  63. package/dist/controls/SkiaSwitch.d.ts +26 -0
  64. package/dist/controls/SkiaSwitch.js +147 -0
  65. package/dist/controls/SkiaToggle.d.ts +50 -0
  66. package/dist/controls/SkiaToggle.js +71 -0
  67. package/dist/controls/SnappingLayout.d.ts +78 -0
  68. package/dist/controls/SnappingLayout.js +217 -0
  69. package/dist/controls/TextSpan.d.ts +70 -0
  70. package/dist/controls/TextSpan.js +76 -0
  71. package/dist/core/Accessibility.d.ts +104 -0
  72. package/dist/core/Accessibility.js +150 -0
  73. package/dist/core/Animators.d.ts +82 -0
  74. package/dist/core/Animators.js +247 -0
  75. package/dist/core/Canvas.d.ts +89 -0
  76. package/dist/core/Canvas.js +328 -0
  77. package/dist/core/ControlStyle.d.ts +6 -0
  78. package/dist/core/ControlStyle.js +11 -0
  79. package/dist/core/Easing.d.ts +13 -0
  80. package/dist/core/Easing.js +15 -0
  81. package/dist/core/Gestures.d.ts +74 -0
  82. package/dist/core/Gestures.js +104 -0
  83. package/dist/core/ImageEffects.d.ts +29 -0
  84. package/dist/core/ImageEffects.js +55 -0
  85. package/dist/core/KeyboardManager.d.ts +25 -0
  86. package/dist/core/KeyboardManager.js +54 -0
  87. package/dist/core/ScrollAnimators.d.ts +90 -0
  88. package/dist/core/ScrollAnimators.js +244 -0
  89. package/dist/core/SkiaControl.d.ts +401 -0
  90. package/dist/core/SkiaControl.js +1239 -0
  91. package/dist/core/SkiaEffect.d.ts +216 -0
  92. package/dist/core/SkiaEffect.js +571 -0
  93. package/dist/core/SkiaImageManager.d.ts +43 -0
  94. package/dist/core/SkiaImageManager.js +122 -0
  95. package/dist/core/Super.d.ts +61 -0
  96. package/dist/core/Super.js +178 -0
  97. package/dist/core/TextInputProxy.d.ts +31 -0
  98. package/dist/core/TextInputProxy.js +144 -0
  99. package/dist/core/Types.d.ts +177 -0
  100. package/dist/core/Types.js +122 -0
  101. package/dist/core/ViewsAdapter.d.ts +41 -0
  102. package/dist/core/ViewsAdapter.js +113 -0
  103. package/dist/index.d.ts +48 -0
  104. package/dist/index.js +48 -0
  105. package/dist/react/SkiaShell.d.ts +144 -0
  106. package/dist/react/SkiaShell.js +490 -0
  107. package/dist/react/index.d.ts +110 -0
  108. package/dist/react/index.js +158 -0
  109. package/dist/react/reconciler.d.ts +14 -0
  110. package/dist/react/reconciler.js +160 -0
  111. package/package.json +60 -0
@@ -0,0 +1,56 @@
1
+ import type { Color, ScrollOrientation } from "../core/Types";
2
+ import { SkiaLayout } from "./SkiaLayout";
3
+ import { SkiaShape } from "./SkiaShape";
4
+ /** C# ScrollBarVisibility flags: which axis a SkiaScroll shows a bar for (matched against its Orientation). */
5
+ export type ScrollBarVisibility = "None" | "Vertical" | "Horizontal" | "Both";
6
+ /** C# ScrollBarDock: edge the bar docks to, perpendicular to the scrolling axis. */
7
+ export type ScrollBarDock = "End" | "Start";
8
+ /**
9
+ * C# IScrollBar: a scroll bar indicator overlay for SkiaScroll; the scroll pushes its state here whenever the offset,
10
+ * content size or scrolling state change.
11
+ */
12
+ export interface IScrollBar {
13
+ SetScrollProgress(orientation: ScrollOrientation, progress: number, thumbSizeRatio: number, overscrollPts: number, isScrolling: boolean): void;
14
+ }
15
+ /**
16
+ * Mirrors DrawnUi SkiaScrollBar: thin rounded thumb over an optional track, docked at the right edge for vertical
17
+ * scrolling or at the bottom edge for horizontal (`Dock`), auto-hides `HideDelaySecs` after scrolling stops
18
+ * (`AutoHide`), squashes on overscroll. Set through `SkiaScroll.ScrollBar` / `ScrollBarHorizontal`, or created by
19
+ * `ScrollBarsVisibility`.
20
+ */
21
+ export declare class SkiaScrollBar extends SkiaLayout implements IScrollBar {
22
+ private dock;
23
+ get Dock(): ScrollBarDock;
24
+ set Dock(v: ScrollBarDock);
25
+ private thumbColor;
26
+ get ThumbColor(): Color;
27
+ set ThumbColor(v: Color);
28
+ private trackColor;
29
+ get TrackColor(): Color;
30
+ set TrackColor(v: Color);
31
+ private thickness;
32
+ /** Thickness of the bar in points. */
33
+ get Thickness(): number;
34
+ set Thickness(v: number);
35
+ private edgeMargin;
36
+ /** Distance in points from the docked edge. */
37
+ get EdgeMargin(): number;
38
+ set EdgeMargin(v: number);
39
+ /** Minimum thumb length in points. */
40
+ MinThumbSize: number;
41
+ /** Fade out after scrolling stops. */
42
+ AutoHide: boolean;
43
+ HideDelaySecs: number;
44
+ protected readonly thumb: SkiaShape;
45
+ protected readonly track: SkiaShape;
46
+ private hideTimer;
47
+ private hideAbort?;
48
+ private lastThumbLen;
49
+ private orientation;
50
+ constructor();
51
+ /** Docks track and thumb to the edge matching the current orientation (C# ApplyOrientation). */
52
+ protected ApplyOrientation(): void;
53
+ SetScrollProgress(orientation: ScrollOrientation, progress: number, thumbSizeRatio: number, overscrollPts: number, isScrolling: boolean): void;
54
+ private CancelHide;
55
+ protected OnDisposing(): void;
56
+ }
@@ -0,0 +1,158 @@
1
+ import { Colors, Thickness } from "../core/Types";
2
+ import { SkiaLayout } from "./SkiaLayout";
3
+ import { SkiaShape } from "./SkiaShape";
4
+ /**
5
+ * Mirrors DrawnUi SkiaScrollBar: thin rounded thumb over an optional track, docked at the right edge for vertical
6
+ * scrolling or at the bottom edge for horizontal (`Dock`), auto-hides `HideDelaySecs` after scrolling stops
7
+ * (`AutoHide`), squashes on overscroll. Set through `SkiaScroll.ScrollBar` / `ScrollBarHorizontal`, or created by
8
+ * `ScrollBarsVisibility`.
9
+ */
10
+ export class SkiaScrollBar extends SkiaLayout {
11
+ dock = "End";
12
+ get Dock() { return this.dock; }
13
+ set Dock(v) { if (this.dock !== v) {
14
+ this.dock = v;
15
+ this.ApplyOrientation();
16
+ } }
17
+ thumbColor = "#66888888";
18
+ get ThumbColor() { return this.thumbColor; }
19
+ set ThumbColor(v) { if (this.thumbColor !== v) {
20
+ this.thumbColor = v;
21
+ this.thumb.BackgroundColor = v;
22
+ this.thumb.InvalidateCache();
23
+ this.RepaintComposition();
24
+ } }
25
+ trackColor = Colors.Transparent;
26
+ get TrackColor() { return this.trackColor; }
27
+ set TrackColor(v) { if (this.trackColor !== v) {
28
+ this.trackColor = v;
29
+ this.track.BackgroundColor = v;
30
+ this.track.InvalidateCache();
31
+ this.RepaintComposition();
32
+ } }
33
+ thickness = 4;
34
+ /** Thickness of the bar in points. */
35
+ get Thickness() { return this.thickness; }
36
+ set Thickness(v) { if (this.thickness !== v) {
37
+ this.thickness = v;
38
+ this.ApplyOrientation();
39
+ } }
40
+ edgeMargin = 2;
41
+ /** Distance in points from the docked edge. */
42
+ get EdgeMargin() { return this.edgeMargin; }
43
+ set EdgeMargin(v) { if (this.edgeMargin !== v) {
44
+ this.edgeMargin = v;
45
+ this.ApplyOrientation();
46
+ } }
47
+ /** Minimum thumb length in points. */
48
+ MinThumbSize = 32;
49
+ /** Fade out after scrolling stops. */
50
+ AutoHide = true;
51
+ HideDelaySecs = 1;
52
+ thumb = new SkiaShape();
53
+ track = new SkiaShape();
54
+ hideTimer = 0;
55
+ hideAbort;
56
+ lastThumbLen = -1;
57
+ orientation = "Vertical";
58
+ constructor() {
59
+ super();
60
+ this.Type = "Absolute";
61
+ this.UseCache = "Operations";
62
+ this.HorizontalOptions = "Fill";
63
+ this.VerticalOptions = "Fill";
64
+ this.InputTransparent = true; // display only, gestures pass through
65
+ this.Opacity = 0; // hidden until the first scroll
66
+ this.track.Type = "Rectangle";
67
+ this.track.BackgroundColor = this.trackColor;
68
+ this.track.UseCache = "Operations";
69
+ this.thumb.Type = "Rectangle";
70
+ this.thumb.BackgroundColor = this.thumbColor;
71
+ this.thumb.UseCache = "Operations";
72
+ this.AddSubView(this.track);
73
+ this.AddSubView(this.thumb);
74
+ this.ApplyOrientation();
75
+ }
76
+ /** Docks track and thumb to the edge matching the current orientation (C# ApplyOrientation). */
77
+ ApplyOrientation() {
78
+ const radius = this.thickness / 2, dockStart = this.dock === "Start";
79
+ const dockOptions = dockStart ? "Start" : "End";
80
+ if (this.orientation === "Horizontal") {
81
+ const margin = dockStart ? new Thickness(0, this.edgeMargin, 0, 0) : new Thickness(0, 0, 0, this.edgeMargin);
82
+ this.track.HorizontalOptions = "Fill";
83
+ this.track.VerticalOptions = dockOptions;
84
+ this.track.HeightRequest = this.thickness;
85
+ this.track.WidthRequest = -1;
86
+ this.track.Margin = margin;
87
+ this.track.CornerRadius = radius;
88
+ this.thumb.HorizontalOptions = "Start";
89
+ this.thumb.VerticalOptions = dockOptions;
90
+ this.thumb.HeightRequest = this.thickness;
91
+ this.thumb.Margin = margin;
92
+ this.thumb.CornerRadius = radius;
93
+ }
94
+ else {
95
+ const margin = dockStart ? new Thickness(this.edgeMargin, 0, 0, 0) : new Thickness(0, 0, this.edgeMargin, 0);
96
+ this.track.HorizontalOptions = dockOptions;
97
+ this.track.VerticalOptions = "Fill";
98
+ this.track.WidthRequest = this.thickness;
99
+ this.track.HeightRequest = -1;
100
+ this.track.Margin = margin;
101
+ this.track.CornerRadius = radius;
102
+ this.thumb.HorizontalOptions = dockOptions;
103
+ this.thumb.VerticalOptions = "Start";
104
+ this.thumb.WidthRequest = this.thickness;
105
+ this.thumb.Margin = margin;
106
+ this.thumb.CornerRadius = radius;
107
+ }
108
+ this.lastThumbLen = -1;
109
+ this.InvalidateMeasure();
110
+ }
111
+ SetScrollProgress(orientation, progress, thumbSizeRatio, overscrollPts, isScrolling) {
112
+ if (orientation !== this.orientation) {
113
+ this.orientation = orientation;
114
+ this.ApplyOrientation();
115
+ }
116
+ if (thumbSizeRatio >= 1) {
117
+ this.CancelHide();
118
+ this.Opacity = 0;
119
+ this.RepaintComposition();
120
+ return;
121
+ } // content fits: nothing to indicate
122
+ const track = orientation === "Horizontal" ? this.MeasuredSize.Units.Width : this.MeasuredSize.Units.Height;
123
+ if (track <= 0)
124
+ return;
125
+ let thumbLen = Math.max(this.MinThumbSize, track * thumbSizeRatio);
126
+ if (overscrollPts !== 0)
127
+ thumbLen = Math.max(this.MinThumbSize / 2, thumbLen - Math.abs(overscrollPts)); // squash on bounce
128
+ const travel = track - thumbLen;
129
+ const offset = Math.max(0, Math.min(1, progress)) * travel;
130
+ if (Math.abs(thumbLen - this.lastThumbLen) > 0.5) {
131
+ this.lastThumbLen = thumbLen;
132
+ if (orientation === "Horizontal")
133
+ this.thumb.WidthRequest = thumbLen;
134
+ else
135
+ this.thumb.HeightRequest = thumbLen;
136
+ this.thumb.InvalidateMeasure();
137
+ }
138
+ if (orientation === "Horizontal")
139
+ this.thumb.TranslationX = offset;
140
+ else
141
+ this.thumb.TranslationY = offset;
142
+ this.CancelHide();
143
+ this.Opacity = 1;
144
+ if (this.AutoHide && !isScrolling) {
145
+ this.hideTimer = window.setTimeout(() => { this.hideTimer = 0; this.hideAbort = new AbortController(); void this.FadeToAsync(0, 250, undefined, this.hideAbort.signal); }, this.HideDelaySecs * 1000);
146
+ }
147
+ this.RepaintComposition();
148
+ }
149
+ CancelHide() {
150
+ if (this.hideTimer) {
151
+ clearTimeout(this.hideTimer);
152
+ this.hideTimer = 0;
153
+ }
154
+ this.hideAbort?.abort();
155
+ this.hideAbort = undefined;
156
+ }
157
+ OnDisposing() { this.CancelHide(); super.OnDisposing(); }
158
+ }
@@ -0,0 +1,67 @@
1
+ import type { DrawingContext } from "../core/SkiaControl";
2
+ import { type GestureEventProcessingInfo, SKPoint, type SkiaGesturesParameters } from "../core/Gestures";
3
+ import { ShaderTransitionEffect } from "../core/SkiaEffect";
4
+ import type { ScaledSize } from "../core/Types";
5
+ import type { SkiaControl } from "../core/SkiaControl";
6
+ import { SkiaCarousel } from "./SkiaCarousel";
7
+ /**
8
+ * Mirrors DrawnUi SkiaShaderCarousel: slides never move, an attached `ShaderTransitionEffect` blends the cached
9
+ * images of the outgoing and incoming slides (`TransitionShader` url of a gl-transitions style `transition(vec2 uv)`
10
+ * SkSL, or `TransitionShaderCode`, optionally a custom `TransitionTemplate`). Slides MUST be cached with
11
+ * `UseCache="Image"` (the effect samples their caches). One gesture moves at most one slide; a swipe during a running
12
+ * transition wraps it up within `InterruptedTransitionMs` first. The carousel itself is cached as Image so the
13
+ * overlapping slides are never shown directly.
14
+ */
15
+ export declare class SkiaShaderCarousel extends SkiaCarousel {
16
+ /** Url of the transition .sksl (fetched, wrapped by the adapter template). Can change at any time. */
17
+ get TransitionShader(): string;
18
+ set TransitionShader(v: string);
19
+ /** Raw SkSL alternative to TransitionShader. */
20
+ get TransitionShaderCode(): string;
21
+ set TransitionShaderCode(v: string);
22
+ /** Url of a custom template replacing the embedded gl-transitions adapter. */
23
+ get TransitionTemplate(): string;
24
+ set TransitionTemplate(v: string);
25
+ /** Wrap-up time of a transition interrupted by a new swipe, ms (0 = instant). */
26
+ InterruptedTransitionMs: number;
27
+ /** The effect rendering the transition (extra uniforms, OnCompilationError). Created by CreateTransitionEffect. */
28
+ readonly TransitionEffect: ShaderTransitionEffect;
29
+ /** Index the current transition starts from, -1 before the first layout. */
30
+ get TransitionFromIndex(): number;
31
+ /** Index the current transition goes to (0 after the last slide when looped). */
32
+ get TransitionToIndex(): number;
33
+ FromToChanged?: (sender: SkiaShaderCarousel) => void;
34
+ private retrySetup;
35
+ private lastProgress;
36
+ private initialized;
37
+ private indexFrom;
38
+ private indexTo;
39
+ private indexFromLast;
40
+ private indexToLast;
41
+ private wasWrapped;
42
+ private wasInTransitionAtDown;
43
+ private inGestureRelease;
44
+ private gestureOrigin;
45
+ private gestureFrom;
46
+ private pendingTarget?;
47
+ constructor();
48
+ /** Factory for the transition effect; override for a subclass with extra uniforms. */
49
+ protected CreateTransitionEffect(): ShaderTransitionEffect;
50
+ protected OnFromToChanged(): void;
51
+ Render(ctx: DrawingContext): void;
52
+ /** Points the effect at the from/to views; false when a view does not exist yet (retried after the next render). */
53
+ protected SetupFromTo(): boolean;
54
+ protected MeasureAbsolute(w: number, h: number, scale: number): ScaledSize;
55
+ protected OnChildrenInitialized(): void;
56
+ protected OnScrollProgressChanged(): void;
57
+ /** Slides never move: the transition effect renders the change (C# AnimateVisibleChild no-op). */
58
+ protected SlideOffset(_offset: SKPoint): SKPoint;
59
+ ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
60
+ /**
61
+ * Gesture targeting (C#): one gesture moves at most one slide from the slide it started on; a flick moves one slide
62
+ * in its direction, a slow drag snaps within one step of the origin; a swipe that interrupted a running transition
63
+ * first wraps that transition up within InterruptedTransitionMs, then the new one plays.
64
+ */
65
+ protected ScrollToOffset(targetOffset: SKPoint, velocity: SKPoint, animate: boolean): boolean;
66
+ protected OnTransitionChanged(): void;
67
+ }
@@ -0,0 +1,246 @@
1
+ import { SKPoint } from "../core/Gestures";
2
+ import { ShaderTransitionEffect } from "../core/SkiaEffect";
3
+ import { SkiaCarousel } from "./SkiaCarousel";
4
+ /**
5
+ * Mirrors DrawnUi SkiaShaderCarousel: slides never move, an attached `ShaderTransitionEffect` blends the cached
6
+ * images of the outgoing and incoming slides (`TransitionShader` url of a gl-transitions style `transition(vec2 uv)`
7
+ * SkSL, or `TransitionShaderCode`, optionally a custom `TransitionTemplate`). Slides MUST be cached with
8
+ * `UseCache="Image"` (the effect samples their caches). One gesture moves at most one slide; a swipe during a running
9
+ * transition wraps it up within `InterruptedTransitionMs` first. The carousel itself is cached as Image so the
10
+ * overlapping slides are never shown directly.
11
+ */
12
+ export class SkiaShaderCarousel extends SkiaCarousel {
13
+ /** Url of the transition .sksl (fetched, wrapped by the adapter template). Can change at any time. */
14
+ get TransitionShader() { return this.TransitionEffect.ShaderSource; }
15
+ set TransitionShader(v) { this.TransitionEffect.ShaderSource = v ?? ""; }
16
+ /** Raw SkSL alternative to TransitionShader. */
17
+ get TransitionShaderCode() { return this.TransitionEffect.ShaderCode; }
18
+ set TransitionShaderCode(v) { this.TransitionEffect.ShaderCode = v ?? ""; }
19
+ /** Url of a custom template replacing the embedded gl-transitions adapter. */
20
+ get TransitionTemplate() { return this.TransitionEffect.ShaderTemplate; }
21
+ set TransitionTemplate(v) { this.TransitionEffect.ShaderTemplate = v ?? ""; }
22
+ /** Wrap-up time of a transition interrupted by a new swipe, ms (0 = instant). */
23
+ InterruptedTransitionMs = 50;
24
+ /** The effect rendering the transition (extra uniforms, OnCompilationError). Created by CreateTransitionEffect. */
25
+ TransitionEffect;
26
+ /** Index the current transition starts from, -1 before the first layout. */
27
+ get TransitionFromIndex() { return this.indexFrom; }
28
+ /** Index the current transition goes to (0 after the last slide when looped). */
29
+ get TransitionToIndex() { return this.indexTo; }
30
+ FromToChanged;
31
+ retrySetup = false;
32
+ lastProgress = 0;
33
+ initialized = false;
34
+ indexFrom = -1;
35
+ indexTo = -1;
36
+ indexFromLast = -1;
37
+ indexToLast = -1;
38
+ wasWrapped = false;
39
+ // gesture targeting
40
+ wasInTransitionAtDown = false;
41
+ inGestureRelease = false;
42
+ gestureOrigin = SKPoint.Empty;
43
+ gestureFrom = SKPoint.Empty;
44
+ pendingTarget;
45
+ constructor() {
46
+ super();
47
+ this.RecyclingTemplate = "Disabled"; // the effect samples specific cells: every index keeps its own view
48
+ this.UseCache = "Image";
49
+ this.TransitionEffect = this.CreateTransitionEffect();
50
+ this.VisualEffects = [this.TransitionEffect];
51
+ }
52
+ /** Factory for the transition effect; override for a subclass with extra uniforms. */
53
+ CreateTransitionEffect() { return new ShaderTransitionEffect(); }
54
+ OnFromToChanged() { this.FromToChanged?.(this); }
55
+ Render(ctx) {
56
+ super.Render(ctx);
57
+ if (this.retrySetup && !this.initialized && this.indexFrom >= 0 && this.indexTo >= 0 && this.indexTo <= this.MaxIndex) {
58
+ this.initialized = this.SetupFromTo();
59
+ if (this.initialized) {
60
+ this.TransitionEffect.Progress = this.lastProgress;
61
+ this.TransitionEffect.Update();
62
+ }
63
+ }
64
+ }
65
+ /** Points the effect at the from/to views; false when a view does not exist yet (retried after the next render). */
66
+ SetupFromTo() {
67
+ this.indexToLast = this.indexTo;
68
+ this.indexFromLast = this.indexFrom;
69
+ const viewFrom = this.GetExistingView(this.indexFrom);
70
+ const viewTo = this.GetExistingView(this.indexTo);
71
+ if (!viewFrom || !viewTo) {
72
+ this.retrySetup = true;
73
+ this.Repaint();
74
+ return false;
75
+ }
76
+ this.retrySetup = false;
77
+ this.TransitionEffect.ControlFrom = viewFrom;
78
+ this.TransitionEffect.ControlTo = viewTo;
79
+ return true;
80
+ }
81
+ MeasureAbsolute(w, h, scale) {
82
+ this.initialized = false;
83
+ return super.MeasureAbsolute(w, h, scale);
84
+ }
85
+ OnChildrenInitialized() {
86
+ this.indexFrom = -1;
87
+ this.indexTo = -1;
88
+ this.indexFromLast = -1;
89
+ this.indexToLast = -1;
90
+ this.initialized = false;
91
+ super.OnChildrenInitialized();
92
+ }
93
+ OnScrollProgressChanged() {
94
+ if (this.IsLooped && this.MaxIndex > 0) {
95
+ const slides = this.MaxIndex + 1;
96
+ let scaled = this.ScrollProgress * this.MaxIndex;
97
+ if (scaled < 0 || scaled > this.MaxIndex) {
98
+ // beyond the real strip: a true wrap only when panning, snapping to a virtual anchor, or continuing a wrap
99
+ // already shown; otherwise it is spring overshoot at an edge — clamp instead of flashing the far slide
100
+ let wrap = this.IsUserPanning || this.wasWrapped;
101
+ if (!wrap) {
102
+ const anchor = this.GetVirtualAnchor(this.CurrentSnap);
103
+ wrap = scaled < 0 ? anchor.Id === -1 : anchor.Id === -2;
104
+ }
105
+ this.wasWrapped = wrap;
106
+ scaled = wrap ? ((scaled % slides) + slides) % slides : Math.max(0, Math.min(this.MaxIndex, scaled));
107
+ }
108
+ else
109
+ this.wasWrapped = false;
110
+ const currentIndex = Math.floor(scaled);
111
+ const progress = scaled - currentIndex;
112
+ if (this.indexFrom !== currentIndex || !this.initialized) {
113
+ this.indexFrom = currentIndex;
114
+ this.indexTo = (currentIndex + 1) % slides;
115
+ if (!this.initialized || this.indexToLast !== this.indexTo || this.indexFromLast !== this.indexFrom)
116
+ this.initialized = this.SetupFromTo();
117
+ this.OnFromToChanged();
118
+ }
119
+ this.lastProgress = progress;
120
+ this.TransitionEffect.Progress = progress;
121
+ this.TransitionEffect.Update();
122
+ return;
123
+ }
124
+ const sp = this.ScrollProgress;
125
+ if (!this.initialized || (sp >= 0 && sp <= 1)) { // ignore bouncing
126
+ let currentIndex = 0;
127
+ if (sp > 0)
128
+ currentIndex = Math.floor(this.MaxIndex * sp);
129
+ let progress = this.TransitionProgress;
130
+ if (this.indexFrom !== currentIndex || !this.initialized) {
131
+ if (currentIndex < this.MaxIndex) {
132
+ this.indexTo = currentIndex + 1;
133
+ this.indexFrom = currentIndex;
134
+ if (!this.initialized || this.indexToLast !== this.indexTo || this.indexFromLast !== this.indexFrom)
135
+ this.initialized = this.SetupFromTo();
136
+ }
137
+ else
138
+ progress = 1;
139
+ this.OnFromToChanged();
140
+ }
141
+ this.lastProgress = progress;
142
+ this.TransitionEffect.Progress = progress;
143
+ this.TransitionEffect.Update();
144
+ }
145
+ }
146
+ /** Slides never move: the transition effect renders the change (C# AnimateVisibleChild no-op). */
147
+ SlideOffset(_offset) { return SKPoint.Empty; }
148
+ ProcessGestures(args, apply) {
149
+ if (args.Type === "Down") {
150
+ const interrupted = this.InTransition;
151
+ const result = super.ProcessGestures(args, apply);
152
+ // read after base: Down may have normalized a looped virtual position; while phase 1 is still wrapping up,
153
+ // the real origin is the pending phase-2 slide
154
+ this.wasInTransitionAtDown = interrupted;
155
+ this.gestureOrigin = this.pendingTarget ?? this.CurrentSnap;
156
+ this.gestureFrom = this.CurrentPosition;
157
+ this.pendingTarget = undefined;
158
+ return result;
159
+ }
160
+ if (args.Type === "Up") {
161
+ this.inGestureRelease = true;
162
+ try {
163
+ return super.ProcessGestures(args, apply);
164
+ }
165
+ finally {
166
+ this.inGestureRelease = false;
167
+ }
168
+ }
169
+ return super.ProcessGestures(args, apply);
170
+ }
171
+ /**
172
+ * Gesture targeting (C#): one gesture moves at most one slide from the slide it started on; a flick moves one slide
173
+ * in its direction, a slow drag snaps within one step of the origin; a swipe that interrupted a running transition
174
+ * first wraps that transition up within InterruptedTransitionMs, then the new one plays.
175
+ */
176
+ ScrollToOffset(targetOffset, velocity, animate) {
177
+ if (!this.inGestureRelease || !animate || this.SnapPoints.length < 2)
178
+ return super.ScrollToOffset(targetOffset, velocity, animate);
179
+ const axis = (p) => (this.IsVertical ? p.Y : p.X);
180
+ const step = new SKPoint(this.SnapPoints[1].X - this.SnapPoints[0].X, this.SnapPoints[1].Y - this.SnapPoints[0].Y);
181
+ const stepAxis = axis(step);
182
+ if (stepAxis !== 0) {
183
+ const vel = axis(velocity);
184
+ const disp = axis(this.CurrentPosition) - axis(this.gestureFrom);
185
+ let k;
186
+ if (Math.abs(vel) >= 100)
187
+ k = Math.sign(vel) * Math.sign(stepAxis);
188
+ else
189
+ k = Math.max(-1, Math.min(1, Math.round(disp / stepAxis)));
190
+ let capped = new SKPoint(this.gestureOrigin.X + step.X * k, this.gestureOrigin.Y + step.Y * k);
191
+ if (!this.IsLooped) {
192
+ const first = this.SnapPoints[0], last = this.SnapPoints[this.SnapPoints.length - 1];
193
+ const lo = new SKPoint(Math.min(first.X, last.X), Math.min(first.Y, last.Y)), hi = new SKPoint(Math.max(first.X, last.X), Math.max(first.Y, last.Y));
194
+ capped = new SKPoint(Math.max(lo.X, Math.min(hi.X, capped.X)), Math.max(lo.Y, Math.min(hi.Y, capped.Y)));
195
+ }
196
+ targetOffset = capped; // looped: may be the virtual slot past the edge, the base wraps it
197
+ }
198
+ if (!this.wasInTransitionAtDown)
199
+ return super.ScrollToOffset(targetOffset, velocity, animate);
200
+ this.wasInTransitionAtDown = false;
201
+ // the pan may already have carried the position PAST the interrupted transition's destination toward the new
202
+ // target: wrapping it up would animate backward first. Only run phase 1 while that destination is still ahead.
203
+ const toOrigin = axis(this.gestureOrigin) - axis(this.CurrentPosition);
204
+ const toTarget = axis(targetOffset) - axis(this.CurrentPosition);
205
+ const same = targetOffset.X === this.gestureOrigin.X && targetOffset.Y === this.gestureOrigin.Y;
206
+ if (!same && (toOrigin === 0 || Math.sign(toOrigin) !== Math.sign(toTarget)))
207
+ return super.ScrollToOffset(targetOffset, velocity, animate);
208
+ this.pendingTarget = same ? undefined : targetOffset;
209
+ if (this.InterruptedTransitionMs <= 0) {
210
+ super.ScrollToOffset(this.gestureOrigin, SKPoint.Empty, false);
211
+ const instant = this.pendingTarget;
212
+ if (instant) {
213
+ this.pendingTarget = undefined;
214
+ return super.ScrollToOffset(instant, velocity, true);
215
+ }
216
+ return true;
217
+ }
218
+ // phase 1: finish the interrupted transition fast; LinearSpeedMs (ms per slide) scaled to the remaining distance
219
+ const cell = this.IsVertical ? this.CellSize.H : this.CellSize.W;
220
+ const remaining = Math.abs(toOrigin);
221
+ const keep = this.LinearSpeedMs;
222
+ this.LinearSpeedMs = remaining > 0 && cell > 0 ? (this.InterruptedTransitionMs * cell) / remaining : keep;
223
+ let started;
224
+ try {
225
+ started = super.ScrollToOffset(this.gestureOrigin, SKPoint.Empty, true);
226
+ }
227
+ finally {
228
+ this.LinearSpeedMs = keep;
229
+ }
230
+ const next = this.pendingTarget;
231
+ if (!started && next) {
232
+ this.pendingTarget = undefined;
233
+ return super.ScrollToOffset(next, velocity, true);
234
+ }
235
+ return started;
236
+ }
237
+ OnTransitionChanged() {
238
+ super.OnTransitionChanged();
239
+ // phase 2: the interrupted transition is wrapped up, now go where the swipe pointed
240
+ const next = this.pendingTarget;
241
+ if (!this.InTransition && next) {
242
+ this.pendingTarget = undefined;
243
+ this.ScrollToOffset(next, SKPoint.Empty, true);
244
+ }
245
+ }
246
+ }
@@ -0,0 +1,63 @@
1
+ import type { Path } from "canvaskit-wasm";
2
+ import { type DrawingContext } from "../core/SkiaControl";
3
+ import { type BevelType, type Color, CornerRadius, SKRect, type ShapeType, SkiaBevel, type SkiaGradient, SkiaShadow, type SkiaPoint, type StrokeCap, Thickness } from "../core/Types";
4
+ import { SkiaLayout } from "./SkiaLayout";
5
+ /**
6
+ * Mirrors DrawnUi SkiaShape: a SkiaLayout (Absolute) that paints a geometric background, strokes its outline
7
+ * INSIDE its bounds (the rect is deflated by half the stroke, so the outer stroke edge sits on DrawingRect),
8
+ * and clips its children to the stroke-inset shape. Cached as Operations by default, like DrawnUi.
9
+ */
10
+ export declare class SkiaShape extends SkiaLayout {
11
+ /** Rectangle | Circle | Ellipse | Arc | Path | Polygon | Line (Squricle/Custom draw as Rectangle). Layout is always Absolute. */
12
+ Type: ShapeType;
13
+ private cornerRadius;
14
+ /** Uniform number or per-corner CornerRadius, points. */
15
+ get CornerRadius(): CornerRadius | number;
16
+ set CornerRadius(v: CornerRadius | number);
17
+ /** Points; a negative value means device pixels. 0 = no stroke. */
18
+ StrokeWidth: number;
19
+ StrokeColor: Color;
20
+ StrokeCap: StrokeCap;
21
+ /** Gradient painted along the stroke instead of StrokeColor (C# StrokeGradient). */
22
+ StrokeGradient?: SkiaGradient;
23
+ /** Raised (Bevel) or pressed-in (Emboss) edges, drawn with Bevel after the background (C# BevelType). */
24
+ BevelType: BevelType;
25
+ private bevel?;
26
+ /** Edge depth / colors / opacity; plain `{ Depth, LightColor, ShadowColor, Opacity }` literals accepted. */
27
+ get Bevel(): SkiaBevel | Partial<SkiaBevel> | undefined;
28
+ set Bevel(v: SkiaBevel | Partial<SkiaBevel> | undefined);
29
+ /** Hollow shape: the background is not filled, only the stroke (and children) are drawn. */
30
+ ClipBackgroundColor: boolean;
31
+ /** Polygon / Line vertices as ratios (0..1) of the shape rect. */
32
+ Points: SkiaPoint[];
33
+ /** SVG path data for Type=Path, fitted and centered inside the rect. */
34
+ PathData?: string;
35
+ private shadows;
36
+ /** Drop shadows painted with the background (DrawnUi SkiaShape.Shadows); plain `{X, Y, Blur, Opacity, Color}` literals accepted. */
37
+ get Shadows(): (SkiaShadow | Partial<SkiaShadow>)[];
38
+ set Shadows(v: (SkiaShadow | Partial<SkiaShadow>)[] | undefined);
39
+ protected PaintsBackgroundWithoutColor(): boolean;
40
+ /** C# MergeShadowMargin: spread = 3 * Blur * scale around the offset shadow. */
41
+ ComputeEffectsMargin(scale: number): Thickness;
42
+ /** C# CreateShadow: DropShadow (shape + shadow) or DropShadowOnly; a fully opaque Color takes the shadow's Opacity. */
43
+ static ShadowFilter(shadow: SkiaShadow, scale: number): import("canvaskit-wasm").ImageFilter;
44
+ constructor();
45
+ private StrokePixels;
46
+ /** Rect the outline is drawn on: bounds deflated by half the stroke so the stroke stays inside. */
47
+ private StrokeAwareRect;
48
+ private UsesStroke;
49
+ /** Builds the outline path for rect (canvas pixels). Caller deletes it. */
50
+ protected CreateShapePath(rect: SKRect, scale: number): Path;
51
+ /** Children live inside the stroke (full width) + Padding. */
52
+ private ChildrenRect;
53
+ /** Clip for overlay effects (ripple) and children: the shape itself, inset by the stroke. */
54
+ CreateClip(): Path;
55
+ protected OnLayoutChanged(): void;
56
+ protected PaintBackground(ctx: DrawingContext): void;
57
+ /** C# PaintBevelEffect: two stroked edge paths (top/left and bottom/right) in the light / shadow colors. */
58
+ private PaintBevelEffect;
59
+ protected Paint(ctx: DrawingContext): void;
60
+ }
61
+ /** DrawnUi SkiaFrame = SkiaShape Type=Rectangle (alias, not a layout type). */
62
+ export declare class SkiaFrame extends SkiaShape {
63
+ }