drawnui-react 0.1.0-preview.2 → 0.1.0-preview.21

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 (87) hide show
  1. package/README.md +17 -4
  2. package/SKIPPED.md +33 -27
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +40 -0
  6. package/dist/controls/DrawnGame.js +64 -0
  7. package/dist/controls/GridStructure.d.ts +16 -2
  8. package/dist/controls/GridStructure.js +40 -4
  9. package/dist/controls/SkiaBackdrop.d.ts +8 -2
  10. package/dist/controls/SkiaBackdrop.js +48 -8
  11. package/dist/controls/SkiaButton.d.ts +19 -2
  12. package/dist/controls/SkiaButton.js +41 -11
  13. package/dist/controls/SkiaCarousel.d.ts +9 -1
  14. package/dist/controls/SkiaCarousel.js +16 -3
  15. package/dist/controls/SkiaCheckbox.d.ts +0 -1
  16. package/dist/controls/SkiaCheckbox.js +0 -6
  17. package/dist/controls/SkiaDrawer.d.ts +2 -0
  18. package/dist/controls/SkiaDrawer.js +2 -0
  19. package/dist/controls/SkiaEditor.d.ts +12 -1
  20. package/dist/controls/SkiaEditor.js +46 -19
  21. package/dist/controls/SkiaGif.d.ts +3 -0
  22. package/dist/controls/SkiaGif.js +12 -2
  23. package/dist/controls/SkiaImage.js +15 -10
  24. package/dist/controls/SkiaLabel.d.ts +54 -3
  25. package/dist/controls/SkiaLabel.js +235 -52
  26. package/dist/controls/SkiaLabelFps.d.ts +16 -0
  27. package/dist/controls/SkiaLabelFps.js +41 -0
  28. package/dist/controls/SkiaLayout.d.ts +64 -7
  29. package/dist/controls/SkiaLayout.js +316 -19
  30. package/dist/controls/SkiaLottie.js +4 -2
  31. package/dist/controls/SkiaProgress.d.ts +2 -1
  32. package/dist/controls/SkiaProgress.js +6 -3
  33. package/dist/controls/SkiaRadioButton.js +4 -3
  34. package/dist/controls/SkiaScroll.d.ts +35 -5
  35. package/dist/controls/SkiaScroll.js +233 -23
  36. package/dist/controls/SkiaScrollBar.d.ts +27 -0
  37. package/dist/controls/SkiaScrollBar.js +51 -2
  38. package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
  39. package/dist/controls/SkiaShaderCarousel.js +5 -0
  40. package/dist/controls/SkiaShape.d.ts +13 -2
  41. package/dist/controls/SkiaShape.js +50 -10
  42. package/dist/controls/SkiaSlider.d.ts +20 -2
  43. package/dist/controls/SkiaSlider.js +59 -5
  44. package/dist/controls/SkiaSprite.d.ts +1 -0
  45. package/dist/controls/SkiaSprite.js +4 -0
  46. package/dist/controls/SkiaSvg.js +1 -1
  47. package/dist/controls/SkiaSwitch.d.ts +0 -1
  48. package/dist/controls/SkiaSwitch.js +0 -6
  49. package/dist/controls/SkiaToggle.d.ts +25 -5
  50. package/dist/controls/SkiaToggle.js +58 -5
  51. package/dist/controls/SnappingLayout.d.ts +12 -1
  52. package/dist/controls/SnappingLayout.js +19 -1
  53. package/dist/core/Accessibility.d.ts +80 -0
  54. package/dist/core/Accessibility.js +365 -8
  55. package/dist/core/Animators.d.ts +37 -1
  56. package/dist/core/Animators.js +102 -9
  57. package/dist/core/Canvas.d.ts +108 -7
  58. package/dist/core/Canvas.js +363 -35
  59. package/dist/core/Easing.d.ts +12 -1
  60. package/dist/core/Easing.js +25 -1
  61. package/dist/core/Gestures.d.ts +48 -2
  62. package/dist/core/Gestures.js +95 -3
  63. package/dist/core/Hover.d.ts +47 -0
  64. package/dist/core/Hover.js +134 -0
  65. package/dist/core/KeyboardManager.d.ts +13 -0
  66. package/dist/core/KeyboardManager.js +18 -0
  67. package/dist/core/ScrollAnimators.js +9 -0
  68. package/dist/core/SkiaControl.d.ts +216 -7
  69. package/dist/core/SkiaControl.js +516 -59
  70. package/dist/core/SkiaEffect.d.ts +17 -0
  71. package/dist/core/SkiaEffect.js +43 -8
  72. package/dist/core/SkiaImageManager.js +1 -1
  73. package/dist/core/Styles.d.ts +51 -0
  74. package/dist/core/Styles.js +92 -0
  75. package/dist/core/Super.d.ts +17 -1
  76. package/dist/core/Super.js +35 -4
  77. package/dist/core/ViewsAdapter.d.ts +7 -2
  78. package/dist/core/ViewsAdapter.js +10 -3
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.js +4 -0
  81. package/dist/react/SkiaShell.js +33 -14
  82. package/dist/react/index.d.ts +15 -4
  83. package/dist/react/index.js +102 -15
  84. package/dist/react/reconciler.js +34 -12
  85. package/dist/vite/index.d.ts +9 -0
  86. package/dist/vite/index.js +36 -3
  87. package/package.json +33 -3
@@ -20,6 +20,10 @@ export declare class SkiaShape extends SkiaLayout {
20
20
  StrokeCap: StrokeCap;
21
21
  /** Gradient painted along the stroke instead of StrokeColor (C# StrokeGradient). */
22
22
  StrokeGradient?: SkiaGradient;
23
+ /** Dashes in points: on, off, on, off... (C# StrokePath, each rounded to pixels). Empty = a solid line. */
24
+ StrokePath?: number[];
25
+ /** Blend mode of the stroke, an SKBlendMode name (C# StrokeBlendMode). */
26
+ StrokeBlendMode: string;
23
27
  /** Raised (Bevel) or pressed-in (Emboss) edges, drawn with Bevel after the background (C# BevelType). */
24
28
  BevelType: BevelType;
25
29
  private bevel?;
@@ -43,11 +47,18 @@ export declare class SkiaShape extends SkiaLayout {
43
47
  static ShadowFilter(shadow: SkiaShadow, scale: number): import("canvaskit-wasm").ImageFilter;
44
48
  constructor();
45
49
  private StrokePixels;
46
- /** Rect the outline is drawn on: bounds deflated by half the stroke so the stroke stays inside. */
50
+ /**
51
+ * The outline the stroke runs on (C# CalculateShapeSizeForStroke): the bounds inset by half the stroke rounded up, at
52
+ * least a pixel, then on whole pixels inward, so a 1 px stroke on a plain rectangle sits on the line between two
53
+ * pixels as upstream draws it.
54
+ */
47
55
  private StrokeAwareRect;
56
+ /** The fill (C# MeasuredStrokeAwareClipSize): a third of the stroke inside the outline, so it does not show under the stroke's edge. */
57
+ private BackgroundRect;
48
58
  private UsesStroke;
49
59
  /** Builds the outline path for rect (canvas pixels). Caller deletes it. */
50
- protected CreateShapePath(rect: SKRect, scale: number): Path;
60
+ /** `radiusReduce`: pixels taken off every corner radius (the fill inside a thick stroke, C# GetCorrectedBackgroundRadii). */
61
+ protected CreateShapePath(rect: SKRect, scale: number, radiusReduce?: number): Path;
51
62
  /** Children live inside the stroke (full width) + Padding. */
52
63
  private ChildrenRect;
53
64
  /** Clip for overlay effects (ripple) and children: the shape itself, inset by the stroke. */
@@ -17,6 +17,10 @@ export class SkiaShape extends SkiaLayout {
17
17
  StrokeCap = "Round";
18
18
  /** Gradient painted along the stroke instead of StrokeColor (C# StrokeGradient). */
19
19
  StrokeGradient;
20
+ /** Dashes in points: on, off, on, off... (C# StrokePath, each rounded to pixels). Empty = a solid line. */
21
+ StrokePath;
22
+ /** Blend mode of the stroke, an SKBlendMode name (C# StrokeBlendMode). */
23
+ StrokeBlendMode = "SrcOver";
20
24
  /** Raised (Bevel) or pressed-in (Emboss) edges, drawn with Bevel after the background (C# BevelType). */
21
25
  BevelType = "None";
22
26
  bevel;
@@ -66,14 +70,28 @@ export class SkiaShape extends SkiaLayout {
66
70
  return 0;
67
71
  return this.StrokeWidth > 0 ? this.StrokeWidth * scale : -this.StrokeWidth;
68
72
  }
69
- /** Rect the outline is drawn on: bounds deflated by half the stroke so the stroke stays inside. */
73
+ /**
74
+ * The outline the stroke runs on (C# CalculateShapeSizeForStroke): the bounds inset by half the stroke rounded up, at
75
+ * least a pixel, then on whole pixels inward, so a 1 px stroke on a plain rectangle sits on the line between two
76
+ * pixels as upstream draws it.
77
+ */
70
78
  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);
79
+ if (!this.UsesStroke())
80
+ return r;
81
+ const half = this.StrokePixels(scale) / 2, inset = half < 0.5 ? 1 : Math.ceil(half);
82
+ return new SKRect(Math.ceil(r.Left + inset), Math.ceil(r.Top + inset), Math.floor(r.Right - inset), Math.floor(r.Bottom - inset));
83
+ }
84
+ /** The fill (C# MeasuredStrokeAwareClipSize): a third of the stroke inside the outline, so it does not show under the stroke's edge. */
85
+ BackgroundRect(r, scale) {
86
+ if (!this.UsesStroke())
87
+ return r;
88
+ const o = this.StrokeAwareRect(r, scale), k = this.StrokePixels(scale) / 3;
89
+ return new SKRect(o.Left + k, o.Top + k, o.Right - k, o.Bottom - k);
73
90
  }
74
91
  UsesStroke() { return this.StrokeWidth !== 0 && this.StrokeColor !== Colors.Transparent; }
75
92
  /** Builds the outline path for rect (canvas pixels). Caller deletes it. */
76
- CreateShapePath(rect, scale) {
93
+ /** `radiusReduce`: pixels taken off every corner radius (the fill inside a thick stroke, C# GetCorrectedBackgroundRadii). */
94
+ CreateShapePath(rect, scale, radiusReduce = 0) {
77
95
  const CK = Super.CK;
78
96
  const b = new CK.PathBuilder();
79
97
  const type = this.Type;
@@ -114,7 +132,8 @@ export class SkiaShape extends SkiaLayout {
114
132
  else {
115
133
  const c = this.cornerRadius;
116
134
  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;
135
+ const k = (v) => Math.max(0, v * scale - radiusReduce);
136
+ const tl = k(c.TopLeft), tr = k(c.TopRight), br = k(c.BottomRight), bl = k(c.BottomLeft);
118
137
  b.addRRect(Float32Array.of(rect.Left, rect.Top, rect.Right, rect.Bottom, tl, tl, tr, tr, br, br, bl, bl));
119
138
  }
120
139
  else {
@@ -151,8 +170,11 @@ export class SkiaShape extends SkiaLayout {
151
170
  return; // open shapes: stroke only
152
171
  const CK = Super.CK;
153
172
  const canvas = ctx.Context.Canvas;
154
- const rect = this.StrokeAwareRect(ctx.Destination, ctx.Scale);
155
- const path = this.CreateShapePath(rect, ctx.Scale);
173
+ const rect = this.BackgroundRect(ctx.Destination, ctx.Scale);
174
+ // inside a stroke wider than a point the corners of the fill are rounded less (C# GetCorrectedBackgroundRadii)
175
+ const outline = this.StrokeAwareRect(ctx.Destination, ctx.Scale);
176
+ const reduce = this.UsesStroke() && this.StrokeWidth > 1 ? Math.min((outline.Width - rect.Width) / 2, (outline.Height - rect.Height) / 2) : 0;
177
+ const path = this.CreateShapePath(rect, ctx.Scale, reduce);
156
178
  // C# PaintWithShadowsInternal: the fill is drawn once per shadow with a drop-shadow filter; a hollow shape
157
179
  // (ClipBackgroundColor) clips its own outline away so only the shadow outside remains
158
180
  for (const shadow of this.shadows) {
@@ -295,18 +317,36 @@ export class SkiaShape extends SkiaLayout {
295
317
  clip.delete();
296
318
  }
297
319
  if (this.UsesStroke()) {
298
- const rect = this.StrokeAwareRect(ctx.Destination, ctx.Scale);
299
- const path = this.CreateShapePath(rect, ctx.Scale);
320
+ const scale = ctx.Scale, px = this.StrokePixels(scale), type = this.Type;
321
+ let rect = this.StrokeAwareRect(ctx.Destination, scale);
322
+ const c = this.cornerRadius;
323
+ const rounded = type === "Rectangle" && !!(c.TopLeft || c.TopRight || c.BottomLeft || c.BottomRight);
324
+ // C# PaintStroke: on curves a stroke of a point or less is thinned (its antialiasing makes it look heavier than
325
+ // the straight parts), a thin rounded rectangle runs through pixel centers, a line is at least a pixel wide
326
+ const curved = rounded || type === "Circle" || type === "Ellipse" || type === "Arc" || type === "Path";
327
+ const width = type === "Line" ? Math.max(1, px) : curved && px <= scale ? px * 0.55 : px;
328
+ if (rounded && px <= 2 * scale) {
329
+ const even = (v) => { const r = Math.round(v); return Math.abs(v % 1) === 0.5 && r % 2 !== 0 ? r - 1 : r; }; // C# Math.Round: ties to even
330
+ rect = new SKRect(even(rect.Left) + 0.5, even(rect.Top) + 0.5, even(rect.Right) - 0.5, even(rect.Bottom) - 0.5);
331
+ }
332
+ const path = this.CreateShapePath(rect, scale);
300
333
  const paint = new CK.Paint();
301
334
  paint.setAntiAlias(true);
302
335
  paint.setStyle(CK.PaintStyle.Stroke);
303
- paint.setStrokeWidth(Math.max(1, this.StrokePixels(ctx.Scale)));
336
+ paint.setStrokeWidth(width);
304
337
  paint.setColor(Super.ParseColor(this.StrokeColor));
305
338
  if (this.StrokeGradient)
306
339
  this.SetupGradient(paint, this.StrokeGradient, rect);
340
+ const blend = CK.BlendMode[this.StrokeBlendMode];
341
+ if (blend && this.StrokeBlendMode !== "SrcOver")
342
+ paint.setBlendMode(blend);
307
343
  paint.setStrokeCap(this.StrokeCap === "Butt" ? CK.StrokeCap.Butt : this.StrokeCap === "Square" ? CK.StrokeCap.Square : CK.StrokeCap.Round);
308
344
  paint.setStrokeJoin(this.StrokeCap === "Round" ? CK.StrokeJoin.Round : CK.StrokeJoin.Miter);
345
+ const dash = this.StrokePath?.length ? CK.PathEffect.MakeDash(this.StrokePath.map((v) => Math.round(v * scale)), 0) : null;
346
+ if (dash)
347
+ paint.setPathEffect(dash);
309
348
  canvas.drawPath(path, paint);
349
+ dash?.delete();
310
350
  paint.delete();
311
351
  path.delete();
312
352
  }
@@ -8,6 +8,8 @@ import { type Color, ScaledSize, Thickness } from "../core/Types";
8
8
  * Track, selected trail and thumbs are painted directly; the C# thumb-position math (SliderHeight = thumb box) is kept.
9
9
  */
10
10
  export declare class SkiaSlider extends SkiaControl {
11
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
12
+ protected ReceivesHoverByDefault(): boolean;
11
13
  static DefaultAccessibilityRole?: string;
12
14
  ControlStyle: PrebuiltControlStyle;
13
15
  Min: number;
@@ -17,7 +19,11 @@ export declare class SkiaSlider extends SkiaControl {
17
19
  /** Minimum distance between the two thumbs, in value units. */
18
20
  RangeMin: number;
19
21
  ClickOnTrailEnabled: boolean;
20
- RespondsToGestures: boolean;
22
+ private respondsToGestures;
23
+ /** False: neither a drag nor the arrow keys move it (code still can), and it is reported disabled (drawnui-cross 6w). */
24
+ get RespondsToGestures(): boolean;
25
+ set RespondsToGestures(v: boolean);
26
+ protected AcceptsInput(): boolean;
21
27
  IsPressed: boolean;
22
28
  IsUserPanning: boolean;
23
29
  /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
@@ -70,5 +76,17 @@ export declare class SkiaSlider extends SkiaControl {
70
76
  protected Paint(ctx: DrawingContext): void;
71
77
  ProcessGestures(args: SkiaGesturesParameters, apply: GestureEventProcessingInfo): SkiaControl | null;
72
78
  protected DefaultAccessibilityCanInteract(): boolean;
73
- protected DefaultAccessibilityLabel(): string | undefined;
79
+ /** Enter / Space do nothing: the base taps the center, which would jump the value to the middle. */
80
+ OnAccessibilityActivated(): void;
81
+ /**
82
+ * Keyboard adjustment while the slider holds keyboard focus (C# SkiaSlider.OnAccessibilityKey): Right / Up increase and
83
+ * Left / Down decrease by Step (a hundredth of the range when Step is 0), PageUp / PageDown move a tenth of the range,
84
+ * Home / End go to Min / Max. A ranged slider moves its End thumb, which stops at Start.
85
+ */
86
+ OnAccessibilityKey(key: string): boolean;
87
+ /**
88
+ * End between Min and Max, read as the number, or "20 – 80" in range mode; the step is the arrow key's. The name is
89
+ * the app's AccessibilityLabel only (C# used to make the value the name, drawnui-cross 6c).
90
+ */
91
+ get AccessibilityValue(): import("../core/Accessibility").AccessibilityValue;
74
92
  }
@@ -9,6 +9,8 @@ import { SkiaShape } from "./SkiaShape";
9
9
  * Track, selected trail and thumbs are painted directly; the C# thumb-position math (SliderHeight = thumb box) is kept.
10
10
  */
11
11
  export class SkiaSlider extends SkiaControl {
12
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault). */
13
+ ReceivesHoverByDefault() { return true; }
12
14
  static DefaultAccessibilityRole = "slider";
13
15
  ControlStyle = "Unset";
14
16
  Min = 0;
@@ -18,7 +20,14 @@ export class SkiaSlider extends SkiaControl {
18
20
  /** Minimum distance between the two thumbs, in value units. */
19
21
  RangeMin = 0;
20
22
  ClickOnTrailEnabled = true;
21
- RespondsToGestures = true;
23
+ respondsToGestures = true;
24
+ /** False: neither a drag nor the arrow keys move it (code still can), and it is reported disabled (drawnui-cross 6w). */
25
+ get RespondsToGestures() { return this.respondsToGestures; }
26
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
27
+ this.respondsToGestures = v;
28
+ this.NotifyAccessibility();
29
+ } }
30
+ AcceptsInput() { return this.respondsToGestures; }
22
31
  IsPressed = false;
23
32
  IsUserPanning = false;
24
33
  /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
@@ -60,11 +69,11 @@ export class SkiaSlider extends SkiaControl {
60
69
  get SliderHeight() { return this.sliderHeight ?? this.Look().thumb + (this.EnableRange && this.UsingControlStyle !== "Unset" ? 8 : 0); }
61
70
  set SliderHeight(v) { this.sliderHeight = v; this.Update(); }
62
71
  get ThumbColor() { return this.thumbColor ?? this.Look().thumbColor; }
63
- set ThumbColor(v) { this.thumbColor = v; this.Update(); }
72
+ set ThumbColor(v) { this.thumbColor = v; this.UpdateDraw(); }
64
73
  get TrackColor() { return this.trackColor ?? this.Look().track; }
65
- set TrackColor(v) { this.trackColor = v; this.Update(); }
74
+ set TrackColor(v) { this.trackColor = v; this.UpdateDraw(); }
66
75
  get TrackSelectedColor() { return this.trackSelectedColor ?? this.Look().selected; }
67
- set TrackSelectedColor(v) { this.trackSelectedColor = v; this.Update(); }
76
+ set TrackSelectedColor(v) { this.trackSelectedColor = v; this.UpdateDraw(); }
68
77
  /** C# style builders: thumb diameter, track height, palette, thumb shadow. */
69
78
  Look() {
70
79
  switch (this.UsingControlStyle) {
@@ -239,5 +248,50 @@ export class SkiaSlider extends SkiaControl {
239
248
  return super.ProcessGestures(args, apply);
240
249
  }
241
250
  DefaultAccessibilityCanInteract() { return this.RespondsToGestures; }
242
- DefaultAccessibilityLabel() { return this.EnableRange ? `${this.start} – ${this.end}` : `${this.end}`; }
251
+ /** Enter / Space do nothing: the base taps the center, which would jump the value to the middle. */
252
+ OnAccessibilityActivated() { }
253
+ /**
254
+ * Keyboard adjustment while the slider holds keyboard focus (C# SkiaSlider.OnAccessibilityKey): Right / Up increase and
255
+ * Left / Down decrease by Step (a hundredth of the range when Step is 0), PageUp / PageDown move a tenth of the range,
256
+ * Home / End go to Min / Max. A ranged slider moves its End thumb, which stops at Start.
257
+ */
258
+ OnAccessibilityKey(key) {
259
+ if (!this.RespondsToGestures)
260
+ return false;
261
+ const range = this.Max - this.Min, small = this.Step > 0 ? this.Step : range / 100, large = Math.max(small, range / 10);
262
+ const low = this.EnableRange ? this.Start : this.Min;
263
+ let value;
264
+ switch (key) {
265
+ case "ArrowRight":
266
+ case "ArrowUp":
267
+ value = this.End + small;
268
+ break;
269
+ case "ArrowLeft":
270
+ case "ArrowDown":
271
+ value = this.End - small;
272
+ break;
273
+ case "PageUp":
274
+ value = this.End + large;
275
+ break;
276
+ case "PageDown":
277
+ value = this.End - large;
278
+ break;
279
+ case "Home":
280
+ value = low;
281
+ break;
282
+ case "End":
283
+ value = this.Max;
284
+ break;
285
+ default: return false;
286
+ }
287
+ this.End = Math.min(this.Max, Math.max(low, value));
288
+ return true;
289
+ }
290
+ /**
291
+ * End between Min and Max, read as the number, or "20 – 80" in range mode; the step is the arrow key's. The name is
292
+ * the app's AccessibilityLabel only (C# used to make the value the name, drawnui-cross 6c).
293
+ */
294
+ get AccessibilityValue() {
295
+ return { Now: this.end, Min: this.Min, Max: this.Max, Step: this.Step > 0 ? this.Step : (this.Max - this.Min) / 100, Text: this.EnableRange ? `${this.start} – ${this.end}` : "" };
296
+ }
243
297
  }
@@ -86,6 +86,7 @@ export declare class SkiaSprite extends AnimatedFramesRenderer {
86
86
  /** Transparent borders of a frame, scanned once (C# ScanFrameContentBounds over the bitmap alpha). */
87
87
  private GetFrameContentBounds;
88
88
  protected OnAnimatorUpdated(value: number): void;
89
+ protected NextChangeMs(value: number): number | undefined;
89
90
  protected OnAnimatorSeeking(time: number): void;
90
91
  protected OnAnimatorInitializing(): void;
91
92
  protected ApplySpeed(): void;
@@ -227,6 +227,10 @@ export class SkiaSprite extends AnimatedFramesRenderer {
227
227
  }
228
228
  // ---- animator ----
229
229
  OnAnimatorUpdated(value) { this.Seek(value); }
230
+ NextChangeMs(value) {
231
+ const fd = this.FrameDurationMs;
232
+ return fd > 0 ? (Math.floor(value / fd) + 1) * fd : undefined;
233
+ }
230
234
  OnAnimatorSeeking(time) {
231
235
  if (!this.SpriteSheet)
232
236
  return;
@@ -42,7 +42,7 @@ export class SkiaSvg extends SkiaControl {
42
42
  }
43
43
  const generation = ++this.loadGeneration;
44
44
  this.IsLoading = true;
45
- fetch(value)
45
+ fetch(Super.ResolveAssetUrl(value))
46
46
  .then((r) => { if (!r.ok)
47
47
  throw new Error(`DrawnUi: ${r.status} loading svg '${value}'`); return r.text(); })
48
48
  .then((text) => { if (generation === this.loadGeneration)
@@ -14,7 +14,6 @@ export declare class SkiaSwitch extends SkiaToggle {
14
14
  private static readonly DefaultTrack;
15
15
  private static readonly MaterialOutline;
16
16
  protected StyleDefault(name: "ColorThumbOn" | "ColorFrameOn" | "ColorThumbOff" | "ColorFrameOff"): Color | undefined;
17
- private SetContentSize;
18
17
  protected CreateDefaultContent(): void;
19
18
  /** C# GetThumbPosForOn: track width minus thumb width and margins, in points. */
20
19
  private ThumbPosForOn;
@@ -25,12 +25,6 @@ export class SkiaSwitch extends SkiaToggle {
25
25
  };
26
26
  return table[s]?.[name];
27
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
28
  CreateDefaultContent() {
35
29
  const frame = new SkiaShape();
36
30
  frame.Tag = "Frame";
@@ -5,16 +5,25 @@ import { type Color, type ScaledSize } from "../core/Types";
5
5
  import { SkiaLayout } from "./SkiaLayout";
6
6
  /**
7
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).
8
+ * `Toggled` reports user changes, `ControlStyle` selects the look the default content is built with (at first
9
+ * measure, like C# CreateDefaultContent; a later change rebuilds it, C# RebuildDefaultContent). Colors left unset
10
+ * take the style defaults (C# SetStyleDefault).
10
11
  */
11
12
  export declare abstract class SkiaToggle extends SkiaLayout {
12
- ControlStyle: PrebuiltControlStyle;
13
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault): switch, checkbox, radio button. */
14
+ protected ReceivesHoverByDefault(): boolean;
15
+ private controlStyle;
16
+ get ControlStyle(): PrebuiltControlStyle;
17
+ set ControlStyle(v: PrebuiltControlStyle);
13
18
  /** Fires on every IsToggled change; C# fires only on user/external changes, not on DefaultValue application. */
14
19
  Toggled?: (sender: SkiaToggle, value: boolean) => void;
15
20
  DefaultValue: boolean;
16
21
  IsAnimated: boolean;
17
- RespondsToGestures: boolean;
22
+ private respondsToGestures;
23
+ /** False: a tap or the keyboard does not toggle it (code still can), a tap on it stops there, and it is reported disabled (drawnui-cross 6w). */
24
+ get RespondsToGestures(): boolean;
25
+ set RespondsToGestures(v: boolean);
26
+ protected AcceptsInput(): boolean;
18
27
  private isToggled;
19
28
  private colorThumbOn?;
20
29
  private colorFrameOn?;
@@ -37,10 +46,21 @@ export declare abstract class SkiaToggle extends SkiaLayout {
37
46
  /** Per-style color defaults (C# SetStyleDefault); undefined = the SkiaToggle default. */
38
47
  protected StyleDefault(_name: "ColorThumbOn" | "ColorFrameOn" | "ColorThumbOff" | "ColorFrameOff"): Color | undefined;
39
48
  protected OnToggledChanged(): void;
40
- /** Builds the look once (C# CreateDefaultContent) — subclasses add their views here. */
49
+ /** Builds the look (C# CreateDefaultContent) — subclasses add their views here; called again after a rebuild. */
41
50
  protected abstract CreateDefaultContent(): void;
42
51
  /** Pushes IsToggled + colors into the views (C# ApplyProperties). */
43
52
  abstract ApplyProperties(): void;
53
+ /** Size requests the style pinned (C# SetDefaultContentSize / SetDefaultMinimumContentSize); a rebuild releases only these. */
54
+ private stylePinned;
55
+ /** C# SetDefaultContentSize: the style's size, only where the app left the request unset. */
56
+ protected SetContentSize(w: number, h: number): void;
57
+ /** C# SetDefaultMinimumContentSize (height only): the style's minimum, only where the app left it unset. */
58
+ protected SetMinimumContentHeight(h: number): void;
59
+ /**
60
+ * C# RebuildDefaultContent: drops the content built for the previous style and the sizes it pinned (the app's own
61
+ * requests stay), so the next Measure builds the current style. No-op before the first build.
62
+ */
63
+ RebuildDefaultContent(): void;
44
64
  /** Content (and the size requests the style sets) must exist before Measure reads WidthRequest/HeightRequest. */
45
65
  Measure(widthConstraint: number, heightConstraint: number, scale: number): ScaledSize;
46
66
  protected DefaultAccessibilityCanInteract(): boolean;
@@ -3,16 +3,33 @@ import { Colors } from "../core/Types";
3
3
  import { SkiaLayout } from "./SkiaLayout";
4
4
  /**
5
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).
6
+ * `Toggled` reports user changes, `ControlStyle` selects the look the default content is built with (at first
7
+ * measure, like C# CreateDefaultContent; a later change rebuilds it, C# RebuildDefaultContent). Colors left unset
8
+ * take the style defaults (C# SetStyleDefault).
8
9
  */
9
10
  export class SkiaToggle extends SkiaLayout {
10
- ControlStyle = "Unset";
11
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault): switch, checkbox, radio button. */
12
+ ReceivesHoverByDefault() { return true; }
13
+ controlStyle = "Unset";
14
+ get ControlStyle() { return this.controlStyle; }
15
+ set ControlStyle(v) {
16
+ if (this.controlStyle === v)
17
+ return;
18
+ this.controlStyle = v;
19
+ this.RebuildDefaultContent();
20
+ }
11
21
  /** Fires on every IsToggled change; C# fires only on user/external changes, not on DefaultValue application. */
12
22
  Toggled;
13
23
  DefaultValue = false;
14
24
  IsAnimated = true;
15
- RespondsToGestures = true;
25
+ respondsToGestures = true;
26
+ /** False: a tap or the keyboard does not toggle it (code still can), a tap on it stops there, and it is reported disabled (drawnui-cross 6w). */
27
+ get RespondsToGestures() { return this.respondsToGestures; }
28
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
29
+ this.respondsToGestures = v;
30
+ this.NotifyAccessibility();
31
+ } }
32
+ AcceptsInput() { return this.respondsToGestures; }
16
33
  isToggled = false;
17
34
  colorThumbOn;
18
35
  colorFrameOn;
@@ -48,6 +65,40 @@ export class SkiaToggle extends SkiaLayout {
48
65
  this.Toggled?.(this, this.isToggled);
49
66
  this.NotifyAccessibility();
50
67
  }
68
+ /** Size requests the style pinned (C# SetDefaultContentSize / SetDefaultMinimumContentSize); a rebuild releases only these. */
69
+ stylePinned = [];
70
+ /** C# SetDefaultContentSize: the style's size, only where the app left the request unset. */
71
+ SetContentSize(w, h) {
72
+ if (this.WidthRequest < 0) {
73
+ this.WidthRequest = w;
74
+ this.stylePinned.push("WidthRequest");
75
+ }
76
+ if (this.HeightRequest < 0) {
77
+ this.HeightRequest = h;
78
+ this.stylePinned.push("HeightRequest");
79
+ }
80
+ }
81
+ /** C# SetDefaultMinimumContentSize (height only): the style's minimum, only where the app left it unset. */
82
+ SetMinimumContentHeight(h) {
83
+ if (this.MinimumHeightRequest < 0) {
84
+ this.MinimumHeightRequest = h;
85
+ this.stylePinned.push("MinimumHeightRequest");
86
+ }
87
+ }
88
+ /**
89
+ * C# RebuildDefaultContent: drops the content built for the previous style and the sizes it pinned (the app's own
90
+ * requests stay), so the next Measure builds the current style. No-op before the first build.
91
+ */
92
+ RebuildDefaultContent() {
93
+ if (!this.contentCreated)
94
+ return;
95
+ this.ClearChildren();
96
+ for (const p of this.stylePinned)
97
+ this[p] = -1;
98
+ this.stylePinned = [];
99
+ this.contentCreated = false;
100
+ this.Update();
101
+ }
51
102
  /** Content (and the size requests the style sets) must exist before Measure reads WidthRequest/HeightRequest. */
52
103
  Measure(widthConstraint, heightConstraint, scale) {
53
104
  if (!this.contentCreated) {
@@ -62,7 +113,9 @@ export class SkiaToggle extends SkiaLayout {
62
113
  get AccessibilityIsPressed() { return this.isToggled; }
63
114
  set AccessibilityIsPressed(_v) { }
64
115
  ProcessGestures(args, apply) {
65
- if (args.Type === "Tapped" && this.RespondsToGestures) {
116
+ if (!this.respondsToGestures)
117
+ return this.BlockedGestureResult(args); // a tap does not toggle it, nor reach controls under it
118
+ if (args.Type === "Tapped") {
66
119
  this.IsToggled = !this.IsToggled;
67
120
  return this;
68
121
  }
@@ -11,7 +11,11 @@ import { SkiaLayout } from "./SkiaLayout";
11
11
  */
12
12
  export declare abstract class SnappingLayout extends SkiaLayout {
13
13
  Animated: boolean;
14
- RespondsToGestures: boolean;
14
+ private respondsToGestures;
15
+ /** False: neither a swipe nor the keyboard moves it (code still can), and it is reported disabled; its content keeps its own taps (drawnui-cross 6w). */
16
+ get RespondsToGestures(): boolean;
17
+ set RespondsToGestures(v: boolean);
18
+ protected AcceptsInput(): boolean;
15
19
  IgnoreWrongDirection: boolean;
16
20
  Bounces: boolean;
17
21
  RubberDamping: number;
@@ -50,6 +54,8 @@ export declare abstract class SnappingLayout extends SkiaLayout {
50
54
  protected StopSnapAnimators(): void;
51
55
  /** C# SnappingLayout.Render: the transition state follows the position every frame. */
52
56
  Render(ctx: DrawingContext): void;
57
+ /** Hover waits while the content slides (C# InTransition) or follows a pan. */
58
+ MovesContent(): boolean;
53
59
  ClampOffset(x: number, y: number, rubber: boolean): SKPoint;
54
60
  /** C# GetContentOffsetBounds: the extent covered by the snap points. */
55
61
  protected BoundsFromSnapPoints(): SKRect;
@@ -59,6 +65,11 @@ export declare abstract class SnappingLayout extends SkiaLayout {
59
65
  protected FindNearestAnchorInternal(current: SKPoint, _velocity: SKPoint): SKPoint;
60
66
  /** The closest anchor lying in the direction of the velocity (dot product), else the origin. */
61
67
  SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
68
+ /**
69
+ * Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
70
+ * anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
71
+ */
72
+ SnapVelocityThreshold: number;
62
73
  ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
63
74
  get CanAnimate(): boolean;
64
75
  protected GetAutoVelocity(displacement: SKPoint): SKPoint;
@@ -11,7 +11,14 @@ import { SkiaLayout } from "./SkiaLayout";
11
11
  */
12
12
  export class SnappingLayout extends SkiaLayout {
13
13
  Animated = true;
14
- RespondsToGestures = true;
14
+ respondsToGestures = true;
15
+ /** False: neither a swipe nor the keyboard moves it (code still can), and it is reported disabled; its content keeps its own taps (drawnui-cross 6w). */
16
+ get RespondsToGestures() { return this.respondsToGestures; }
17
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
18
+ this.respondsToGestures = v;
19
+ this.NotifyAccessibility();
20
+ } }
21
+ AcceptsInput() { return this.respondsToGestures; }
15
22
  IgnoreWrongDirection = false;
16
23
  Bounces = false;
17
24
  RubberDamping = 0.7;
@@ -80,9 +87,12 @@ export class SnappingLayout extends SkiaLayout {
80
87
  }
81
88
  /** C# SnappingLayout.Render: the transition state follows the position every frame. */
82
89
  Render(ctx) {
90
+ this.Superview?.Hover?.RegisterMover(this);
83
91
  super.Render(ctx);
84
92
  this.InTransition = !this.CheckTransitionEnded();
85
93
  }
94
+ /** Hover waits while the content slides (C# InTransition) or follows a pan. */
95
+ MovesContent() { return this.InTransition || this.IsUserPanning; }
86
96
  // ---- clamping ----
87
97
  ClampOffset(x, y, rubber) {
88
98
  const b = this.ContentOffsetBounds;
@@ -135,9 +145,17 @@ export class SnappingLayout extends SkiaLayout {
135
145
  }
136
146
  return origin;
137
147
  }
148
+ /**
149
+ * Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
150
+ * anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
151
+ */
152
+ SnapVelocityThreshold = 100;
138
153
  ScrollToNearestAnchor(location, velocity) {
139
154
  if (this.SnapPoints.length === 0)
140
155
  return;
156
+ if (Math.abs(velocity.X) < this.SnapVelocityThreshold || Math.abs(velocity.Y) < this.SnapVelocityThreshold) {
157
+ velocity = new SKPoint(Math.abs(velocity.X) < this.SnapVelocityThreshold ? 0 : velocity.X, Math.abs(velocity.Y) < this.SnapVelocityThreshold ? 0 : velocity.Y);
158
+ }
141
159
  const origin = this.FindNearestAnchorInternal(location, velocity);
142
160
  const target = this.SelectNextAnchor(origin, velocity);
143
161
  if (SnappingLayout.Dist(location, target) >= 0.5)