Files
TREK/client/src/native/windowFold.ts
T
Maurice 6906f80ff8 feat(native): Android tablets get the shortcuts, two panes meet at a foldable's fold
The Android shell now reports keyCommands and windowLayout, so the large
screen layout no longer assumes an iPad:

- The keyboard host hands the shortcuts to the app whenever the shell has
  the keyCommands feature, whatever the platform. Before it also required
  iOS, so an Android tablet with a keyboard answered the keys in the page
  and never listed them in the system's shortcuts.
- useWindowFold follows the layout the Android shell reports (feature
  windowLayout). When a vertical fold separates the window, a foldable half
  open like a book, MobileShell marks it with data-fold and its place, and
  the leading pane (the trip's plan, a tab's summary, a list beside its
  detail) ends at the fold while the trailing one starts there, on either
  reading direction. A flat fold that does not separate stays one screen.
- The split size class starts at 840pt, Android's expanded width, instead
  of 900pt, so a foldable unfolded on its side (about 841dp) gets two panes
  like an iPad does. An 11" iPad upright (834pt) stays a single pane. The
  address screen of the native shell follows the same width.
- TrekShell exports NativeWindowLayout for it.

Also: the sheet's width and anchor types and the collab board type stay
private to their modules, which nothing else imports (knip).
2026-11-07 23:25:13 +01:00

73 lines
2.6 KiB
TypeScript

import { useEffect, useSyncExternalStore } from 'react';
import { hasNativeFeature, useNativeCapabilities } from './capabilities';
import { reportNativeFailure } from './reportFailure';
import { TrekShell, type NativeWindowLayout } from './trekShell';
/** A fold down the window, in CSS pixels from its left edge: where it begins and ends. */
export interface WindowFold {
start: number;
end: number;
}
/**
* The fold a two-pane screen should respect: a vertical one that separates the
* window into two sides, as a foldable half open like a book has, or a device
* whose two screens meet there. A flat fold that does not separate (a Galaxy
* Fold opened all the way) is one seamless screen, and a horizontal one runs
* across both panes rather than between them.
*/
export function foldOf(layout: NativeWindowLayout | null): WindowFold | null {
if (!layout?.hinge || layout.posture === 'none') return null;
if (layout.orientation !== 'vertical' || !layout.separating) return null;
const { left, right } = layout.hinge;
if (!(left > 0 && right >= left && right < layout.width)) return null;
return { start: left, end: right };
}
let layout: NativeWindowLayout | null = null;
let listening = false;
const listeners = new Set<() => void>();
function publish(next: NativeWindowLayout): void {
layout = next;
for (const listener of listeners) listener();
}
/** Starts following the window layout the Android shell reports (feature windowLayout), once. */
function follow(): void {
if (listening || !hasNativeFeature('windowLayout')) return;
listening = true;
TrekShell.getWindowLayout().then(publish, reportNativeFailure);
TrekShell.addListener('windowLayout', publish).catch(reportNativeFailure);
}
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
const current = () => layout;
/**
* The fold of the window, while the app's shell reports one (Android, feature
* windowLayout); null in a browser, on iOS and on a device without one. Two-pane
* screens end their leading pane at it, so no row runs across the hinge.
*/
export function useWindowFold(): WindowFold | null {
const { caps } = useNativeCapabilities();
const supported = caps.features.includes('windowLayout');
useEffect(() => {
if (supported) follow();
}, [supported]);
const reported = useSyncExternalStore(subscribe, current, current);
return supported ? foldOf(reported) : null;
}
/** Test helper: forget the layout and the subscription. */
export function _resetWindowFold(): void {
layout = null;
listening = false;
listeners.clear();
}