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

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 (50) hide show
  1. package/README.md +1 -1
  2. package/SKIPPED.md +5 -5
  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/SkiaCarousel.d.ts +1 -1
  10. package/dist/controls/SkiaCarousel.js +3 -2
  11. package/dist/controls/SkiaEditor.d.ts +5 -1
  12. package/dist/controls/SkiaEditor.js +19 -17
  13. package/dist/controls/SkiaGif.d.ts +3 -0
  14. package/dist/controls/SkiaGif.js +11 -1
  15. package/dist/controls/SkiaImage.js +15 -10
  16. package/dist/controls/SkiaLabel.d.ts +52 -3
  17. package/dist/controls/SkiaLabel.js +222 -50
  18. package/dist/controls/SkiaLayout.d.ts +5 -0
  19. package/dist/controls/SkiaLayout.js +7 -4
  20. package/dist/controls/SkiaLottie.js +3 -1
  21. package/dist/controls/SkiaProgress.d.ts +2 -1
  22. package/dist/controls/SkiaProgress.js +4 -1
  23. package/dist/controls/SkiaScroll.d.ts +12 -0
  24. package/dist/controls/SkiaScroll.js +76 -8
  25. package/dist/controls/SkiaShape.d.ts +13 -2
  26. package/dist/controls/SkiaShape.js +50 -10
  27. package/dist/controls/SkiaSlider.d.ts +5 -1
  28. package/dist/controls/SkiaSlider.js +7 -1
  29. package/dist/controls/SkiaSprite.d.ts +1 -0
  30. package/dist/controls/SkiaSprite.js +4 -0
  31. package/dist/controls/SnappingLayout.d.ts +5 -0
  32. package/dist/controls/SnappingLayout.js +8 -0
  33. package/dist/core/Accessibility.d.ts +13 -0
  34. package/dist/core/Accessibility.js +10 -4
  35. package/dist/core/Animators.d.ts +28 -1
  36. package/dist/core/Animators.js +76 -8
  37. package/dist/core/Canvas.d.ts +21 -4
  38. package/dist/core/Canvas.js +122 -23
  39. package/dist/core/Easing.d.ts +12 -1
  40. package/dist/core/Easing.js +25 -1
  41. package/dist/core/Gestures.d.ts +24 -1
  42. package/dist/core/Gestures.js +75 -2
  43. package/dist/core/Hover.d.ts +14 -4
  44. package/dist/core/Hover.js +38 -22
  45. package/dist/core/ScrollAnimators.js +9 -0
  46. package/dist/core/SkiaControl.d.ts +17 -5
  47. package/dist/core/SkiaControl.js +69 -37
  48. package/dist/react/SkiaShell.js +33 -14
  49. package/dist/react/index.js +29 -2
  50. package/package.json +15 -1
@@ -333,6 +333,8 @@ export class SkiaScroll extends SkiaControl {
333
333
  loadMoreTopArmed = false;
334
334
  /** 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
335
  CheckLoadMore() {
336
+ if (this.indexOrder)
337
+ return; // an open ScrollToIndex has the viewport on its way: the edges are looked at after it
336
338
  const vertical = this.Orientation !== "Horizontal";
337
339
  const offset = vertical ? this.offsetY : this.offsetX;
338
340
  const min = vertical ? this.ContentOffsetBounds.Top : this.ContentOffsetBounds.Left; // most negative offset
@@ -635,6 +637,7 @@ export class SkiaScroll extends SkiaControl {
635
637
  }
636
638
  Paint(ctx) {
637
639
  this.Superview?.Hover?.RegisterMover(this);
640
+ this.PursueIndexOrder();
638
641
  const c = this.content;
639
642
  if (!c)
640
643
  return;
@@ -737,6 +740,8 @@ export class SkiaScroll extends SkiaControl {
737
740
  this.ViewportOffsetY = target;
738
741
  }
739
742
  ScrollTo(x, y, maxSpeedSecs, clamp = true) {
743
+ if (!this.pursuing)
744
+ this.indexOrder = undefined; // another scroll drops an open ScrollToIndex
740
745
  this.StopAnimators(); // also forgets any pending edge bounce: a programmatic scroll never bounces
741
746
  let tx = x, ty = y;
742
747
  if (clamp) {
@@ -769,16 +774,82 @@ export class SkiaScroll extends SkiaControl {
769
774
  else
770
775
  this.ScrollTo(this.offsetX, this.ContentOffsetBounds.Top, maxTimeSecs);
771
776
  }
772
- StopScrolling() { this.StopAnimators(); this.IsUserPanning = false; }
777
+ StopScrolling() { this.indexOrder = undefined; this.StopAnimators(); this.IsUserPanning = false; }
773
778
  /**
774
779
  * Scrolls so that item `index` is at the viewport start (or end). Like DrawnUi, Content must BE the
775
780
  * templated layout (the recycled list is the scroll's only child; a header goes above the scroll or in Header).
776
781
  */
777
782
  ScrollToIndex(index, animate, option = "Start") {
783
+ this.indexOrder = { Index: index, Option: option, Secs: animate ? this.ScrollingSpeedMs / 1000 : 0, Stalled: 0 };
784
+ this.PursueIndexOrder();
785
+ this.Repaint();
786
+ }
787
+ /**
788
+ * An open ScrollToIndex (C# OrderedScrollToIndex, DrawnUi.Rust scroll_to_index): a row not measured yet is gone to by
789
+ * the sizes as they are estimated, and the order is aimed again every frame until the row has its real place and the
790
+ * viewport stands on it; an animated move goes on to a moved target in the time it has left, later corrections jump.
791
+ * A press, the wheel or another scroll drops it; while it is open LoadMore waits.
792
+ */
793
+ indexOrder;
794
+ pursuing = false;
795
+ /** One step of the open order (C# ExecuteScrollToIndexOrder, Rust `pursue`), at the start of every paint. */
796
+ PursueIndexOrder() {
797
+ const order = this.indexOrder;
798
+ if (!order)
799
+ return;
800
+ const horizontal = this.Orientation === "Horizontal";
801
+ const before = horizontal ? this.offsetX : this.offsetY;
802
+ const running = horizontal ? this.animatorFlingX : this.animatorFlingY;
803
+ const aim = this.AimIndex(order.Index, order.Option);
804
+ let done = false;
805
+ if (aim) {
806
+ const c = this.ClampOffset(horizontal ? aim.Offset : this.offsetX, horizontal ? this.offsetY : aim.Offset, this.ContentOffsetBounds, true);
807
+ const target = horizontal ? c.X : c.Y;
808
+ const runningTo = horizontal ? this.scrollToTargetX : this.scrollToTargetY;
809
+ const move = (to, secs) => {
810
+ this.pursuing = true;
811
+ try {
812
+ this.ScrollTo(horizontal ? to : this.offsetX, horizontal ? this.offsetY : to, secs, true);
813
+ }
814
+ finally {
815
+ this.pursuing = false;
816
+ }
817
+ };
818
+ if (running.IsRunning && runningTo !== null) {
819
+ if (Math.abs(runningTo - target) > 0.5) {
820
+ // on the way: the move goes on to the new target in the time it has left
821
+ const now = this.Superview?.FrameTimeNanos || performance.now() * 1_000_000;
822
+ move(target, running.StartFrameTimeNanos ? Math.max(0.016, running.Speed - (now - running.StartFrameTimeNanos) / 1e9) : running.Speed);
823
+ running.StartFrameTimeNanos = running.LastFrameTimeNanos = now;
824
+ }
825
+ }
826
+ else if (Math.abs(before - target) <= 0.5) {
827
+ if (horizontal)
828
+ this.ViewportOffsetX = target;
829
+ else
830
+ this.ViewportOffsetY = target;
831
+ done = aim.Exact;
832
+ }
833
+ else {
834
+ move(target, order.Secs);
835
+ order.Secs = 0;
836
+ }
837
+ }
838
+ // no list yet, or a row that never gets its real place: not forever
839
+ order.Stalled = (horizontal ? this.offsetX : this.offsetY) === before && !running.IsRunning ? order.Stalled + 1 : 0;
840
+ if (done || order.Stalled >= 10) {
841
+ this.indexOrder = undefined;
842
+ this.CheckLoadMore();
843
+ return;
844
+ }
845
+ this.Repaint(); // the next frame aims again
846
+ }
847
+ /** The offset (points) that puts item `index` where `option` says, by the sizes the list knows now; Exact = its place is measured. */
848
+ AimIndex(index, option) {
778
849
  const layout = this.content instanceof SkiaLayout && this.content.IsTemplated ? this.content : undefined;
779
850
  const items = layout?.ItemsSource;
780
851
  if (!layout || !items || items.length === 0 || !this.content)
781
- return;
852
+ return undefined;
782
853
  const i = Math.max(0, Math.min(items.length - 1, index));
783
854
  const scale = this.RenderingScale;
784
855
  const horizontal = this.Orientation === "Horizontal";
@@ -795,7 +866,7 @@ export class SkiaScroll extends SkiaControl {
795
866
  // arranged rect, like C# reads the structure cell's Destination.
796
867
  const cell = layout.ChildrenFactory.GetViewForIndex(i);
797
868
  if (!cell)
798
- return;
869
+ return undefined;
799
870
  const r = cell.DrawingRect, c = this.content.DrawingRect;
800
871
  itemStart = (horizontal ? r.Left - c.Left : r.Top - c.Top) / scale;
801
872
  itemSize = (horizontal ? r.Width : r.Height) / scale;
@@ -805,11 +876,7 @@ export class SkiaScroll extends SkiaControl {
805
876
  target -= viewport - itemSize;
806
877
  else if (option === "Center")
807
878
  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);
879
+ return { Offset: -target, Exact: layout.IsItemMeasured(i) };
813
880
  }
814
881
  StopAnimators() {
815
882
  this.flingEdgeX = null;
@@ -895,6 +962,7 @@ export class SkiaScroll extends SkiaControl {
895
962
  if (this.ProcessScrollBarGestures(args, apply))
896
963
  return this;
897
964
  if (args.Type === "Down") {
965
+ this.indexOrder = undefined; // a press drops an open ScrollToIndex
898
966
  this.hadDown = true;
899
967
  this.snapped = false;
900
968
  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
  }
@@ -80,5 +80,9 @@ export declare class SkiaSlider extends SkiaControl {
80
80
  * Home / End go to Min / Max. A ranged slider moves its End thumb, which stops at Start.
81
81
  */
82
82
  OnAccessibilityKey(key: string): boolean;
83
- protected DefaultAccessibilityLabel(): string | undefined;
83
+ /**
84
+ * 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
85
+ * the app's AccessibilityLabel only (C# used to make the value the name, drawnui-cross 6c).
86
+ */
87
+ get AccessibilityValue(): import("../core/Accessibility").AccessibilityValue;
84
88
  }
@@ -280,5 +280,11 @@ export class SkiaSlider extends SkiaControl {
280
280
  this.End = Math.min(this.Max, Math.max(low, value));
281
281
  return true;
282
282
  }
283
- DefaultAccessibilityLabel() { return this.EnableRange ? `${this.start} – ${this.end}` : `${this.end}`; }
283
+ /**
284
+ * 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
285
+ * the app's AccessibilityLabel only (C# used to make the value the name, drawnui-cross 6c).
286
+ */
287
+ get AccessibilityValue() {
288
+ 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}` : "" };
289
+ }
284
290
  }
@@ -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;
@@ -61,6 +61,11 @@ export declare abstract class SnappingLayout extends SkiaLayout {
61
61
  protected FindNearestAnchorInternal(current: SKPoint, _velocity: SKPoint): SKPoint;
62
62
  /** The closest anchor lying in the direction of the velocity (dot product), else the origin. */
63
63
  SelectNextAnchor(origin: SKPoint, velocity: SKPoint): SKPoint;
64
+ /**
65
+ * Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
66
+ * anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
67
+ */
68
+ SnapVelocityThreshold: number;
64
69
  ScrollToNearestAnchor(location: SKPoint, velocity: SKPoint): void;
65
70
  get CanAnimate(): boolean;
66
71
  protected GetAutoVelocity(displacement: SKPoint): SKPoint;
@@ -138,9 +138,17 @@ export class SnappingLayout extends SkiaLayout {
138
138
  }
139
139
  return origin;
140
140
  }
141
+ /**
142
+ * Release speed, points per second along an axis, under which the snap ignores the velocity and goes to the nearest
143
+ * anchor by distance (C# SnapVelocityThreshold, drawnui-cross 6o): a finger held still before lifting is not a flick.
144
+ */
145
+ SnapVelocityThreshold = 100;
141
146
  ScrollToNearestAnchor(location, velocity) {
142
147
  if (this.SnapPoints.length === 0)
143
148
  return;
149
+ if (Math.abs(velocity.X) < this.SnapVelocityThreshold || Math.abs(velocity.Y) < this.SnapVelocityThreshold) {
150
+ velocity = new SKPoint(Math.abs(velocity.X) < this.SnapVelocityThreshold ? 0 : velocity.X, Math.abs(velocity.Y) < this.SnapVelocityThreshold ? 0 : velocity.Y);
151
+ }
144
152
  const origin = this.FindNearestAnchorInternal(location, velocity);
145
153
  const target = this.SelectNextAnchor(origin, velocity);
146
154
  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);
@@ -16,7 +16,20 @@ export class AnimatorBase {
16
16
  StartFrameTimeNanos = 0;
17
17
  OnStart;
18
18
  OnStop;
19
+ /** Paused: the canvas does not tick it and it wakes no frames; Resume goes on from where it was (C# Pause / Resume). */
20
+ IsPaused = false;
21
+ /** Paused by the canvas because its control is hidden; resumed when it shows again. */
22
+ PausedWhileHidden = false;
23
+ /**
24
+ * Frame time (ns) before which nothing it shows changes: the canvas does not tick it and it keeps no frames coming
25
+ * until then, a timer wakes the frame loop at that time (a GIF or a sprite between two frames; DrawnUi.Rust frame
26
+ * animators sleep between changes). 0 = tick every frame.
27
+ */
28
+ SleepUntilNanos = 0;
19
29
  delayHandle = 0;
30
+ delayEnd = 0;
31
+ delayLeft = 0;
32
+ pausedAt = 0;
20
33
  constructor(parent) { this.Parent = parent; }
21
34
  Register() {
22
35
  const parent = this.Parent;
@@ -60,6 +73,8 @@ export class AnimatorBase {
60
73
  this.delayHandle = 0;
61
74
  }
62
75
  if (delayMs > 0) {
76
+ // the delay waits without frames
77
+ this.delayEnd = performance.now() + delayMs;
63
78
  this.delayHandle = window.setTimeout(() => { this.delayHandle = 0; this.Start(); }, delayMs);
64
79
  return;
65
80
  }
@@ -72,7 +87,43 @@ export class AnimatorBase {
72
87
  }
73
88
  }
74
89
  }
90
+ /** Freezes the run (and what is left of a start delay); Resume goes on from there. */
91
+ Pause() {
92
+ if (this.IsPaused)
93
+ return;
94
+ this.IsPaused = true;
95
+ this.pausedAt = performance.now();
96
+ if (this.delayHandle) {
97
+ clearTimeout(this.delayHandle);
98
+ this.delayHandle = 0;
99
+ this.delayLeft = Math.max(0, this.delayEnd - this.pausedAt);
100
+ }
101
+ }
102
+ /** Goes on from where Pause froze it: the paused time is not counted. */
103
+ Resume() {
104
+ if (!this.IsPaused)
105
+ return;
106
+ this.IsPaused = false;
107
+ const paused = (performance.now() - this.pausedAt) * 1_000_000;
108
+ if (this.StartFrameTimeNanos !== 0) {
109
+ this.StartFrameTimeNanos += paused;
110
+ this.LastFrameTimeNanos += paused;
111
+ }
112
+ if (this.SleepUntilNanos !== 0)
113
+ this.SleepUntilNanos += paused;
114
+ if (this.delayLeft > 0) {
115
+ const left = this.delayLeft;
116
+ this.delayLeft = 0;
117
+ this.Start(left);
118
+ return;
119
+ }
120
+ this.Parent?.Repaint(); // frames again
121
+ }
75
122
  Stop() {
123
+ this.IsPaused = false;
124
+ this.PausedWhileHidden = false;
125
+ this.SleepUntilNanos = 0;
126
+ this.delayLeft = 0;
76
127
  if (this.delayHandle) {
77
128
  clearTimeout(this.delayHandle);
78
129
  this.delayHandle = 0;
@@ -155,23 +206,21 @@ export class SkiaValueAnimator extends AnimatorBase {
155
206
  }
156
207
  /** Passed over mValue; subclasses derive their own reported value here. */
157
208
  TransformReportedValue(_deltaT) { return this.mValue; }
158
- /** Updates mValue from elapsed time; true when the target was reached. */
209
+ /**
210
+ * Updates mValue from elapsed time; true when the run is over. The eased value is not clamped to the range: a spring
211
+ * easing goes past its ends, and a run whose mMinValue is above mMaxValue (a ping-pong's way back) goes down.
212
+ */
159
213
  UpdateValue(_deltaT, deltaFromStart) {
160
214
  const elapsedMs = deltaFromStart / 1_000_000;
161
215
  const progress = this.Speed > 0 ? elapsedMs / this.Speed : 1;
162
216
  const deltaValue = this.mMaxValue - this.mMinValue;
163
217
  const eased = this.Easing.Ease(Math.min(progress, 1));
164
218
  this.Progress = eased;
165
- const value = deltaValue * eased + this.mMinValue;
166
- if (value < this.mMinValue) {
167
- this.mValue = this.mMinValue;
168
- return false;
169
- }
170
- if (value >= this.mMaxValue || progress >= 1) {
219
+ if (progress >= 1) {
171
220
  this.mValue = this.mMaxValue;
172
221
  return true;
173
222
  }
174
- this.mValue = value;
223
+ this.mValue = deltaValue * eased + this.mMinValue;
175
224
  return false;
176
225
  }
177
226
  FinishedRunning() {
@@ -188,6 +237,25 @@ export class SkiaValueAnimator extends AnimatorBase {
188
237
  return true;
189
238
  }
190
239
  }
240
+ /** DrawnUi PingPongAnimator: every repeat runs the other way (Repeat -1 = forever by default). */
241
+ export class PingPongAnimator extends SkiaValueAnimator {
242
+ /** One way done, the next starts (C# CycleFInished). */
243
+ CycleFinished;
244
+ constructor(parent) { super(parent); this.Repeat = -1; }
245
+ FinishedRunning() {
246
+ if (this.Repeat !== 0) {
247
+ this.CycleFinished?.();
248
+ if (this.Repeat > 0)
249
+ this.Repeat--;
250
+ [this.mMinValue, this.mMaxValue] = [this.mMaxValue, this.mMinValue];
251
+ this.mValue = this.mMinValue;
252
+ this.LastFrameTimeNanos = 0;
253
+ this.StartFrameTimeNanos = 0;
254
+ return false;
255
+ }
256
+ return super.FinishedRunning();
257
+ }
258
+ }
191
259
  /** DrawnUi RenderingAnimator: a value animator that paints itself above its Parent (IOverlayEffect). */
192
260
  export class RenderingAnimator extends SkiaValueAnimator {
193
261
  constructor(parent) { super(parent); }