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,603 @@
1
+ import { SKPoint } from "../core/Gestures";
2
+ import { Easing } from "../core/Easing";
3
+ import { Spring } from "../core/ScrollAnimators";
4
+ import { SKRect, ScaledSize, Thickness } from "../core/Types";
5
+ import { SnappingLayout } from "./SnappingLayout";
6
+ /**
7
+ * Mirrors DrawnUi SkiaCarousel: every child (static `Children` or a recycled `ItemsSource` + `ItemTemplate` cell)
8
+ * is a full-size slide laid along the axis, `SidesOffset` peeks the neighbours, `Spacing` separates slides; a
9
+ * swipe / release snaps to the nearest slide by velocity, `SelectedIndex` drives and reports the position,
10
+ * `IsLooped` wraps around through virtual anchors, `PreloadNeighboors` draws the hidden neighbours,
11
+ * `DynamicSize` sizes an auto-sized carousel from the selected slide, `SwipeSpeed` / `LinearSpeedMs` tune the snap.
12
+ */
13
+ export class SkiaCarousel extends SnappingLayout {
14
+ IsVertical = false;
15
+ /** Side padding in points so the previous/next slides peek in. */
16
+ SidesOffset = 0;
17
+ /** Accepted for parity; C# marks it TODO. */
18
+ IsRightToLeft = false;
19
+ /** Wrap from the last slide to the first and back. */
20
+ IsLooped = false;
21
+ /** Draw the hidden neighbour slides too (preloads images etc.), C# default true. */
22
+ PreloadNeighboors = true;
23
+ /** An auto-sized carousel takes the size of the selected slide instead of the largest one. */
24
+ DynamicSize = false;
25
+ /** Multiplies the snap velocity / spring stiffness (C# SwipeSpeed). */
26
+ SwipeSpeed = 1;
27
+ /** Milliseconds a whole slide takes when snapping without Bounces; 0 = derived from the velocity (C# LinearSpeedMs). */
28
+ LinearSpeedMs = 0;
29
+ SelectedIndexChanged;
30
+ ItemAppearing;
31
+ ItemDisappearing;
32
+ selectedIndex = 0;
33
+ lastIndex = -1;
34
+ childrenInitialized = false;
35
+ layoutKey = "";
36
+ lastLooped = false;
37
+ cellSize = { W: 0, H: 0 };
38
+ /** Slide size in points (C# CellSize). */
39
+ get CellSize() { return this.cellSize; }
40
+ panningOffset = SKPoint.Empty;
41
+ panningStart = SKPoint.Empty;
42
+ wrongDirection = false;
43
+ snapIfNoPanOnUp = false;
44
+ hadDown = false;
45
+ itemsVisibility = [];
46
+ snapPointsVirtual;
47
+ scrollAmount = 0;
48
+ /** Slides drawn on screen by the last frame: the only gesture targets (C# rendering tree). */
49
+ visibleViews = [];
50
+ constructor() {
51
+ super();
52
+ this.HorizontalOptions = "Fill";
53
+ this.IsClippedToBounds = true; // C# WillClipBounds => true
54
+ this.SnapEasing = Easing.SinOut;
55
+ }
56
+ // ---- state ----
57
+ get SelectedIndex() { return this.selectedIndex; }
58
+ set SelectedIndex(v) {
59
+ if (this.selectedIndex === v)
60
+ return;
61
+ this.InterruptSnapping();
62
+ this.lastIndex = this.selectedIndex;
63
+ this.selectedIndex = v;
64
+ this.OnSelectedIndexChanged();
65
+ if (this.SnapPoints.length > 0 && !this.IsUserPanning)
66
+ this.ApplyIndex(false);
67
+ }
68
+ /** Previously selected index (C# LastIndex). */
69
+ get LastIndex() { return this.lastIndex; }
70
+ get MaxIndex() { return this.ChildrenTotal - 1; }
71
+ get ChildrenTotal() { return this.IsTemplated ? (this.ItemsSource?.length ?? 0) : this.Children.length; }
72
+ get ChildrenCount() { return this.ChildrenTotal; }
73
+ get IsAtStart() { return this.selectedIndex === 0; }
74
+ get IsAtEnd() { return this.selectedIndex === this.MaxIndex; }
75
+ /** 0..1 progress of the content along all slides (C# ScrollProgress). */
76
+ get ScrollProgress() {
77
+ const last = this.SnapPoints[this.SnapPoints.length - 1];
78
+ if (!last)
79
+ return 0;
80
+ const max = this.IsVertical ? last.Y : last.X;
81
+ return max === 0 ? 0 : (this.IsVertical ? this.CurrentPosition.Y : this.CurrentPosition.X) / max;
82
+ }
83
+ /** Scroll amount of the selected slide, 0..1 of the track (C# ScrollAmount). */
84
+ get ScrollAmount() { return this.scrollAmount; }
85
+ /** Fraction of the current transition between two slides (C# TransitionProgress). */
86
+ get TransitionProgress() {
87
+ if (this.MaxIndex < 1)
88
+ return 0;
89
+ const scaled = this.ScrollProgress * this.MaxIndex;
90
+ return scaled - Math.floor(scaled);
91
+ }
92
+ OnSelectedIndexChanged() {
93
+ this.SelectedIndexChanged?.(this, this.selectedIndex);
94
+ this.NotifyAccessibility();
95
+ if (this.DynamicSize)
96
+ this.InvalidateMeasure();
97
+ }
98
+ /** Index reached by scrolling: state only, no snap (C# SelectedIndex set from UpdateReportedPosition). */
99
+ ReportIndex(i) {
100
+ if (i === this.selectedIndex)
101
+ return;
102
+ this.lastIndex = this.selectedIndex;
103
+ this.selectedIndex = i;
104
+ this.OnSelectedIndexChanged();
105
+ }
106
+ GoNext() {
107
+ if (this.selectedIndex < this.MaxIndex)
108
+ this.SelectedIndex = this.selectedIndex + 1;
109
+ else if (this.IsLooped)
110
+ this.SelectedIndex = 0;
111
+ }
112
+ GoPrev() {
113
+ if (this.selectedIndex > 0)
114
+ this.SelectedIndex = this.selectedIndex - 1;
115
+ else if (this.IsLooped)
116
+ this.SelectedIndex = this.MaxIndex;
117
+ }
118
+ /** C# ScrollTo(index, animate). */
119
+ ScrollTo(index, animate = true) {
120
+ const i = Math.max(0, Math.min(this.MaxIndex, index));
121
+ if (i === this.selectedIndex) {
122
+ this.ApplyIndex(!animate);
123
+ return;
124
+ }
125
+ if (!animate) {
126
+ this.InterruptSnapping();
127
+ this.lastIndex = this.selectedIndex;
128
+ this.selectedIndex = i;
129
+ this.OnSelectedIndexChanged();
130
+ this.ApplyIndex(true);
131
+ return;
132
+ }
133
+ this.SelectedIndex = i;
134
+ }
135
+ get ApplyLoopedLogic() { return this.IsLooped && this.Animated && this.CanAnimate && this.SnapPoints.length > 1; }
136
+ /** Step between slides in points: slide size + Spacing - 2 * SidesOffset (C# InitializeChildren). */
137
+ Step() { const size = this.IsVertical ? this.cellSize.H : this.cellSize.W; return size + this.Spacing - 2 * this.SidesOffset; }
138
+ /**
139
+ * C# InterruptSnapping: stops an in-flight snap before a programmatic index change and continues from the visual
140
+ * position; for IsLooped the position is shifted by whole strips into the neighbourhood of the current index.
141
+ */
142
+ InterruptSnapping() {
143
+ if (!this.IsSnapAnimating)
144
+ return;
145
+ this.StopSnapAnimators();
146
+ let pos = this.CurrentPosition;
147
+ if (this.IsLooped && this.SnapPoints.length > 1 && this.selectedIndex >= 0 && this.selectedIndex < this.SnapPoints.length) {
148
+ const n = this.SnapPoints.length;
149
+ const strip = new SKPoint((this.SnapPoints[1].X - this.SnapPoints[0].X) * n, (this.SnapPoints[1].Y - this.SnapPoints[0].Y) * n);
150
+ const target = this.SnapPoints[this.selectedIndex];
151
+ const d = (p) => SnappingLayout.Dist(p, target);
152
+ while (d(new SKPoint(pos.X + strip.X, pos.Y + strip.Y)) < d(pos))
153
+ pos = new SKPoint(pos.X + strip.X, pos.Y + strip.Y);
154
+ while (d(new SKPoint(pos.X - strip.X, pos.Y - strip.Y)) < d(pos))
155
+ pos = new SKPoint(pos.X - strip.X, pos.Y - strip.Y);
156
+ }
157
+ this.CurrentPosition = pos;
158
+ this.CurrentSnap = pos;
159
+ }
160
+ ApplyIndex(instant) {
161
+ if (this.selectedIndex < 0 || this.selectedIndex >= this.SnapPoints.length)
162
+ return;
163
+ let target = this.SnapPoints[this.selectedIndex];
164
+ if (!instant && this.ApplyLoopedLogic) {
165
+ if (this.selectedIndex === 0 && this.lastIndex === this.MaxIndex)
166
+ target = this.GetVirtualSnapPoints().find((p) => p.Id === -2).Location;
167
+ else if (this.selectedIndex === this.MaxIndex && this.lastIndex === 0)
168
+ target = this.GetVirtualSnapPoints().find((p) => p.Id === -1).Location;
169
+ }
170
+ this.ScrollToOffset(target, SKPoint.Empty, !instant && this.CanAnimate && this.Animated);
171
+ }
172
+ UpdateReportedPosition() {
173
+ if (this.SnapPoints.length === 0)
174
+ return;
175
+ if (this.IsLooped && this.SnapPoints.length > 1) {
176
+ const c = this.GetVirtualAnchor(this.CurrentSnap);
177
+ this.ReportIndex(c.Id === -1 ? this.MaxIndex : c.Id === -2 ? 0 : c.Id);
178
+ }
179
+ else {
180
+ const i = this.SnapPoints.findIndex((p) => Math.abs(p.X - this.CurrentSnap.X) <= 1 && Math.abs(p.Y - this.CurrentSnap.Y) <= 1);
181
+ if (i >= 0 && i < this.ChildrenTotal)
182
+ this.ReportIndex(i);
183
+ }
184
+ }
185
+ ApplyPosition(position) {
186
+ super.ApplyPosition(position);
187
+ this.OnScrollProgressChanged();
188
+ this.RepaintComposition();
189
+ }
190
+ /** The position changed (C# OnScrollProgressChanged); SkiaShaderCarousel drives its transition here. */
191
+ OnScrollProgressChanged() { }
192
+ /** Snap points were (re)built (C# OnChildrenInitialized). */
193
+ OnChildrenInitialized() { }
194
+ /** Where a drawn slide goes for its scroll offset; SkiaShaderCarousel keeps every slide in place (C# AnimateVisibleChild). */
195
+ SlideOffset(offset) { return offset; }
196
+ /** An already realized slide (C# ChildrenFactory.GetViewForIndex): no cell is created. */
197
+ GetExistingView(index) { return this.IsTemplated ? this.ChildrenFactory.GetViewForIndex(index) : this.Children[index]; }
198
+ // ---- looped: virtual anchors one step before the first and after the last slide ----
199
+ GetVirtualSnapPoints() {
200
+ if (!this.snapPointsVirtual) {
201
+ const s = this.SnapPoints, n = s.length;
202
+ const dx = s[1].X - s[0].X, dy = s[1].Y - s[0].Y;
203
+ this.snapPointsVirtual = [
204
+ { Id: -1, Location: new SKPoint(s[0].X - dx, s[0].Y - dy) },
205
+ { Id: -2, Location: new SKPoint(s[n - 1].X + dx, s[n - 1].Y + dy) },
206
+ ...s.map((p, i) => ({ Id: i, Location: p })),
207
+ ];
208
+ }
209
+ return this.snapPointsVirtual;
210
+ }
211
+ GetVirtualAnchor(current) {
212
+ let best = this.GetVirtualSnapPoints()[0], d = Infinity;
213
+ for (const p of this.GetVirtualSnapPoints()) {
214
+ const dd = SnappingLayout.Dist(p.Location, current);
215
+ if (dd < d) {
216
+ d = dd;
217
+ best = p;
218
+ }
219
+ }
220
+ return best;
221
+ }
222
+ FindNearestAnchorInternal(current, velocity) {
223
+ if (this.IsLooped && this.SnapPoints.length > 1) {
224
+ const c = this.GetVirtualAnchor(current);
225
+ if (c.Id === -1)
226
+ return this.SnapPoints[this.SnapPoints.length - 1];
227
+ if (c.Id === -2)
228
+ return this.SnapPoints[0];
229
+ return c.Location;
230
+ }
231
+ return super.FindNearestAnchorInternal(current, velocity);
232
+ }
233
+ /** C# SkiaCarousel.ScrollToNearestAnchor: velocity below 100 counts as none; looped snaps around virtual anchors. */
234
+ ScrollToNearestAnchor(location, velocity) {
235
+ velocity = new SKPoint(Math.abs(velocity.X) < 100 ? 0 : velocity.X, Math.abs(velocity.Y) < 100 ? 0 : velocity.Y);
236
+ if (this.ApplyLoopedLogic) {
237
+ const origin = this.FindNearestAnchorInternal(location, velocity);
238
+ const target = this.SelectNextAnchor(origin, velocity);
239
+ if (SnappingLayout.Dist(location, target) >= 0.5)
240
+ this.ScrollToOffset(target, velocity, this.CanAnimate);
241
+ else
242
+ this.UpdateReportedPosition();
243
+ return;
244
+ }
245
+ super.ScrollToNearestAnchor(location, velocity);
246
+ }
247
+ /** C# SkiaCarousel.SelectNextAnchor: base choice, plus a wrap to the virtual anchor at the strip borders when looped. */
248
+ SelectNextAnchor(origin, velocity) {
249
+ if (!this.ApplyLoopedLogic)
250
+ return super.SelectNextAnchor(origin, velocity);
251
+ const baseTarget = super.SelectNextAnchor(origin, velocity);
252
+ let originSnap = this.FindNearestAnchorInternal(origin, velocity);
253
+ let originIndex = this.SnapPoints.indexOf(originSnap);
254
+ if (originIndex < 0) {
255
+ originSnap = this.FindNearestAnchor(origin);
256
+ originIndex = this.SnapPoints.indexOf(originSnap);
257
+ }
258
+ if (originSnap.X !== baseTarget.X || originSnap.Y !== baseTarget.Y)
259
+ return baseTarget;
260
+ const axis = (p) => (this.IsVertical ? p.Y : p.X);
261
+ const stepAxis = this.SnapPoints.length > 1 ? Math.abs(axis(this.SnapPoints[1]) - axis(this.SnapPoints[0])) : 0;
262
+ const disp = axis(this.CurrentPosition) - axis(this.panningStart);
263
+ let dirSign = Math.sign(axis(velocity));
264
+ if (dirSign === 0) {
265
+ // tiny velocity at finger-up: infer from the pan displacement, past SnapDistanceRatio of a step
266
+ const minDisplacement = stepAxis > 0 ? stepAxis * this.SnapDistanceRatio : 0;
267
+ dirSign = stepAxis <= 0 || Math.abs(disp) < minDisplacement ? 0 : Math.sign(disp);
268
+ }
269
+ if (dirSign === 0)
270
+ return baseTarget;
271
+ // zero velocity needs stronger intent (>= 50% of a step) before wrapping
272
+ if (velocity.X === 0 && velocity.Y === 0 && stepAxis > 0 && Math.abs(disp) < stepAxis * 0.5)
273
+ return baseTarget;
274
+ if (dirSign < 0 && originIndex === this.MaxIndex)
275
+ return this.GetVirtualSnapPoints().find((p) => p.Id === -2).Location;
276
+ if (dirSign > 0 && originIndex === 0)
277
+ return this.GetVirtualSnapPoints().find((p) => p.Id === -1).Location;
278
+ return baseTarget;
279
+ }
280
+ /** C# FixIndex: a virtual anchor becomes the real one, the position keeps its offset (visually identical, looped drawing wraps). */
281
+ FixIndex() {
282
+ if (this.IsLooped && this.SnapPoints.length > 1 && this.selectedIndex >= 0 && this.selectedIndex < this.SnapPoints.length) {
283
+ const snap = this.GetVirtualAnchor(this.CurrentPosition);
284
+ const ox = this.CurrentPosition.X - snap.Location.X, oy = this.CurrentPosition.Y - snap.Location.Y;
285
+ const real = this.SnapPoints[this.selectedIndex];
286
+ this.CurrentSnap = real;
287
+ this.CurrentPosition = new SKPoint(real.X + ox, real.Y + oy);
288
+ }
289
+ }
290
+ FixPosition() {
291
+ if (this.IsLooped && this.SnapPoints.length > 1) {
292
+ const v = this.GetVirtualAnchor(this.CurrentSnap);
293
+ if (v.Id === -1 || v.Id === -2)
294
+ this.FixIndex();
295
+ }
296
+ }
297
+ OnTransitionChanged() {
298
+ if (!this.InTransition)
299
+ this.FixPosition();
300
+ super.OnTransitionChanged();
301
+ }
302
+ // ---- snap speed (C# SkiaCarousel.ScrollToOffset) ----
303
+ ScaleSnapVelocity(velocity) { const k = this.SwipeSpeed / 2; return new SKPoint(velocity.X * k, velocity.Y * k); }
304
+ CreateSnapSpring() { return new Spring(1 * (1 + this.RubberDamping), 200 * this.SwipeSpeed, 0.5 * (1 + this.RubberDamping)); }
305
+ SpringVelocity(velocity) { return new SKPoint(velocity.X * this.SwipeSpeed, velocity.Y * this.SwipeSpeed); }
306
+ GetSnapDurationSeconds(start, end, velocity, displacement) {
307
+ const speedK = this.SwipeSpeed / 2;
308
+ const maxSpeed = 0.25 / speedK;
309
+ const h = this.cellSize.H || 1; // C# normalizes both axes by Height
310
+ let speed = maxSpeed;
311
+ if (this.IsVertical) {
312
+ if (velocity.Y !== 0)
313
+ speed = Math.abs(displacement.Y / velocity.Y) * (Math.abs(end.Y - start.Y) / h);
314
+ }
315
+ else if (velocity.X !== 0)
316
+ speed = Math.abs(displacement.X / velocity.X) * (Math.abs(end.X - start.X) / h);
317
+ if (speed > maxSpeed)
318
+ speed = maxSpeed;
319
+ if (this.LinearSpeedMs > 0) {
320
+ const denom = this.IsVertical ? this.cellSize.H : this.cellSize.W;
321
+ const delta = this.IsVertical ? Math.abs(end.Y - start.Y) : Math.abs(end.X - start.X);
322
+ if (denom > 0)
323
+ speed = (delta / denom) * (this.LinearSpeedMs / 1000);
324
+ }
325
+ return speed;
326
+ }
327
+ // ---- children ----
328
+ InnerRect() {
329
+ const scale = this.RenderingScale, p = this.Padding, r = this.DrawingRect;
330
+ return new SKRect(r.Left + p.Left * scale, r.Top + p.Top * scale, r.Right - p.Right * scale, r.Bottom - p.Bottom * scale);
331
+ }
332
+ /** C# AdaptTemplate: slides fill the carousel (Start only when the carousel auto-sizes on that axis), SidesOffset as margin. */
333
+ AdaptTemplate(view) {
334
+ const vo = this.HeightRequest < 0 && this.VerticalOptions === "Start" ? "Start" : "Fill";
335
+ const ho = this.WidthRequest < 0 && this.HorizontalOptions === "Start" ? "Start" : "Fill";
336
+ if (view.VerticalOptions !== vo)
337
+ view.VerticalOptions = vo;
338
+ if (view.HorizontalOptions !== ho)
339
+ view.HorizontalOptions = ho;
340
+ const m = this.SidesOffset, cur = view.Margin;
341
+ const want = this.IsVertical ? new Thickness(0, m, 0, m) : new Thickness(m, 0, m, 0);
342
+ if (cur.Left !== want.Left || cur.Top !== want.Top || cur.Right !== want.Right || cur.Bottom !== want.Bottom)
343
+ view.Margin = want;
344
+ }
345
+ GetChild(index) {
346
+ if (this.IsTemplated) {
347
+ const v = this.ChildrenFactory.GetOrCreateViewForIndex(index);
348
+ if (v)
349
+ this.AdaptTemplate(v);
350
+ return v;
351
+ }
352
+ return this.Children[index];
353
+ }
354
+ MeasureAbsolute(w, h, scale) {
355
+ const px = this.Padding.HorizontalThickness * scale, py = this.Padding.VerticalThickness * scale;
356
+ const cw = isFinite(w) ? w - px : Infinity, ch = isFinite(h) ? h - py : Infinity;
357
+ let maxW = 0, maxH = 0;
358
+ if (!this.IsTemplated) {
359
+ this.Children.forEach((v, i) => {
360
+ if (!v.IsVisible)
361
+ return;
362
+ this.AdaptTemplate(v);
363
+ const s = v.Measure(cw, ch, scale);
364
+ if (this.DynamicSize) {
365
+ if (i === this.selectedIndex) {
366
+ maxW = s.Pixels.Width;
367
+ maxH = s.Pixels.Height;
368
+ }
369
+ }
370
+ else {
371
+ maxW = Math.max(maxW, s.Pixels.Width);
372
+ maxH = Math.max(maxH, s.Pixels.Height);
373
+ }
374
+ });
375
+ }
376
+ else if (this.ChildrenTotal > 0) {
377
+ // C# measures one template; DynamicSize follows the selected slide (ApplyDynamicSize)
378
+ const view = this.GetChild(this.DynamicSize ? Math.max(0, Math.min(this.MaxIndex, this.selectedIndex)) : 0);
379
+ if (view) {
380
+ const s = view.Measure(cw, ch, scale);
381
+ maxW = s.Pixels.Width;
382
+ maxH = s.Pixels.Height;
383
+ }
384
+ }
385
+ return ScaledSize.FromPixels(isFinite(w) ? w : maxW + px, isFinite(h) ? h : maxH + py, scale);
386
+ }
387
+ OnLayoutChanged() {
388
+ const scale = this.RenderingScale;
389
+ const inner = this.InnerRect();
390
+ const cellW = inner.Width / scale, cellH = inner.Height / scale;
391
+ // snap points depend on the axis size, orientation, Spacing and SidesOffset; a cross-axis change (DynamicSize) keeps them
392
+ const key = `${this.IsVertical}|${this.Spacing}|${this.SidesOffset}|${this.IsVertical ? cellH : cellW}`;
393
+ this.cellSize = { W: cellW, H: cellH };
394
+ if (key !== this.layoutKey || !this.childrenInitialized || this.SnapPoints.length !== this.ChildrenTotal) {
395
+ this.layoutKey = key;
396
+ this.InitializeChildren();
397
+ }
398
+ else if (this.IsLooped !== this.lastLooped) {
399
+ this.lastLooped = this.IsLooped;
400
+ this.snapPointsVirtual = undefined;
401
+ this.ContentOffsetBounds = this.GetContentOffsetBounds();
402
+ }
403
+ }
404
+ /** C# InitializeChildren: snap points from the slide size, bounds, and the current index applied instantly. */
405
+ InitializeChildren() {
406
+ const count = this.ChildrenTotal;
407
+ if (this.IsTemplated && !this.ItemTemplate)
408
+ return;
409
+ this.childrenInitialized = true;
410
+ this.lastLooped = this.IsLooped;
411
+ this.itemsVisibility = new Array(count).fill(false);
412
+ const step = this.Step();
413
+ this.SnapPoints = Array.from({ length: count }, (_, i) => (this.IsVertical ? new SKPoint(0, -i * step) : new SKPoint(-i * step, 0)));
414
+ this.snapPointsVirtual = undefined;
415
+ this.ContentOffsetBounds = this.GetContentOffsetBounds();
416
+ this.CurrentSnap = new SKPoint(-1, -1);
417
+ if (count > 0 && (this.selectedIndex < 0 || this.selectedIndex > this.MaxIndex)) {
418
+ this.lastIndex = this.selectedIndex;
419
+ this.selectedIndex = 0;
420
+ this.OnSelectedIndexChanged();
421
+ }
422
+ this.OnChildrenInitialized();
423
+ this.ApplyIndex(true);
424
+ }
425
+ /** C# GetContentOffsetBounds: the snap extent, or unbounded along the axis when looped. */
426
+ GetContentOffsetBounds() {
427
+ if (this.SnapPoints.length === 0)
428
+ return SKRect.Empty;
429
+ const b = this.BoundsFromSnapPoints();
430
+ if (!this.IsLooped || this.SnapPoints.length < 2)
431
+ return b;
432
+ const far = 1e9;
433
+ return this.IsVertical ? new SKRect(b.Left, -far, b.Right, far) : new SKRect(-far, b.Top, far, b.Bottom);
434
+ }
435
+ /** C# CalculateChildPosition: slide offset in points, visibility, and "next to screen"; looped draws the edge slides on the other side. */
436
+ CalculateChildPosition(current, index, count) {
437
+ const W = this.cellSize.W, H = this.cellSize.H, S = this.SidesOffset, gap = this.Spacing;
438
+ const nextToScreenOffset = 10;
439
+ const size = this.IsVertical ? H : W;
440
+ const snap = this.SnapPoints[index];
441
+ let pos = this.IsVertical ? current.Y + Math.abs(snap.Y) : current.X + Math.abs(snap.X);
442
+ const test = (p) => ({ visible: p + S * 2 <= size && p + size >= 0, next: Math.abs(p) - size - S - gap <= nextToScreenOffset });
443
+ let { visible, next } = test(pos);
444
+ if (this.IsLooped && count > 1) {
445
+ const step = this.SnapPoints.length >= 2 ? Math.abs((this.IsVertical ? this.SnapPoints[1].Y : this.SnapPoints[1].X) - (this.IsVertical ? this.SnapPoints[0].Y : this.SnapPoints[0].X)) : size + gap - S * 2;
446
+ if (index === 0 || index === count - 1) {
447
+ const alt = index === 0 ? pos + step * count : pos - step * count; // first after last / last before first
448
+ const t = test(alt);
449
+ if ((t.visible || t.next) && !visible) {
450
+ pos = alt;
451
+ visible = t.visible;
452
+ next = t.next;
453
+ }
454
+ }
455
+ }
456
+ return { Offset: this.IsVertical ? new SKPoint(0, pos) : new SKPoint(pos, 0), OnScreen: visible, NextToScreen: next };
457
+ }
458
+ SendVisibility(index, state) {
459
+ if (this.itemsVisibility[index] === state)
460
+ return;
461
+ this.itemsVisibility[index] = state;
462
+ if (state)
463
+ this.ItemAppearing?.(this, index);
464
+ else
465
+ this.ItemDisappearing?.(this, index);
466
+ }
467
+ /** C# RenderViewsList: position every slide, draw the visible ones (+ neighbours when PreloadNeighboors), release the rest. */
468
+ Paint(ctx) {
469
+ const count = this.ChildrenTotal;
470
+ if (this.SnapPoints.length === 0 || count === 0 || (this.IsTemplated && !this.ItemTemplate)) {
471
+ this.visibleViews = [];
472
+ return;
473
+ }
474
+ const scale = this.RenderingScale;
475
+ const inner = this.InnerRect();
476
+ const keep = new Set();
477
+ const cells = [];
478
+ for (let i = 0; i < count; i++) {
479
+ const p = this.CalculateChildPosition(this.CurrentPosition, i, count);
480
+ if (p.OnScreen || p.NextToScreen) {
481
+ cells.push({ index: i, visible: p.OnScreen, offset: p.Offset });
482
+ keep.add(i);
483
+ }
484
+ else if (!this.IsTemplated) { // static slides keep a truthful (off-screen) box for accessibility / hit tests
485
+ const v = this.Children[i];
486
+ if (v?.IsVisible)
487
+ v.Arrange(this.SlideRect(inner, p.Offset, scale), v.WidthRequest, v.HeightRequest, scale);
488
+ }
489
+ this.SendVisibility(i, p.OnScreen);
490
+ }
491
+ if (this.IsTemplated)
492
+ this.ChildrenFactory.ReleaseExcept(keep);
493
+ const track = (this.IsVertical ? inner.Height : inner.Width) - this.SidesOffset * scale;
494
+ const visible = [];
495
+ for (const cell of cells) {
496
+ if (!cell.visible && !this.PreloadNeighboors)
497
+ continue;
498
+ const view = this.GetChild(cell.index);
499
+ if (!view || !view.IsVisible)
500
+ continue;
501
+ if (cell.index === this.selectedIndex && track > 0)
502
+ this.scrollAmount = ((this.IsVertical ? cell.offset.Y : cell.offset.X) * scale) / track;
503
+ if (this.IsTemplated)
504
+ view.Measure(inner.Width, inner.Height, scale); // recycled cells carry new content
505
+ view.Arrange(this.SlideRect(inner, this.SlideOffset(cell.offset), scale), view.WidthRequest, view.HeightRequest, scale);
506
+ view.Render(ctx);
507
+ if (cell.visible)
508
+ visible.push(view);
509
+ }
510
+ this.visibleViews = visible;
511
+ }
512
+ SlideRect(inner, offset, scale) {
513
+ return this.IsVertical
514
+ ? SKRect.Create(inner.Left, inner.Top + offset.Y * scale, inner.Width, inner.Height)
515
+ : SKRect.Create(inner.Left + offset.X * scale, inner.Top, inner.Width, inner.Height);
516
+ }
517
+ GetGestureListeners() { return this.visibleViews; }
518
+ // ---- gestures (port of C# SkiaCarousel.ProcessGestures) ----
519
+ ProcessGestures(args, apply) {
520
+ const consumedDefault = this.BlockGesturesBelow ? this : null;
521
+ let passed = false;
522
+ const passToChildren = () => { passed = true; return super.ProcessGestures(args, apply); };
523
+ let consumed = null;
524
+ if (!this.IsUserPanning || !this.RespondsToGestures || args.Type === "Tapped") {
525
+ consumed = passToChildren();
526
+ if (consumed === this)
527
+ consumed = null;
528
+ if (consumed && !(args.Type === "Up" && this.snapIfNoPanOnUp))
529
+ return consumed;
530
+ }
531
+ if (!this.RespondsToGestures || this.ChildrenTotal < 2)
532
+ return consumedDefault;
533
+ const scale = this.RenderingScale;
534
+ const e = args.Event;
535
+ const resetPan = () => {
536
+ this.wrongDirection = false;
537
+ this.IsUserFocused = true;
538
+ this.IsUserPanning = false;
539
+ this.snapIfNoPanOnUp = this.IsSnapAnimating || this.InTransition;
540
+ this.StopSnapAnimators();
541
+ this.velocityAccumulator.Clear();
542
+ this.FixPosition();
543
+ this.panningOffset = this.CurrentPosition;
544
+ this.panningStart = this.CurrentPosition;
545
+ };
546
+ switch (args.Type) {
547
+ case "Down":
548
+ this.hadDown = true;
549
+ resetPan();
550
+ consumed = this;
551
+ break;
552
+ case "Panning": {
553
+ if (!this.hadDown || this.wrongDirection)
554
+ return consumedDefault;
555
+ if (!this.IsUserPanning) {
556
+ const movex = Math.abs(e.Distance.Total.X), movey = Math.abs(e.Distance.Total.Y);
557
+ const along = this.IsVertical ? movey : movex, across = this.IsVertical ? movex : movey;
558
+ if (along < scale * 2 || across > along) {
559
+ this.wrongDirection = true;
560
+ return consumedDefault;
561
+ }
562
+ }
563
+ if (!this.IsUserFocused)
564
+ resetPan();
565
+ this.IsUserPanning = true;
566
+ this.snapIfNoPanOnUp = false;
567
+ const x = this.panningOffset.X + e.Distance.Delta.X / scale, y = this.panningOffset.Y + e.Distance.Delta.Y / scale;
568
+ const vx = this.IsVertical ? 0 : e.Distance.Velocity.X / scale, vy = this.IsVertical ? e.Distance.Velocity.Y / scale : 0;
569
+ this.velocityAccumulator.CaptureVelocity(vx, vy, args.ArrivedTimeNanos);
570
+ this.panningOffset = new SKPoint(this.IsVertical ? 0 : x, this.IsVertical ? y : 0);
571
+ this.ApplyPosition(this.ClampOffset(this.panningOffset.X, this.panningOffset.Y, this.Bounces));
572
+ consumed = this;
573
+ break;
574
+ }
575
+ case "Up":
576
+ this.hadDown = false;
577
+ if (this.IsUserPanning) {
578
+ consumed = this;
579
+ const final = this.velocityAccumulator.CalculateFinalVelocity(500);
580
+ this.CurrentSnap = this.CurrentPosition;
581
+ this.ScrollToNearestAnchor(this.CurrentSnap, new SKPoint(final.X, final.Y));
582
+ this.IsUserPanning = false;
583
+ this.IsUserFocused = false;
584
+ this.snapIfNoPanOnUp = false;
585
+ }
586
+ else if (this.snapIfNoPanOnUp) {
587
+ // Down interrupted a snap but no carousel pan followed: still settle on the nearest slide, leave Up to the children
588
+ this.CurrentSnap = this.CurrentPosition;
589
+ this.ScrollToNearestAnchor(this.CurrentSnap, SKPoint.Empty);
590
+ this.IsUserFocused = false;
591
+ this.IsUserPanning = false;
592
+ this.snapIfNoPanOnUp = false;
593
+ }
594
+ break;
595
+ }
596
+ if (consumed || this.IsUserPanning)
597
+ return consumed ?? (args.Type !== "Up" ? this : consumedDefault);
598
+ if (!passed)
599
+ return passToChildren();
600
+ return consumedDefault;
601
+ }
602
+ DefaultAccessibilityLabel() { return `${this.selectedIndex + 1} / ${this.ChildrenTotal}`; }
603
+ }
@@ -0,0 +1,21 @@
1
+ import { type Color } from "../core/Types";
2
+ import type { SkiaControl } from "../core/SkiaControl";
3
+ import { SkiaShape } from "./SkiaShape";
4
+ import { SkiaToggle } from "./SkiaToggle";
5
+ /** Mirrors DrawnUi SkiaCheckbox: `FrameOff` outline, `FrameOn` filled frame with a `ViewCheckOn` mark, per-style looks. */
6
+ export declare class SkiaCheckbox extends SkiaToggle {
7
+ static DefaultAccessibilityRole?: string;
8
+ FrameOff: SkiaShape;
9
+ FrameOn: SkiaShape;
10
+ ViewCheckOn: SkiaControl;
11
+ private colorCheckOn?;
12
+ private static readonly DefaultAccent;
13
+ private static readonly DefaultOutline;
14
+ /** Color of the check mark (inner square for the default look). */
15
+ get ColorCheckOn(): Color;
16
+ set ColorCheckOn(v: Color);
17
+ protected StyleDefault(name: "ColorThumbOn" | "ColorFrameOn" | "ColorThumbOff" | "ColorFrameOff"): Color | undefined;
18
+ private SetContentSize;
19
+ protected CreateDefaultContent(): void;
20
+ ApplyProperties(): void;
21
+ }