drawnui-react 0.1.0-preview.15 → 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 (64) hide show
  1. package/README.md +1 -1
  2. package/SKIPPED.md +9 -8
  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/SkiaBackdrop.d.ts +2 -0
  10. package/dist/controls/SkiaBackdrop.js +2 -0
  11. package/dist/controls/SkiaButton.d.ts +2 -0
  12. package/dist/controls/SkiaButton.js +2 -0
  13. package/dist/controls/SkiaCarousel.d.ts +3 -1
  14. package/dist/controls/SkiaCarousel.js +5 -2
  15. package/dist/controls/SkiaDrawer.d.ts +2 -0
  16. package/dist/controls/SkiaDrawer.js +2 -0
  17. package/dist/controls/SkiaEditor.d.ts +5 -1
  18. package/dist/controls/SkiaEditor.js +29 -18
  19. package/dist/controls/SkiaGif.d.ts +3 -0
  20. package/dist/controls/SkiaGif.js +11 -1
  21. package/dist/controls/SkiaImage.js +15 -10
  22. package/dist/controls/SkiaLabel.d.ts +54 -3
  23. package/dist/controls/SkiaLabel.js +235 -52
  24. package/dist/controls/SkiaLayout.d.ts +20 -0
  25. package/dist/controls/SkiaLayout.js +78 -6
  26. package/dist/controls/SkiaLottie.js +3 -1
  27. package/dist/controls/SkiaProgress.d.ts +2 -1
  28. package/dist/controls/SkiaProgress.js +6 -3
  29. package/dist/controls/SkiaScroll.d.ts +16 -0
  30. package/dist/controls/SkiaScroll.js +85 -8
  31. package/dist/controls/SkiaShape.d.ts +13 -2
  32. package/dist/controls/SkiaShape.js +50 -10
  33. package/dist/controls/SkiaSlider.d.ts +7 -1
  34. package/dist/controls/SkiaSlider.js +12 -4
  35. package/dist/controls/SkiaSprite.d.ts +1 -0
  36. package/dist/controls/SkiaSprite.js +4 -0
  37. package/dist/controls/SkiaToggle.d.ts +2 -0
  38. package/dist/controls/SkiaToggle.js +3 -1
  39. package/dist/controls/SnappingLayout.d.ts +7 -0
  40. package/dist/controls/SnappingLayout.js +11 -0
  41. package/dist/core/Accessibility.d.ts +21 -0
  42. package/dist/core/Accessibility.js +66 -9
  43. package/dist/core/Animators.d.ts +28 -1
  44. package/dist/core/Animators.js +76 -8
  45. package/dist/core/Canvas.d.ts +26 -4
  46. package/dist/core/Canvas.js +136 -23
  47. package/dist/core/Easing.d.ts +12 -1
  48. package/dist/core/Easing.js +25 -1
  49. package/dist/core/Gestures.d.ts +24 -1
  50. package/dist/core/Gestures.js +75 -2
  51. package/dist/core/Hover.d.ts +47 -0
  52. package/dist/core/Hover.js +134 -0
  53. package/dist/core/KeyboardManager.d.ts +13 -0
  54. package/dist/core/KeyboardManager.js +18 -0
  55. package/dist/core/ScrollAnimators.js +9 -0
  56. package/dist/core/SkiaControl.d.ts +97 -1
  57. package/dist/core/SkiaControl.js +271 -38
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.js +1 -0
  60. package/dist/react/SkiaShell.js +33 -14
  61. package/dist/react/index.d.ts +1 -1
  62. package/dist/react/index.js +30 -3
  63. package/dist/react/reconciler.js +29 -10
  64. package/package.json +18 -1
@@ -112,7 +112,22 @@ export declare class SkiaLayout extends SkiaControl {
112
112
  get Views(): readonly SkiaControl[];
113
113
  /** Settable children list like DrawnUi Children (ignored while templated). */
114
114
  get Children(): readonly SkiaControl[];
115
+ /**
116
+ * C# Children reset (ChildrenCollectionSync, 66a1eb02): the app's children the new list no longer holds are removed,
117
+ * the ones it gained are added, then all of them take the list's order inside the slots app children occupy, so the
118
+ * parts a control made itself keep their place.
119
+ */
115
120
  set Children(value: readonly SkiaControl[]);
121
+ /**
122
+ * C# Insert / Move of a Children collection (the reconciler's appendChild / insertBefore): `child` goes before
123
+ * `before` among the app's children, or last without it. A new child is attached at the end of the views and an
124
+ * append stops there; otherwise the app's children take their new order inside the slots they occupy.
125
+ */
126
+ InsertChild(child: SkiaControl, before?: SkiaControl): void;
127
+ /** C# OrderViews: the given children, in their order, inside the slots they occupy in the views; nothing else moves. */
128
+ private OrderChildren;
129
+ /** C# ClearChildren: removes every subview, the control's own parts included. */
130
+ ClearChildren(): void;
116
131
  private orderedViews?;
117
132
  /** Static children sorted by ZIndex (stable), computed once per change (C# GetOrderedSubviews). */
118
133
  protected GetOrderedSubviews(): readonly SkiaControl[];
@@ -150,6 +165,11 @@ export declare class SkiaLayout extends SkiaControl {
150
165
  private MvScheduleBackground;
151
166
  /** Binds a cell to items[index] (through the adapter, so MeasureFirst's cell 0 stays realized) and measures it. */
152
167
  private MeasureItem;
168
+ /**
169
+ * The item's offset and height are real, not estimated (C# LastMeasuredIndexLocal >= index): MeasureVisible knows
170
+ * them once the measured prefix passed the item; the other strategies always do.
171
+ */
172
+ IsItemMeasured(index: number): boolean;
153
173
  /** Pixel offset of item index from the top of the layout content (inside padding). */
154
174
  GetItemOffsetPixels(index: number): number;
155
175
  private ItemHeightPixels;
@@ -359,11 +359,67 @@ export class SkiaLayout extends SkiaControl {
359
359
  get Views() { return this.IsTemplated ? this.ChildrenFactory.GetViewsInUse() : this.views; }
360
360
  /** Settable children list like DrawnUi Children (ignored while templated). */
361
361
  get Children() { return this.views; }
362
+ /**
363
+ * C# Children reset (ChildrenCollectionSync, 66a1eb02): the app's children the new list no longer holds are removed,
364
+ * the ones it gained are added, then all of them take the list's order inside the slots app children occupy, so the
365
+ * parts a control made itself keep their place.
366
+ */
362
367
  set Children(value) {
368
+ const keep = new Set(value);
369
+ for (const v of [...this.views])
370
+ if (v.IsChildrenItem && !keep.has(v))
371
+ this.RemoveSubView(v);
372
+ const inViews = new Set(this.views);
373
+ for (const v of value) {
374
+ v.IsChildrenItem = true;
375
+ if (!inViews.has(v))
376
+ this.AddSubView(v);
377
+ }
378
+ this.OrderChildren(value);
379
+ }
380
+ /**
381
+ * C# Insert / Move of a Children collection (the reconciler's appendChild / insertBefore): `child` goes before
382
+ * `before` among the app's children, or last without it. A new child is attached at the end of the views and an
383
+ * append stops there; otherwise the app's children take their new order inside the slots they occupy.
384
+ */
385
+ InsertChild(child, before) {
386
+ child.IsChildrenItem = true;
387
+ const attached = child.Parent === this && this.views.includes(child);
388
+ if (!attached)
389
+ this.AddSubView(child);
390
+ if (!attached && !before)
391
+ return;
392
+ const order = this.views.filter((v) => v.IsChildrenItem && v !== child);
393
+ const at = before ? order.indexOf(before) : -1;
394
+ order.splice(at < 0 ? order.length : at, 0, child);
395
+ this.OrderChildren(order);
396
+ }
397
+ /** C# OrderViews: the given children, in their order, inside the slots they occupy in the views; nothing else moves. */
398
+ OrderChildren(ordered) {
399
+ const inViews = new Set(this.views);
400
+ const present = ordered.filter((v) => inViews.delete(v));
401
+ if (present.length < 2)
402
+ return;
403
+ const isChild = new Set(present);
404
+ let next = 0, changed = false;
405
+ for (let i = 0; i < this.views.length && next < present.length; i++) {
406
+ if (!isChild.has(this.views[i]))
407
+ continue;
408
+ if (this.views[i] !== present[next]) {
409
+ this.views[i] = present[next];
410
+ changed = true;
411
+ }
412
+ next++;
413
+ }
414
+ if (changed) {
415
+ this.orderedViews = undefined;
416
+ this.InvalidateMeasure();
417
+ }
418
+ }
419
+ /** C# ClearChildren: removes every subview, the control's own parts included. */
420
+ ClearChildren() {
363
421
  for (const v of [...this.views])
364
422
  this.RemoveSubView(v);
365
- for (const v of value)
366
- this.AddSubView(v);
367
423
  }
368
424
  orderedViews;
369
425
  /** Static children sorted by ZIndex (stable), computed once per change (C# GetOrderedSubviews). */
@@ -390,10 +446,18 @@ export class SkiaLayout extends SkiaControl {
390
446
  }
391
447
  AddSubView(control) { this.InsertSubView(this.views.length, control); }
392
448
  InsertSubView(index, control) {
449
+ // already a child: a move (React reorders keyed children by inserting an existing one elsewhere), never a 2nd copy
450
+ const at = control.Parent === this ? this.views.indexOf(control) : -1;
451
+ if (at >= 0) {
452
+ this.views.splice(at, 1);
453
+ if (at < index)
454
+ index--;
455
+ }
393
456
  control.Parent = this;
394
- this.views.splice(index, 0, control);
457
+ this.views.splice(Math.min(Math.max(0, index), this.views.length), 0, control);
395
458
  this.orderedViews = undefined;
396
459
  this.InvalidateMeasure();
460
+ this.Superview?.Hover?.RequestCheck(); // a popup / page under a still mouse
397
461
  }
398
462
  RemoveSubView(control) {
399
463
  const i = this.views.indexOf(control);
@@ -403,6 +467,7 @@ export class SkiaLayout extends SkiaControl {
403
467
  this.orderedViews = undefined;
404
468
  control.Parent = undefined;
405
469
  this.InvalidateMeasure();
470
+ this.Superview?.Hover?.RequestCheck();
406
471
  }
407
472
  // ---- measure ----
408
473
  MeasureAbsolute(widthConstraint, heightConstraint, scale) {
@@ -666,6 +731,11 @@ export class SkiaLayout extends SkiaControl {
666
731
  this.ChildrenFactory.ReleaseViewAt(index);
667
732
  return h;
668
733
  }
734
+ /**
735
+ * The item's offset and height are real, not estimated (C# LastMeasuredIndexLocal >= index): MeasureVisible knows
736
+ * them once the measured prefix passed the item; the other strategies always do.
737
+ */
738
+ IsItemMeasured(index) { return this.MeasureItemsStrategy !== "MeasureVisible" || index < this.mvMeasured; }
669
739
  /** Pixel offset of item index from the top of the layout content (inside padding). */
670
740
  GetItemOffsetPixels(index) {
671
741
  const scale = this.RenderingScale;
@@ -758,9 +828,11 @@ export class SkiaLayout extends SkiaControl {
758
828
  return;
759
829
  }
760
830
  const composing = this.IsRenderingWithComposition;
761
- for (const v of this.GetOrderedSubviews())
762
- if (!composing || this.DirtyChildrenInternal.has(v))
763
- v.Render(ctx);
831
+ for (const v of this.GetOrderedSubviews()) {
832
+ if (composing && !this.DirtyChildrenInternal.has(v))
833
+ continue;
834
+ v.Render(ctx); // a child entirely outside the clip skips itself
835
+ }
764
836
  }
765
837
  GetCompositeChildren() { return this.IsTemplatedList || this.IsRecycledLayout ? [] : this.GetOrderedSubviews(); }
766
838
  drawnSlots = new Set();
@@ -32,7 +32,9 @@ export class SkiaLottie extends AnimatedFramesRenderer {
32
32
  Animation;
33
33
  constructor() {
34
34
  super();
35
- this.UseCache = "ImageDoubleBuffered";
35
+ // C# ImageDoubleBuffered pays off where a thread bakes the bitmap; on the web it is one offscreen pass per frame
36
+ // while playing, so the web engines default to Operations (drawnui-cross rule 3)
37
+ this.UseCache = "Operations";
36
38
  }
37
39
  get Source() { return this.source; }
38
40
  set Source(v) { if (this.source !== v) {
@@ -36,5 +36,6 @@ export declare class SkiaProgress extends SkiaControl {
36
36
  get Ratio(): number;
37
37
  protected MeasureAbsolute(w: number, _h: number, scale: number): ScaledSize;
38
38
  protected Paint(ctx: DrawingContext): void;
39
- protected DefaultAccessibilityLabel(): string | undefined;
39
+ /** Value between Min and Max, read as "65%"; the name is the app's AccessibilityLabel only (drawnui-cross 6c). */
40
+ get AccessibilityValue(): import("../core/Accessibility").AccessibilityValue;
40
41
  }
@@ -33,9 +33,9 @@ export class SkiaProgress extends SkiaControl {
33
33
  set Max(v) { this.max = v; this.Update(); }
34
34
  /** Unset = per-style palette. */
35
35
  get TrackColor() { return this.trackColor ?? SkiaProgress.Palette(this.UsingControlStyle).track; }
36
- set TrackColor(v) { this.trackColor = v; this.Update(); }
36
+ set TrackColor(v) { this.trackColor = v; this.UpdateDraw(); }
37
37
  get ProgressColor() { return this.progressColor ?? SkiaProgress.Palette(this.UsingControlStyle).progress; }
38
- set ProgressColor(v) { this.progressColor = v; this.Update(); }
38
+ set ProgressColor(v) { this.progressColor = v; this.UpdateDraw(); }
39
39
  /** C# ResolvedTrackColor / ResolvedProgressColor / track heights per style. */
40
40
  static Palette(s) {
41
41
  switch (s) {
@@ -76,5 +76,8 @@ export class SkiaProgress extends SkiaControl {
76
76
  }
77
77
  paint.delete();
78
78
  }
79
- DefaultAccessibilityLabel() { return `${Math.round(this.Ratio * 100)}%`; }
79
+ /** Value between Min and Max, read as "65%"; the name is the app's AccessibilityLabel only (drawnui-cross 6c). */
80
+ get AccessibilityValue() {
81
+ return { Now: this.value, Min: this.min, Max: this.max, Step: 0, Text: `${Math.round(this.Ratio * 100)}%` };
82
+ }
80
83
  }
@@ -156,6 +156,10 @@ export declare class SkiaScroll extends SkiaControl {
156
156
  set ViewportOffsetX(value: number);
157
157
  get ViewportOffsetY(): number;
158
158
  set ViewportOffsetY(value: number);
159
+ /** An offset set without a glide or a pan (a jump, a touchpad step): the controls under a still mouse changed. */
160
+ private OffsetJumped;
161
+ /** Hover waits while the content glides, flings, bounces or follows a pan (C# IsScrolling). */
162
+ MovesContent(): boolean;
159
163
  private readonly animatorFlingX;
160
164
  private readonly animatorFlingY;
161
165
  private readonly bounceX;
@@ -214,6 +218,18 @@ export declare class SkiaScroll extends SkiaControl {
214
218
  * templated layout (the recycled list is the scroll's only child; a header goes above the scroll or in Header).
215
219
  */
216
220
  ScrollToIndex(index: number, animate: boolean, option?: RelativePositionType): void;
221
+ /**
222
+ * An open ScrollToIndex (C# OrderedScrollToIndex, DrawnUi.Rust scroll_to_index): a row not measured yet is gone to by
223
+ * the sizes as they are estimated, and the order is aimed again every frame until the row has its real place and the
224
+ * viewport stands on it; an animated move goes on to a moved target in the time it has left, later corrections jump.
225
+ * A press, the wheel or another scroll drops it; while it is open LoadMore waits.
226
+ */
227
+ private indexOrder?;
228
+ private pursuing;
229
+ /** One step of the open order (C# ExecuteScrollToIndexOrder, Rust `pursue`), at the start of every paint. */
230
+ private PursueIndexOrder;
231
+ /** The offset (points) that puts item `index` where `option` says, by the sizes the list knows now; Exact = its place is measured. */
232
+ private AimIndex;
217
233
  private StopAnimators;
218
234
  /** DrawnUi DecelerationRatio: FrictionScrolled / 100, floored at 0.1 friction. */
219
235
  private get DecelerationRatio();
@@ -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
@@ -399,12 +401,19 @@ export class SkiaScroll extends SkiaControl {
399
401
  set ViewportOffsetX(value) { if (this.offsetX !== value) {
400
402
  this.offsetX = value;
401
403
  this.OnScrolled();
404
+ this.OffsetJumped();
402
405
  } }
403
406
  get ViewportOffsetY() { return this.offsetY; }
404
407
  set ViewportOffsetY(value) { if (this.offsetY !== value) {
405
408
  this.offsetY = value;
406
409
  this.OnScrolled();
410
+ this.OffsetJumped();
407
411
  } }
412
+ /** An offset set without a glide or a pan (a jump, a touchpad step): the controls under a still mouse changed. */
413
+ OffsetJumped() { if (!this.MovesContent())
414
+ this.Superview?.Hover?.RequestCheck(); }
415
+ /** Hover waits while the content glides, flings, bounces or follows a pan (C# IsScrolling). */
416
+ MovesContent() { return this.IsScrolling || this.IsUserPanning || !!this.draggedScrollBar; }
408
417
  animatorFlingX = new ScrollFlingAnimator(this);
409
418
  animatorFlingY = new ScrollFlingAnimator(this);
410
419
  bounceX = new SpringWithVelocityAnimator(this);
@@ -506,6 +515,7 @@ export class SkiaScroll extends SkiaControl {
506
515
  }
507
516
  InsertSubView(_index, control) { this.AddSubView(control); }
508
517
  RemoveSubView(control) {
518
+ this.Superview?.Hover?.RequestCheck();
509
519
  if (this.content === control)
510
520
  this.Content = undefined;
511
521
  else if (this.header === control)
@@ -626,6 +636,8 @@ export class SkiaScroll extends SkiaControl {
626
636
  }
627
637
  }
628
638
  Paint(ctx) {
639
+ this.Superview?.Hover?.RegisterMover(this);
640
+ this.PursueIndexOrder();
629
641
  const c = this.content;
630
642
  if (!c)
631
643
  return;
@@ -728,6 +740,8 @@ export class SkiaScroll extends SkiaControl {
728
740
  this.ViewportOffsetY = target;
729
741
  }
730
742
  ScrollTo(x, y, maxSpeedSecs, clamp = true) {
743
+ if (!this.pursuing)
744
+ this.indexOrder = undefined; // another scroll drops an open ScrollToIndex
731
745
  this.StopAnimators(); // also forgets any pending edge bounce: a programmatic scroll never bounces
732
746
  let tx = x, ty = y;
733
747
  if (clamp) {
@@ -760,16 +774,82 @@ export class SkiaScroll extends SkiaControl {
760
774
  else
761
775
  this.ScrollTo(this.offsetX, this.ContentOffsetBounds.Top, maxTimeSecs);
762
776
  }
763
- StopScrolling() { this.StopAnimators(); this.IsUserPanning = false; }
777
+ StopScrolling() { this.indexOrder = undefined; this.StopAnimators(); this.IsUserPanning = false; }
764
778
  /**
765
779
  * Scrolls so that item `index` is at the viewport start (or end). Like DrawnUi, Content must BE the
766
780
  * templated layout (the recycled list is the scroll's only child; a header goes above the scroll or in Header).
767
781
  */
768
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) {
769
849
  const layout = this.content instanceof SkiaLayout && this.content.IsTemplated ? this.content : undefined;
770
850
  const items = layout?.ItemsSource;
771
851
  if (!layout || !items || items.length === 0 || !this.content)
772
- return;
852
+ return undefined;
773
853
  const i = Math.max(0, Math.min(items.length - 1, index));
774
854
  const scale = this.RenderingScale;
775
855
  const horizontal = this.Orientation === "Horizontal";
@@ -786,7 +866,7 @@ export class SkiaScroll extends SkiaControl {
786
866
  // arranged rect, like C# reads the structure cell's Destination.
787
867
  const cell = layout.ChildrenFactory.GetViewForIndex(i);
788
868
  if (!cell)
789
- return;
869
+ return undefined;
790
870
  const r = cell.DrawingRect, c = this.content.DrawingRect;
791
871
  itemStart = (horizontal ? r.Left - c.Left : r.Top - c.Top) / scale;
792
872
  itemSize = (horizontal ? r.Width : r.Height) / scale;
@@ -796,11 +876,7 @@ export class SkiaScroll extends SkiaControl {
796
876
  target -= viewport - itemSize;
797
877
  else if (option === "Center")
798
878
  target -= (viewport - itemSize) / 2;
799
- const time = animate ? this.ScrollingSpeedMs / 1000 : 0;
800
- if (horizontal)
801
- this.ScrollTo(-target, this.offsetY, time, true);
802
- else
803
- this.ScrollTo(this.offsetX, -target, time, true);
879
+ return { Offset: -target, Exact: layout.IsItemMeasured(i) };
804
880
  }
805
881
  StopAnimators() {
806
882
  this.flingEdgeX = null;
@@ -886,6 +962,7 @@ export class SkiaScroll extends SkiaControl {
886
962
  if (this.ProcessScrollBarGestures(args, apply))
887
963
  return this;
888
964
  if (args.Type === "Down") {
965
+ this.indexOrder = undefined; // a press drops an open ScrollToIndex
889
966
  this.hadDown = true;
890
967
  this.snapped = false;
891
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
  }
@@ -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;
@@ -78,5 +80,9 @@ export declare class SkiaSlider extends SkiaControl {
78
80
  * Home / End go to Min / Max. A ranged slider moves its End thumb, which stops at Start.
79
81
  */
80
82
  OnAccessibilityKey(key: string): boolean;
81
- 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;
82
88
  }
@@ -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;
@@ -60,11 +62,11 @@ export class SkiaSlider extends SkiaControl {
60
62
  get SliderHeight() { return this.sliderHeight ?? this.Look().thumb + (this.EnableRange && this.UsingControlStyle !== "Unset" ? 8 : 0); }
61
63
  set SliderHeight(v) { this.sliderHeight = v; this.Update(); }
62
64
  get ThumbColor() { return this.thumbColor ?? this.Look().thumbColor; }
63
- set ThumbColor(v) { this.thumbColor = v; this.Update(); }
65
+ set ThumbColor(v) { this.thumbColor = v; this.UpdateDraw(); }
64
66
  get TrackColor() { return this.trackColor ?? this.Look().track; }
65
- set TrackColor(v) { this.trackColor = v; this.Update(); }
67
+ set TrackColor(v) { this.trackColor = v; this.UpdateDraw(); }
66
68
  get TrackSelectedColor() { return this.trackSelectedColor ?? this.Look().selected; }
67
- set TrackSelectedColor(v) { this.trackSelectedColor = v; this.Update(); }
69
+ set TrackSelectedColor(v) { this.trackSelectedColor = v; this.UpdateDraw(); }
68
70
  /** C# style builders: thumb diameter, track height, palette, thumb shadow. */
69
71
  Look() {
70
72
  switch (this.UsingControlStyle) {
@@ -278,5 +280,11 @@ export class SkiaSlider extends SkiaControl {
278
280
  this.End = Math.min(this.Max, Math.max(low, value));
279
281
  return true;
280
282
  }
281
- 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
+ }
282
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;
@@ -10,6 +10,8 @@ import { SkiaLayout } from "./SkiaLayout";
10
10
  * take the style defaults (C# SetStyleDefault).
11
11
  */
12
12
  export declare abstract class SkiaToggle extends SkiaLayout {
13
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault): switch, checkbox, radio button. */
14
+ protected ReceivesHoverByDefault(): boolean;
13
15
  private controlStyle;
14
16
  get ControlStyle(): PrebuiltControlStyle;
15
17
  set ControlStyle(v: PrebuiltControlStyle);
@@ -8,6 +8,8 @@ import { SkiaLayout } from "./SkiaLayout";
8
8
  * take the style defaults (C# SetStyleDefault).
9
9
  */
10
10
  export class SkiaToggle extends SkiaLayout {
11
+ /** Takes mouse hover unless ReceivesHover is set false (C# ReceivesHoverByDefault): switch, checkbox, radio button. */
12
+ ReceivesHoverByDefault() { return true; }
11
13
  controlStyle = "Unset";
12
14
  get ControlStyle() { return this.controlStyle; }
13
15
  set ControlStyle(v) {
@@ -83,7 +85,7 @@ export class SkiaToggle extends SkiaLayout {
83
85
  RebuildDefaultContent() {
84
86
  if (!this.contentCreated)
85
87
  return;
86
- this.Children = [];
88
+ this.ClearChildren();
87
89
  for (const p of this.stylePinned)
88
90
  this[p] = -1;
89
91
  this.stylePinned = [];