@deck.gl/widgets 9.3.7 → 9.4.0-alpha.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.
@@ -0,0 +1,897 @@
1
+ // deck.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {View} from '@deck.gl/core';
6
+
7
+ import {
8
+ assertViewLayout,
9
+ getViewLayoutChildren,
10
+ getViewLayoutType,
11
+ isViewLayout
12
+ } from './view-layout';
13
+
14
+ import type {
15
+ ViewLayoutChild,
16
+ ColumnViewLayout,
17
+ OverlayViewLayout,
18
+ ViewLayoutWithChildren,
19
+ SplitViewLayout,
20
+ ViewLayoutInsets,
21
+ ViewLayout,
22
+ ViewLayoutLength,
23
+ RowViewLayout
24
+ } from './view-layout';
25
+
26
+ /** Rectangle resolved by the layout compiler for one view id. */
27
+ export type ViewLayoutRect = {
28
+ /** Left coordinate in deck pixels. */
29
+ x: number;
30
+ /** Top coordinate in deck pixels. */
31
+ y: number;
32
+ /** Width in deck pixels. */
33
+ width: number;
34
+ /** Height in deck pixels. */
35
+ height: number;
36
+ };
37
+
38
+ /** Concrete views plus resolved rectangles produced by `buildViewsFromViewLayout`. */
39
+ export type CompiledDeckViews = {
40
+ /** Concrete deck.gl views with numeric bounds resolved for the current canvas size. */
41
+ views: View[];
42
+ /** Resolved rectangles keyed by deck view id. */
43
+ rectsById: Record<string, ViewLayoutRect>;
44
+ /** User-adjustable split handles keyed by split id. */
45
+ splittersById: Record<string, ViewLayoutSplitter>;
46
+ };
47
+
48
+ /** Layout-only view prop overrides keyed by deck view id. */
49
+ export type ViewLayoutViewPropsById = Readonly<Record<string, ViewLayoutViewOverrideProps>>;
50
+
51
+ /** Layout-only props that can be controlled outside the static layout tree. */
52
+ export type ViewLayoutViewOverrideProps = {
53
+ /** Left offset relative to the current parent bounds. */
54
+ x?: ViewLayoutLength;
55
+ /** Top offset relative to the current parent bounds. */
56
+ y?: ViewLayoutLength;
57
+ /** Width expression resolved against the current parent bounds. */
58
+ width?: ViewLayoutLength;
59
+ /** Height expression resolved against the current parent bounds. */
60
+ height?: ViewLayoutLength;
61
+ };
62
+
63
+ /** Values for user-adjustable splits keyed by split id. */
64
+ export type ViewLayoutSplitValues = Readonly<Record<string, number>>;
65
+
66
+ /** Resolved splitter metadata produced by `buildViewsFromViewLayout`. */
67
+ export type ViewLayoutSplitter = {
68
+ /** Stable id for this split. */
69
+ id: string;
70
+ /** Stacking orientation controlled by this split. */
71
+ orientation: 'horizontal' | 'vertical';
72
+ /** Left position as a percentage of the deck canvas. */
73
+ x: number;
74
+ /** Top position as a percentage of the deck canvas. */
75
+ y: number;
76
+ /** Width as a percentage of the deck canvas. */
77
+ width: number;
78
+ /** Height as a percentage of the deck canvas. */
79
+ height: number;
80
+ /** Ratio of the split boundary over the split axis. */
81
+ split: number;
82
+ /** Minimum split ratio. */
83
+ minSplit: number;
84
+ /** Maximum split ratio. */
85
+ maxSplit: number;
86
+ };
87
+
88
+ /** Minimal deck.gl view props shape needed by the layout compiler. */
89
+ type LayoutAwareViewProps = {
90
+ /** Stable deck.gl view id. */
91
+ id?: string;
92
+ /** Left offset relative to the current parent bounds. */
93
+ x?: ViewLayoutLength;
94
+ /** Top offset relative to the current parent bounds. */
95
+ y?: ViewLayoutLength;
96
+ /** Width expression resolved against the current parent bounds. */
97
+ width?: ViewLayoutLength;
98
+ /** Height expression resolved against the current parent bounds. */
99
+ height?: ViewLayoutLength;
100
+ /** Minimum size in pixels along the parent stack axis. */
101
+ minPixels?: number;
102
+ /** Maximum size in pixels along the parent stack axis. */
103
+ maxPixels?: number;
104
+ } & Record<string, unknown>;
105
+
106
+ type StackChildAxisConstraints = {
107
+ explicitLength?: number;
108
+ minPixels: number;
109
+ maxPixels: number;
110
+ };
111
+
112
+ type StackSplitConfig = {
113
+ splitId: string;
114
+ split: number;
115
+ minSplit: number;
116
+ maxSplit: number;
117
+ };
118
+
119
+ /**
120
+ * Compiles one `ViewLayout` tree into concrete deck.gl views and resolved rectangles.
121
+ *
122
+ * @param args - Root layout plus the current canvas size and optional previous compilation.
123
+ * @returns Concrete views with numeric bounds plus resolved rectangles keyed by view id.
124
+ */
125
+ export function buildViewsFromViewLayout(args: {
126
+ /** Root layout tree to compile. */
127
+ layout: ViewLayout;
128
+ /** Current deck width in pixels. */
129
+ width: number;
130
+ /** Current deck height in pixels. */
131
+ height: number;
132
+ /** Optional previous compilation used for structural view reuse. */
133
+ previous?: CompiledDeckViews;
134
+ /** Controlled split values keyed by layout `splitId`. */
135
+ splitValues?: ViewLayoutSplitValues;
136
+ /** Controlled layout-only view prop overrides keyed by deck view id. */
137
+ viewPropsById?: ViewLayoutViewPropsById;
138
+ }): CompiledDeckViews {
139
+ assertViewLayout(args.layout);
140
+
141
+ const rootRect: ViewLayoutRect = {
142
+ x: 0,
143
+ y: 0,
144
+ width: Math.max(1, args.width),
145
+ height: Math.max(1, args.height)
146
+ };
147
+ const previousViewsById = new Map(
148
+ (args.previous?.views ?? [])
149
+ .map(view => [getViewId(view), view] as const)
150
+ .filter((entry): entry is [string, View] => Boolean(entry[0]))
151
+ );
152
+ const outputViews: View[] = [];
153
+ const rectsById: Record<string, ViewLayoutRect> = {};
154
+ const splittersById: Record<string, ViewLayoutSplitter> = {};
155
+
156
+ compileLayoutItem({
157
+ item: args.layout,
158
+ rect: rootRect,
159
+ rootRect,
160
+ outputViews,
161
+ rectsById,
162
+ splittersById,
163
+ previousViewsById,
164
+ splitValues: args.splitValues ?? {},
165
+ viewPropsById: args.viewPropsById ?? {}
166
+ });
167
+
168
+ return {
169
+ views: outputViews,
170
+ rectsById,
171
+ splittersById
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Compiles one validated layout node inside the given parent rectangle.
177
+ *
178
+ * @param args - Layout item, allocated bounds, and compilation accumulators.
179
+ */
180
+ function compileLayoutItem(args: {
181
+ item: ViewLayout;
182
+ rect: ViewLayoutRect;
183
+ rootRect: ViewLayoutRect;
184
+ outputViews: View[];
185
+ rectsById: Record<string, ViewLayoutRect>;
186
+ splittersById: Record<string, ViewLayoutSplitter>;
187
+ previousViewsById: Map<string, View>;
188
+ splitValues: ViewLayoutSplitValues;
189
+ viewPropsById: ViewLayoutViewPropsById;
190
+ }): void {
191
+ const {item} = args;
192
+
193
+ const type = getViewLayoutType(item);
194
+ switch (type) {
195
+ case 'row':
196
+ compileStackLayout({
197
+ ...args,
198
+ item: item as RowViewLayout | ColumnViewLayout | SplitViewLayout,
199
+ orientation: 'row',
200
+ children: getViewLayoutChildren(item as ViewLayoutWithChildren)
201
+ });
202
+ return;
203
+ case 'column':
204
+ compileStackLayout({
205
+ ...args,
206
+ item: item as RowViewLayout | ColumnViewLayout | SplitViewLayout,
207
+ orientation: 'column',
208
+ children: getViewLayoutChildren(item as ViewLayoutWithChildren)
209
+ });
210
+ return;
211
+ case 'overlay':
212
+ compileOverlayLayout({
213
+ ...args,
214
+ item: item as OverlayViewLayout,
215
+ children: getViewLayoutChildren(item as ViewLayoutWithChildren)
216
+ });
217
+ return;
218
+ case 'spacer':
219
+ return;
220
+ default:
221
+ throw new Error(`Unsupported view layout item: ${String(type)}`);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Compiles one stack container by splitting the stack axis across its children.
227
+ *
228
+ * @param args - Stack orientation, children, and compilation accumulators.
229
+ */
230
+ function compileStackLayout(args: {
231
+ item: RowViewLayout | ColumnViewLayout | SplitViewLayout;
232
+ rect: ViewLayoutRect;
233
+ rootRect: ViewLayoutRect;
234
+ orientation: 'row' | 'column';
235
+ children: readonly ViewLayoutChild[];
236
+ outputViews: View[];
237
+ rectsById: Record<string, ViewLayoutRect>;
238
+ splittersById: Record<string, ViewLayoutSplitter>;
239
+ previousViewsById: Map<string, View>;
240
+ splitValues: ViewLayoutSplitValues;
241
+ viewPropsById: ViewLayoutViewPropsById;
242
+ }): void {
243
+ const containerRect = applyInsets(args.rect, args.item.inset);
244
+ const children = args.children.filter(isPresentChild);
245
+ if (children.length === 0) {
246
+ return;
247
+ }
248
+
249
+ const containerAxisLength =
250
+ args.orientation === 'row' ? containerRect.width : containerRect.height;
251
+ const stackAxisConfig = getStackAxisConfig({
252
+ children,
253
+ containerAxisLength,
254
+ item: args.item,
255
+ orientation: args.orientation,
256
+ splitValues: args.splitValues
257
+ });
258
+
259
+ let cursor = args.orientation === 'row' ? containerRect.x : containerRect.y;
260
+ for (const splitConfig of stackAxisConfig.splitConfigs) {
261
+ args.splittersById[splitConfig.splitId] = {
262
+ id: splitConfig.splitId,
263
+ orientation: args.orientation === 'row' ? 'horizontal' : 'vertical',
264
+ x: (containerRect.x / args.rootRect.width) * 100,
265
+ y: (containerRect.y / args.rootRect.height) * 100,
266
+ width: (containerRect.width / args.rootRect.width) * 100,
267
+ height: (containerRect.height / args.rootRect.height) * 100,
268
+ split: splitConfig.split,
269
+ minSplit: splitConfig.minSplit,
270
+ maxSplit: splitConfig.maxSplit
271
+ };
272
+ }
273
+
274
+ children.forEach((child, index) => {
275
+ const childAxisLength = stackAxisConfig.childAxisLengths[index];
276
+ const childRect =
277
+ args.orientation === 'row'
278
+ ? {
279
+ x: cursor,
280
+ y: containerRect.y,
281
+ width: childAxisLength,
282
+ height: containerRect.height
283
+ }
284
+ : {
285
+ x: containerRect.x,
286
+ y: cursor,
287
+ width: containerRect.width,
288
+ height: childAxisLength
289
+ };
290
+ cursor += childAxisLength;
291
+ compileLayoutChild({
292
+ child,
293
+ rect: childRect,
294
+ rootRect: args.rootRect,
295
+ outputViews: args.outputViews,
296
+ rectsById: args.rectsById,
297
+ splittersById: args.splittersById,
298
+ previousViewsById: args.previousViewsById,
299
+ splitValues: args.splitValues,
300
+ viewPropsById: args.viewPropsById
301
+ });
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Compiles one overlay container by giving every child the same parent rectangle.
307
+ *
308
+ * @param args - Overlay children, parent rectangle, and compilation accumulators.
309
+ */
310
+ function compileOverlayLayout(args: {
311
+ item: OverlayViewLayout;
312
+ rect: ViewLayoutRect;
313
+ rootRect: ViewLayoutRect;
314
+ children: readonly ViewLayoutChild[];
315
+ outputViews: View[];
316
+ rectsById: Record<string, ViewLayoutRect>;
317
+ splittersById: Record<string, ViewLayoutSplitter>;
318
+ previousViewsById: Map<string, View>;
319
+ splitValues: ViewLayoutSplitValues;
320
+ viewPropsById: ViewLayoutViewPropsById;
321
+ }): void {
322
+ const containerRect = applyInsets(args.rect, args.item.inset);
323
+ args.children.filter(isPresentChild).forEach(child =>
324
+ compileLayoutChild({
325
+ child,
326
+ rect: containerRect,
327
+ rootRect: args.rootRect,
328
+ outputViews: args.outputViews,
329
+ rectsById: args.rectsById,
330
+ splittersById: args.splittersById,
331
+ previousViewsById: args.previousViewsById,
332
+ splitValues: args.splitValues,
333
+ viewPropsById: args.viewPropsById
334
+ })
335
+ );
336
+ }
337
+
338
+ /**
339
+ * Compiles one child entry, which may be a nested layout object or a raw deck view.
340
+ *
341
+ * @param args - Child node plus the rectangle allocated for it.
342
+ */
343
+ function compileLayoutChild(args: {
344
+ child: Exclude<ViewLayoutChild, null | false | undefined>;
345
+ rect: ViewLayoutRect;
346
+ rootRect: ViewLayoutRect;
347
+ outputViews: View[];
348
+ rectsById: Record<string, ViewLayoutRect>;
349
+ splittersById: Record<string, ViewLayoutSplitter>;
350
+ previousViewsById: Map<string, View>;
351
+ splitValues: ViewLayoutSplitValues;
352
+ viewPropsById: ViewLayoutViewPropsById;
353
+ }): void {
354
+ if (isViewLayout(args.child)) {
355
+ compileLayoutItem({
356
+ item: args.child,
357
+ rect: args.rect,
358
+ rootRect: args.rootRect,
359
+ outputViews: args.outputViews,
360
+ rectsById: args.rectsById,
361
+ splittersById: args.splittersById,
362
+ previousViewsById: args.previousViewsById,
363
+ splitValues: args.splitValues,
364
+ viewPropsById: args.viewPropsById
365
+ });
366
+ return;
367
+ }
368
+
369
+ compileResolvedView({
370
+ view: args.child,
371
+ rect: args.rect,
372
+ outputViews: args.outputViews,
373
+ rectsById: args.rectsById,
374
+ previousViewsById: args.previousViewsById,
375
+ viewPropsById: args.viewPropsById
376
+ });
377
+ }
378
+
379
+ function getStackAxisConfig(args: {
380
+ children: Array<Exclude<ViewLayoutChild, null | false | undefined>>;
381
+ containerAxisLength: number;
382
+ item: RowViewLayout | ColumnViewLayout | SplitViewLayout;
383
+ orientation: 'row' | 'column';
384
+ splitValues: ViewLayoutSplitValues;
385
+ }): {childAxisLengths: number[]; splitConfigs: StackSplitConfig[]} {
386
+ const constraints = args.children.map(child =>
387
+ getStackChildAxisConstraints(child, args.orientation, args.containerAxisLength)
388
+ );
389
+ if (args.item.splitId) {
390
+ return getSplitStackAxisConfig({...args, constraints});
391
+ }
392
+ return {
393
+ childAxisLengths: getConstrainedStackChildAxisLengths(args.containerAxisLength, constraints),
394
+ splitConfigs: []
395
+ };
396
+ }
397
+
398
+ function getSplitStackAxisConfig(args: {
399
+ children: Array<Exclude<ViewLayoutChild, null | false | undefined>>;
400
+ containerAxisLength: number;
401
+ item: RowViewLayout | ColumnViewLayout | SplitViewLayout;
402
+ orientation: 'row' | 'column';
403
+ splitValues: ViewLayoutSplitValues;
404
+ constraints: StackChildAxisConstraints[];
405
+ }): {childAxisLengths: number[]; splitConfigs: StackSplitConfig[]} {
406
+ const axisLength = Math.max(args.containerAxisLength, 1);
407
+ const boundaries = getDefaultSplitBoundaries(args);
408
+ const splitConfigs: StackSplitConfig[] = [];
409
+
410
+ for (let index = 0; index < boundaries.length; index++) {
411
+ const splitId = getSplitId(args.item.splitId!, args.children.length, index);
412
+ const desiredSplit =
413
+ args.splitValues[splitId] ??
414
+ (args.children.length === 2 ? args.item.initialSplit : undefined) ??
415
+ boundaries[index];
416
+ const {minSplit, maxSplit} = getSplitBoundaryLimits({
417
+ index,
418
+ boundaries,
419
+ constraints: args.constraints,
420
+ axisLength,
421
+ item: args.item
422
+ });
423
+ const split = Math.min(Math.max(desiredSplit, minSplit), maxSplit);
424
+ boundaries[index] = split;
425
+ splitConfigs.push({splitId, split, minSplit, maxSplit});
426
+ }
427
+
428
+ return {
429
+ childAxisLengths: getStackAxisLengthsFromBoundaries(args.containerAxisLength, boundaries),
430
+ splitConfigs
431
+ };
432
+ }
433
+
434
+ function getDefaultSplitBoundaries(args: {
435
+ children: Array<Exclude<ViewLayoutChild, null | false | undefined>>;
436
+ containerAxisLength: number;
437
+ constraints: StackChildAxisConstraints[];
438
+ }): number[] {
439
+ const childAxisLengths = getConstrainedStackChildAxisLengths(
440
+ args.containerAxisLength,
441
+ args.constraints
442
+ );
443
+ const totalLength = childAxisLengths.reduce((sum, length) => sum + length, 0);
444
+ if (totalLength <= 0) {
445
+ return args.children.slice(1).map((_, index) => (index + 1) / args.children.length);
446
+ }
447
+
448
+ let cursor = 0;
449
+ return childAxisLengths.slice(0, -1).map(length => {
450
+ cursor += length;
451
+ return cursor / totalLength;
452
+ });
453
+ }
454
+
455
+ function getSplitBoundaryLimits(args: {
456
+ index: number;
457
+ boundaries: number[];
458
+ constraints: StackChildAxisConstraints[];
459
+ axisLength: number;
460
+ item: RowViewLayout | ColumnViewLayout | SplitViewLayout;
461
+ }): {minSplit: number; maxSplit: number} {
462
+ const previousSplit = args.index === 0 ? 0 : args.boundaries[args.index - 1];
463
+ const nextSplit = args.index === args.boundaries.length - 1 ? 1 : args.boundaries[args.index + 1];
464
+ const leftConstraints = args.constraints[args.index];
465
+ const rightConstraints = args.constraints[args.index + 1];
466
+ const leftMaxSplit = Number.isFinite(leftConstraints.maxPixels)
467
+ ? previousSplit + leftConstraints.maxPixels / args.axisLength
468
+ : 1;
469
+ const rightMaxSplit = Number.isFinite(rightConstraints.maxPixels)
470
+ ? nextSplit - rightConstraints.maxPixels / args.axisLength
471
+ : 0;
472
+ const minSplit = Math.max(
473
+ args.item.minSplit ?? 0.05,
474
+ previousSplit + leftConstraints.minPixels / args.axisLength,
475
+ rightMaxSplit
476
+ );
477
+ const maxSplit = Math.max(
478
+ minSplit,
479
+ Math.min(
480
+ args.item.maxSplit ?? 0.95,
481
+ nextSplit - rightConstraints.minPixels / args.axisLength,
482
+ leftMaxSplit
483
+ )
484
+ );
485
+ return {minSplit, maxSplit};
486
+ }
487
+
488
+ function getSplitId(splitId: string, childCount: number, index: number): string {
489
+ return childCount === 2 ? splitId : `${splitId}-${index}`;
490
+ }
491
+
492
+ function getStackAxisLengthsFromBoundaries(
493
+ containerAxisLength: number,
494
+ boundaries: number[]
495
+ ): number[] {
496
+ let previousBoundary = 0;
497
+ const childAxisLengths = boundaries.map(boundary => {
498
+ const length = containerAxisLength * (boundary - previousBoundary);
499
+ previousBoundary = boundary;
500
+ return length;
501
+ });
502
+ childAxisLengths.push(containerAxisLength * (1 - previousBoundary));
503
+ return childAxisLengths;
504
+ }
505
+
506
+ function getConstrainedStackChildAxisLengths(
507
+ containerAxisLength: number,
508
+ constraints: StackChildAxisConstraints[]
509
+ ): number[] {
510
+ const {childAxisLengths, flexibleChildIndexes, fixedLength} =
511
+ getInitialStackChildAxisLengths(constraints);
512
+ let remainingLength = Math.max(containerAxisLength - fixedLength, 0);
513
+ let unsettledIndexes = flexibleChildIndexes;
514
+
515
+ while (unsettledIndexes.length > 0) {
516
+ const sharedLength = remainingLength / unsettledIndexes.length;
517
+ const nextIndexes: number[] = [];
518
+ let didClamp = false;
519
+
520
+ for (const index of unsettledIndexes) {
521
+ const {minPixels, maxPixels} = constraints[index];
522
+ if (sharedLength < minPixels) {
523
+ childAxisLengths[index] = minPixels;
524
+ remainingLength -= minPixels;
525
+ didClamp = true;
526
+ } else if (sharedLength > maxPixels) {
527
+ childAxisLengths[index] = maxPixels;
528
+ remainingLength -= maxPixels;
529
+ didClamp = true;
530
+ } else {
531
+ nextIndexes.push(index);
532
+ }
533
+ }
534
+
535
+ if (!didClamp) {
536
+ for (const index of unsettledIndexes) {
537
+ childAxisLengths[index] = sharedLength;
538
+ }
539
+ break;
540
+ }
541
+ unsettledIndexes = nextIndexes;
542
+ }
543
+
544
+ return childAxisLengths.map(length => length ?? 0);
545
+ }
546
+
547
+ function getInitialStackChildAxisLengths(constraints: StackChildAxisConstraints[]): {
548
+ childAxisLengths: Array<number | undefined>;
549
+ flexibleChildIndexes: number[];
550
+ fixedLength: number;
551
+ } {
552
+ const childAxisLengths = constraints.map(({explicitLength}) => explicitLength);
553
+ const flexibleChildIndexes: number[] = [];
554
+ let fixedLength = 0;
555
+
556
+ childAxisLengths.forEach((length, index) => {
557
+ if (length === undefined) {
558
+ flexibleChildIndexes.push(index);
559
+ } else {
560
+ fixedLength += length;
561
+ }
562
+ });
563
+
564
+ return {childAxisLengths, flexibleChildIndexes, fixedLength};
565
+ }
566
+
567
+ /**
568
+ * Resolves one raw deck view against a parent rectangle and reuses a previous instance when equal.
569
+ *
570
+ * @param args - View instance, allocated rectangle, and compilation accumulators.
571
+ */
572
+ function compileResolvedView(args: {
573
+ view: View;
574
+ rect: ViewLayoutRect;
575
+ outputViews: View[];
576
+ rectsById: Record<string, ViewLayoutRect>;
577
+ previousViewsById: Map<string, View>;
578
+ viewPropsById: ViewLayoutViewPropsById;
579
+ }): void {
580
+ const viewId = getRequiredViewId(args.view);
581
+ const resolvedRect = resolveViewRectWithinParent(
582
+ args.view,
583
+ args.rect,
584
+ args.viewPropsById[viewId]
585
+ );
586
+ args.rectsById[viewId] = resolvedRect;
587
+ const nextProps = {
588
+ ...args.view.props,
589
+ x: resolvedRect.x,
590
+ y: resolvedRect.y,
591
+ width: resolvedRect.width,
592
+ height: resolvedRect.height
593
+ };
594
+ const previousView = args.previousViewsById.get(viewId);
595
+ const nextView =
596
+ previousView &&
597
+ previousView.constructor === args.view.constructor &&
598
+ arePropsEqual(previousView.props as Record<string, unknown>, nextProps)
599
+ ? previousView
600
+ : instantiateView(args.view, nextProps);
601
+ args.outputViews.push(nextView);
602
+ }
603
+
604
+ /**
605
+ * Resolves one stack child's sizing constraints in the active stack axis.
606
+ *
607
+ * @param child - Stack child to inspect.
608
+ * @param orientation - Active stack orientation.
609
+ * @param parentAxisLength - Parent length in the active stack axis.
610
+ * @returns Explicit length and pixel constraints in the active stack axis.
611
+ */
612
+ function getStackChildAxisConstraints(
613
+ child: Exclude<ViewLayoutChild, null | false | undefined>,
614
+ orientation: 'row' | 'column',
615
+ parentAxisLength: number
616
+ ): StackChildAxisConstraints {
617
+ const axisValue = getAxisLengthValue(child, orientation);
618
+ const minPixels = getPixelConstraintValue(child, 'minPixels') ?? 0;
619
+ const maxPixels = getPixelConstraintValue(child, 'maxPixels') ?? Number.POSITIVE_INFINITY;
620
+ if (axisValue === undefined) {
621
+ return {minPixels, maxPixels};
622
+ }
623
+ const explicitLength = Math.min(
624
+ Math.max(resolveLength(axisValue, parentAxisLength), minPixels),
625
+ maxPixels
626
+ );
627
+ return {explicitLength, minPixels, maxPixels};
628
+ }
629
+
630
+ /**
631
+ * Resolves one raw deck view rectangle within the given parent rectangle.
632
+ *
633
+ * @param view - Raw deck view instance to position.
634
+ * @param parentRect - Parent rectangle allocated by the layout compiler.
635
+ * @returns Concrete rectangle for the compiled view.
636
+ */
637
+ function resolveViewRectWithinParent(
638
+ view: View,
639
+ parentRect: ViewLayoutRect,
640
+ overrides?: ViewLayoutViewOverrideProps
641
+ ): ViewLayoutRect {
642
+ const props = {...(view.props as LayoutAwareViewProps), ...overrides};
643
+ const x = parentRect.x + resolveOffset(props.x, parentRect.width);
644
+ const y = parentRect.y + resolveOffset(props.y, parentRect.height);
645
+ const width = resolveDimension(props.width, parentRect.width, parentRect.width);
646
+ const height = resolveDimension(props.height, parentRect.height, parentRect.height);
647
+ return {
648
+ x,
649
+ y,
650
+ width,
651
+ height
652
+ };
653
+ }
654
+
655
+ /**
656
+ * Re-instantiates one deck view with a new resolved props object.
657
+ *
658
+ * @param view - Existing deck view instance whose constructor should be reused.
659
+ * @param props - Resolved props for the compiled view.
660
+ * @returns Fresh deck view instance with resolved numeric bounds.
661
+ */
662
+ function instantiateView(view: View, props: Record<string, unknown>): View {
663
+ const ViewConstructor = view.constructor as new (viewProps: Record<string, unknown>) => View;
664
+ return new ViewConstructor(props);
665
+ }
666
+
667
+ /**
668
+ * Reads the stack-axis length expression from one child, whether nested or raw.
669
+ *
670
+ * @param child - Candidate child entry.
671
+ * @param orientation - Active stack orientation.
672
+ * @returns Width for rows, height for columns, or `undefined` for flexible children.
673
+ */
674
+ function getAxisLengthValue(
675
+ child: Exclude<ViewLayoutChild, null | false | undefined>,
676
+ orientation: 'row' | 'column'
677
+ ): ViewLayoutLength | undefined {
678
+ if (isViewLayout(child)) {
679
+ return orientation === 'row' ? child.width : child.height;
680
+ }
681
+ const props = child.props as LayoutAwareViewProps;
682
+ return orientation === 'row' ? props.width : props.height;
683
+ }
684
+
685
+ function getPixelConstraintValue(
686
+ child: Exclude<ViewLayoutChild, null | false | undefined>,
687
+ propName: 'minPixels' | 'maxPixels'
688
+ ): number | undefined {
689
+ const value = isViewLayout(child)
690
+ ? child[propName]
691
+ : (child.props as LayoutAwareViewProps)[propName];
692
+ return typeof value === 'number' ? value : undefined;
693
+ }
694
+
695
+ /**
696
+ * Shrinks one rectangle by optional insets.
697
+ *
698
+ * @param rect - Rectangle allocated by the parent layout step.
699
+ * @param inset - Insets to apply inside that rectangle.
700
+ * @returns Inset rectangle clamped to non-negative size.
701
+ */
702
+ function applyInsets(rect: ViewLayoutRect, inset: ViewLayoutInsets | undefined): ViewLayoutRect {
703
+ if (!inset) {
704
+ return rect;
705
+ }
706
+ const top = inset.top ?? 0;
707
+ const right = inset.right ?? 0;
708
+ const bottom = inset.bottom ?? 0;
709
+ const left = inset.left ?? 0;
710
+ return {
711
+ x: rect.x + left,
712
+ y: rect.y + top,
713
+ width: Math.max(0, rect.width - left - right),
714
+ height: Math.max(0, rect.height - top - bottom)
715
+ };
716
+ }
717
+
718
+ /**
719
+ * Resolves one optional x/y offset against a parent length.
720
+ *
721
+ * @param value - Authored offset expression.
722
+ * @param parentLength - Parent width or height used for percentage resolution.
723
+ * @returns Numeric offset in deck pixels.
724
+ */
725
+ function resolveOffset(value: unknown, parentLength: number): number {
726
+ if (value === undefined || value === null) {
727
+ return 0;
728
+ }
729
+ return resolveLength(value as ViewLayoutLength, parentLength);
730
+ }
731
+
732
+ /**
733
+ * Resolves one optional width or height against a parent length.
734
+ *
735
+ * @param value - Authored dimension expression.
736
+ * @param parentLength - Parent width or height used for percentage resolution.
737
+ * @param fallback - Fallback dimension used when no explicit expression is provided.
738
+ * @returns Numeric dimension in deck pixels.
739
+ */
740
+ function resolveDimension(value: unknown, parentLength: number, fallback: number): number {
741
+ if (value === undefined || value === null) {
742
+ return fallback;
743
+ }
744
+ return resolveLength(value as ViewLayoutLength, parentLength);
745
+ }
746
+
747
+ /**
748
+ * Resolves one numeric, percentage, or `calc(...)` length expression.
749
+ *
750
+ * @param value - Authored length expression.
751
+ * @param parentLength - Parent length used for percentage resolution.
752
+ * @returns Numeric length in deck pixels.
753
+ */
754
+ function resolveLength(value: ViewLayoutLength, parentLength: number): number {
755
+ if (typeof value === 'number') {
756
+ return value;
757
+ }
758
+
759
+ const normalized = value.trim();
760
+ if (normalized.length === 0) {
761
+ throw new Error('View layout length must not be empty.');
762
+ }
763
+ if (normalized.startsWith('calc(') && normalized.endsWith(')')) {
764
+ return resolveCalcExpression(normalized.slice(5, -1), parentLength);
765
+ }
766
+ return resolveLengthTerm(normalized, parentLength);
767
+ }
768
+
769
+ /**
770
+ * Resolves one simple `calc(...)` expression made of additive length terms.
771
+ *
772
+ * @param expression - Inner `calc(...)` expression without the wrapper.
773
+ * @param parentLength - Parent length used for percentage resolution.
774
+ * @returns Numeric length in deck pixels.
775
+ */
776
+ function resolveCalcExpression(expression: string, parentLength: number): number {
777
+ const sanitized = expression.replace(/\s+/g, '');
778
+ const terms = sanitized.match(/[+-]?[^+-]+/g);
779
+ if (!terms) {
780
+ throw new Error(`Unsupported calc expression: calc(${expression})`);
781
+ }
782
+ return terms.reduce((sum, term) => sum + resolveLengthTerm(term, parentLength), 0);
783
+ }
784
+
785
+ /**
786
+ * Resolves one atomic length term such as `10`, `10px`, or `50%`.
787
+ *
788
+ * @param term - One atomic length term.
789
+ * @param parentLength - Parent length used for percentage resolution.
790
+ * @returns Numeric length in deck pixels.
791
+ */
792
+ function resolveLengthTerm(term: string, parentLength: number): number {
793
+ if (term.endsWith('%')) {
794
+ return (Number.parseFloat(term.slice(0, -1)) / 100) * parentLength;
795
+ }
796
+ if (term.endsWith('px')) {
797
+ return Number.parseFloat(term.slice(0, -2));
798
+ }
799
+ return Number.parseFloat(term);
800
+ }
801
+
802
+ /**
803
+ * Reads the string id from one deck view when present.
804
+ *
805
+ * @param view - Deck view instance to inspect.
806
+ * @returns String id or `null` when absent.
807
+ */
808
+ function getViewId(view: View): string | null {
809
+ const viewId = (view.props as LayoutAwareViewProps).id;
810
+ return typeof viewId === 'string' && viewId.length > 0 ? viewId : null;
811
+ }
812
+
813
+ /**
814
+ * Reads the required id from one deck view, throwing when missing.
815
+ *
816
+ * @param view - Deck view instance to inspect.
817
+ * @returns Non-empty string id.
818
+ */
819
+ function getRequiredViewId(view: View): string {
820
+ const viewId = getViewId(view);
821
+ if (!viewId) {
822
+ throw new Error('Every compiled deck view must have a string id.');
823
+ }
824
+ return viewId;
825
+ }
826
+
827
+ /**
828
+ * Compares two props objects for structural equality.
829
+ *
830
+ * @param left - Previous props object.
831
+ * @param right - Next props object.
832
+ * @returns Whether both props objects are structurally equal.
833
+ */
834
+ function arePropsEqual(left: Record<string, unknown>, right: Record<string, unknown>): boolean {
835
+ if (left === right) {
836
+ return true;
837
+ }
838
+
839
+ const leftKeys = Object.keys(left);
840
+ const rightKeys = Object.keys(right);
841
+ if (leftKeys.length !== rightKeys.length) {
842
+ return false;
843
+ }
844
+ return leftKeys.every(key => areValuesEqual(left[key], right[key]));
845
+ }
846
+
847
+ /**
848
+ * Recursively compares two primitive, array, or plain-object values.
849
+ *
850
+ * @param left - Previous value.
851
+ * @param right - Next value.
852
+ * @returns Whether both values are structurally equal.
853
+ */
854
+ function areValuesEqual(left: unknown, right: unknown): boolean {
855
+ if (Object.is(left, right)) {
856
+ return true;
857
+ }
858
+ if (Array.isArray(left) && Array.isArray(right)) {
859
+ return (
860
+ left.length === right.length &&
861
+ left.every((value, index) => areValuesEqual(value, right[index]))
862
+ );
863
+ }
864
+ if (isPlainObject(left) && isPlainObject(right)) {
865
+ const leftEntries = Object.keys(left);
866
+ const rightEntries = Object.keys(right);
867
+ return (
868
+ leftEntries.length === rightEntries.length &&
869
+ leftEntries.every(key => areValuesEqual(left[key], right[key]))
870
+ );
871
+ }
872
+ return false;
873
+ }
874
+
875
+ /**
876
+ * Detects plain object records used by structural equality.
877
+ *
878
+ * @param value - Candidate value.
879
+ * @returns Whether the value is a plain object record.
880
+ */
881
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
882
+ return (
883
+ Boolean(value) && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype
884
+ );
885
+ }
886
+
887
+ /**
888
+ * Removes falsey optional children from authored child arrays.
889
+ *
890
+ * @param child - Candidate authored child entry.
891
+ * @returns Whether the child should be compiled.
892
+ */
893
+ function isPresentChild(
894
+ child: ViewLayoutChild
895
+ ): child is Exclude<ViewLayoutChild, null | false | undefined> {
896
+ return Boolean(child);
897
+ }