drawnui-react 0.1.0-preview.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/PARITY.md +287 -0
  3. package/README.md +92 -0
  4. package/SKIPPED.md +107 -0
  5. package/dist/controls/AnimatedFramesRenderer.d.ts +58 -0
  6. package/dist/controls/AnimatedFramesRenderer.js +133 -0
  7. package/dist/controls/GridStructure.d.ts +79 -0
  8. package/dist/controls/GridStructure.js +403 -0
  9. package/dist/controls/RefreshIndicator.d.ts +35 -0
  10. package/dist/controls/RefreshIndicator.js +82 -0
  11. package/dist/controls/SkiaBackdrop.d.ts +25 -0
  12. package/dist/controls/SkiaBackdrop.js +80 -0
  13. package/dist/controls/SkiaButton.d.ts +58 -0
  14. package/dist/controls/SkiaButton.js +148 -0
  15. package/dist/controls/SkiaCarousel.d.ts +133 -0
  16. package/dist/controls/SkiaCarousel.js +603 -0
  17. package/dist/controls/SkiaCheckbox.d.ts +21 -0
  18. package/dist/controls/SkiaCheckbox.js +118 -0
  19. package/dist/controls/SkiaDecoratedGrid.d.ts +15 -0
  20. package/dist/controls/SkiaDecoratedGrid.js +46 -0
  21. package/dist/controls/SkiaDrawer.d.ts +46 -0
  22. package/dist/controls/SkiaDrawer.js +212 -0
  23. package/dist/controls/SkiaDynamicDrawnCell.d.ts +11 -0
  24. package/dist/controls/SkiaDynamicDrawnCell.js +17 -0
  25. package/dist/controls/SkiaEditor.d.ts +163 -0
  26. package/dist/controls/SkiaEditor.js +803 -0
  27. package/dist/controls/SkiaGif.d.ts +54 -0
  28. package/dist/controls/SkiaGif.js +206 -0
  29. package/dist/controls/SkiaHotspot.d.ts +13 -0
  30. package/dist/controls/SkiaHotspot.js +36 -0
  31. package/dist/controls/SkiaImage.d.ts +75 -0
  32. package/dist/controls/SkiaImage.js +222 -0
  33. package/dist/controls/SkiaImageTiles.d.ts +27 -0
  34. package/dist/controls/SkiaImageTiles.js +76 -0
  35. package/dist/controls/SkiaLabel.d.ts +160 -0
  36. package/dist/controls/SkiaLabel.js +580 -0
  37. package/dist/controls/SkiaLayout.d.ts +163 -0
  38. package/dist/controls/SkiaLayout.js +723 -0
  39. package/dist/controls/SkiaLottie.d.ts +75 -0
  40. package/dist/controls/SkiaLottie.js +290 -0
  41. package/dist/controls/SkiaProgress.d.ts +40 -0
  42. package/dist/controls/SkiaProgress.js +80 -0
  43. package/dist/controls/SkiaRadioButton.d.ts +32 -0
  44. package/dist/controls/SkiaRadioButton.js +157 -0
  45. package/dist/controls/SkiaRichLabel.d.ts +77 -0
  46. package/dist/controls/SkiaRichLabel.js +274 -0
  47. package/dist/controls/SkiaScroll.d.ts +231 -0
  48. package/dist/controls/SkiaScroll.js +958 -0
  49. package/dist/controls/SkiaScrollBar.d.ts +56 -0
  50. package/dist/controls/SkiaScrollBar.js +158 -0
  51. package/dist/controls/SkiaShaderCarousel.d.ts +67 -0
  52. package/dist/controls/SkiaShaderCarousel.js +246 -0
  53. package/dist/controls/SkiaShape.d.ts +63 -0
  54. package/dist/controls/SkiaShape.js +317 -0
  55. package/dist/controls/SkiaSlider.d.ts +74 -0
  56. package/dist/controls/SkiaSlider.js +243 -0
  57. package/dist/controls/SkiaSprite.d.ts +99 -0
  58. package/dist/controls/SkiaSprite.js +324 -0
  59. package/dist/controls/SkiaSpriteSet.d.ts +27 -0
  60. package/dist/controls/SkiaSpriteSet.js +75 -0
  61. package/dist/controls/SkiaSvg.d.ts +43 -0
  62. package/dist/controls/SkiaSvg.js +181 -0
  63. package/dist/controls/SkiaSwitch.d.ts +26 -0
  64. package/dist/controls/SkiaSwitch.js +147 -0
  65. package/dist/controls/SkiaToggle.d.ts +50 -0
  66. package/dist/controls/SkiaToggle.js +71 -0
  67. package/dist/controls/SnappingLayout.d.ts +78 -0
  68. package/dist/controls/SnappingLayout.js +217 -0
  69. package/dist/controls/TextSpan.d.ts +70 -0
  70. package/dist/controls/TextSpan.js +76 -0
  71. package/dist/core/Accessibility.d.ts +104 -0
  72. package/dist/core/Accessibility.js +150 -0
  73. package/dist/core/Animators.d.ts +82 -0
  74. package/dist/core/Animators.js +247 -0
  75. package/dist/core/Canvas.d.ts +89 -0
  76. package/dist/core/Canvas.js +328 -0
  77. package/dist/core/ControlStyle.d.ts +6 -0
  78. package/dist/core/ControlStyle.js +11 -0
  79. package/dist/core/Easing.d.ts +13 -0
  80. package/dist/core/Easing.js +15 -0
  81. package/dist/core/Gestures.d.ts +74 -0
  82. package/dist/core/Gestures.js +104 -0
  83. package/dist/core/ImageEffects.d.ts +29 -0
  84. package/dist/core/ImageEffects.js +55 -0
  85. package/dist/core/KeyboardManager.d.ts +25 -0
  86. package/dist/core/KeyboardManager.js +54 -0
  87. package/dist/core/ScrollAnimators.d.ts +90 -0
  88. package/dist/core/ScrollAnimators.js +244 -0
  89. package/dist/core/SkiaControl.d.ts +401 -0
  90. package/dist/core/SkiaControl.js +1239 -0
  91. package/dist/core/SkiaEffect.d.ts +216 -0
  92. package/dist/core/SkiaEffect.js +571 -0
  93. package/dist/core/SkiaImageManager.d.ts +43 -0
  94. package/dist/core/SkiaImageManager.js +122 -0
  95. package/dist/core/Super.d.ts +61 -0
  96. package/dist/core/Super.js +178 -0
  97. package/dist/core/TextInputProxy.d.ts +31 -0
  98. package/dist/core/TextInputProxy.js +144 -0
  99. package/dist/core/Types.d.ts +177 -0
  100. package/dist/core/Types.js +122 -0
  101. package/dist/core/ViewsAdapter.d.ts +41 -0
  102. package/dist/core/ViewsAdapter.js +113 -0
  103. package/dist/index.d.ts +48 -0
  104. package/dist/index.js +48 -0
  105. package/dist/react/SkiaShell.d.ts +144 -0
  106. package/dist/react/SkiaShell.js +490 -0
  107. package/dist/react/index.d.ts +110 -0
  108. package/dist/react/index.js +158 -0
  109. package/dist/react/reconciler.d.ts +14 -0
  110. package/dist/react/reconciler.js +160 -0
  111. package/package.json +60 -0
@@ -0,0 +1,723 @@
1
+ import { SkiaControl } from "../core/SkiaControl";
2
+ import { ViewsAdapter } from "../core/ViewsAdapter";
3
+ import { SKRect, ScaledSize, Thickness } from "../core/Types";
4
+ import { SkiaGridStructure } from "./GridStructure";
5
+ /**
6
+ * Mirrors DrawnUi SkiaLayout (Absolute / Column / Row / Wrap / Grid).
7
+ * Column/Row give children an infinite main axis (MAUI stack semantics: Fill on the main axis = auto-sized).
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.
12
+ */
13
+ export class SkiaLayout extends SkiaControl {
14
+ /** Layout type. SkiaShape redeclares it as ShapeType (any shape value lays out as Absolute), like the C# hidden Type. */
15
+ Type = "Absolute";
16
+ Spacing = 0;
17
+ Padding = Thickness.Zero;
18
+ // ---- grid (same names as DrawnUi; definitions as "*, 2*, Auto, 100" or an array) ----
19
+ ColumnDefinitions;
20
+ RowDefinitions;
21
+ /** Track used for columns/rows a child references but no definition declares (C# default: Auto). */
22
+ DefaultColumnDefinition = "Auto";
23
+ DefaultRowDefinition = "Auto";
24
+ ColumnSpacing = 0;
25
+ RowSpacing = 0;
26
+ /** Structure computed by the last measure; cells are arranged from it. */
27
+ GridStructure;
28
+ // ---- templated children (same names as DrawnUi) ----
29
+ RecyclingTemplate = "Enabled";
30
+ /** Column count for Column / Wrap content and a templated Grid (0 = free flow); C# Split. */
31
+ Split = 0;
32
+ /** Split > 0: every slot keeps the same width (true) or the cell keeps its measured width (C# SplitAlign). */
33
+ SplitAlign = true;
34
+ /** A short last row spreads over the whole width instead of keeping empty Split slots (C# DynamicColumns). */
35
+ DynamicColumns = false;
36
+ /** Templated Grid: fill column-major (top to bottom, then next column) instead of row-major (C# Invert). */
37
+ Invert = false;
38
+ MeasureItemsStrategy = "MeasureFirst";
39
+ /** Realized views per item (Disabled) or a recycled pool for the visible range (Enabled). */
40
+ ChildrenFactory = new ViewsAdapter(this);
41
+ FirstVisibleIndex = -1;
42
+ LastVisibleIndex = -1;
43
+ itemsSource;
44
+ itemTemplate;
45
+ structureDirty = true;
46
+ /** Per-item heights in pixels (MeasureAll) or a single uniform height (MeasureFirst). */
47
+ itemHeights = [];
48
+ uniformHeight = 0;
49
+ measuredWidthPx = 0;
50
+ // ---- MeasureVisible (DrawnUi experimental strategy): measured prefix + average estimate for the rest ----
51
+ /** Items measured per idle slice before the estimate is refreshed (DrawnUi BackgroundMeasurementBatchSize). */
52
+ BackgroundMeasurementBatchSize = 10;
53
+ /** Pixel heights of measured items (0 = not measured yet), any order: visible cells are measured on demand. */
54
+ mvHeights = new Float64Array(0);
55
+ /** mvPrefix[i] = offset of item i (inside padding, gaps included) for i <= mvMeasured; items 0..mvMeasured-1 are exact. */
56
+ mvPrefix = new Float64Array(1);
57
+ mvMeasured = 0;
58
+ mvSum = 0;
59
+ mvCount = 0;
60
+ mvIdle = 0;
61
+ /** Index of the last item measured by the background pass (DrawnUi LastMeasuredIndex); -1 = none. */
62
+ get LastMeasuredIndex() { return this.mvMeasured - 1; }
63
+ get ItemsSource() { return this.itemsSource; }
64
+ set ItemsSource(value) {
65
+ if (this.itemsSource === value)
66
+ return;
67
+ const old = this.itemsSource;
68
+ this.itemsSource = value;
69
+ if (value && old && this.IsTemplated && !this.structureDirty && this.ApplyIncrementalChange(old, value)) {
70
+ this.InvalidateMeasure();
71
+ return;
72
+ }
73
+ if (value && this.IsTemplated)
74
+ this.ChildrenFactory.UpdateItems(value);
75
+ this.structureDirty = true;
76
+ this.InvalidateMeasure();
77
+ }
78
+ /**
79
+ * Raised after items were inserted at the head and measured, with the inserted extent in pixels; SkiaScroll uses it to
80
+ * keep the visible rows where they are (DrawnUi head-insert rebase).
81
+ */
82
+ ItemsInsertedAtStart;
83
+ /**
84
+ * DrawnUi keeps the structure on ObservableCollection Add/Insert; React apps replace the array, so the relation is
85
+ * detected instead: same items appended (page loaded) or prepended (chat history) keep every measured height,
86
+ * anything else rebuilds. Returns false when a full rebuild is needed.
87
+ */
88
+ ApplyIncrementalChange(old, items) {
89
+ const n = items.length, o = old.length;
90
+ if (n <= o || o === 0)
91
+ return false;
92
+ const k = n - o;
93
+ const scale = this.RenderingScale, gap = this.Spacing * scale, w = this.measuredWidthPx;
94
+ if (this.MeasureItemsStrategy === "MeasureFirst" && this.uniformHeight <= 0)
95
+ return false;
96
+ const sameAt = (offset) => old[0] === items[offset] && old[o - 1] === items[offset + o - 1] && old[o >> 1] === items[offset + (o >> 1)];
97
+ if (sameAt(0)) {
98
+ // append: heights of the new tail are measured lazily (MeasureAll now, MeasureVisible on demand / in idle time)
99
+ this.ChildrenFactory.UpdateItems(items);
100
+ if (this.MeasureItemsStrategy === "MeasureAll")
101
+ for (let i = o; i < n; i++)
102
+ this.itemHeights.push(this.MeasureItem(i, w, scale));
103
+ else if (this.MeasureItemsStrategy === "MeasureVisible") {
104
+ const heights = new Float64Array(n);
105
+ heights.set(this.mvHeights);
106
+ const prefix = new Float64Array(n + 1);
107
+ prefix.set(this.mvPrefix);
108
+ this.mvHeights = heights;
109
+ this.mvPrefix = prefix;
110
+ }
111
+ return true;
112
+ }
113
+ if (sameAt(k)) {
114
+ // prepend: shift indices, measure the new head synchronously (bounded), report the inserted extent
115
+ this.ChildrenFactory.ShiftIndices(k, items);
116
+ let insertedPx = 0;
117
+ if (this.MeasureItemsStrategy === "MeasureAll") {
118
+ const head = [];
119
+ for (let i = 0; i < k; i++)
120
+ head.push(this.MeasureItem(i, w, scale));
121
+ this.itemHeights.unshift(...head);
122
+ for (const h of head)
123
+ insertedPx += h + gap;
124
+ }
125
+ else if (this.MeasureItemsStrategy === "MeasureVisible") {
126
+ const heights = new Float64Array(n);
127
+ heights.set(this.mvHeights, k);
128
+ this.mvHeights = heights;
129
+ this.mvPrefix = new Float64Array(n + 1);
130
+ this.mvMeasured = 0;
131
+ const sync = Math.min(k, Math.max(this.BackgroundMeasurementBatchSize, 200));
132
+ for (let i = 0; i < sync; i++)
133
+ insertedPx += this.MvMeasure(i, w, scale, true) + gap;
134
+ if (sync < k)
135
+ insertedPx += (k - sync) * this.MvStride(gap); // the rest is estimated until the idle pass reaches it
136
+ this.MvExtendPrefix(gap);
137
+ }
138
+ else {
139
+ insertedPx = k * (this.uniformHeight + gap);
140
+ }
141
+ this.ItemsInsertedAtStart?.(this, insertedPx);
142
+ return true;
143
+ }
144
+ return false;
145
+ }
146
+ /** Factory creating one cell (DrawnUi DataTemplate). Cells receive the item as BindingContext. */
147
+ get ItemTemplate() { return this.itemTemplate; }
148
+ set ItemTemplate(value) {
149
+ if (this.itemTemplate === value)
150
+ return;
151
+ this.itemTemplate = value;
152
+ this.ApplyItemsSource();
153
+ }
154
+ get IsTemplated() { return !!this.itemTemplate && !!this.itemsSource; }
155
+ /** The virtualized list case: a templated single-column Column. Templated Row / Wrap / Grid realize every item. */
156
+ 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). */
158
+ LayoutViews() {
159
+ if (!this.IsTemplated)
160
+ return this.views;
161
+ const n = this.itemsSource.length, out = [];
162
+ for (let i = 0; i < n; i++) {
163
+ const v = this.ChildrenFactory.GetOrCreateViewForIndex(i);
164
+ if (v)
165
+ out.push(v);
166
+ }
167
+ return out;
168
+ }
169
+ /** Drops realized cells and rebuilds the structure (DrawnUi ApplyItemsSource). */
170
+ ApplyItemsSource() {
171
+ this.ChildrenFactory.Initialize(this.itemTemplate, this.itemsSource ?? [], this.RecyclingTemplate);
172
+ this.structureDirty = true;
173
+ this.InvalidateMeasure();
174
+ }
175
+ /** Diagnostics like DrawnUi DebugString: visible range, realized views, pool. */
176
+ get DebugString() {
177
+ if (!this.IsTemplated)
178
+ return `views ${this.views.length}`;
179
+ const f = this.ChildrenFactory;
180
+ const measured = this.MeasureItemsStrategy === "MeasureVisible" ? ` measured ${this.mvMeasured}/${this.itemsSource.length}` : "";
181
+ return `items ${this.itemsSource.length} visible ${this.FirstVisibleIndex}-${this.LastVisibleIndex}${measured} inuse ${f.InUseCount} pool ${f.PoolSize} created ${f.Created}`;
182
+ }
183
+ /** A layout paints outside its box whatever its children paint outside theirs (C# aggregated effects margin). */
184
+ ComputeEffectsMargin(scale) {
185
+ let l = 0, t = 0, r = 0, b = 0;
186
+ const mine = this.DrawingRect;
187
+ for (const v of this.views) {
188
+ if (!v.IsVisible)
189
+ continue;
190
+ const m = v.EffectsMargin(scale);
191
+ if (m.Left === 0 && m.Top === 0 && m.Right === 0 && m.Bottom === 0)
192
+ continue;
193
+ const cr = v.DrawingRect; // child overflow beyond this box, if already arranged
194
+ l = Math.max(l, m.Left - Math.max(0, cr.Left - mine.Left));
195
+ t = Math.max(t, m.Top - Math.max(0, cr.Top - mine.Top));
196
+ r = Math.max(r, m.Right - Math.max(0, mine.Right - cr.Right));
197
+ b = Math.max(b, m.Bottom - Math.max(0, mine.Bottom - cr.Bottom));
198
+ }
199
+ return new Thickness(Math.max(0, l), Math.max(0, t), Math.max(0, r), Math.max(0, b));
200
+ }
201
+ // ---- static children ----
202
+ views = [];
203
+ /** Read-only live children like DrawnUi Views: realized cells when templated, else static children. */
204
+ get Views() { return this.IsTemplated ? this.ChildrenFactory.GetViewsInUse() : this.views; }
205
+ /** Settable children list like DrawnUi Children (ignored while templated). */
206
+ get Children() { return this.views; }
207
+ set Children(value) {
208
+ for (const v of [...this.views])
209
+ this.RemoveSubView(v);
210
+ for (const v of value)
211
+ this.AddSubView(v);
212
+ }
213
+ orderedViews;
214
+ /** Static children sorted by ZIndex (stable), computed once per change (C# GetOrderedSubviews). */
215
+ GetOrderedSubviews() {
216
+ if (this.IsTemplated)
217
+ return this.Views;
218
+ if (!this.orderedViews) {
219
+ let sorted = false;
220
+ for (let i = 1; i < this.views.length && !sorted; i++)
221
+ if (this.views[i].ZIndex !== this.views[0].ZIndex)
222
+ sorted = true;
223
+ this.orderedViews = sorted ? this.views.map((v, i) => ({ v, i })).sort((a, b) => a.v.ZIndex - b.v.ZIndex || a.i - b.i).map((x) => x.v) : this.views;
224
+ }
225
+ return this.orderedViews;
226
+ }
227
+ InvalidateViewsOrder() { this.orderedViews = undefined; }
228
+ GetGestureListeners() { return this.GetOrderedSubviews(); }
229
+ DisposeChildren() {
230
+ for (const v of [...this.views])
231
+ v.Dispose();
232
+ this.views.length = 0;
233
+ this.orderedViews = undefined;
234
+ this.ChildrenFactory.DisposeAll();
235
+ }
236
+ AddSubView(control) { this.InsertSubView(this.views.length, control); }
237
+ InsertSubView(index, control) {
238
+ control.Parent = this;
239
+ this.views.splice(index, 0, control);
240
+ this.orderedViews = undefined;
241
+ this.InvalidateMeasure();
242
+ }
243
+ RemoveSubView(control) {
244
+ const i = this.views.indexOf(control);
245
+ if (i < 0)
246
+ return;
247
+ this.views.splice(i, 1);
248
+ this.orderedViews = undefined;
249
+ control.Parent = undefined;
250
+ this.InvalidateMeasure();
251
+ }
252
+ // ---- measure ----
253
+ MeasureAbsolute(widthConstraint, heightConstraint, scale) {
254
+ if (this.IsTemplatedList)
255
+ return this.MeasureTemplated(widthConstraint, heightConstraint, scale);
256
+ const px = this.Padding.HorizontalThickness * scale;
257
+ const py = this.Padding.VerticalThickness * scale;
258
+ const w = widthConstraint - px;
259
+ const h = heightConstraint - py;
260
+ const gap = this.Spacing * scale;
261
+ let cw = 0, ch = 0, n = 0;
262
+ if (this.IsWrapFlow) {
263
+ const s = this.MeasureWrap(w, scale);
264
+ return ScaledSize.FromPixels(s.w + px, s.h + py, scale);
265
+ }
266
+ if (this.Type === "Grid") {
267
+ const s = this.MeasureGrid(w, h, scale);
268
+ return ScaledSize.FromPixels(s.w + px, s.h + py, scale);
269
+ }
270
+ for (const v of this.LayoutViews()) {
271
+ if (!v.IsVisible)
272
+ continue;
273
+ let s;
274
+ if (this.Type === "Column") {
275
+ s = v.Measure(w, Infinity, scale);
276
+ cw = Math.max(cw, s.Pixels.Width);
277
+ ch += s.Pixels.Height;
278
+ }
279
+ else if (this.Type === "Row") {
280
+ s = v.Measure(Infinity, h, scale);
281
+ cw += s.Pixels.Width;
282
+ ch = Math.max(ch, s.Pixels.Height);
283
+ }
284
+ else {
285
+ s = v.Measure(w, h, scale);
286
+ cw = Math.max(cw, s.Pixels.Width);
287
+ ch = Math.max(ch, s.Pixels.Height);
288
+ }
289
+ n++;
290
+ }
291
+ const gaps = Math.max(0, n - 1) * gap;
292
+ if (this.Type === "Column")
293
+ ch += gaps;
294
+ if (this.Type === "Row")
295
+ cw += gaps;
296
+ return ScaledSize.FromPixels(cw + px, ch + py, scale);
297
+ }
298
+ /** Wrap rows computed by the last measure: [childIndex, x, y, w, h] in pixels relative to the padded box. */
299
+ wrapSlots = [];
300
+ /** Wrap, or a Column with Split > 1 (C# lays a multi-column Column out like a wrap with fixed slots). */
301
+ get IsWrapFlow() { return this.Type === "Wrap" || (this.Type === "Column" && this.Split > 1); }
302
+ /**
303
+ * Flow children left to right, wrapping when the row overflows; Spacing applies between items and rows.
304
+ * Split > 0: fixed column count with slot width (width - (Split - 1) * gap) / Split (`SplitAlign`), a new row every
305
+ * Split items; `DynamicColumns` lets a short last row spread over the width.
306
+ */
307
+ MeasureWrap(width, scale) {
308
+ const gap = this.Spacing * scale;
309
+ const split = this.Split > 0 ? this.Split : 0;
310
+ const slotW = split > 0 && isFinite(width) ? (width - (split - 1) * gap) / split : 0;
311
+ this.wrapSlots = [];
312
+ let x = 0, y = 0, rowH = 0, maxW = 0;
313
+ const row = [];
314
+ const finishRow = (last) => {
315
+ if (last && split > 0 && this.DynamicColumns && row.length > 0 && row.length < split && isFinite(width)) {
316
+ // C# DynamicColumns: the last short row takes the whole width
317
+ const w = (width - (row.length - 1) * gap) / row.length;
318
+ let rx = 0;
319
+ rowH = 0;
320
+ for (const s of row) {
321
+ const m = s.view.Measure(w, Infinity, scale);
322
+ s.x = rx;
323
+ s.w = w;
324
+ s.h = m.Pixels.Height;
325
+ rowH = Math.max(rowH, s.h);
326
+ rx += w + gap;
327
+ }
328
+ maxW = Math.max(maxW, rx - gap);
329
+ }
330
+ for (const s of row)
331
+ s.h = rowH;
332
+ row.length = 0;
333
+ };
334
+ for (const v of this.LayoutViews()) {
335
+ if (!v.IsVisible)
336
+ continue;
337
+ const s = v.Measure(split > 0 && slotW > 0 ? slotW : width, Infinity, scale);
338
+ const cw = split > 0 && slotW > 0 && this.SplitAlign ? slotW : s.Pixels.Width, ch = s.Pixels.Height;
339
+ const wrap = split > 0 ? row.length >= split : x > 0 && isFinite(width) && x + cw > width;
340
+ if (wrap) {
341
+ finishRow(false);
342
+ y += rowH + gap;
343
+ x = 0;
344
+ rowH = 0;
345
+ }
346
+ const slot = { view: v, x, y, w: cw, h: ch };
347
+ this.wrapSlots.push(slot);
348
+ row.push(slot);
349
+ x += cw + gap;
350
+ rowH = Math.max(rowH, ch);
351
+ maxW = Math.max(maxW, x - gap);
352
+ }
353
+ finishRow(true);
354
+ return { w: maxW, h: this.wrapSlots.length ? y + rowH : 0 };
355
+ }
356
+ /** Port of C# MeasureGrid: build the structure in points, stretch the last track when the grid fills, remeasure at final cells. */
357
+ MeasureGrid(widthPx, heightPx, scale) {
358
+ const wPts = widthPx / scale, hPts = heightPx / scale;
359
+ if (this.IsTemplated) {
360
+ // C# templated grid: item i goes to (i % Split, i / Split), column-major when Invert
361
+ const views = this.LayoutViews(), split = Math.max(1, this.Split);
362
+ const rowsPerColumn = this.Invert && split > 1 ? Math.ceil(views.length / split) : 0;
363
+ views.forEach((v, i) => {
364
+ if (rowsPerColumn > 0) {
365
+ v.Column = Math.floor(i / rowsPerColumn);
366
+ v.Row = i % rowsPerColumn;
367
+ }
368
+ else {
369
+ v.Row = Math.floor(i / split);
370
+ v.Column = i % split;
371
+ }
372
+ });
373
+ }
374
+ const g = new SkiaGridStructure(this, wPts, hPts, scale);
375
+ g.DecompressStars(wPts, hPts);
376
+ const needAutoWidth = this.WidthRequest < 0 && this.HorizontalOptions !== "Fill";
377
+ const needAutoHeight = this.HeightRequest < 0 && this.VerticalOptions !== "Fill";
378
+ if (!needAutoWidth && g.Columns.length > 0 && isFinite(wPts) && g.GridWidth() < wPts)
379
+ g.Columns[g.Columns.length - 1].Size += wPts - g.GridWidth();
380
+ if (!needAutoHeight && g.Rows.length > 0 && isFinite(hPts) && g.GridHeight() < hPts)
381
+ g.Rows[g.Rows.length - 1].Size += hPts - g.GridHeight();
382
+ g.RemeasureChildrenAtFinalCells();
383
+ this.GridStructure = g;
384
+ return { w: g.GridWidth() * scale, h: g.GridHeight() * scale };
385
+ }
386
+ /** Templated Column: content = padding + sum of item heights + gaps; width = the constraint (cells fill). */
387
+ MeasureTemplated(widthConstraint, _heightConstraint, scale) {
388
+ const items = this.itemsSource;
389
+ const px = this.Padding.HorizontalThickness * scale;
390
+ const py = this.Padding.VerticalThickness * scale;
391
+ const w = isFinite(widthConstraint) ? widthConstraint - px : 0;
392
+ const gap = this.Spacing * scale;
393
+ if (this.structureDirty || this.measuredWidthPx !== w) {
394
+ this.measuredWidthPx = w;
395
+ this.itemHeights = [];
396
+ this.uniformHeight = 0;
397
+ this.MvReset(items.length);
398
+ if (items.length > 0) {
399
+ if (this.MeasureItemsStrategy === "MeasureAll") {
400
+ for (let i = 0; i < items.length; i++)
401
+ this.itemHeights.push(this.MeasureItem(i, w, scale));
402
+ }
403
+ else if (this.MeasureItemsStrategy === "MeasureVisible") {
404
+ // initial pass: enough items to fill the viewport (at least one batch), the rest is estimated
405
+ const viewportH = this.GetVisibleViewport().Height;
406
+ for (let i = 0; i < items.length; i++) {
407
+ this.MvMeasure(i, w, scale, true);
408
+ this.MvExtendPrefix(gap);
409
+ if (i + 1 >= this.BackgroundMeasurementBatchSize && this.mvPrefix[this.mvMeasured] >= viewportH + this.VirtualisationInflated * scale)
410
+ break;
411
+ }
412
+ }
413
+ else {
414
+ this.uniformHeight = this.MeasureItem(0, w, scale);
415
+ }
416
+ }
417
+ this.structureDirty = false;
418
+ }
419
+ let total = 0;
420
+ if (this.MeasureItemsStrategy === "MeasureAll") {
421
+ while (this.itemHeights.length < items.length)
422
+ this.itemHeights.push(this.MeasureItem(this.itemHeights.length, w, scale));
423
+ for (const h of this.itemHeights)
424
+ total += h;
425
+ total += Math.max(0, items.length - 1) * gap;
426
+ }
427
+ else if (this.MeasureItemsStrategy === "MeasureVisible") {
428
+ total = this.MvContentHeight(gap);
429
+ this.MvScheduleBackground();
430
+ }
431
+ else
432
+ total = this.uniformHeight * items.length + Math.max(0, items.length - 1) * gap;
433
+ return ScaledSize.FromPixels(w + px, total + py, scale);
434
+ }
435
+ MvReset(count) {
436
+ if (this.mvIdle) {
437
+ (window.cancelIdleCallback ?? clearTimeout)(this.mvIdle);
438
+ this.mvIdle = 0;
439
+ }
440
+ this.mvHeights = new Float64Array(count);
441
+ this.mvPrefix = new Float64Array(count + 1);
442
+ this.mvMeasured = 0;
443
+ this.mvSum = 0;
444
+ this.mvCount = 0;
445
+ }
446
+ /** Average item stride (height + gap) from everything measured so far. */
447
+ MvStride(gap) { return (this.mvCount > 0 ? this.mvSum / this.mvCount : 0) + gap; }
448
+ /** DrawnUi MeasureList estimate: exact prefix + average × remaining. */
449
+ MvContentHeight(gap) {
450
+ const n = this.mvHeights.length;
451
+ if (n === 0)
452
+ return 0;
453
+ return this.mvPrefix[this.mvMeasured] + (n - this.mvMeasured) * this.MvStride(gap) - gap;
454
+ }
455
+ /** Measures one item (binding it through the adapter); release = give the cell back to the pool right away. */
456
+ MvMeasure(index, widthPx, scale, release) {
457
+ const known = this.mvHeights[index];
458
+ if (known > 0)
459
+ return known;
460
+ const view = this.ChildrenFactory.GetOrCreateViewForIndex(index);
461
+ if (!view)
462
+ return 0;
463
+ const h = view.Measure(widthPx, Infinity, scale).Pixels.Height;
464
+ if (release && this.RecyclingTemplate === "Enabled")
465
+ this.ChildrenFactory.ReleaseViewAt(index);
466
+ this.mvHeights[index] = h;
467
+ this.mvSum += h;
468
+ this.mvCount++;
469
+ return h;
470
+ }
471
+ /** Grows the exact prefix over every already-measured item that follows it. */
472
+ MvExtendPrefix(gap) {
473
+ const n = this.mvHeights.length;
474
+ while (this.mvMeasured < n && this.mvHeights[this.mvMeasured] > 0) {
475
+ this.mvPrefix[this.mvMeasured + 1] = this.mvPrefix[this.mvMeasured] + this.mvHeights[this.mvMeasured] + gap;
476
+ this.mvMeasured++;
477
+ }
478
+ }
479
+ /** Background measurement in idle time (DrawnUi StartBackgroundMeasurement): time-sliced, then the estimate is refreshed once. */
480
+ MvScheduleBackground() {
481
+ if (this.mvIdle || this.mvMeasured >= this.mvHeights.length || !this.Superview)
482
+ return;
483
+ const run = (deadline) => {
484
+ this.mvIdle = 0;
485
+ if (this.MeasureItemsStrategy !== "MeasureVisible" || !this.IsTemplated || !this.Superview)
486
+ return;
487
+ const scale = this.RenderingScale, gap = this.Spacing * scale, w = this.measuredWidthPx;
488
+ const before = this.MvContentHeight(gap);
489
+ const started = performance.now();
490
+ const budget = () => (deadline ? deadline.timeRemaining() > 1 : performance.now() - started < 8);
491
+ let measured = 0;
492
+ while (this.mvMeasured < this.mvHeights.length && (measured < this.BackgroundMeasurementBatchSize || budget())) {
493
+ this.MvMeasure(this.mvMeasured, w, scale, true);
494
+ this.MvExtendPrefix(gap);
495
+ measured++;
496
+ }
497
+ if (Math.abs(this.MvContentHeight(gap) - before) > 0.5)
498
+ this.InvalidateMeasure(); // parents (the scroll) pick up the new extent
499
+ if (this.mvMeasured < this.mvHeights.length)
500
+ this.MvScheduleBackground();
501
+ };
502
+ this.mvIdle = window.requestIdleCallback ? window.requestIdleCallback(run, { timeout: 200 }) : window.setTimeout(() => run(), 16);
503
+ }
504
+ /** Binds a cell to items[index] (through the adapter, so MeasureFirst's cell 0 stays realized) and measures it. */
505
+ MeasureItem(index, widthPx, scale) {
506
+ const view = this.ChildrenFactory.GetOrCreateViewForIndex(index);
507
+ if (!view)
508
+ return 0;
509
+ const h = view.Measure(widthPx, Infinity, scale).Pixels.Height;
510
+ if (this.MeasureItemsStrategy === "MeasureAll" && this.RecyclingTemplate === "Enabled")
511
+ this.ChildrenFactory.ReleaseViewAt(index);
512
+ return h;
513
+ }
514
+ /** Pixel offset of item index from the top of the layout content (inside padding). */
515
+ GetItemOffsetPixels(index) {
516
+ const scale = this.RenderingScale;
517
+ const gap = this.Spacing * scale;
518
+ let y = this.Padding.Top * scale;
519
+ if (this.MeasureItemsStrategy === "MeasureAll")
520
+ for (let i = 0; i < index && i < this.itemHeights.length; i++)
521
+ y += this.itemHeights[i] + gap;
522
+ else if (this.MeasureItemsStrategy === "MeasureVisible")
523
+ y += index <= this.mvMeasured ? this.mvPrefix[index] : this.mvPrefix[this.mvMeasured] + (index - this.mvMeasured) * this.MvStride(gap);
524
+ else
525
+ y += index * (this.uniformHeight + gap);
526
+ return y;
527
+ }
528
+ ItemHeightPixels(index) {
529
+ if (this.MeasureItemsStrategy === "MeasureAll")
530
+ return this.itemHeights[index] ?? 0;
531
+ if (this.MeasureItemsStrategy === "MeasureVisible")
532
+ return this.mvHeights[index] || this.MvStride(this.Spacing * this.RenderingScale) - this.Spacing * this.RenderingScale;
533
+ return this.uniformHeight;
534
+ }
535
+ // ---- arrange ----
536
+ OnLayoutChanged() {
537
+ if (this.IsTemplatedList)
538
+ return; // cells are arranged per frame for the visible range only
539
+ const scale = this.RenderingScale;
540
+ const p = this.Padding;
541
+ const r = this.DrawingRect;
542
+ const inner = new SKRect(r.Left + p.Left * scale, r.Top + p.Top * scale, r.Right - p.Right * scale, r.Bottom - p.Bottom * scale);
543
+ const gap = this.Spacing * scale;
544
+ let cursor = this.Type === "Row" ? inner.Left : inner.Top;
545
+ if (this.IsWrapFlow) {
546
+ for (const s of this.wrapSlots)
547
+ s.view.Arrange(SKRect.Create(inner.Left + s.x, inner.Top + s.y, s.w, s.h), s.view.WidthRequest, s.view.HeightRequest, scale);
548
+ return;
549
+ }
550
+ if (this.Type === "Grid") {
551
+ const g = this.GridStructure;
552
+ if (!g)
553
+ return;
554
+ for (const v of this.LayoutViews()) {
555
+ if (!v.IsVisible)
556
+ continue;
557
+ const c = g.GetCellBoundsFor(v, inner.Left / scale, inner.Top / scale);
558
+ v.Arrange(SKRect.Create(c.Left * scale, c.Top * scale, c.Width * scale, c.Height * scale), v.WidthRequest, v.HeightRequest, scale);
559
+ }
560
+ return;
561
+ }
562
+ for (const v of this.LayoutViews()) {
563
+ if (!v.IsVisible)
564
+ continue;
565
+ if (this.Type === "Column") {
566
+ const h = v.MeasuredSize.Pixels.Height;
567
+ v.Arrange(new SKRect(inner.Left, cursor, inner.Right, cursor + h), v.WidthRequest, v.HeightRequest, scale);
568
+ cursor += h + gap;
569
+ }
570
+ else if (this.Type === "Row") {
571
+ const w = v.MeasuredSize.Pixels.Width;
572
+ v.Arrange(new SKRect(cursor, inner.Top, cursor + w, inner.Bottom), v.WidthRequest, v.HeightRequest, scale);
573
+ cursor += w + gap;
574
+ }
575
+ else {
576
+ v.Arrange(inner, v.WidthRequest, v.HeightRequest, scale);
577
+ }
578
+ }
579
+ }
580
+ Paint(ctx) {
581
+ if (this.IsTemplatedList) {
582
+ this.PaintTemplated(ctx);
583
+ return;
584
+ }
585
+ const composing = this.IsRenderingWithComposition;
586
+ for (const v of this.GetOrderedSubviews())
587
+ if (!composing || this.DirtyChildrenInternal.has(v))
588
+ v.Render(ctx);
589
+ }
590
+ GetCompositeChildren() { return this.IsTemplatedList ? [] : this.GetOrderedSubviews(); }
591
+ /** Realizes, binds, arranges and draws only the cells intersecting the visible viewport (+ inflation). */
592
+ PaintTemplated(ctx) {
593
+ const items = this.itemsSource;
594
+ const scale = this.RenderingScale;
595
+ const r = this.DrawingRect;
596
+ const p = this.Padding;
597
+ const gap = this.Spacing * scale;
598
+ const left = r.Left + p.Left * scale;
599
+ const width = r.Width - p.HorizontalThickness * scale;
600
+ const viewport = this.GetVisibleViewport();
601
+ const inflate = this.VirtualisationInflated * scale;
602
+ const visTop = viewport.Top - inflate, visBottom = viewport.Bottom + inflate;
603
+ if (this.MeasureItemsStrategy === "MeasureVisible") {
604
+ this.PaintMeasureVisible(ctx, visTop, visBottom, left, width);
605
+ return;
606
+ }
607
+ let first = -1, last = -1;
608
+ if (items.length > 0 && visBottom > visTop) {
609
+ if (this.MeasureItemsStrategy === "MeasureAll") {
610
+ let y = r.Top + p.Top * scale;
611
+ for (let i = 0; i < items.length; i++) {
612
+ const h = this.itemHeights[i] ?? 0;
613
+ if (y + h >= visTop && y <= visBottom) {
614
+ if (first < 0)
615
+ first = i;
616
+ last = i;
617
+ }
618
+ else if (first >= 0)
619
+ break;
620
+ y += h + gap;
621
+ }
622
+ }
623
+ else {
624
+ const stride = this.uniformHeight + gap;
625
+ if (stride > 0) {
626
+ first = Math.max(0, Math.floor((visTop - (r.Top + p.Top * scale)) / stride));
627
+ last = Math.min(items.length - 1, Math.floor((visBottom - (r.Top + p.Top * scale)) / stride));
628
+ }
629
+ }
630
+ }
631
+ this.FirstVisibleIndex = first;
632
+ this.LastVisibleIndex = last;
633
+ if (first < 0) {
634
+ this.ChildrenFactory.ReleaseOutside(1, 0);
635
+ return;
636
+ }
637
+ this.ChildrenFactory.ReleaseOutside(first, last);
638
+ for (let i = first; i <= last; i++) {
639
+ const view = this.ChildrenFactory.GetOrCreateViewForIndex(i);
640
+ if (!view)
641
+ continue;
642
+ const top = r.Top + this.GetItemOffsetPixels(i);
643
+ const h = this.ItemHeightPixels(i);
644
+ view.Measure(width, Infinity, scale); // recycled cells carry new content; height stays the structure's
645
+ view.Arrange(SKRect.Create(left, top, width, h), view.WidthRequest, view.HeightRequest, scale);
646
+ view.Render(ctx);
647
+ }
648
+ }
649
+ /**
650
+ * MeasureVisible frame: the first visible index comes from the exact prefix (binary search) or the estimate,
651
+ * then cells are laid out contiguously with their REAL measured heights (measured on demand, kept for the prefix).
652
+ */
653
+ PaintMeasureVisible(ctx, visTop, visBottom, left, width) {
654
+ const n = this.mvHeights.length;
655
+ const scale = this.RenderingScale, gap = this.Spacing * scale;
656
+ const top0 = this.DrawingRect.Top + this.Padding.Top * scale;
657
+ let first = -1, last = -1;
658
+ if (n > 0 && visBottom > visTop) {
659
+ const rel = visTop - top0;
660
+ if (rel <= this.mvPrefix[this.mvMeasured]) {
661
+ let lo = 0, hi = this.mvMeasured; // largest i with prefix[i] <= rel
662
+ while (lo < hi) {
663
+ const mid = (lo + hi + 1) >> 1;
664
+ if (this.mvPrefix[mid] <= rel)
665
+ lo = mid;
666
+ else
667
+ hi = mid - 1;
668
+ }
669
+ first = Math.min(lo, n - 1);
670
+ }
671
+ else {
672
+ const stride = this.MvStride(gap);
673
+ first = Math.min(n - 1, this.mvMeasured + (stride > 0 ? Math.floor((rel - this.mvPrefix[this.mvMeasured]) / stride) : 0));
674
+ }
675
+ first = Math.max(0, first);
676
+ }
677
+ if (first < 0) {
678
+ this.FirstVisibleIndex = this.LastVisibleIndex = -1;
679
+ this.ChildrenFactory.ReleaseOutside(1, 0);
680
+ return;
681
+ }
682
+ let y = top0 + this.GetItemOffsetPixels(first) - this.Padding.Top * scale;
683
+ const drawn = [];
684
+ for (let i = first; i < n && y <= visBottom; i++) {
685
+ const h = this.MvMeasure(i, width, scale, false);
686
+ const view = this.ChildrenFactory.GetOrCreateViewForIndex(i);
687
+ if (view && y + h >= visTop)
688
+ drawn.push({ view, top: y, h });
689
+ last = i;
690
+ y += h + gap;
691
+ }
692
+ this.MvExtendPrefix(gap);
693
+ this.FirstVisibleIndex = first;
694
+ this.LastVisibleIndex = last;
695
+ this.ChildrenFactory.ReleaseOutside(first, last);
696
+ for (const d of drawn) {
697
+ d.view.Measure(width, Infinity, scale);
698
+ d.view.Arrange(SKRect.Create(left, d.top, width, d.h), d.view.WidthRequest, d.view.HeightRequest, scale);
699
+ d.view.Render(ctx);
700
+ }
701
+ this.MvScheduleBackground();
702
+ }
703
+ }
704
+ /** SkiaLayout Type=Column + HorizontalOptions=Fill (DrawnUi alias). */
705
+ export class SkiaStack extends SkiaLayout {
706
+ constructor() { super(); this.Type = "Column"; this.HorizontalOptions = "Fill"; }
707
+ }
708
+ /** SkiaLayout Type=Row (DrawnUi alias). */
709
+ export class SkiaRow extends SkiaLayout {
710
+ constructor() { super(); this.Type = "Row"; }
711
+ }
712
+ /** SkiaLayout Type=Absolute + HorizontalOptions=Fill (DrawnUi alias). */
713
+ export class SkiaLayer extends SkiaLayout {
714
+ constructor() { super(); this.Type = "Absolute"; this.HorizontalOptions = "Fill"; }
715
+ }
716
+ /** SkiaLayout Type=Grid + HorizontalOptions=Fill (DrawnUi alias): MAUI Grid alternative. */
717
+ export class SkiaGrid extends SkiaLayout {
718
+ constructor() { super(); this.Type = "Grid"; this.HorizontalOptions = "Fill"; }
719
+ }
720
+ /** SkiaLayout Type=Wrap + HorizontalOptions=Fill (DrawnUi alias): responsive flow of fixed-size children. */
721
+ export class SkiaWrap extends SkiaLayout {
722
+ constructor() { super(); this.Type = "Wrap"; this.HorizontalOptions = "Fill"; }
723
+ }