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,181 @@
1
+ import { SkiaControl } from "../core/SkiaControl";
2
+ import { Super } from "../core/Super";
3
+ import { Colors, SKRect, ScaledSize } from "../core/Types";
4
+ import { SkiaImage } from "./SkiaImage";
5
+ /**
6
+ * Mirrors DrawnUi SkiaSvg. CanvasKit's npm build has no SVG renderer, so the browser decodes the
7
+ * SVG (HTMLImageElement) and it is rasterized at the exact pixel size it is displayed at, re-rasterized
8
+ * only when that size changes — crisp at any scale, one bitmap per size. TintColor recolors via SrcIn.
9
+ */
10
+ export class SkiaSvg extends SkiaControl {
11
+ Aspect = "AspectFitFill";
12
+ HorizontalAlignment = "Center";
13
+ VerticalAlignment = "Center";
14
+ /** Transparent = keep the SVG's own colors. */
15
+ TintColor = Colors.Transparent;
16
+ Success;
17
+ Error;
18
+ IsLoading = false;
19
+ /** Where the scaled svg was drawn last frame, canvas pixels. */
20
+ DisplayRect = SKRect.Empty;
21
+ source = "";
22
+ svgString = "";
23
+ loadGeneration = 0;
24
+ element;
25
+ constructor() {
26
+ super();
27
+ this.UseCache = "Operations"; // DrawnUi default for SkiaSvg
28
+ }
29
+ intrinsic = { Width: 0, Height: 0 };
30
+ raster;
31
+ rasterKey = "";
32
+ /** URL of an .svg file (relative to the page or absolute). */
33
+ get Source() { return this.source; }
34
+ set Source(value) {
35
+ if (this.source === value)
36
+ return;
37
+ this.source = value;
38
+ this.svgString = "";
39
+ if (!value) {
40
+ this.Clear();
41
+ return;
42
+ }
43
+ const generation = ++this.loadGeneration;
44
+ this.IsLoading = true;
45
+ fetch(value)
46
+ .then((r) => { if (!r.ok)
47
+ throw new Error(`DrawnUi: ${r.status} loading svg '${value}'`); return r.text(); })
48
+ .then((text) => { if (generation === this.loadGeneration)
49
+ return this.Decode(text, value, generation); })
50
+ .catch((e) => { if (generation !== this.loadGeneration)
51
+ return; this.IsLoading = false; this.Error?.(this, e); });
52
+ }
53
+ /** Inline SVG markup; takes precedence over Source. */
54
+ get SvgString() { return this.svgString; }
55
+ set SvgString(value) {
56
+ if (this.svgString === value)
57
+ return;
58
+ this.svgString = value;
59
+ this.source = "";
60
+ if (!value) {
61
+ this.Clear();
62
+ return;
63
+ }
64
+ const generation = ++this.loadGeneration;
65
+ void this.Decode(value, "", generation);
66
+ }
67
+ Clear() {
68
+ this.loadGeneration++;
69
+ this.element = undefined;
70
+ this.intrinsic = { Width: 0, Height: 0 };
71
+ this.DropRaster();
72
+ this.IsLoading = false;
73
+ this.Update();
74
+ }
75
+ OnDisposing() { this.loadGeneration++; this.DropRaster(); }
76
+ DropRaster() {
77
+ this.raster?.delete();
78
+ this.raster = undefined;
79
+ this.rasterKey = "";
80
+ }
81
+ async Decode(svg, sourceName, generation) {
82
+ this.IsLoading = true;
83
+ // browsers refuse to rasterize an <img> SVG whose root has no xmlns (SkiaSharp's parser does not care)
84
+ if (!/<svg[^>]*\sxmlns\s*=/.test(svg))
85
+ svg = svg.replace(/<svg(?=[\s>])/, '<svg xmlns="http://www.w3.org/2000/svg"');
86
+ const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }));
87
+ try {
88
+ const img = new window.Image();
89
+ img.src = url;
90
+ await img.decode();
91
+ if (generation !== this.loadGeneration)
92
+ return;
93
+ this.element = img;
94
+ this.intrinsic = SkiaSvg.IntrinsicSize(img, svg);
95
+ this.DropRaster();
96
+ this.IsLoading = false;
97
+ this.Update();
98
+ this.Success?.(this, sourceName);
99
+ }
100
+ catch (e) {
101
+ if (generation !== this.loadGeneration)
102
+ return;
103
+ this.IsLoading = false;
104
+ this.Error?.(this, e instanceof Error ? e : new Error(String(e)));
105
+ }
106
+ finally {
107
+ URL.revokeObjectURL(url);
108
+ }
109
+ }
110
+ /** Natural size: the decoded image's, else the viewBox (SVGs without width/height decode as 0x0 or 300x150). */
111
+ static IntrinsicSize(img, svg) {
112
+ const vb = /viewBox\s*=\s*"([^"]+)"/i.exec(svg);
113
+ if (vb) {
114
+ const p = vb[1].trim().split(/[\s,]+/).map(Number);
115
+ if (p.length === 4 && p[2] > 0 && p[3] > 0)
116
+ return { Width: p[2], Height: p[3] };
117
+ }
118
+ return { Width: img.naturalWidth || 1, Height: img.naturalHeight || 1 };
119
+ }
120
+ /** Bitmap of the svg at exactly w x h pixels (cached until the size changes). */
121
+ GetRaster(w, h) {
122
+ const el = this.element;
123
+ if (!el || w < 1 || h < 1)
124
+ return undefined;
125
+ const key = `${w}x${h}`;
126
+ if (this.raster && this.rasterKey === key)
127
+ return this.raster;
128
+ this.DropRaster();
129
+ const surface = typeof OffscreenCanvas !== "undefined" ? new OffscreenCanvas(w, h) : Object.assign(document.createElement("canvas"), { width: w, height: h });
130
+ const ctx = surface.getContext("2d");
131
+ if (!ctx)
132
+ return undefined;
133
+ ctx.drawImage(el, 0, 0, w, h);
134
+ this.raster = Super.CK.MakeImageFromCanvasImageSource(surface);
135
+ this.rasterKey = key;
136
+ return this.raster;
137
+ }
138
+ MeasureAbsolute(widthConstraint, heightConstraint, scale) {
139
+ let w = widthConstraint, h = heightConstraint;
140
+ const { Width, Height } = this.intrinsic;
141
+ if (Width > 0 && Height > 0) {
142
+ const aspect = Width / Height;
143
+ if (!isFinite(w) && isFinite(h))
144
+ w = h * aspect;
145
+ else if (!isFinite(h) && isFinite(w))
146
+ h = w / aspect;
147
+ }
148
+ if (!isFinite(w) || !isFinite(h))
149
+ return ScaledSize.FromPixels(0, 0, scale);
150
+ return ScaledSize.FromPixels(w, h, scale);
151
+ }
152
+ Paint(ctx) {
153
+ const { Width, Height } = this.intrinsic;
154
+ if (!this.element || Width <= 0 || Height <= 0)
155
+ return;
156
+ const dest = ctx.Destination;
157
+ const scaled = SkiaImage.RescaleAspect(Width, Height, dest, this.Aspect);
158
+ const display = SkiaImage.CalculateDisplayRect(dest, Width * scaled.X, Height * scaled.Y, this.HorizontalAlignment, this.VerticalAlignment);
159
+ this.DisplayRect = display;
160
+ const raster = this.GetRaster(Math.round(display.Width), Math.round(display.Height));
161
+ if (!raster)
162
+ return;
163
+ const CK = Super.CK;
164
+ const canvas = ctx.Context.Canvas;
165
+ const overflows = display.Left < dest.Left || display.Top < dest.Top || display.Right > dest.Right || display.Bottom > dest.Bottom;
166
+ const saved = canvas.save();
167
+ if (overflows)
168
+ canvas.clipRect(CK.LTRBRect(dest.Left, dest.Top, dest.Right, dest.Bottom), CK.ClipOp.Intersect, true);
169
+ const paint = new CK.Paint();
170
+ paint.setAntiAlias(true);
171
+ const tint = Super.ParseColor(this.TintColor);
172
+ if (tint[3] > 0) {
173
+ const filter = CK.ColorFilter.MakeBlend(tint, CK.BlendMode.SrcIn);
174
+ paint.setColorFilter(filter);
175
+ filter.delete();
176
+ }
177
+ canvas.drawImageRectOptions(raster, CK.LTRBRect(0, 0, raster.width(), raster.height()), CK.LTRBRect(display.Left, display.Top, display.Right, display.Bottom), CK.FilterMode.Linear, CK.MipmapMode.None, paint);
178
+ paint.delete();
179
+ canvas.restoreToCount(saved);
180
+ }
181
+ }
@@ -0,0 +1,26 @@
1
+ import { type Color } from "../core/Types";
2
+ import { SkiaShape } from "./SkiaShape";
3
+ import { SkiaToggle } from "./SkiaToggle";
4
+ /**
5
+ * Mirrors DrawnUi SkiaSwitch: a `Frame` SkiaShape + a `Thumb` circle translated to the on/off end, with the
6
+ * Default / Cupertino / Material / Material3 / Windows looks and geometry of the C# style builders. Shadows omitted.
7
+ */
8
+ export declare class SkiaSwitch extends SkiaToggle {
9
+ static AnimationSpeed: number;
10
+ static DefaultAccessibilityRole?: string;
11
+ Track: SkiaShape;
12
+ Thumb: SkiaShape;
13
+ private static readonly DefaultAccent;
14
+ private static readonly DefaultTrack;
15
+ private static readonly MaterialOutline;
16
+ protected StyleDefault(name: "ColorThumbOn" | "ColorFrameOn" | "ColorThumbOff" | "ColorFrameOff"): Color | undefined;
17
+ private SetContentSize;
18
+ protected CreateDefaultContent(): void;
19
+ /** C# GetThumbPosForOn: track width minus thumb width and margins, in points. */
20
+ private ThumbPosForOn;
21
+ ApplyProperties(): void;
22
+ private animating;
23
+ /** Moves the thumb to its resting position; animated once laid out, snapped otherwise (C# CanAnimate = LayoutReady && IsAnimated). */
24
+ private PositionThumb;
25
+ protected OnLayoutChanged(): void;
26
+ }
@@ -0,0 +1,147 @@
1
+ import { Colors, SkiaShadow, Thickness } from "../core/Types";
2
+ import { Easing } from "../core/Easing";
3
+ import { SkiaShape } from "./SkiaShape";
4
+ import { SkiaToggle } from "./SkiaToggle";
5
+ /**
6
+ * Mirrors DrawnUi SkiaSwitch: a `Frame` SkiaShape + a `Thumb` circle translated to the on/off end, with the
7
+ * Default / Cupertino / Material / Material3 / Windows looks and geometry of the C# style builders. Shadows omitted.
8
+ */
9
+ export class SkiaSwitch extends SkiaToggle {
10
+ static AnimationSpeed = 200;
11
+ static DefaultAccessibilityRole = "switch";
12
+ Track;
13
+ Thumb;
14
+ static DefaultAccent = "#DC143C"; // Crimson
15
+ static DefaultTrack = "#D7DBE0";
16
+ static MaterialOutline = "#79747E";
17
+ StyleDefault(name) {
18
+ const s = this.UsingControlStyle;
19
+ const table = {
20
+ Unset: { ColorFrameOn: SkiaSwitch.DefaultAccent, ColorFrameOff: SkiaSwitch.DefaultTrack },
21
+ Cupertino: { ColorFrameOff: "#E5E5E5", ColorFrameOn: "#30D158", ColorThumbOff: Colors.White, ColorThumbOn: Colors.White },
22
+ Material: { ColorFrameOff: "#9E9E9E", ColorFrameOn: "#2196F3", ColorThumbOff: Colors.White, ColorThumbOn: Colors.White },
23
+ Material3: { ColorFrameOff: "#E6E0E9", ColorFrameOn: "#6750A4", ColorThumbOff: SkiaSwitch.MaterialOutline, ColorThumbOn: Colors.White },
24
+ Windows: { ColorFrameOff: "#767676", ColorFrameOn: "#0078D7", ColorThumbOff: "#767676", ColorThumbOn: Colors.White },
25
+ };
26
+ return table[s]?.[name];
27
+ }
28
+ SetContentSize(w, h) {
29
+ if (this.WidthRequest < 0)
30
+ this.WidthRequest = w;
31
+ if (this.HeightRequest < 0)
32
+ this.HeightRequest = h;
33
+ }
34
+ CreateDefaultContent() {
35
+ const frame = new SkiaShape();
36
+ frame.Tag = "Frame";
37
+ frame.Type = "Rectangle";
38
+ frame.HorizontalOptions = "Fill";
39
+ frame.VerticalOptions = "Fill";
40
+ const thumb = new SkiaShape();
41
+ thumb.Tag = "Thumb";
42
+ thumb.Type = "Circle";
43
+ thumb.HorizontalOptions = "Start";
44
+ thumb.VerticalOptions = "Fill";
45
+ thumb.LockRatio = -1;
46
+ thumb.UseCache = "Operations";
47
+ switch (this.usingStyle) {
48
+ case "Cupertino":
49
+ this.SetContentSize(51, 31);
50
+ frame.CornerRadius = 100;
51
+ thumb.Margin = new Thickness(2);
52
+ thumb.Shadows = [new SkiaShadow({ X: 0, Y: 3, Blur: 3, Opacity: 0.1, Color: Colors.Black })];
53
+ break;
54
+ case "Material":
55
+ this.SetContentSize(46, 28);
56
+ frame.CornerRadius = 7;
57
+ frame.HeightRequest = 15;
58
+ frame.VerticalOptions = "Center";
59
+ thumb.Margin = Thickness.Zero;
60
+ thumb.Shadows = [new SkiaShadow({ X: 1, Y: 1, Blur: 3, Opacity: 0.1, Color: Colors.Black })];
61
+ break;
62
+ case "Material3":
63
+ this.SetContentSize(52, 32);
64
+ frame.CornerRadius = 16;
65
+ frame.StrokeWidth = 2;
66
+ frame.StrokeColor = SkiaSwitch.MaterialOutline;
67
+ thumb.Margin = new Thickness(4);
68
+ thumb.WidthRequest = 24;
69
+ thumb.LockRatio = 1;
70
+ thumb.VerticalOptions = "Center";
71
+ thumb.Shadows = [new SkiaShadow({ X: 1, Y: 1, Blur: 3, Opacity: 0.1, Color: Colors.Black })];
72
+ break;
73
+ case "Windows":
74
+ this.SetContentSize(48, 22);
75
+ frame.CornerRadius = 12;
76
+ frame.StrokeWidth = 2.5;
77
+ frame.StrokeColor = "#767676";
78
+ thumb.Margin = new Thickness(5.5);
79
+ break;
80
+ default:
81
+ this.SetContentSize(46, 28);
82
+ frame.CornerRadius = 20;
83
+ thumb.Margin = new Thickness(2);
84
+ break;
85
+ }
86
+ this.Track = frame;
87
+ this.Thumb = thumb;
88
+ this.AddSubView(frame);
89
+ this.AddSubView(thumb);
90
+ }
91
+ /** C# GetThumbPosForOn: track width minus thumb width and margins, in points. */
92
+ ThumbPosForOn() {
93
+ const scale = this.RenderingScale || 1;
94
+ const trackW = this.Track.DrawingRect.Width / scale + this.Track.Margin.HorizontalThickness;
95
+ const thumbW = (this.Thumb.DrawingRect.Width > 0 ? this.Thumb.DrawingRect.Width / scale : this.Thumb.WidthRequest > 0 ? this.Thumb.WidthRequest : this.HeightRequest - this.Thumb.Margin.VerticalThickness);
96
+ return trackW - thumbW - this.Thumb.Margin.HorizontalThickness;
97
+ }
98
+ ApplyProperties() {
99
+ if (!this.contentCreated)
100
+ return;
101
+ const on = this.IsToggled;
102
+ const s = this.usingStyle;
103
+ this.Thumb.BackgroundColor = on ? this.ColorThumbOn : this.ColorThumbOff;
104
+ if (on) {
105
+ this.Track.BackgroundColor = this.ColorFrameOn;
106
+ if (s === "Windows" || s === "Material3")
107
+ this.Track.StrokeColor = this.ColorFrameOn;
108
+ }
109
+ else if (s === "Windows") {
110
+ this.Track.BackgroundColor = Colors.Transparent;
111
+ this.Track.StrokeColor = this.ColorFrameOff;
112
+ }
113
+ else if (s === "Material3") {
114
+ this.Track.BackgroundColor = this.ColorFrameOff;
115
+ this.Track.StrokeColor = SkiaSwitch.MaterialOutline;
116
+ }
117
+ else {
118
+ this.Track.BackgroundColor = this.ColorFrameOff;
119
+ }
120
+ this.Track.Update();
121
+ this.Thumb.Update();
122
+ this.PositionThumb();
123
+ }
124
+ animating = false;
125
+ /** Moves the thumb to its resting position; animated once laid out, snapped otherwise (C# CanAnimate = LayoutReady && IsAnimated). */
126
+ PositionThumb() {
127
+ if (this.Thumb.DrawingRect.Width <= 0)
128
+ return; // not laid out yet: OnLayoutChanged snaps it
129
+ const target = this.IsToggled ? this.ThumbPosForOn() : 0;
130
+ if (this.IsAnimated && Math.abs(this.Thumb.TranslationX - target) > 0.5) {
131
+ this.animating = true;
132
+ void this.Thumb.TranslateToAsync(target, 0, SkiaSwitch.AnimationSpeed, Easing.CubicOut).catch(() => { }).finally(() => { this.animating = false; });
133
+ }
134
+ else {
135
+ this.Thumb.TranslationX = target;
136
+ this.Thumb.RepaintComposition();
137
+ }
138
+ }
139
+ OnLayoutChanged() {
140
+ super.OnLayoutChanged();
141
+ if (!this.contentCreated || this.animating)
142
+ return;
143
+ const target = this.IsToggled ? this.ThumbPosForOn() : 0;
144
+ if (Math.abs(this.Thumb.TranslationX - target) > 0.5)
145
+ this.Thumb.TranslationX = target;
146
+ }
147
+ }
@@ -0,0 +1,50 @@
1
+ import type { SkiaControl } from "../core/SkiaControl";
2
+ import type { GestureEventProcessingInfo, SkiaGesturesParameters } from "../core/Gestures";
3
+ import { type PrebuiltControlStyle, type ResolvedControlStyle } from "../core/ControlStyle";
4
+ import { type Color, type ScaledSize } from "../core/Types";
5
+ import { SkiaLayout } from "./SkiaLayout";
6
+ /**
7
+ * Mirrors DrawnUi SkiaToggle: base of SkiaSwitch / SkiaCheckbox / SkiaRadioButton. Tapped flips IsToggled,
8
+ * `Toggled` reports user changes, `ControlStyle` selects the look the default content is built with (once, at first
9
+ * measure, like C# CreateDefaultContent). Colors left unset take the style defaults (C# SetStyleDefault).
10
+ */
11
+ export declare abstract class SkiaToggle extends SkiaLayout {
12
+ ControlStyle: PrebuiltControlStyle;
13
+ /** Fires on every IsToggled change; C# fires only on user/external changes, not on DefaultValue application. */
14
+ Toggled?: (sender: SkiaToggle, value: boolean) => void;
15
+ DefaultValue: boolean;
16
+ IsAnimated: boolean;
17
+ RespondsToGestures: boolean;
18
+ private isToggled;
19
+ private colorThumbOn?;
20
+ private colorFrameOn?;
21
+ private colorThumbOff?;
22
+ private colorFrameOff?;
23
+ protected contentCreated: boolean;
24
+ protected usingStyle: ResolvedControlStyle;
25
+ constructor();
26
+ get UsingControlStyle(): ResolvedControlStyle;
27
+ get IsToggled(): boolean;
28
+ set IsToggled(v: boolean);
29
+ get ColorThumbOn(): Color;
30
+ set ColorThumbOn(v: Color);
31
+ get ColorFrameOn(): Color;
32
+ set ColorFrameOn(v: Color);
33
+ get ColorThumbOff(): Color;
34
+ set ColorThumbOff(v: Color);
35
+ get ColorFrameOff(): Color;
36
+ set ColorFrameOff(v: Color);
37
+ /** Per-style color defaults (C# SetStyleDefault); undefined = the SkiaToggle default. */
38
+ protected StyleDefault(_name: "ColorThumbOn" | "ColorFrameOn" | "ColorThumbOff" | "ColorFrameOff"): Color | undefined;
39
+ protected OnToggledChanged(): void;
40
+ /** Builds the look once (C# CreateDefaultContent) — subclasses add their views here. */
41
+ protected abstract CreateDefaultContent(): void;
42
+ /** Pushes IsToggled + colors into the views (C# ApplyProperties). */
43
+ abstract ApplyProperties(): void;
44
+ /** Content (and the size requests the style sets) must exist before Measure reads WidthRequest/HeightRequest. */
45
+ Measure(widthConstraint: number, heightConstraint: number, scale: number): ScaledSize;
46
+ protected DefaultAccessibilityCanInteract(): boolean;
47
+ get AccessibilityIsPressed(): boolean | undefined;
48
+ set AccessibilityIsPressed(_v: boolean | undefined);
49
+ ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
50
+ }
@@ -0,0 +1,71 @@
1
+ import { ResolveControlStyle } from "../core/ControlStyle";
2
+ import { Colors } from "../core/Types";
3
+ import { SkiaLayout } from "./SkiaLayout";
4
+ /**
5
+ * Mirrors DrawnUi SkiaToggle: base of SkiaSwitch / SkiaCheckbox / SkiaRadioButton. Tapped flips IsToggled,
6
+ * `Toggled` reports user changes, `ControlStyle` selects the look the default content is built with (once, at first
7
+ * measure, like C# CreateDefaultContent). Colors left unset take the style defaults (C# SetStyleDefault).
8
+ */
9
+ export class SkiaToggle extends SkiaLayout {
10
+ ControlStyle = "Unset";
11
+ /** Fires on every IsToggled change; C# fires only on user/external changes, not on DefaultValue application. */
12
+ Toggled;
13
+ DefaultValue = false;
14
+ IsAnimated = true;
15
+ RespondsToGestures = true;
16
+ isToggled = false;
17
+ colorThumbOn;
18
+ colorFrameOn;
19
+ colorThumbOff;
20
+ colorFrameOff;
21
+ contentCreated = false;
22
+ usingStyle = "Unset";
23
+ constructor() {
24
+ super();
25
+ this.Type = "Absolute";
26
+ }
27
+ get UsingControlStyle() { return this.contentCreated ? this.usingStyle : ResolveControlStyle(this.ControlStyle); }
28
+ get IsToggled() { return this.isToggled; }
29
+ set IsToggled(v) {
30
+ if (this.isToggled === v)
31
+ return;
32
+ this.isToggled = v;
33
+ this.OnToggledChanged();
34
+ }
35
+ // C# defaults: thumb White / frame Red (on), thumb White / frame DarkGray (off); styles override the unset ones
36
+ get ColorThumbOn() { return this.colorThumbOn ?? this.StyleDefault("ColorThumbOn") ?? Colors.White; }
37
+ set ColorThumbOn(v) { this.colorThumbOn = v; this.ApplyProperties(); }
38
+ get ColorFrameOn() { return this.colorFrameOn ?? this.StyleDefault("ColorFrameOn") ?? Colors.Red; }
39
+ set ColorFrameOn(v) { this.colorFrameOn = v; this.ApplyProperties(); }
40
+ get ColorThumbOff() { return this.colorThumbOff ?? this.StyleDefault("ColorThumbOff") ?? Colors.White; }
41
+ set ColorThumbOff(v) { this.colorThumbOff = v; this.ApplyProperties(); }
42
+ get ColorFrameOff() { return this.colorFrameOff ?? this.StyleDefault("ColorFrameOff") ?? Colors.DarkGray; }
43
+ set ColorFrameOff(v) { this.colorFrameOff = v; this.ApplyProperties(); }
44
+ /** Per-style color defaults (C# SetStyleDefault); undefined = the SkiaToggle default. */
45
+ StyleDefault(_name) { return undefined; }
46
+ OnToggledChanged() {
47
+ this.ApplyProperties();
48
+ this.Toggled?.(this, this.isToggled);
49
+ this.NotifyAccessibility();
50
+ }
51
+ /** Content (and the size requests the style sets) must exist before Measure reads WidthRequest/HeightRequest. */
52
+ Measure(widthConstraint, heightConstraint, scale) {
53
+ if (!this.contentCreated) {
54
+ this.contentCreated = true;
55
+ this.usingStyle = ResolveControlStyle(this.ControlStyle);
56
+ this.CreateDefaultContent();
57
+ this.ApplyProperties();
58
+ }
59
+ return super.Measure(widthConstraint, heightConstraint, scale);
60
+ }
61
+ DefaultAccessibilityCanInteract() { return this.RespondsToGestures; }
62
+ get AccessibilityIsPressed() { return this.isToggled; }
63
+ set AccessibilityIsPressed(_v) { }
64
+ ProcessGestures(args, apply) {
65
+ if (args.Type === "Tapped" && this.RespondsToGestures) {
66
+ this.IsToggled = !this.IsToggled;
67
+ return this;
68
+ }
69
+ return super.ProcessGestures(args, apply);
70
+ }
71
+ }
@@ -0,0 +1,78 @@
1
+ import { SKPoint } from "../core/Gestures";
2
+ import { Easing } from "../core/Easing";
3
+ import { Spring, VelocityAccumulator } from "../core/ScrollAnimators";
4
+ import { SKRect } from "../core/Types";
5
+ import type { DrawingContext } from "../core/SkiaControl";
6
+ import { SkiaLayout } from "./SkiaLayout";
7
+ /**
8
+ * Mirrors DrawnUi SnappingLayout: a layout whose content position snaps to `SnapPoints` (points). Base of
9
+ * SkiaCarousel and SkiaDrawer: nearest/next anchor selection by velocity, spring (Bounces) or linear snap
10
+ * animation, rubber-band clamping, `Scrolled` / `TransitionChanged` / `Stopped` callbacks.
11
+ */
12
+ export declare abstract class SnappingLayout extends SkiaLayout {
13
+ Animated: boolean;
14
+ RespondsToGestures: boolean;
15
+ IgnoreWrongDirection: boolean;
16
+ Bounces: boolean;
17
+ RubberDamping: number;
18
+ AutoVelocityMultiplyPts: number;
19
+ RubberEffect: number;
20
+ /** Fraction of a snap step a drag must cover before it counts as a direction (C# SnapDistanceRatio). */
21
+ SnapDistanceRatio: number;
22
+ SnapPoints: SKPoint[];
23
+ /** Current content offset, points (C# CurrentPosition). */
24
+ CurrentPosition: SKPoint;
25
+ /** Anchor the content rests on or travels to (C# CurrentSnap). */
26
+ CurrentSnap: SKPoint;
27
+ ContentOffsetBounds: SKRect;
28
+ IsUserPanning: boolean;
29
+ IsUserFocused: boolean;
30
+ Scrolled?: (sender: SnappingLayout, position: SKPoint) => void;
31
+ TransitionChanged?: (sender: SnappingLayout, inTransition: boolean) => void;
32
+ Stopped?: (sender: SnappingLayout, position: SKPoint) => void;
33
+ private inTransition;
34
+ get InTransition(): boolean;
35
+ protected set InTransition(v: boolean);
36
+ protected OnTransitionChanged(): void;
37
+ protected readonly velocityAccumulator: VelocityAccumulator;
38
+ private readonly springX;
39
+ private readonly springY;
40
+ private readonly range;
41
+ private rangeFrom;
42
+ private rangeTo;
43
+ /** Easing of the linear (non-bouncing) snap. */
44
+ protected SnapEasing: Easing;
45
+ constructor();
46
+ private stoppingSilently;
47
+ private OnAnimationStopped;
48
+ protected OnDisposing(): void;
49
+ protected get IsSnapAnimating(): boolean;
50
+ protected StopSnapAnimators(): void;
51
+ /** C# SnappingLayout.Render: the transition state follows the position every frame. */
52
+ Render(ctx: DrawingContext): void;
53
+ ClampOffset(x: number, y: number, rubber: boolean): SKPoint;
54
+ /** C# GetContentOffsetBounds: the extent covered by the snap points. */
55
+ protected BoundsFromSnapPoints(): SKRect;
56
+ protected static Dist(a: SKPoint, b: SKPoint): number;
57
+ FindNearestAnchor(current: SKPoint): SKPoint;
58
+ /** Anchor used as the origin of a snap decision (C# FindNearestAnchorInternal; a looped carousel maps virtual anchors here). */
59
+ protected FindNearestAnchorInternal(current: SKPoint, _velocity: SKPoint): SKPoint;
60
+ /** The closest anchor lying in the direction of the velocity (dot product), else the origin. */
61
+ SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
62
+ ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
63
+ get CanAnimate(): boolean;
64
+ protected GetAutoVelocity(displacement: SKPoint): SKPoint;
65
+ protected ScaleSnapVelocity(velocity: SKPoint): SKPoint;
66
+ protected CreateSnapSpring(): Spring;
67
+ protected SpringVelocity(velocity: SKPoint): SKPoint;
68
+ /** Seconds of the linear snap (C# SnappingLayout: 0.3 s scaled by distance, or 0.1–0.8 s from the velocity). */
69
+ protected GetSnapDurationSeconds(_start: SKPoint, _end: SKPoint, velocity: SKPoint, displacement: SKPoint): number;
70
+ /** C# ScrollToOffset: spring when Bounces, else a linear animation with the duration from GetSnapDurationSeconds. */
71
+ protected ScrollToOffset(target: SKPoint, velocity: SKPoint, animate: boolean): boolean;
72
+ /** Subclasses move their content here; base records the position and reports (C# ApplyPosition). */
73
+ ApplyPosition(position: SKPoint): void;
74
+ /** C# SkiaCarousel.CheckTransitionEnded: never "ended" while a snap animator still runs (the position may touch the target early). */
75
+ CheckTransitionEnded(): boolean;
76
+ /** Subclasses translate CurrentSnap into their own state (SelectedIndex, IsOpen). */
77
+ UpdateReportedPosition(): void;
78
+ }