drawnui-react 0.1.0-preview.16 → 0.1.0-preview.18

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 (55) hide show
  1. package/README.md +1 -1
  2. package/SKIPPED.md +7 -7
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +4 -1
  6. package/dist/controls/DrawnGame.js +7 -3
  7. package/dist/controls/GridStructure.d.ts +13 -1
  8. package/dist/controls/GridStructure.js +38 -2
  9. package/dist/controls/SkiaButton.d.ts +12 -2
  10. package/dist/controls/SkiaButton.js +19 -6
  11. package/dist/controls/SkiaCarousel.d.ts +1 -1
  12. package/dist/controls/SkiaCarousel.js +3 -2
  13. package/dist/controls/SkiaEditor.d.ts +5 -1
  14. package/dist/controls/SkiaEditor.js +19 -17
  15. package/dist/controls/SkiaGif.d.ts +3 -0
  16. package/dist/controls/SkiaGif.js +11 -1
  17. package/dist/controls/SkiaImage.js +15 -10
  18. package/dist/controls/SkiaLabel.d.ts +52 -3
  19. package/dist/controls/SkiaLabel.js +222 -50
  20. package/dist/controls/SkiaLayout.d.ts +5 -0
  21. package/dist/controls/SkiaLayout.js +7 -4
  22. package/dist/controls/SkiaLottie.js +3 -1
  23. package/dist/controls/SkiaProgress.d.ts +2 -1
  24. package/dist/controls/SkiaProgress.js +4 -1
  25. package/dist/controls/SkiaRadioButton.js +3 -1
  26. package/dist/controls/SkiaScroll.d.ts +17 -1
  27. package/dist/controls/SkiaScroll.js +84 -9
  28. package/dist/controls/SkiaShape.d.ts +13 -2
  29. package/dist/controls/SkiaShape.js +50 -10
  30. package/dist/controls/SkiaSlider.d.ts +10 -2
  31. package/dist/controls/SkiaSlider.js +15 -2
  32. package/dist/controls/SkiaSprite.d.ts +1 -0
  33. package/dist/controls/SkiaSprite.js +4 -0
  34. package/dist/controls/SkiaToggle.d.ts +5 -1
  35. package/dist/controls/SkiaToggle.js +11 -2
  36. package/dist/controls/SnappingLayout.d.ts +10 -1
  37. package/dist/controls/SnappingLayout.js +16 -1
  38. package/dist/core/Accessibility.d.ts +13 -0
  39. package/dist/core/Accessibility.js +10 -4
  40. package/dist/core/Animators.d.ts +28 -1
  41. package/dist/core/Animators.js +76 -8
  42. package/dist/core/Canvas.d.ts +21 -4
  43. package/dist/core/Canvas.js +122 -23
  44. package/dist/core/Easing.d.ts +12 -1
  45. package/dist/core/Easing.js +25 -1
  46. package/dist/core/Gestures.d.ts +24 -1
  47. package/dist/core/Gestures.js +75 -2
  48. package/dist/core/Hover.d.ts +14 -4
  49. package/dist/core/Hover.js +38 -22
  50. package/dist/core/ScrollAnimators.js +9 -0
  51. package/dist/core/SkiaControl.d.ts +27 -6
  52. package/dist/core/SkiaControl.js +81 -38
  53. package/dist/react/SkiaShell.js +33 -14
  54. package/dist/react/index.js +29 -2
  55. package/package.json +16 -1
@@ -28,7 +28,14 @@ export class SkiaScroll extends SkiaControl {
28
28
  AutoScrollingSpeedMs = 600;
29
29
  ScrollingSpeedMs = 400;
30
30
  IgnoreWrongDirection = false;
31
- RespondsToGestures = true;
31
+ respondsToGestures = true;
32
+ /** False: neither a drag, the wheel nor the keyboard scrolls it (code still can), and it is reported disabled; its content keeps its own taps (drawnui-cross 6w). */
33
+ get RespondsToGestures() { return this.respondsToGestures; }
34
+ set RespondsToGestures(v) { if (this.respondsToGestures !== v) {
35
+ this.respondsToGestures = v;
36
+ this.NotifyAccessibility();
37
+ } }
38
+ AcceptsInput() { return this.respondsToGestures; }
32
39
  // ---- header / footer (C# SkiaScroll.Header / Footer) ----
33
40
  header;
34
41
  footer;
@@ -333,6 +340,8 @@ export class SkiaScroll extends SkiaControl {
333
340
  loadMoreTopArmed = false;
334
341
  /** C# CheckNeedToLoadMore: fires once per content extent near an edge; re-arms when the content grows or the user moves away (>offset+100pt, >2 s). */
335
342
  CheckLoadMore() {
343
+ if (this.indexOrder)
344
+ return; // an open ScrollToIndex has the viewport on its way: the edges are looked at after it
336
345
  const vertical = this.Orientation !== "Horizontal";
337
346
  const offset = vertical ? this.offsetY : this.offsetX;
338
347
  const min = vertical ? this.ContentOffsetBounds.Top : this.ContentOffsetBounds.Left; // most negative offset
@@ -635,6 +644,7 @@ export class SkiaScroll extends SkiaControl {
635
644
  }
636
645
  Paint(ctx) {
637
646
  this.Superview?.Hover?.RegisterMover(this);
647
+ this.PursueIndexOrder();
638
648
  const c = this.content;
639
649
  if (!c)
640
650
  return;
@@ -737,6 +747,8 @@ export class SkiaScroll extends SkiaControl {
737
747
  this.ViewportOffsetY = target;
738
748
  }
739
749
  ScrollTo(x, y, maxSpeedSecs, clamp = true) {
750
+ if (!this.pursuing)
751
+ this.indexOrder = undefined; // another scroll drops an open ScrollToIndex
740
752
  this.StopAnimators(); // also forgets any pending edge bounce: a programmatic scroll never bounces
741
753
  let tx = x, ty = y;
742
754
  if (clamp) {
@@ -769,16 +781,82 @@ export class SkiaScroll extends SkiaControl {
769
781
  else
770
782
  this.ScrollTo(this.offsetX, this.ContentOffsetBounds.Top, maxTimeSecs);
771
783
  }
772
- StopScrolling() { this.StopAnimators(); this.IsUserPanning = false; }
784
+ StopScrolling() { this.indexOrder = undefined; this.StopAnimators(); this.IsUserPanning = false; }
773
785
  /**
774
786
  * Scrolls so that item `index` is at the viewport start (or end). Like DrawnUi, Content must BE the
775
787
  * templated layout (the recycled list is the scroll's only child; a header goes above the scroll or in Header).
776
788
  */
777
789
  ScrollToIndex(index, animate, option = "Start") {
790
+ this.indexOrder = { Index: index, Option: option, Secs: animate ? this.ScrollingSpeedMs / 1000 : 0, Stalled: 0 };
791
+ this.PursueIndexOrder();
792
+ this.Repaint();
793
+ }
794
+ /**
795
+ * An open ScrollToIndex (C# OrderedScrollToIndex, DrawnUi.Rust scroll_to_index): a row not measured yet is gone to by
796
+ * the sizes as they are estimated, and the order is aimed again every frame until the row has its real place and the
797
+ * viewport stands on it; an animated move goes on to a moved target in the time it has left, later corrections jump.
798
+ * A press, the wheel or another scroll drops it; while it is open LoadMore waits.
799
+ */
800
+ indexOrder;
801
+ pursuing = false;
802
+ /** One step of the open order (C# ExecuteScrollToIndexOrder, Rust `pursue`), at the start of every paint. */
803
+ PursueIndexOrder() {
804
+ const order = this.indexOrder;
805
+ if (!order)
806
+ return;
807
+ const horizontal = this.Orientation === "Horizontal";
808
+ const before = horizontal ? this.offsetX : this.offsetY;
809
+ const running = horizontal ? this.animatorFlingX : this.animatorFlingY;
810
+ const aim = this.AimIndex(order.Index, order.Option);
811
+ let done = false;
812
+ if (aim) {
813
+ const c = this.ClampOffset(horizontal ? aim.Offset : this.offsetX, horizontal ? this.offsetY : aim.Offset, this.ContentOffsetBounds, true);
814
+ const target = horizontal ? c.X : c.Y;
815
+ const runningTo = horizontal ? this.scrollToTargetX : this.scrollToTargetY;
816
+ const move = (to, secs) => {
817
+ this.pursuing = true;
818
+ try {
819
+ this.ScrollTo(horizontal ? to : this.offsetX, horizontal ? this.offsetY : to, secs, true);
820
+ }
821
+ finally {
822
+ this.pursuing = false;
823
+ }
824
+ };
825
+ if (running.IsRunning && runningTo !== null) {
826
+ if (Math.abs(runningTo - target) > 0.5) {
827
+ // on the way: the move goes on to the new target in the time it has left
828
+ const now = this.Superview?.FrameTimeNanos || performance.now() * 1_000_000;
829
+ move(target, running.StartFrameTimeNanos ? Math.max(0.016, running.Speed - (now - running.StartFrameTimeNanos) / 1e9) : running.Speed);
830
+ running.StartFrameTimeNanos = running.LastFrameTimeNanos = now;
831
+ }
832
+ }
833
+ else if (Math.abs(before - target) <= 0.5) {
834
+ if (horizontal)
835
+ this.ViewportOffsetX = target;
836
+ else
837
+ this.ViewportOffsetY = target;
838
+ done = aim.Exact;
839
+ }
840
+ else {
841
+ move(target, order.Secs);
842
+ order.Secs = 0;
843
+ }
844
+ }
845
+ // no list yet, or a row that never gets its real place: not forever
846
+ order.Stalled = (horizontal ? this.offsetX : this.offsetY) === before && !running.IsRunning ? order.Stalled + 1 : 0;
847
+ if (done || order.Stalled >= 10) {
848
+ this.indexOrder = undefined;
849
+ this.CheckLoadMore();
850
+ return;
851
+ }
852
+ this.Repaint(); // the next frame aims again
853
+ }
854
+ /** The offset (points) that puts item `index` where `option` says, by the sizes the list knows now; Exact = its place is measured. */
855
+ AimIndex(index, option) {
778
856
  const layout = this.content instanceof SkiaLayout && this.content.IsTemplated ? this.content : undefined;
779
857
  const items = layout?.ItemsSource;
780
858
  if (!layout || !items || items.length === 0 || !this.content)
781
- return;
859
+ return undefined;
782
860
  const i = Math.max(0, Math.min(items.length - 1, index));
783
861
  const scale = this.RenderingScale;
784
862
  const horizontal = this.Orientation === "Horizontal";
@@ -795,7 +873,7 @@ export class SkiaScroll extends SkiaControl {
795
873
  // arranged rect, like C# reads the structure cell's Destination.
796
874
  const cell = layout.ChildrenFactory.GetViewForIndex(i);
797
875
  if (!cell)
798
- return;
876
+ return undefined;
799
877
  const r = cell.DrawingRect, c = this.content.DrawingRect;
800
878
  itemStart = (horizontal ? r.Left - c.Left : r.Top - c.Top) / scale;
801
879
  itemSize = (horizontal ? r.Width : r.Height) / scale;
@@ -805,11 +883,7 @@ export class SkiaScroll extends SkiaControl {
805
883
  target -= viewport - itemSize;
806
884
  else if (option === "Center")
807
885
  target -= (viewport - itemSize) / 2;
808
- const time = animate ? this.ScrollingSpeedMs / 1000 : 0;
809
- if (horizontal)
810
- this.ScrollTo(-target, this.offsetY, time, true);
811
- else
812
- this.ScrollTo(this.offsetX, -target, time, true);
886
+ return { Offset: -target, Exact: layout.IsItemMeasured(i) };
813
887
  }
814
888
  StopAnimators() {
815
889
  this.flingEdgeX = null;
@@ -895,6 +969,7 @@ export class SkiaScroll extends SkiaControl {
895
969
  if (this.ProcessScrollBarGestures(args, apply))
896
970
  return this;
897
971
  if (args.Type === "Down") {
972
+ this.indexOrder = undefined; // a press drops an open ScrollToIndex
898
973
  this.hadDown = true;
899
974
  this.snapped = false;
900
975
  if (this.RespondsToGestures) {
@@ -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
  }
@@ -19,7 +19,11 @@ export declare class SkiaSlider extends SkiaControl {
19
19
  /** Minimum distance between the two thumbs, in value units. */
20
20
  RangeMin: number;
21
21
  ClickOnTrailEnabled: boolean;
22
- 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;
23
27
  IsPressed: boolean;
24
28
  IsUserPanning: boolean;
25
29
  /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
@@ -80,5 +84,9 @@ export declare class SkiaSlider extends SkiaControl {
80
84
  * Home / End go to Min / Max. A ranged slider moves its End thumb, which stops at Start.
81
85
  */
82
86
  OnAccessibilityKey(key: string): boolean;
83
- protected DefaultAccessibilityLabel(): string | undefined;
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;
84
92
  }
@@ -20,7 +20,14 @@ export class SkiaSlider extends SkiaControl {
20
20
  /** Minimum distance between the two thumbs, in value units. */
21
21
  RangeMin = 0;
22
22
  ClickOnTrailEnabled = true;
23
- 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; }
24
31
  IsPressed = false;
25
32
  IsUserPanning = false;
26
33
  /** Extra points around a thumb that still count as grabbing it (C# moreHotspotSize). */
@@ -280,5 +287,11 @@ export class SkiaSlider extends SkiaControl {
280
287
  this.End = Math.min(this.Max, Math.max(low, value));
281
288
  return true;
282
289
  }
283
- DefaultAccessibilityLabel() { return this.EnableRange ? `${this.start} – ${this.end}` : `${this.end}`; }
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
+ }
284
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;
@@ -19,7 +19,11 @@ export declare abstract class SkiaToggle extends SkiaLayout {
19
19
  Toggled?: (sender: SkiaToggle, value: boolean) => void;
20
20
  DefaultValue: boolean;
21
21
  IsAnimated: boolean;
22
- 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;
23
27
  private isToggled;
24
28
  private colorThumbOn?;
25
29
  private colorFrameOn?;
@@ -22,7 +22,14 @@ export class SkiaToggle extends SkiaLayout {
22
22
  Toggled;
23
23
  DefaultValue = false;
24
24
  IsAnimated = true;
25
- 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; }
26
33
  isToggled = false;
27
34
  colorThumbOn;
28
35
  colorFrameOn;
@@ -106,7 +113,9 @@ export class SkiaToggle extends SkiaLayout {
106
113
  get AccessibilityIsPressed() { return this.isToggled; }
107
114
  set AccessibilityIsPressed(_v) { }
108
115
  ProcessGestures(args, apply) {
109
- 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") {
110
119
  this.IsToggled = !this.IsToggled;
111
120
  return this;
112
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;
@@ -61,6 +65,11 @@ export declare abstract class SnappingLayout extends SkiaLayout {
61
65
  protected FindNearestAnchorInternal(current: SKPoint, _velocity: SKPoint): SKPoint;
62
66
  /** The closest anchor lying in the direction of the velocity (dot product), else the origin. */
63
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;
64
73
  ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
65
74
  get CanAnimate(): boolean;
66
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;
@@ -138,9 +145,17 @@ export class SnappingLayout extends SkiaLayout {
138
145
  }
139
146
  return origin;
140
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;
141
153
  ScrollToNearestAnchor(location, velocity) {
142
154
  if (this.SnapPoints.length === 0)
143
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
+ }
144
159
  const origin = this.FindNearestAnchorInternal(location, velocity);
145
160
  const target = this.SelectNextAnchor(origin, velocity);
146
161
  if (SnappingLayout.Dist(location, target) >= 0.5)
@@ -70,6 +70,17 @@ export interface AccessibilityTextLine {
70
70
  FontWeight: number;
71
71
  FontSize: number;
72
72
  }
73
+ /**
74
+ * The value of a range control (a slider, a progress bar): what a screen reader reads and adjusts (drawnui-cross 6c,
75
+ * C# AccessibilityValue): aria-valuenow / min / max, and aria-valuetext when Text is not empty ("65%", "20 – 80").
76
+ */
77
+ export interface AccessibilityValue {
78
+ Now: number;
79
+ Min: number;
80
+ Max: number;
81
+ Step: number;
82
+ Text: string;
83
+ }
73
84
  /** One entry of the accessibility snapshot (DrawnUi AccessibilityNode). Rect is CSS pixels relative to the canvas. */
74
85
  export interface AccessibilityNode {
75
86
  Id: number;
@@ -83,6 +94,8 @@ export interface AccessibilityNode {
83
94
  IsPressed?: boolean;
84
95
  Live?: string;
85
96
  Source: SkiaControl;
97
+ /** A range control's value (aria-valuenow / min / max / valuetext); its name stays the app's label. */
98
+ Value?: AccessibilityValue;
86
99
  /** Real text lines rendered into the overlay so the browser can select / copy them (opt-in per control). */
87
100
  TextLines?: AccessibilityTextLine[];
88
101
  }
@@ -80,6 +80,7 @@ export class SkiaAccessibilityManager {
80
80
  if (!node || !node.AccessibilityCanInteract)
81
81
  return false;
82
82
  node.OnAccessibilityActivated();
83
+ node.Superview?.AccessibilityManager.ForceRebuildOnNextFrame(); // a screen reader reads the new state right back
83
84
  return true;
84
85
  }
85
86
  /**
@@ -90,8 +91,10 @@ export class SkiaAccessibilityManager {
90
91
  static Key(node, key) {
91
92
  if (!node)
92
93
  return false;
93
- if (node.CanReceiveGesture("Panning") && node.OnAccessibilityKey(key))
94
+ if (node.CanReceiveGesture("Panning") && node.OnAccessibilityKey(key)) {
95
+ node.Superview?.AccessibilityManager.ForceRebuildOnNextFrame(); // the new value is read right back, not a second later
94
96
  return true;
97
+ }
95
98
  return node.Superview?.AccessibilityManager.MoveInGroup(node, key) ?? false;
96
99
  }
97
100
  // ---- arrow-key groups (C# SkiaAccessibilityManager, "Arrow-key groups") ----
@@ -255,7 +258,7 @@ export class SkiaAccessibilityManager {
255
258
  for (const cb of this.liveUpdated)
256
259
  cb(node);
257
260
  }
258
- ForceRebuildOnNextFrame() { this.lastRebuild = 0; this.dirty = true; }
261
+ ForceRebuildOnNextFrame() { this.lastRebuild = -Infinity; this.dirty = true; }
259
262
  Unregister(node) {
260
263
  if (this.nodes.delete(node)) {
261
264
  node.OnAccessibilityUnregistered();
@@ -311,7 +314,7 @@ export class SkiaAccessibilityManager {
311
314
  if (px.Right < -canvasWidthPx || px.Bottom < -canvasHeightPx || px.Left > 2 * canvasWidthPx || px.Top > 2 * canvasHeightPx)
312
315
  continue;
313
316
  list.push({
314
- Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole,
317
+ Id: n.AccessibilityId, Label: n.AccessibilityLabel, Hint: n.AccessibilityHint, Role: n.AccessibilityRole, Value: n.AccessibilityValue,
315
318
  Rect: new SKRect(px.Left / scale, px.Top / scale, px.Right / scale, px.Bottom / scale),
316
319
  CanInteract: n.AccessibilityCanInteract, IsPressed: n.AccessibilityIsPressed, Live: n.AccessibilityLive, Source: n,
317
320
  Disabled: !n.AccessibilityCanInteract && Aria.IsInteractiveRole(n.AccessibilityRole),
@@ -347,7 +350,7 @@ export class SkiaAccessibilityManager {
347
350
  for (let i = 0; i < a.length; i++) {
348
351
  const x = a[i], y = b[i];
349
352
  if (x.Source !== y.Source || x.Label !== y.Label || x.Hint !== y.Hint || x.Role !== y.Role || x.CanInteract !== y.CanInteract || x.Disabled !== y.Disabled
350
- || x.IsPressed !== y.IsPressed || x.Live !== y.Live
353
+ || x.IsPressed !== y.IsPressed || x.Live !== y.Live || !SameValue(x.Value, y.Value)
351
354
  || Math.abs(x.Rect.Left - y.Rect.Left) > 0.5 || Math.abs(x.Rect.Top - y.Rect.Top) > 0.5
352
355
  || Math.abs(x.Rect.Right - y.Rect.Right) > 0.5 || Math.abs(x.Rect.Bottom - y.Rect.Bottom) > 0.5)
353
356
  return false;
@@ -389,6 +392,9 @@ function SayNamesOnce(list) {
389
392
  }
390
393
  return said.size ? list.filter((x) => !said.has(x)) : list;
391
394
  }
395
+ function SameValue(a, b) {
396
+ return a === b || (!!a && !!b && a.Now === b.Now && a.Min === b.Min && a.Max === b.Max && a.Step === b.Step && a.Text === b.Text);
397
+ }
392
398
  // ---- group helpers (duck-typed layouts and scrolls) ----
393
399
  /** Children of a control (a layout's Views, a scroll's Content). */
394
400
  function ViewsOf(c) {
@@ -18,11 +18,28 @@ export declare class AnimatorBase {
18
18
  StartFrameTimeNanos: number;
19
19
  OnStart?: () => void;
20
20
  OnStop?: () => void;
21
+ /** Paused: the canvas does not tick it and it wakes no frames; Resume goes on from where it was (C# Pause / Resume). */
22
+ IsPaused: boolean;
23
+ /** Paused by the canvas because its control is hidden; resumed when it shows again. */
24
+ PausedWhileHidden: boolean;
25
+ /**
26
+ * Frame time (ns) before which nothing it shows changes: the canvas does not tick it and it keeps no frames coming
27
+ * until then, a timer wakes the frame loop at that time (a GIF or a sprite between two frames; DrawnUi.Rust frame
28
+ * animators sleep between changes). 0 = tick every frame.
29
+ */
30
+ SleepUntilNanos: number;
21
31
  private delayHandle;
32
+ private delayEnd;
33
+ private delayLeft;
34
+ private pausedAt;
22
35
  constructor(parent: SkiaControl);
23
36
  protected Register(): boolean;
24
37
  protected Unregister(): void;
25
38
  Start(delayMs?: number): void;
39
+ /** Freezes the run (and what is left of a start delay); Resume goes on from there. */
40
+ Pause(): void;
41
+ /** Goes on from where Pause froze it: the paused time is not counted. */
42
+ Resume(): void;
26
43
  Stop(): void;
27
44
  /** Returns true when there is nothing more to do (not running / finished). */
28
45
  TickFrame(frameTimeNanos: number): boolean;
@@ -55,10 +72,20 @@ export declare class SkiaValueAnimator extends AnimatorBase {
55
72
  TickFrame(frameTimeNanos: number): boolean;
56
73
  /** Passed over mValue; subclasses derive their own reported value here. */
57
74
  protected TransformReportedValue(_deltaT: number): number;
58
- /** Updates mValue from elapsed time; true when the target was reached. */
75
+ /**
76
+ * Updates mValue from elapsed time; true when the run is over. The eased value is not clamped to the range: a spring
77
+ * easing goes past its ends, and a run whose mMinValue is above mMaxValue (a ping-pong's way back) goes down.
78
+ */
59
79
  protected UpdateValue(_deltaT: number, deltaFromStart: number): boolean;
60
80
  protected FinishedRunning(): boolean;
61
81
  }
82
+ /** DrawnUi PingPongAnimator: every repeat runs the other way (Repeat -1 = forever by default). */
83
+ export declare class PingPongAnimator extends SkiaValueAnimator {
84
+ /** One way done, the next starts (C# CycleFInished). */
85
+ CycleFinished?: () => void;
86
+ constructor(parent: SkiaControl);
87
+ protected FinishedRunning(): boolean;
88
+ }
62
89
  /** DrawnUi RenderingAnimator: a value animator that paints itself above its Parent (IOverlayEffect). */
63
90
  export declare class RenderingAnimator extends SkiaValueAnimator implements IOverlayEffect {
64
91
  constructor(parent: SkiaControl);