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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +17 -4
  2. package/SKIPPED.md +33 -27
  3. package/dist/controls/AnimatedFramesRenderer.d.ts +7 -0
  4. package/dist/controls/AnimatedFramesRenderer.js +16 -1
  5. package/dist/controls/DrawnGame.d.ts +40 -0
  6. package/dist/controls/DrawnGame.js +64 -0
  7. package/dist/controls/GridStructure.d.ts +16 -2
  8. package/dist/controls/GridStructure.js +40 -4
  9. package/dist/controls/SkiaBackdrop.d.ts +8 -2
  10. package/dist/controls/SkiaBackdrop.js +48 -8
  11. package/dist/controls/SkiaButton.d.ts +19 -2
  12. package/dist/controls/SkiaButton.js +41 -11
  13. package/dist/controls/SkiaCarousel.d.ts +9 -1
  14. package/dist/controls/SkiaCarousel.js +16 -3
  15. package/dist/controls/SkiaCheckbox.d.ts +0 -1
  16. package/dist/controls/SkiaCheckbox.js +0 -6
  17. package/dist/controls/SkiaDrawer.d.ts +2 -0
  18. package/dist/controls/SkiaDrawer.js +2 -0
  19. package/dist/controls/SkiaEditor.d.ts +12 -1
  20. package/dist/controls/SkiaEditor.js +46 -19
  21. package/dist/controls/SkiaGif.d.ts +3 -0
  22. package/dist/controls/SkiaGif.js +12 -2
  23. package/dist/controls/SkiaImage.js +15 -10
  24. package/dist/controls/SkiaLabel.d.ts +54 -3
  25. package/dist/controls/SkiaLabel.js +235 -52
  26. package/dist/controls/SkiaLabelFps.d.ts +16 -0
  27. package/dist/controls/SkiaLabelFps.js +41 -0
  28. package/dist/controls/SkiaLayout.d.ts +64 -7
  29. package/dist/controls/SkiaLayout.js +316 -19
  30. package/dist/controls/SkiaLottie.js +4 -2
  31. package/dist/controls/SkiaProgress.d.ts +2 -1
  32. package/dist/controls/SkiaProgress.js +6 -3
  33. package/dist/controls/SkiaRadioButton.js +4 -3
  34. package/dist/controls/SkiaScroll.d.ts +35 -5
  35. package/dist/controls/SkiaScroll.js +233 -23
  36. package/dist/controls/SkiaScrollBar.d.ts +27 -0
  37. package/dist/controls/SkiaScrollBar.js +51 -2
  38. package/dist/controls/SkiaShaderCarousel.d.ts +5 -0
  39. package/dist/controls/SkiaShaderCarousel.js +5 -0
  40. package/dist/controls/SkiaShape.d.ts +13 -2
  41. package/dist/controls/SkiaShape.js +50 -10
  42. package/dist/controls/SkiaSlider.d.ts +20 -2
  43. package/dist/controls/SkiaSlider.js +59 -5
  44. package/dist/controls/SkiaSprite.d.ts +1 -0
  45. package/dist/controls/SkiaSprite.js +4 -0
  46. package/dist/controls/SkiaSvg.js +1 -1
  47. package/dist/controls/SkiaSwitch.d.ts +0 -1
  48. package/dist/controls/SkiaSwitch.js +0 -6
  49. package/dist/controls/SkiaToggle.d.ts +25 -5
  50. package/dist/controls/SkiaToggle.js +58 -5
  51. package/dist/controls/SnappingLayout.d.ts +12 -1
  52. package/dist/controls/SnappingLayout.js +19 -1
  53. package/dist/core/Accessibility.d.ts +80 -0
  54. package/dist/core/Accessibility.js +365 -8
  55. package/dist/core/Animators.d.ts +37 -1
  56. package/dist/core/Animators.js +102 -9
  57. package/dist/core/Canvas.d.ts +108 -7
  58. package/dist/core/Canvas.js +363 -35
  59. package/dist/core/Easing.d.ts +12 -1
  60. package/dist/core/Easing.js +25 -1
  61. package/dist/core/Gestures.d.ts +48 -2
  62. package/dist/core/Gestures.js +95 -3
  63. package/dist/core/Hover.d.ts +47 -0
  64. package/dist/core/Hover.js +134 -0
  65. package/dist/core/KeyboardManager.d.ts +13 -0
  66. package/dist/core/KeyboardManager.js +18 -0
  67. package/dist/core/ScrollAnimators.js +9 -0
  68. package/dist/core/SkiaControl.d.ts +216 -7
  69. package/dist/core/SkiaControl.js +516 -59
  70. package/dist/core/SkiaEffect.d.ts +17 -0
  71. package/dist/core/SkiaEffect.js +43 -8
  72. package/dist/core/SkiaImageManager.js +1 -1
  73. package/dist/core/Styles.d.ts +51 -0
  74. package/dist/core/Styles.js +92 -0
  75. package/dist/core/Super.d.ts +17 -1
  76. package/dist/core/Super.js +35 -4
  77. package/dist/core/ViewsAdapter.d.ts +7 -2
  78. package/dist/core/ViewsAdapter.js +10 -3
  79. package/dist/index.d.ts +4 -0
  80. package/dist/index.js +4 -0
  81. package/dist/react/SkiaShell.js +33 -14
  82. package/dist/react/index.d.ts +15 -4
  83. package/dist/react/index.js +102 -15
  84. package/dist/react/reconciler.js +34 -12
  85. package/dist/vite/index.d.ts +9 -0
  86. package/dist/vite/index.js +36 -3
  87. package/package.json +33 -3
@@ -6,9 +6,11 @@ import { SkiaGridStructure } from "./GridStructure";
6
6
  * Mirrors DrawnUi SkiaLayout (Absolute / Column / Row / Wrap / Grid).
7
7
  * Column/Row give children an infinite main axis (MAUI stack semantics: Fill on the main axis = auto-sized).
8
8
  *
9
- * Templated mode (ItemsSource + ItemTemplate) is a Column only: cells are created through the ViewsAdapter for
10
- * the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
11
- * MeasureFirst measures one cell and assumes uniform size, MeasureAll measures every item once.
9
+ * Templated mode (ItemsSource + ItemTemplate): a single-column Column is the virtualized list, cells are created through
10
+ * the ViewsAdapter for the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
11
+ * MeasureAll (default) measures every item once, MeasureFirst measures one cell and assumes uniform size. A templated
12
+ * Row / Wrap / Grid / split Column follows RecyclingTemplate: Enabled measures every item on pooled views and binds views
13
+ * only to the slots drawn, Disabled keeps one view per item.
12
14
  */
13
15
  export declare class SkiaLayout extends SkiaControl {
14
16
  /** Layout type. SkiaShape redeclares it as ShapeType (any shape value lays out as Absolute), like the C# hidden Type. */
@@ -24,7 +26,10 @@ export declare class SkiaLayout extends SkiaControl {
24
26
  RowSpacing: number;
25
27
  /** Structure computed by the last measure; cells are arranged from it. */
26
28
  GridStructure?: SkiaGridStructure;
27
- RecyclingTemplate: RecyclingTemplate;
29
+ private recyclingTemplate;
30
+ /** Enabled: views come from a pool and are bound to the items being drawn; Disabled: one view per item, never rebound. */
31
+ get RecyclingTemplate(): RecyclingTemplate;
32
+ set RecyclingTemplate(value: RecyclingTemplate);
28
33
  /** Column count for Column / Wrap content and a templated Grid (0 = free flow); C# Split. */
29
34
  Split: number;
30
35
  /** Split > 0: every slot keeps the same width (true) or the cell keeps its measured width (C# SplitAlign). */
@@ -33,6 +38,7 @@ export declare class SkiaLayout extends SkiaControl {
33
38
  DynamicColumns: boolean;
34
39
  /** Templated Grid: fill column-major (top to bottom, then next column) instead of row-major (C# Invert). */
35
40
  Invert: boolean;
41
+ /** Default MeasureAll (DrawnUi 1.10.6.20): every item keeps its own size; MeasureFirst is explicit uniform rows. */
36
42
  MeasureItemsStrategy: MeasuringStrategy;
37
43
  /** Realized views per item (Disabled) or a recycled pool for the visible range (Enabled). */
38
44
  readonly ChildrenFactory: ViewsAdapter;
@@ -41,7 +47,7 @@ export declare class SkiaLayout extends SkiaControl {
41
47
  private itemsSource?;
42
48
  private itemTemplate?;
43
49
  private structureDirty;
44
- /** Per-item heights in pixels (MeasureAll) or a single uniform height (MeasureFirst). */
50
+ /** Per-item heights in pixels (MeasureAll, the default) or a single uniform height (MeasureFirst). */
45
51
  private itemHeights;
46
52
  private uniformHeight;
47
53
  private measuredWidthPx;
@@ -70,13 +76,30 @@ export declare class SkiaLayout extends SkiaControl {
70
76
  * anything else rebuilds. Returns false when a full rebuild is needed.
71
77
  */
72
78
  private ApplyIncrementalChange;
79
+ /**
80
+ * Same items in another order — a drag-to-reorder, or any code writing a permuted array. DrawnUi handles the
81
+ * matching `ObservableCollection.Move` by keeping the structure and only rebinding the contexts, because its
82
+ * arrange re-flows each row from its bound view (`HandleStructurePreservingMove` / `ApplyMoveChange`, 2026-09-08).
83
+ * The measured heights here are index-keyed and authoritative, so they travel with their items instead: nothing is
84
+ * remeasured, the extent cannot change, and the scroll keeps its offset. Rebuilding instead would reset
85
+ * `MeasureVisible` to "exact for the first screen, estimated after" and throw a scrolled list back towards the top.
86
+ * Returns false when the two arrays are not a permutation of each other, so a real content change still rebuilds.
87
+ */
88
+ private ApplyReorderChange;
73
89
  /** Factory creating one cell (DrawnUi DataTemplate). Cells receive the item as BindingContext. */
74
90
  get ItemTemplate(): (() => SkiaControl) | undefined;
75
91
  set ItemTemplate(value: (() => SkiaControl) | undefined);
76
92
  get IsTemplated(): boolean;
77
- /** The virtualized list case: a templated single-column Column. Templated Row / Wrap / Grid realize every item. */
93
+ /** The virtualized list case: a templated single-column Column. */
78
94
  private get IsTemplatedList();
79
- /** Views taking part in the layout pass: static children, or every item of a templated Row / Wrap / Grid (C# non-list layouts are not virtualized). */
95
+ /** A templated Row / Wrap / Grid / split Column with RecyclingTemplate Enabled: slots measured on pooled views, views bound to the slots drawn. */
96
+ private get IsRecycledLayout();
97
+ /** Slots of a recycled layout, one per item. */
98
+ private slots?;
99
+ /**
100
+ * What the layout pass measures and arranges: static children; the slots of a recycled templated layout; or, with
101
+ * RecyclingTemplate Disabled, one realized view per item.
102
+ */
80
103
  private LayoutViews;
81
104
  /** Drops realized cells and rebuilds the structure (DrawnUi ApplyItemsSource). */
82
105
  ApplyItemsSource(): void;
@@ -89,7 +112,22 @@ export declare class SkiaLayout extends SkiaControl {
89
112
  get Views(): readonly SkiaControl[];
90
113
  /** Settable children list like DrawnUi Children (ignored while templated). */
91
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
+ */
92
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;
93
131
  private orderedViews?;
94
132
  /** Static children sorted by ZIndex (stable), computed once per change (C# GetOrderedSubviews). */
95
133
  protected GetOrderedSubviews(): readonly SkiaControl[];
@@ -127,12 +165,31 @@ export declare class SkiaLayout extends SkiaControl {
127
165
  private MvScheduleBackground;
128
166
  /** Binds a cell to items[index] (through the adapter, so MeasureFirst's cell 0 stays realized) and measures it. */
129
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;
130
173
  /** Pixel offset of item index from the top of the layout content (inside padding). */
131
174
  GetItemOffsetPixels(index: number): number;
132
175
  private ItemHeightPixels;
133
176
  protected OnLayoutChanged(): void;
177
+ /** Static children and realized views are arranged; the slots of a recycled layout record where their item goes. */
178
+ private ArrangeChildren;
134
179
  protected Paint(ctx: DrawingContext): void;
135
180
  protected GetCompositeChildren(): readonly SkiaControl[];
181
+ private readonly drawnSlots;
182
+ /**
183
+ * Recycled templated layout (C# DrawStack): a view from the pool is bound to each slot that can be seen and drawn
184
+ * there; the views of the other slots go back to the pool.
185
+ */
186
+ private PaintSlots;
187
+ /**
188
+ * Where slots are drawn (C# GetOnScreenVisibleArea): what can be seen, inflated by VirtualisationInflated. Under a
189
+ * cache (this layout's or an ancestor's, below the nearest scroll) every slot, because the cache is blitted later at
190
+ * other offsets.
191
+ */
192
+ private SlotsArea;
136
193
  /** Realizes, binds, arranges and draws only the cells intersecting the visible viewport (+ inflation). */
137
194
  private PaintTemplated;
138
195
  /**
@@ -2,13 +2,81 @@ import { SkiaControl } from "../core/SkiaControl";
2
2
  import { ViewsAdapter } from "../core/ViewsAdapter";
3
3
  import { SKRect, ScaledSize, Thickness } from "../core/Types";
4
4
  import { SkiaGridStructure } from "./GridStructure";
5
+ /**
6
+ * The place of one item in a recycled templated Row / Wrap / Grid / split Column. C# measures the structure on one
7
+ * template instance bound to each item in turn: Measure here binds a pooled view to the item, keeps its size, the
8
+ * constraints and what the template decided for that item, then gives the view back unless the item is on screen.
9
+ * The same item measured again with the same constraints keeps its size without a rebind (as the list keeps its item
10
+ * heights: a new ItemsSource array is how content changes). Arrange only records the slot; the drawing pass binds a
11
+ * view to each slot it draws.
12
+ */
13
+ class TemplatedSlot {
14
+ Index;
15
+ factory;
16
+ IsVisible = true;
17
+ Row = 0;
18
+ Column = 0;
19
+ RowSpan = 1;
20
+ ColumnSpan = 1;
21
+ HorizontalOptions = "Start";
22
+ VerticalOptions = "Start";
23
+ Margin = Thickness.Zero;
24
+ WidthRequest = -1;
25
+ HeightRequest = -1;
26
+ MinimumWidthRequest = -1;
27
+ MinimumHeightRequest = -1;
28
+ MeasuredSize = ScaledSize.Default;
29
+ WidthConstraint = Infinity;
30
+ HeightConstraint = Infinity;
31
+ /** The rect the layout arranged this slot in (pixels). */
32
+ Destination = SKRect.Empty;
33
+ /** The view last arranged in this slot and the Destination it got, so an unchanged view is not arranged twice. */
34
+ ArrangedView;
35
+ ArrangedFor;
36
+ measuredItem;
37
+ measuredScale = 0;
38
+ constructor(Index, factory) {
39
+ this.Index = Index;
40
+ this.factory = factory;
41
+ }
42
+ Measure(widthConstraint, heightConstraint, scale) {
43
+ const shown = this.factory.GetViewForIndex(this.Index), item = this.factory.Items[this.Index];
44
+ if (!shown && item === this.measuredItem && scale === this.measuredScale
45
+ && Object.is(widthConstraint, this.WidthConstraint) && Object.is(heightConstraint, this.HeightConstraint))
46
+ return this.MeasuredSize;
47
+ const view = shown ?? this.factory.GetOrCreateViewForIndex(this.Index, true);
48
+ if (!view)
49
+ return (this.MeasuredSize = ScaledSize.Default);
50
+ this.MeasuredSize = view.Measure(widthConstraint, heightConstraint, scale);
51
+ this.measuredItem = item;
52
+ this.measuredScale = scale;
53
+ this.WidthConstraint = widthConstraint;
54
+ this.HeightConstraint = heightConstraint;
55
+ this.IsVisible = view.IsVisible;
56
+ this.RowSpan = view.RowSpan;
57
+ this.ColumnSpan = view.ColumnSpan;
58
+ this.HorizontalOptions = view.HorizontalOptions;
59
+ this.VerticalOptions = view.VerticalOptions;
60
+ this.Margin = view.Margin;
61
+ this.WidthRequest = view.WidthRequest;
62
+ this.HeightRequest = view.HeightRequest;
63
+ this.MinimumWidthRequest = view.MinimumWidthRequest;
64
+ this.MinimumHeightRequest = view.MinimumHeightRequest;
65
+ if (!shown)
66
+ this.factory.ReleaseViewAt(this.Index);
67
+ return this.MeasuredSize;
68
+ }
69
+ Arrange(destination, _widthRequest, _heightRequest, _scale) { this.Destination = destination; }
70
+ }
5
71
  /**
6
72
  * Mirrors DrawnUi SkiaLayout (Absolute / Column / Row / Wrap / Grid).
7
73
  * Column/Row give children an infinite main axis (MAUI stack semantics: Fill on the main axis = auto-sized).
8
74
  *
9
- * Templated mode (ItemsSource + ItemTemplate) is a Column only: cells are created through the ViewsAdapter for
10
- * the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
11
- * MeasureFirst measures one cell and assumes uniform size, MeasureAll measures every item once.
75
+ * Templated mode (ItemsSource + ItemTemplate): a single-column Column is the virtualized list, cells are created through
76
+ * the ViewsAdapter for the indexes inside the visible viewport (+ VirtualisationInflated), everything else is arithmetic —
77
+ * MeasureAll (default) measures every item once, MeasureFirst measures one cell and assumes uniform size. A templated
78
+ * Row / Wrap / Grid / split Column follows RecyclingTemplate: Enabled measures every item on pooled views and binds views
79
+ * only to the slots drawn, Disabled keeps one view per item.
12
80
  */
13
81
  export class SkiaLayout extends SkiaControl {
14
82
  /** Layout type. SkiaShape redeclares it as ShapeType (any shape value lays out as Absolute), like the C# hidden Type. */
@@ -26,7 +94,16 @@ export class SkiaLayout extends SkiaControl {
26
94
  /** Structure computed by the last measure; cells are arranged from it. */
27
95
  GridStructure;
28
96
  // ---- templated children (same names as DrawnUi) ----
29
- RecyclingTemplate = "Enabled";
97
+ recyclingTemplate = "Enabled";
98
+ /** Enabled: views come from a pool and are bound to the items being drawn; Disabled: one view per item, never rebound. */
99
+ get RecyclingTemplate() { return this.recyclingTemplate; }
100
+ set RecyclingTemplate(value) {
101
+ if (this.recyclingTemplate === value)
102
+ return;
103
+ this.recyclingTemplate = value;
104
+ if (this.itemTemplate)
105
+ this.ApplyItemsSource(); // the pool follows the mode
106
+ }
30
107
  /** Column count for Column / Wrap content and a templated Grid (0 = free flow); C# Split. */
31
108
  Split = 0;
32
109
  /** Split > 0: every slot keeps the same width (true) or the cell keeps its measured width (C# SplitAlign). */
@@ -35,7 +112,8 @@ export class SkiaLayout extends SkiaControl {
35
112
  DynamicColumns = false;
36
113
  /** Templated Grid: fill column-major (top to bottom, then next column) instead of row-major (C# Invert). */
37
114
  Invert = false;
38
- MeasureItemsStrategy = "MeasureFirst";
115
+ /** Default MeasureAll (DrawnUi 1.10.6.20): every item keeps its own size; MeasureFirst is explicit uniform rows. */
116
+ MeasureItemsStrategy = "MeasureAll";
39
117
  /** Realized views per item (Disabled) or a recycled pool for the visible range (Enabled). */
40
118
  ChildrenFactory = new ViewsAdapter(this);
41
119
  FirstVisibleIndex = -1;
@@ -43,7 +121,7 @@ export class SkiaLayout extends SkiaControl {
43
121
  itemsSource;
44
122
  itemTemplate;
45
123
  structureDirty = true;
46
- /** Per-item heights in pixels (MeasureAll) or a single uniform height (MeasureFirst). */
124
+ /** Per-item heights in pixels (MeasureAll, the default) or a single uniform height (MeasureFirst). */
47
125
  itemHeights = [];
48
126
  uniformHeight = 0;
49
127
  measuredWidthPx = 0;
@@ -87,7 +165,11 @@ export class SkiaLayout extends SkiaControl {
87
165
  */
88
166
  ApplyIncrementalChange(old, items) {
89
167
  const n = items.length, o = old.length;
90
- if (n <= o || o === 0)
168
+ if (o === 0)
169
+ return false;
170
+ if (n === o)
171
+ return this.ApplyReorderChange(old, items);
172
+ if (n < o)
91
173
  return false;
92
174
  const k = n - o;
93
175
  const scale = this.RenderingScale, gap = this.Spacing * scale, w = this.measuredWidthPx;
@@ -143,6 +225,65 @@ export class SkiaLayout extends SkiaControl {
143
225
  }
144
226
  return false;
145
227
  }
228
+ /**
229
+ * Same items in another order — a drag-to-reorder, or any code writing a permuted array. DrawnUi handles the
230
+ * matching `ObservableCollection.Move` by keeping the structure and only rebinding the contexts, because its
231
+ * arrange re-flows each row from its bound view (`HandleStructurePreservingMove` / `ApplyMoveChange`, 2026-09-08).
232
+ * The measured heights here are index-keyed and authoritative, so they travel with their items instead: nothing is
233
+ * remeasured, the extent cannot change, and the scroll keeps its offset. Rebuilding instead would reset
234
+ * `MeasureVisible` to "exact for the first screen, estimated after" and throw a scrolled list back towards the top.
235
+ * Returns false when the two arrays are not a permutation of each other, so a real content change still rebuilds.
236
+ */
237
+ ApplyReorderChange(old, items) {
238
+ const n = items.length;
239
+ if (this.MeasureItemsStrategy === "MeasureAll" && this.itemHeights.length !== n)
240
+ return false;
241
+ if (this.MeasureItemsStrategy === "MeasureVisible" && this.mvHeights.length !== n)
242
+ return false;
243
+ const moved = [];
244
+ for (let i = 0; i < n; i++)
245
+ if (old[i] !== items[i])
246
+ moved.push(i);
247
+ if (moved.length === 0) {
248
+ this.ChildrenFactory.UpdateItems(items);
249
+ return true;
250
+ } // same order, new array reference
251
+ // every slot that changed must hold an item that was in one of those slots before, otherwise this is not a reorder
252
+ const free = new Map();
253
+ for (const i of moved) {
254
+ const q = free.get(old[i]);
255
+ if (q)
256
+ q.push(i);
257
+ else
258
+ free.set(old[i], [i]);
259
+ }
260
+ const from = new Int32Array(n);
261
+ for (let i = 0; i < n; i++)
262
+ from[i] = i;
263
+ for (const i of moved) {
264
+ const q = free.get(items[i]);
265
+ if (!q || q.length === 0)
266
+ return false;
267
+ from[i] = q.shift();
268
+ }
269
+ if (this.MeasureItemsStrategy === "MeasureAll") {
270
+ const was = this.itemHeights;
271
+ this.itemHeights = Array.from({ length: n }, (_, i) => was[from[i]]);
272
+ }
273
+ else if (this.MeasureItemsStrategy === "MeasureVisible") {
274
+ const was = this.mvHeights, heights = new Float64Array(n);
275
+ for (let i = 0; i < n; i++)
276
+ heights[i] = was[from[i]];
277
+ this.mvHeights = heights;
278
+ // an item that was never measured can land inside the exact prefix: rebuild it over the leading measured run
279
+ this.mvPrefix = new Float64Array(n + 1);
280
+ this.mvMeasured = 0;
281
+ this.MvExtendPrefix(this.Spacing * this.RenderingScale);
282
+ }
283
+ // MeasureFirst has one height for every row, so a reorder is a pure rebind there (same as the C# note).
284
+ this.ChildrenFactory.UpdateItems(items);
285
+ return true;
286
+ }
146
287
  /** Factory creating one cell (DrawnUi DataTemplate). Cells receive the item as BindingContext. */
147
288
  get ItemTemplate() { return this.itemTemplate; }
148
289
  set ItemTemplate(value) {
@@ -152,13 +293,26 @@ export class SkiaLayout extends SkiaControl {
152
293
  this.ApplyItemsSource();
153
294
  }
154
295
  get IsTemplated() { return !!this.itemTemplate && !!this.itemsSource; }
155
- /** The virtualized list case: a templated single-column Column. Templated Row / Wrap / Grid realize every item. */
296
+ /** The virtualized list case: a templated single-column Column. */
156
297
  get IsTemplatedList() { return this.IsTemplated && this.Type === "Column" && this.Split <= 1; }
157
- /** Views taking part in the layout pass: static children, or every item of a templated Row / Wrap / Grid (C# non-list layouts are not virtualized). */
298
+ /** A templated Row / Wrap / Grid / split Column with RecyclingTemplate Enabled: slots measured on pooled views, views bound to the slots drawn. */
299
+ get IsRecycledLayout() { return this.IsTemplated && !this.IsTemplatedList && this.recyclingTemplate === "Enabled"; }
300
+ /** Slots of a recycled layout, one per item. */
301
+ slots;
302
+ /**
303
+ * What the layout pass measures and arranges: static children; the slots of a recycled templated layout; or, with
304
+ * RecyclingTemplate Disabled, one realized view per item.
305
+ */
158
306
  LayoutViews() {
159
307
  if (!this.IsTemplated)
160
308
  return this.views;
161
- const n = this.itemsSource.length, out = [];
309
+ const n = this.itemsSource.length;
310
+ if (this.IsRecycledLayout) {
311
+ if (this.slots?.length !== n)
312
+ this.slots = Array.from({ length: n }, (_, i) => new TemplatedSlot(i, this.ChildrenFactory));
313
+ return this.slots;
314
+ }
315
+ const out = [];
162
316
  for (let i = 0; i < n; i++) {
163
317
  const v = this.ChildrenFactory.GetOrCreateViewForIndex(i);
164
318
  if (v)
@@ -168,7 +322,8 @@ export class SkiaLayout extends SkiaControl {
168
322
  }
169
323
  /** Drops realized cells and rebuilds the structure (DrawnUi ApplyItemsSource). */
170
324
  ApplyItemsSource() {
171
- this.ChildrenFactory.Initialize(this.itemTemplate, this.itemsSource ?? [], this.RecyclingTemplate);
325
+ this.ChildrenFactory.Initialize(this.itemTemplate, this.itemsSource ?? [], this.recyclingTemplate);
326
+ this.slots = undefined;
172
327
  this.structureDirty = true;
173
328
  this.InvalidateMeasure();
174
329
  }
@@ -204,11 +359,67 @@ export class SkiaLayout extends SkiaControl {
204
359
  get Views() { return this.IsTemplated ? this.ChildrenFactory.GetViewsInUse() : this.views; }
205
360
  /** Settable children list like DrawnUi Children (ignored while templated). */
206
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
+ */
207
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() {
208
421
  for (const v of [...this.views])
209
422
  this.RemoveSubView(v);
210
- for (const v of value)
211
- this.AddSubView(v);
212
423
  }
213
424
  orderedViews;
214
425
  /** Static children sorted by ZIndex (stable), computed once per change (C# GetOrderedSubviews). */
@@ -235,10 +446,18 @@ export class SkiaLayout extends SkiaControl {
235
446
  }
236
447
  AddSubView(control) { this.InsertSubView(this.views.length, control); }
237
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
+ }
238
456
  control.Parent = this;
239
- this.views.splice(index, 0, control);
457
+ this.views.splice(Math.min(Math.max(0, index), this.views.length), 0, control);
240
458
  this.orderedViews = undefined;
241
459
  this.InvalidateMeasure();
460
+ this.Superview?.Hover?.RequestCheck(); // a popup / page under a still mouse
242
461
  }
243
462
  RemoveSubView(control) {
244
463
  const i = this.views.indexOf(control);
@@ -248,6 +467,7 @@ export class SkiaLayout extends SkiaControl {
248
467
  this.orderedViews = undefined;
249
468
  control.Parent = undefined;
250
469
  this.InvalidateMeasure();
470
+ this.Superview?.Hover?.RequestCheck();
251
471
  }
252
472
  // ---- measure ----
253
473
  MeasureAbsolute(widthConstraint, heightConstraint, scale) {
@@ -371,7 +591,7 @@ export class SkiaLayout extends SkiaControl {
371
591
  }
372
592
  });
373
593
  }
374
- const g = new SkiaGridStructure(this, wPts, hPts, scale);
594
+ const g = new SkiaGridStructure(this, wPts, hPts, scale, this.LayoutViews());
375
595
  g.DecompressStars(wPts, hPts);
376
596
  const needAutoWidth = this.WidthRequest < 0 && this.HorizontalOptions !== "Fill";
377
597
  const needAutoHeight = this.HeightRequest < 0 && this.VerticalOptions !== "Fill";
@@ -511,6 +731,11 @@ export class SkiaLayout extends SkiaControl {
511
731
  this.ChildrenFactory.ReleaseViewAt(index);
512
732
  return h;
513
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; }
514
739
  /** Pixel offset of item index from the top of the layout content (inside padding). */
515
740
  GetItemOffsetPixels(index) {
516
741
  const scale = this.RenderingScale;
@@ -536,6 +761,22 @@ export class SkiaLayout extends SkiaControl {
536
761
  OnLayoutChanged() {
537
762
  if (this.IsTemplatedList)
538
763
  return; // cells are arranged per frame for the visible range only
764
+ this.ArrangeChildren();
765
+ // views on screen follow their slots also when a cache above skips the drawing pass (gestures, accessibility)
766
+ if (this.IsRecycledLayout) {
767
+ const scale = this.RenderingScale;
768
+ for (const v of this.ChildrenFactory.GetViewsInUse()) {
769
+ const s = this.slots?.[v.ContextIndex];
770
+ if (s) {
771
+ v.Arrange(s.Destination, v.WidthRequest, v.HeightRequest, scale);
772
+ s.ArrangedView = v;
773
+ s.ArrangedFor = s.Destination;
774
+ }
775
+ }
776
+ }
777
+ }
778
+ /** Static children and realized views are arranged; the slots of a recycled layout record where their item goes. */
779
+ ArrangeChildren() {
539
780
  const scale = this.RenderingScale;
540
781
  const p = this.Padding;
541
782
  const r = this.DrawingRect;
@@ -582,12 +823,68 @@ export class SkiaLayout extends SkiaControl {
582
823
  this.PaintTemplated(ctx);
583
824
  return;
584
825
  }
826
+ if (this.IsRecycledLayout) {
827
+ this.PaintSlots(ctx);
828
+ return;
829
+ }
585
830
  const composing = this.IsRenderingWithComposition;
586
- for (const v of this.GetOrderedSubviews())
587
- if (!composing || this.DirtyChildrenInternal.has(v))
588
- 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
+ }
836
+ }
837
+ GetCompositeChildren() { return this.IsTemplatedList || this.IsRecycledLayout ? [] : this.GetOrderedSubviews(); }
838
+ drawnSlots = new Set();
839
+ /**
840
+ * Recycled templated layout (C# DrawStack): a view from the pool is bound to each slot that can be seen and drawn
841
+ * there; the views of the other slots go back to the pool.
842
+ */
843
+ PaintSlots(ctx) {
844
+ const slots = this.slots ?? [], f = this.ChildrenFactory, scale = this.RenderingScale, area = this.SlotsArea();
845
+ const drawn = this.drawnSlots;
846
+ drawn.clear();
847
+ for (const s of slots) {
848
+ const d = s.Destination;
849
+ if (s.IsVisible && d.Right > area.Left && d.Left < area.Right && d.Bottom > area.Top && d.Top < area.Bottom)
850
+ drawn.add(s.Index);
851
+ }
852
+ f.ReleaseExcept(drawn);
853
+ this.FirstVisibleIndex = this.LastVisibleIndex = -1;
854
+ for (const i of drawn) {
855
+ const s = slots[i], view = f.GetOrCreateViewForIndex(i, true);
856
+ if (!view)
857
+ continue;
858
+ // a view last measured for another item is measured for this one; the whole size is compared because a Wrap has
859
+ // no main-axis size key (C# 3d7bd78f)
860
+ const has = view.MeasuredSize.Pixels, want = s.MeasuredSize.Pixels;
861
+ if (Math.abs(has.Width - want.Width) > 1 || Math.abs(has.Height - want.Height) > 1)
862
+ view.NeedMeasure = true;
863
+ const size = view.MeasuredSize;
864
+ view.Measure(s.WidthConstraint, s.HeightConstraint, scale);
865
+ if (s.ArrangedView !== view || s.ArrangedFor !== s.Destination || view.MeasuredSize !== size) {
866
+ view.Arrange(s.Destination, view.WidthRequest, view.HeightRequest, scale);
867
+ s.ArrangedView = view;
868
+ s.ArrangedFor = s.Destination;
869
+ }
870
+ view.Render(ctx);
871
+ if (this.FirstVisibleIndex < 0)
872
+ this.FirstVisibleIndex = i;
873
+ this.LastVisibleIndex = i;
874
+ }
875
+ }
876
+ /**
877
+ * Where slots are drawn (C# GetOnScreenVisibleArea): what can be seen, inflated by VirtualisationInflated. Under a
878
+ * cache (this layout's or an ancestor's, below the nearest scroll) every slot, because the cache is blitted later at
879
+ * other offsets.
880
+ */
881
+ SlotsArea() {
882
+ for (let c = this; c && !("ViewportOffsetY" in c); c = c.Parent)
883
+ if (c.UsingCacheType !== "None")
884
+ return this.DrawingRect;
885
+ const v = this.GetVisibleViewport(), inflate = this.VirtualisationInflated * this.RenderingScale;
886
+ return new SKRect(v.Left - inflate, v.Top - inflate, v.Right + inflate, v.Bottom + inflate);
589
887
  }
590
- GetCompositeChildren() { return this.IsTemplatedList ? [] : this.GetOrderedSubviews(); }
591
888
  /** Realizes, binds, arranges and draws only the cells intersecting the visible viewport (+ inflation). */
592
889
  PaintTemplated(ctx) {
593
890
  const items = this.itemsSource;
@@ -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) {
@@ -174,7 +176,7 @@ export class SkiaLottie extends AnimatedFramesRenderer {
174
176
  return Promise.resolve(cached);
175
177
  let p = SkiaLottie.inFlight.get(source);
176
178
  if (!p) {
177
- p = fetch(source).then(async (r) => { if (!r.ok)
179
+ p = fetch(Super.ResolveAssetUrl(source)).then(async (r) => { if (!r.ok)
178
180
  throw new Error(`${r.status} ${source}`); const json = await r.text(); SkiaLottie.CachedAnimations.set(source, json); return json; })
179
181
  .finally(() => SkiaLottie.inFlight.delete(source));
180
182
  SkiaLottie.inFlight.set(source, p);
@@ -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
  }
@@ -37,8 +37,7 @@ export class SkiaRadioButton extends SkiaToggle {
37
37
  return table[this.UsingControlStyle]?.[name];
38
38
  }
39
39
  CreateDefaultContent() {
40
- if (this.MinimumHeightRequest < 0)
41
- this.MinimumHeightRequest = 24;
40
+ this.SetMinimumContentHeight(24);
42
41
  const s = this.usingStyle;
43
42
  const size = s === "Unset" ? 18 : 20;
44
43
  const box = new SkiaLayout();
@@ -145,8 +144,10 @@ export class SkiaRadioButton extends SkiaToggle {
145
144
  super.OnToggledChanged();
146
145
  }
147
146
  ProcessGestures(args, apply) {
147
+ if (!this.RespondsToGestures)
148
+ return this.BlockedGestureResult(args); // a tap does not select it, nor reach controls under it
148
149
  if (args.Type === "Tapped") {
149
- if (!this.IsToggled && this.RespondsToGestures) {
150
+ if (!this.IsToggled) {
150
151
  this.IsToggled = true;
151
152
  return this;
152
153
  }