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,317 @@
1
+ import { Super } from "../core/Super";
2
+ import { Colors, CornerRadius, SKRect, SkiaBevel, SkiaShadow, Thickness } from "../core/Types";
3
+ import { SkiaLayout } from "./SkiaLayout";
4
+ /**
5
+ * Mirrors DrawnUi SkiaShape: a SkiaLayout (Absolute) that paints a geometric background, strokes its outline
6
+ * INSIDE its bounds (the rect is deflated by half the stroke, so the outer stroke edge sits on DrawingRect),
7
+ * and clips its children to the stroke-inset shape. Cached as Operations by default, like DrawnUi.
8
+ */
9
+ export class SkiaShape extends SkiaLayout {
10
+ cornerRadius = CornerRadius.Zero;
11
+ /** Uniform number or per-corner CornerRadius, points. */
12
+ get CornerRadius() { return this.cornerRadius; }
13
+ set CornerRadius(v) { this.cornerRadius = typeof v === "number" ? new CornerRadius(v) : v; this.Update(); }
14
+ /** Points; a negative value means device pixels. 0 = no stroke. */
15
+ StrokeWidth = 0;
16
+ StrokeColor = Colors.Gray;
17
+ StrokeCap = "Round";
18
+ /** Gradient painted along the stroke instead of StrokeColor (C# StrokeGradient). */
19
+ StrokeGradient;
20
+ /** Raised (Bevel) or pressed-in (Emboss) edges, drawn with Bevel after the background (C# BevelType). */
21
+ BevelType = "None";
22
+ bevel;
23
+ /** Edge depth / colors / opacity; plain `{ Depth, LightColor, ShadowColor, Opacity }` literals accepted. */
24
+ get Bevel() { return this.bevel; }
25
+ set Bevel(v) { this.bevel = v ? SkiaBevel.From(v) : undefined; this.Update(); }
26
+ /** Hollow shape: the background is not filled, only the stroke (and children) are drawn. */
27
+ ClipBackgroundColor = false;
28
+ /** Polygon / Line vertices as ratios (0..1) of the shape rect. */
29
+ Points = [];
30
+ /** SVG path data for Type=Path, fitted and centered inside the rect. */
31
+ PathData;
32
+ shadows = [];
33
+ /** Drop shadows painted with the background (DrawnUi SkiaShape.Shadows); plain `{X, Y, Blur, Opacity, Color}` literals accepted. */
34
+ get Shadows() { return this.shadows; }
35
+ set Shadows(v) { this.shadows = (v ?? []).map(SkiaShadow.From); this.Update(); }
36
+ PaintsBackgroundWithoutColor() { return this.shadows.length > 0; }
37
+ /** C# MergeShadowMargin: spread = 3 * Blur * scale around the offset shadow. */
38
+ ComputeEffectsMargin(scale) {
39
+ const inner = super.ComputeEffectsMargin(scale);
40
+ let l = inner.Left, t = inner.Top, r = inner.Right, b = inner.Bottom;
41
+ for (const s of this.shadows) {
42
+ const spread = 3 * s.Blur * scale, dx = s.X * scale, dy = s.Y * scale;
43
+ l = Math.max(l, spread - dx);
44
+ t = Math.max(t, spread - dy);
45
+ r = Math.max(r, spread + dx);
46
+ b = Math.max(b, spread + dy);
47
+ }
48
+ return new Thickness(l, t, r, b);
49
+ }
50
+ /** C# CreateShadow: DropShadow (shape + shadow) or DropShadowOnly; a fully opaque Color takes the shadow's Opacity. */
51
+ static ShadowFilter(shadow, scale) {
52
+ const CK = Super.CK;
53
+ const c = Super.ParseColor(shadow.Color);
54
+ const color = c[3] >= 0.999 ? CK.Color4f(c[0], c[1], c[2], shadow.Opacity) : c;
55
+ const make = shadow.ShadowOnly ? CK.ImageFilter.MakeDropShadowOnly : CK.ImageFilter.MakeDropShadow;
56
+ return make(shadow.X * scale, shadow.Y * scale, shadow.Blur * scale, shadow.Blur * scale, color, null);
57
+ }
58
+ constructor() {
59
+ super();
60
+ this.Type = "Rectangle";
61
+ this.UseCache = "Operations";
62
+ }
63
+ // ---- geometry ----
64
+ StrokePixels(scale) {
65
+ if (this.StrokeWidth === 0 || this.StrokeColor === Colors.Transparent)
66
+ return 0;
67
+ return this.StrokeWidth > 0 ? this.StrokeWidth * scale : -this.StrokeWidth;
68
+ }
69
+ /** Rect the outline is drawn on: bounds deflated by half the stroke so the stroke stays inside. */
70
+ StrokeAwareRect(r, scale) {
71
+ const half = this.StrokePixels(scale) / 2;
72
+ return new SKRect(r.Left + half, r.Top + half, r.Right - half, r.Bottom - half);
73
+ }
74
+ UsesStroke() { return this.StrokeWidth !== 0 && this.StrokeColor !== Colors.Transparent; }
75
+ /** Builds the outline path for rect (canvas pixels). Caller deletes it. */
76
+ CreateShapePath(rect, scale) {
77
+ const CK = Super.CK;
78
+ const b = new CK.PathBuilder();
79
+ const type = this.Type;
80
+ if (type === "Circle") {
81
+ const d = Math.min(rect.Width, rect.Height);
82
+ const cx = rect.Left + rect.Width / 2, cy = rect.Top + rect.Height / 2;
83
+ b.addOval(CK.LTRBRect(cx - d / 2, cy - d / 2, cx + d / 2, cy + d / 2));
84
+ }
85
+ else if (type === "Ellipse") {
86
+ b.addOval(CK.LTRBRect(rect.Left, rect.Top, rect.Right, rect.Bottom));
87
+ }
88
+ else if (type === "Arc") {
89
+ b.addArc(CK.LTRBRect(rect.Left, rect.Top, rect.Right, rect.Bottom), this.Value1, this.Value2);
90
+ }
91
+ else if (type === "Polygon" || type === "Line") {
92
+ const pts = this.Points;
93
+ if (pts.length >= 2) {
94
+ const p = (i) => [Math.round(rect.Left + pts[i].X * rect.Width), Math.round(rect.Top + pts[i].Y * rect.Height)];
95
+ b.moveTo(...p(0));
96
+ for (let i = 1; i < pts.length; i++)
97
+ b.lineTo(...p(i));
98
+ if (type === "Polygon")
99
+ b.close();
100
+ }
101
+ }
102
+ else if (type === "Path" && this.PathData) {
103
+ const src = CK.Path.MakeFromSVGString(this.PathData);
104
+ if (src) {
105
+ const bounds = src.getBounds(); // [l, t, r, b]
106
+ const w = bounds[2] - bounds[0], h = bounds[3] - bounds[1];
107
+ const s = w > 0 && h > 0 ? Math.min(rect.Width / w, rect.Height / h) : 1;
108
+ const tx = rect.Left + (rect.Width - w * s) / 2 - bounds[0] * s;
109
+ const ty = rect.Top + (rect.Height - h * s) / 2 - bounds[1] * s;
110
+ b.addPath(src, s, 0, tx, 0, s, ty, 0, 0, 1); // addPath(path, 3x3 matrix)
111
+ src.delete();
112
+ }
113
+ }
114
+ else {
115
+ const c = this.cornerRadius;
116
+ if (c.TopLeft || c.TopRight || c.BottomLeft || c.BottomRight) {
117
+ const tl = c.TopLeft * scale, tr = c.TopRight * scale, br = c.BottomRight * scale, bl = c.BottomLeft * scale;
118
+ b.addRRect(Float32Array.of(rect.Left, rect.Top, rect.Right, rect.Bottom, tl, tl, tr, tr, br, br, bl, bl));
119
+ }
120
+ else {
121
+ b.addRect(CK.LTRBRect(rect.Left, rect.Top, rect.Right, rect.Bottom));
122
+ }
123
+ }
124
+ const path = b.detach();
125
+ b.delete();
126
+ return path;
127
+ }
128
+ /** Children live inside the stroke (full width) + Padding. */
129
+ ChildrenRect() {
130
+ const scale = this.RenderingScale;
131
+ const s = this.StrokePixels(scale);
132
+ const r = this.DrawingRect;
133
+ return new SKRect(r.Left + s, r.Top + s, r.Right - s, r.Bottom - s);
134
+ }
135
+ /** Clip for overlay effects (ripple) and children: the shape itself, inset by the stroke. */
136
+ CreateClip() {
137
+ return this.CreateShapePath(this.ChildrenRect(), this.RenderingScale);
138
+ }
139
+ // ---- layout ----
140
+ OnLayoutChanged() {
141
+ // Absolute layout of children inside the stroke-inset rect, then Padding (handled by base via DrawingRect).
142
+ const saved = this.DrawingRect;
143
+ this.DrawingRect = this.ChildrenRect();
144
+ super.OnLayoutChanged();
145
+ this.DrawingRect = saved;
146
+ }
147
+ // ---- paint ----
148
+ PaintBackground(ctx) {
149
+ const type = this.Type;
150
+ if (type === "Arc" || type === "Line")
151
+ return; // open shapes: stroke only
152
+ const CK = Super.CK;
153
+ const canvas = ctx.Context.Canvas;
154
+ const rect = this.StrokeAwareRect(ctx.Destination, ctx.Scale);
155
+ const path = this.CreateShapePath(rect, ctx.Scale);
156
+ // C# PaintWithShadowsInternal: the fill is drawn once per shadow with a drop-shadow filter; a hollow shape
157
+ // (ClipBackgroundColor) clips its own outline away so only the shadow outside remains
158
+ for (const shadow of this.shadows) {
159
+ const paint = this.CreateBackgroundPaint(rect);
160
+ if (this.ClipBackgroundColor)
161
+ paint.setColor(Super.ParseColor(this.StrokeColor === Colors.Transparent ? Colors.Black : this.StrokeColor));
162
+ const filter = SkiaShape.ShadowFilter(shadow, ctx.Scale);
163
+ paint.setImageFilter(filter);
164
+ const saved = canvas.save();
165
+ if (this.ClipBackgroundColor)
166
+ canvas.clipPath(path, CK.ClipOp.Difference, true);
167
+ canvas.drawPath(path, paint);
168
+ canvas.restoreToCount(saved);
169
+ filter.delete();
170
+ paint.delete();
171
+ }
172
+ if (!this.ClipBackgroundColor && (this.shadows.length === 0 || this.shadows.some((s) => s.ShadowOnly))) {
173
+ const paint = this.CreateBackgroundPaint(rect);
174
+ canvas.drawPath(path, paint);
175
+ paint.delete();
176
+ }
177
+ path.delete();
178
+ }
179
+ /** C# PaintBevelEffect: two stroked edge paths (top/left and bottom/right) in the light / shadow colors. */
180
+ PaintBevelEffect(ctx, rect, depth) {
181
+ const bevel = this.bevel;
182
+ if (this.BevelType === "None" || !bevel || depth <= 0)
183
+ return;
184
+ const CK = Super.CK;
185
+ const tl = new CK.PathBuilder(), br = new CK.PathBuilder();
186
+ const half = depth / 2;
187
+ const type = this.Type;
188
+ const arc = (b, l, t, r, bt, start, sweep) => b.addArc(CK.LTRBRect(l, t, r, bt), start, sweep);
189
+ if (type === "Rectangle") {
190
+ const c = this.cornerRadius, s = ctx.Scale;
191
+ const rTL = c.TopLeft * s, rTR = c.TopRight * s, rBR = c.BottomRight * s, rBL = c.BottomLeft * s;
192
+ if (rTL === 0 && rTR === 0 && rBR === 0 && rBL === 0) {
193
+ tl.moveTo(rect.Left, rect.Top + half);
194
+ tl.lineTo(rect.Right, rect.Top + half);
195
+ tl.moveTo(rect.Left + half, rect.Top);
196
+ tl.lineTo(rect.Left + half, rect.Bottom);
197
+ br.moveTo(rect.Left, rect.Bottom - half);
198
+ br.lineTo(rect.Right, rect.Bottom - half);
199
+ br.moveTo(rect.Right - half, rect.Top);
200
+ br.lineTo(rect.Right - half, rect.Bottom);
201
+ }
202
+ else {
203
+ if (rTL > half)
204
+ arc(tl, rect.Left + half, rect.Top + half, rect.Left + 2 * rTL - half, rect.Top + 2 * rTL - half, 180, 90);
205
+ tl.moveTo(rect.Left + rTL, rect.Top + half);
206
+ tl.lineTo(rect.Right - rTR, rect.Top + half);
207
+ if (rTR > half)
208
+ arc(tl, rect.Right - 2 * rTR + half, rect.Top + half, rect.Right - half, rect.Top + 2 * rTR - half, 270, 45);
209
+ if (rBL > half)
210
+ arc(tl, rect.Left + half, rect.Bottom - 2 * rBL + half, rect.Left + 2 * rBL - half, rect.Bottom - half, 135, 45);
211
+ tl.moveTo(rect.Left + half, rect.Top + rTL);
212
+ tl.lineTo(rect.Left + half, rect.Bottom - rBL);
213
+ if (rTR > half)
214
+ arc(br, rect.Right - 2 * rTR + half, rect.Top + half, rect.Right - half, rect.Top + 2 * rTR - half, 315, 45);
215
+ br.moveTo(rect.Right - half, rect.Top + rTR);
216
+ br.lineTo(rect.Right - half, rect.Bottom - rBR);
217
+ if (rBR > half)
218
+ arc(br, rect.Right - 2 * rBR + half, rect.Bottom - 2 * rBR + half, rect.Right - half, rect.Bottom - half, 0, 90);
219
+ if (rBL > half)
220
+ arc(br, rect.Left + half, rect.Bottom - 2 * rBL + half, rect.Left + 2 * rBL - half, rect.Bottom - half, 90, 45);
221
+ br.moveTo(rect.Left + rBL, rect.Bottom - half);
222
+ br.lineTo(rect.Right - rBR, rect.Bottom - half);
223
+ }
224
+ }
225
+ else if (type === "Circle" || type === "Ellipse") {
226
+ let l = rect.Left, t = rect.Top, r = rect.Right, b = rect.Bottom;
227
+ if (type === "Circle") {
228
+ const d = Math.min(rect.Width, rect.Height), cx = rect.Left + rect.Width / 2, cy = rect.Top + rect.Height / 2;
229
+ l = cx - d / 2;
230
+ r = cx + d / 2;
231
+ t = cy - d / 2;
232
+ b = cy + d / 2;
233
+ }
234
+ arc(tl, l + half, t + half, r - half, b - half, 135, 180);
235
+ arc(br, l + half, t + half, r - half, b - half, 315, 180);
236
+ }
237
+ else if (type === "Path" && this.PathData) {
238
+ // C# SKPathMeasure: first half of the outline is the light edge, second half the shadow edge
239
+ const path = this.CreateShapePath(rect, ctx.Scale);
240
+ const iter = new CK.ContourMeasureIter(path, false, 1);
241
+ const contour = iter.next();
242
+ if (contour) {
243
+ const len = contour.length();
244
+ const a = contour.getSegment(0, len / 2, true), b = contour.getSegment(len / 2, len, true);
245
+ tl.addPath(a);
246
+ br.addPath(b);
247
+ a.delete();
248
+ b.delete();
249
+ contour.delete();
250
+ }
251
+ iter.delete();
252
+ path.delete();
253
+ }
254
+ else if (type === "Polygon" && this.Points.length >= 3) {
255
+ const pts = this.Points, cx = rect.Left + rect.Width / 2, cy = rect.Top + rect.Height / 2;
256
+ const p = (i) => [rect.Left + pts[i].X * rect.Width, rect.Top + pts[i].Y * rect.Height];
257
+ for (let i = 0; i < pts.length; i++) {
258
+ const [x1, y1] = p(i), [x2, y2] = p((i + 1) % pts.length);
259
+ const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
260
+ const light = (mx <= cx && my <= cy) || (mx <= cx && my >= cy) || (mx >= cx && my <= cy);
261
+ const b = light ? tl : br;
262
+ b.moveTo(x1, y1);
263
+ b.lineTo(x2, y2);
264
+ }
265
+ }
266
+ const topLeft = tl.detach(), bottomRight = br.detach();
267
+ tl.delete();
268
+ br.delete();
269
+ const paint = new CK.Paint();
270
+ paint.setAntiAlias(true);
271
+ paint.setStyle(CK.PaintStyle.Stroke);
272
+ paint.setStrokeWidth(depth);
273
+ const withAlpha = (c) => { const f = Super.ParseColor(c); return Float32Array.of(f[0], f[1], f[2], bevel.Opacity); };
274
+ const light = withAlpha(bevel.LightColor), shadow = withAlpha(bevel.ShadowColor);
275
+ const canvas = ctx.Context.Canvas;
276
+ paint.setColor(this.BevelType === "Bevel" ? light : shadow);
277
+ canvas.drawPath(topLeft, paint);
278
+ paint.setColor(this.BevelType === "Bevel" ? shadow : light);
279
+ canvas.drawPath(bottomRight, paint);
280
+ paint.delete();
281
+ topLeft.delete();
282
+ bottomRight.delete();
283
+ }
284
+ Paint(ctx) {
285
+ const CK = Super.CK;
286
+ const canvas = ctx.Context.Canvas;
287
+ if (this.BevelType !== "None" && this.bevel)
288
+ this.PaintBevelEffect(ctx, this.StrokeAwareRect(ctx.Destination, ctx.Scale), this.bevel.Depth * ctx.Scale);
289
+ if (this.Views.length > 0) {
290
+ const clip = this.CreateClip();
291
+ const saved = canvas.save();
292
+ canvas.clipPath(clip, CK.ClipOp.Intersect, true);
293
+ super.Paint(ctx); // children
294
+ canvas.restoreToCount(saved);
295
+ clip.delete();
296
+ }
297
+ if (this.UsesStroke()) {
298
+ const rect = this.StrokeAwareRect(ctx.Destination, ctx.Scale);
299
+ const path = this.CreateShapePath(rect, ctx.Scale);
300
+ const paint = new CK.Paint();
301
+ paint.setAntiAlias(true);
302
+ paint.setStyle(CK.PaintStyle.Stroke);
303
+ paint.setStrokeWidth(Math.max(1, this.StrokePixels(ctx.Scale)));
304
+ paint.setColor(Super.ParseColor(this.StrokeColor));
305
+ if (this.StrokeGradient)
306
+ this.SetupGradient(paint, this.StrokeGradient, rect);
307
+ paint.setStrokeCap(this.StrokeCap === "Butt" ? CK.StrokeCap.Butt : this.StrokeCap === "Square" ? CK.StrokeCap.Square : CK.StrokeCap.Round);
308
+ paint.setStrokeJoin(this.StrokeCap === "Round" ? CK.StrokeJoin.Round : CK.StrokeJoin.Miter);
309
+ canvas.drawPath(path, paint);
310
+ paint.delete();
311
+ path.delete();
312
+ }
313
+ }
314
+ }
315
+ /** DrawnUi SkiaFrame = SkiaShape Type=Rectangle (alias, not a layout type). */
316
+ export class SkiaFrame extends SkiaShape {
317
+ }
@@ -0,0 +1,74 @@
1
+ import { type DrawingContext, 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, ScaledSize, Thickness } from "../core/Types";
5
+ /**
6
+ * Mirrors DrawnUi SkiaSlider: single or range (`EnableRange`) slider with Min/Max/Step, `Start`/`End` values,
7
+ * thumb drag, click-on-trail, per-style looks (Default / Cupertino / Material / Material3 / Windows).
8
+ * Track, selected trail and thumbs are painted directly; the C# thumb-position math (SliderHeight = thumb box) is kept.
9
+ */
10
+ export declare class SkiaSlider extends SkiaControl {
11
+ static DefaultAccessibilityRole?: string;
12
+ ControlStyle: PrebuiltControlStyle;
13
+ Min: number;
14
+ Max: number;
15
+ Step: number;
16
+ EnableRange: boolean;
17
+ /** Minimum distance between the two thumbs, in value units. */
18
+ RangeMin: number;
19
+ ClickOnTrailEnabled: boolean;
20
+ RespondsToGestures: boolean;
21
+ IsPressed: boolean;
22
+ IsUserPanning: boolean;
23
+ /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
24
+ MoreHotspotSize: number;
25
+ StartChanged?: (sender: SkiaSlider, value: number) => void;
26
+ EndChanged?: (sender: SkiaSlider, value: number) => void;
27
+ private start;
28
+ private end;
29
+ private sliderHeight?;
30
+ private thumbColor?;
31
+ private trackColor?;
32
+ private trackSelectedColor?;
33
+ private touchArea;
34
+ private lastTouchX;
35
+ /** Thumb positions in points from the left edge (C# StartThumbX / EndThumbX). */
36
+ StartThumbX: number;
37
+ EndThumbX: number;
38
+ constructor();
39
+ get UsingControlStyle(): ResolvedControlStyle;
40
+ get Start(): number;
41
+ set Start(v: number);
42
+ get End(): number;
43
+ set End(v: number);
44
+ /** Thumb box size in points (C# SliderHeight); unset = per style (35 default, 28 Cupertino, 20 Material/Windows, +8 in range mode). */
45
+ get SliderHeight(): number;
46
+ set SliderHeight(v: number);
47
+ get ThumbColor(): Color;
48
+ set ThumbColor(v: Color);
49
+ get TrackColor(): Color;
50
+ set TrackColor(v: Color);
51
+ get TrackSelectedColor(): Color;
52
+ set TrackSelectedColor(v: Color);
53
+ /** C# style builders: thumb diameter, track height, palette, thumb shadow. */
54
+ private Look;
55
+ /** Thumb shadows paint outside the box: the cache must include them. */
56
+ ComputeEffectsMargin(scale: number): Thickness;
57
+ private Clamp;
58
+ private get WidthPts();
59
+ private get TotalLength();
60
+ private PositionFromValue;
61
+ private ValueFromPosition;
62
+ private AdjustToStep;
63
+ private SyncThumbsFromValues;
64
+ private SetStartOffsetClamped;
65
+ private SetEndOffsetClamped;
66
+ /** C# RecalculateValues: positions -> stepped values, raising the *Changed callbacks. */
67
+ private RecalculateValues;
68
+ protected MeasureAbsolute(w: number, _h: number, scale: number): ScaledSize;
69
+ protected OnLayoutChanged(): void;
70
+ protected Paint(ctx: DrawingContext): void;
71
+ ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
72
+ protected DefaultAccessibilityCanInteract(): boolean;
73
+ protected DefaultAccessibilityLabel(): string | undefined;
74
+ }
@@ -0,0 +1,243 @@
1
+ import { SkiaControl } from "../core/SkiaControl";
2
+ import { Super } from "../core/Super";
3
+ import { ResolveControlStyle } from "../core/ControlStyle";
4
+ import { Colors, ScaledSize, SkiaShadow, Thickness } from "../core/Types";
5
+ import { SkiaShape } from "./SkiaShape";
6
+ /**
7
+ * Mirrors DrawnUi SkiaSlider: single or range (`EnableRange`) slider with Min/Max/Step, `Start`/`End` values,
8
+ * thumb drag, click-on-trail, per-style looks (Default / Cupertino / Material / Material3 / Windows).
9
+ * Track, selected trail and thumbs are painted directly; the C# thumb-position math (SliderHeight = thumb box) is kept.
10
+ */
11
+ export class SkiaSlider extends SkiaControl {
12
+ static DefaultAccessibilityRole = "slider";
13
+ ControlStyle = "Unset";
14
+ Min = 0;
15
+ Max = 100;
16
+ Step = 1;
17
+ EnableRange = false;
18
+ /** Minimum distance between the two thumbs, in value units. */
19
+ RangeMin = 0;
20
+ ClickOnTrailEnabled = true;
21
+ RespondsToGestures = true;
22
+ IsPressed = false;
23
+ IsUserPanning = false;
24
+ /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
25
+ MoreHotspotSize = 10;
26
+ StartChanged;
27
+ EndChanged;
28
+ start = 0;
29
+ end = 0;
30
+ sliderHeight;
31
+ thumbColor;
32
+ trackColor;
33
+ trackSelectedColor;
34
+ touchArea = "Unknown";
35
+ lastTouchX = 0;
36
+ /** Thumb positions in points from the left edge (C# StartThumbX / EndThumbX). */
37
+ StartThumbX = 0;
38
+ EndThumbX = 0;
39
+ constructor() {
40
+ super();
41
+ this.HorizontalOptions = "Fill";
42
+ this.MinimumWidthRequest = 64;
43
+ this.UseCache = "ImageDoubleBuffered";
44
+ }
45
+ get UsingControlStyle() { return ResolveControlStyle(this.ControlStyle); }
46
+ get Start() { return this.start; }
47
+ set Start(v) { v = this.Clamp(v); if (this.start !== v) {
48
+ this.start = v;
49
+ this.StartChanged?.(this, v);
50
+ this.SyncThumbsFromValues();
51
+ } }
52
+ get End() { return this.end; }
53
+ set End(v) { v = this.Clamp(v); if (this.end !== v) {
54
+ this.end = v;
55
+ this.EndChanged?.(this, v);
56
+ this.SyncThumbsFromValues();
57
+ this.NotifyAccessibility();
58
+ } }
59
+ /** Thumb box size in points (C# SliderHeight); unset = per style (35 default, 28 Cupertino, 20 Material/Windows, +8 in range mode). */
60
+ get SliderHeight() { return this.sliderHeight ?? this.Look().thumb + (this.EnableRange && this.UsingControlStyle !== "Unset" ? 8 : 0); }
61
+ set SliderHeight(v) { this.sliderHeight = v; this.Update(); }
62
+ get ThumbColor() { return this.thumbColor ?? this.Look().thumbColor; }
63
+ set ThumbColor(v) { this.thumbColor = v; this.Update(); }
64
+ get TrackColor() { return this.trackColor ?? this.Look().track; }
65
+ set TrackColor(v) { this.trackColor = v; this.Update(); }
66
+ get TrackSelectedColor() { return this.trackSelectedColor ?? this.Look().selected; }
67
+ set TrackSelectedColor(v) { this.trackSelectedColor = v; this.Update(); }
68
+ /** C# style builders: thumb diameter, track height, palette, thumb shadow. */
69
+ Look() {
70
+ switch (this.UsingControlStyle) {
71
+ case "Cupertino": return { thumb: 28, trackH: 2, track: "#CCCCCC", selected: "#007AFF", thumbColor: Colors.White, shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 3, Opacity: 0.2, Color: Colors.Gray }) };
72
+ case "Material": return { thumb: 20, trackH: 4, track: "#E8EAED", selected: "#2196F3", thumbColor: "#2196F3", shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.3, Color: Colors.Black }) };
73
+ case "Material3": return { thumb: 20, trackH: 4, track: "#E6E0E9", selected: "#6750A4", thumbColor: "#6750A4", shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.3, Color: Colors.Black }) };
74
+ case "Windows": return { thumb: 20, trackH: 4, track: "#C6C6C6", selected: "#0078D4", thumbColor: "#0078D4", shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 2, Opacity: 0.25, Color: Colors.Black }) };
75
+ default: return { thumb: 35, trackH: 6, track: "#D7DBE0", selected: "#DC143C", thumbColor: "#DC143C", shadow: new SkiaShadow({ X: 0, Y: 1, Blur: 3, Opacity: 0.25, Color: Colors.Black }) };
76
+ }
77
+ }
78
+ /** Thumb shadows paint outside the box: the cache must include them. */
79
+ ComputeEffectsMargin(scale) {
80
+ const s = this.Look().shadow;
81
+ const spread = 3 * s.Blur * scale;
82
+ return new Thickness(Math.max(0, spread - s.X * scale), Math.max(0, spread - s.Y * scale), spread + s.X * scale, spread + s.Y * scale);
83
+ }
84
+ Clamp(v) { return Math.max(this.Min, Math.min(this.Max, v)); }
85
+ get WidthPts() { return this.DrawingRect.Width / (this.RenderingScale || 1); }
86
+ get TotalLength() { return Math.max(0, this.WidthPts - this.SliderHeight); }
87
+ PositionFromValue(v) { return this.Max > this.Min ? ((v - this.Min) / (this.Max - this.Min)) * this.TotalLength : 0; }
88
+ ValueFromPosition(x) { const t = this.TotalLength; return t <= 0 ? this.Min : this.Min + (x / t) * (this.Max - this.Min); }
89
+ AdjustToStep(v) { return this.Step > 0 ? this.Min + Math.round((v - this.Min) / this.Step) * this.Step : v; }
90
+ SyncThumbsFromValues() {
91
+ this.StartThumbX = this.PositionFromValue(this.start);
92
+ this.EndThumbX = this.PositionFromValue(this.end);
93
+ this.Update();
94
+ }
95
+ SetStartOffsetClamped(x) {
96
+ const max = this.EnableRange ? this.EndThumbX - this.RangeMin / (this.Step || 1) : this.TotalLength;
97
+ this.StartThumbX = Math.max(0, Math.min(max, x));
98
+ }
99
+ SetEndOffsetClamped(x) {
100
+ const min = this.EnableRange ? this.StartThumbX + this.RangeMin / (this.Step || 1) : 0;
101
+ this.EndThumbX = Math.max(min, Math.min(this.TotalLength, x));
102
+ }
103
+ /** C# RecalculateValues: positions -> stepped values, raising the *Changed callbacks. */
104
+ RecalculateValues() {
105
+ if (this.EnableRange) {
106
+ const s = this.AdjustToStep(this.ValueFromPosition(this.StartThumbX));
107
+ if (s !== this.start) {
108
+ this.start = this.Clamp(s);
109
+ this.StartChanged?.(this, this.start);
110
+ }
111
+ }
112
+ const e = this.AdjustToStep(this.ValueFromPosition(this.EndThumbX));
113
+ if (e !== this.end) {
114
+ this.end = this.Clamp(e);
115
+ this.EndChanged?.(this, this.end);
116
+ this.NotifyAccessibility();
117
+ }
118
+ this.Update();
119
+ }
120
+ MeasureAbsolute(w, _h, scale) {
121
+ return ScaledSize.FromPixels(isFinite(w) ? w : 200 * scale, this.SliderHeight * scale, scale);
122
+ }
123
+ OnLayoutChanged() { this.StartThumbX = this.PositionFromValue(this.start); this.EndThumbX = this.PositionFromValue(this.end); }
124
+ Paint(ctx) {
125
+ const CK = Super.CK;
126
+ const look = this.Look();
127
+ const scale = ctx.Scale;
128
+ const d = ctx.Destination;
129
+ const H = this.SliderHeight * scale;
130
+ const cy = d.Top + d.Height / 2;
131
+ const trackH = look.trackH * scale, r = trackH / 2;
132
+ const thumb = look.thumb * scale;
133
+ const paint = new CK.Paint();
134
+ paint.setAntiAlias(true);
135
+ const canvas = ctx.Context.Canvas;
136
+ // track
137
+ paint.setColor(Super.ParseColor(this.TrackColor));
138
+ canvas.drawRRect(CK.RRectXY(CK.LTRBRect(d.Left, cy - r, d.Right, cy + r), r, r), paint);
139
+ // selected trail between thumb centers (from the left edge in single mode)
140
+ const endC = d.Left + this.EndThumbX * scale + H / 2;
141
+ const startC = this.EnableRange ? d.Left + this.StartThumbX * scale + H / 2 : d.Left;
142
+ paint.setColor(Super.ParseColor(this.TrackSelectedColor));
143
+ if (endC > startC)
144
+ canvas.drawRRect(CK.RRectXY(CK.LTRBRect(startC, cy - r, endC, cy + r), r, r), paint);
145
+ const drawThumb = (c) => {
146
+ const s = this.UsingControlStyle;
147
+ // the thumb's body carries the style shadow (C# SliderThumb shape Shadows)
148
+ const shadowed = (radius, color) => {
149
+ const filter = SkiaShape.ShadowFilter(look.shadow, scale);
150
+ paint.setImageFilter(filter);
151
+ paint.setColor(Super.ParseColor(color));
152
+ canvas.drawCircle(c, cy, radius, paint);
153
+ paint.setImageFilter(null);
154
+ filter.delete();
155
+ };
156
+ if (s === "Unset") {
157
+ const inner = thumb - 10 * scale; // C# Margin 5 around the accent circle
158
+ shadowed(inner / 2, this.ThumbColor);
159
+ paint.setColor(Super.ParseColor(Colors.White));
160
+ canvas.drawCircle(c, cy, 3 * scale, paint);
161
+ }
162
+ else if (s === "Windows") {
163
+ shadowed(thumb / 2, Colors.White);
164
+ paint.setStyle(CK.PaintStyle.Stroke);
165
+ paint.setStrokeWidth(scale);
166
+ paint.setColor(Super.ParseColor("#E5E5E5"));
167
+ canvas.drawCircle(c, cy, thumb / 2 - scale / 2, paint);
168
+ paint.setStyle(CK.PaintStyle.Fill);
169
+ paint.setColor(Super.ParseColor(this.ThumbColor));
170
+ canvas.drawCircle(c, cy, 5 * scale, paint);
171
+ }
172
+ else if (s === "Cupertino") {
173
+ shadowed(thumb / 2, this.ThumbColor);
174
+ paint.setStyle(CK.PaintStyle.Stroke);
175
+ paint.setStrokeWidth(0.5 * scale);
176
+ paint.setColor(Super.ParseColor("#CCCCCC"));
177
+ canvas.drawCircle(c, cy, thumb / 2, paint);
178
+ paint.setStyle(CK.PaintStyle.Fill);
179
+ }
180
+ else {
181
+ shadowed(thumb / 2, this.ThumbColor);
182
+ }
183
+ };
184
+ if (this.EnableRange)
185
+ drawThumb(startC);
186
+ drawThumb(endC);
187
+ paint.delete();
188
+ }
189
+ // ---- gestures (port of C# SkiaSlider.ProcessGestures, single pointer) ----
190
+ ProcessGestures(args, apply) {
191
+ if (!this.RespondsToGestures)
192
+ return super.ProcessGestures(args, apply);
193
+ const scale = this.RenderingScale || 1;
194
+ const localX = (apply.MappedLocation.X + apply.ChildOffset.X - this.DrawingRect.Left) / scale;
195
+ const H = this.SliderHeight, more = this.MoreHotspotSize;
196
+ if (args.Type === "Down") {
197
+ this.IsUserPanning = false;
198
+ if (this.EnableRange && localX >= this.StartThumbX - more && localX <= this.StartThumbX + H + more)
199
+ this.touchArea = "Start";
200
+ else if (localX >= this.EndThumbX - more && localX <= this.EndThumbX + H + more)
201
+ this.touchArea = "End";
202
+ else
203
+ this.touchArea = "Unknown";
204
+ this.IsPressed = true;
205
+ if (this.touchArea === "Unknown" && this.ClickOnTrailEnabled) {
206
+ const half = H / 2;
207
+ if (this.EnableRange && localX <= this.WidthPts / 2) {
208
+ this.touchArea = "Start";
209
+ this.SetStartOffsetClamped(localX - half);
210
+ }
211
+ else {
212
+ this.touchArea = "End";
213
+ this.SetEndOffsetClamped(localX - half);
214
+ }
215
+ this.RecalculateValues();
216
+ }
217
+ this.lastTouchX = this.touchArea === "Start" ? this.StartThumbX : this.EndThumbX;
218
+ return this;
219
+ }
220
+ if (args.Type === "Panning") {
221
+ if (this.touchArea === "Unknown")
222
+ return null;
223
+ this.IsUserPanning = true;
224
+ const maybe = this.lastTouchX + args.Event.Distance.Total.X / scale;
225
+ if (this.touchArea === "Start")
226
+ this.SetStartOffsetClamped(maybe);
227
+ else
228
+ this.SetEndOffsetClamped(maybe);
229
+ this.RecalculateValues();
230
+ return this;
231
+ }
232
+ if (args.Type === "Up") {
233
+ this.IsUserPanning = false;
234
+ this.IsPressed = false;
235
+ return null;
236
+ }
237
+ if (args.Type === "Tapped")
238
+ return this.touchArea !== "Unknown" ? this : null;
239
+ return super.ProcessGestures(args, apply);
240
+ }
241
+ DefaultAccessibilityCanInteract() { return this.RespondsToGestures; }
242
+ DefaultAccessibilityLabel() { return this.EnableRange ? `${this.start} – ${this.end}` : `${this.end}`; }
243
+ }