1export interface ReservedRegion {
2 readonly kind: "division" | "occlusion";
3 readonly x: number;
4 readonly y: number;
5 readonly width: number;
6 readonly height: number;
7}
8
9export interface NativeLayoutMetrics {
10 readonly width: number;
11 readonly height: number;
12 readonly hasHinge?: boolean;
13 readonly horizontalSizeClass: "compact" | "regular";
14 readonly verticalBarEdge: "none" | "left" | "right";
15 readonly safeArea: {
16 readonly top: number;
17 readonly bottom: number;
18 readonly left: number;
19 readonly right: number;
20 };
21 readonly reservedRegions: ReadonlyArray<ReservedRegion>;
22}
23
24/**
25 * Keep bottom controls in one usable region. A book-like fold selects the
26 * larger side; a horizontal fold leaves the controls on the lower display.
27 * Text and other scrollable content can continue to use the full canvas.
28 */
29export function deriveBottomControlInsets(metrics: NativeLayoutMetrics | null) {
30 if (!metrics) return { left: 0, right: 0, bottom: 0 };
31 let left = metrics.safeArea.left;
32 let right = metrics.width - metrics.safeArea.right;
33 let bottom = metrics.height;
34 for (const region of metrics.reservedRegions) {
35 const x = Math.max(left, region.x);
36 const endX = Math.min(right, region.x + region.width);
37 const y = Math.max(0, region.y);
38 const endY = Math.min(bottom, region.y + region.height);
39 if (endX <= x || endY <= y) continue;
40 if (region.kind === "division" && region.height >= metrics.height / 2) {
41 if (right - endX >= x - left) left = endX;
42 else right = x;
43 } else if (endY >= bottom - metrics.safeArea.bottom) {
44 // An occlusion at the bottom can also occupy just one corner.
45 if (region.width >= right - left) bottom = y;
46 else if (right - endX >= x - left) left = endX;
47 else right = x;
48 }
49 }
50 return { left, right: metrics.width - right, bottom: metrics.height - bottom };
51}