UNPKG

one

Version:

One is a React framework for web, iOS, and Android apps.

188 lines (165 loc) • 5.85 kB
import { useSyncExternalStore } from 'react' import { NitroModules } from 'react-native-nitro-modules' import type { OneAdaptive } from '../specs/OneAdaptive.nitro' import type { HingeState, SizeClass } from './types' import * as ReservedRegions from './ReservedRegions.native' export type * from './types' export { ReservedRegions } export { useRegions as useReservedRegions, useReady as useReservedRegionsReady, useSegments as useWindowSegments, useSpanning, } from './reservedRegionsContext' // the OneAdaptive nitro hybrid object, created once at import and cached. // every binary carries the One pod, so a missing hybrid throws instead of // falling back. live updates arrive through its callback listeners. let hybrid: OneAdaptive | undefined function native(): OneAdaptive { if (hybrid === undefined) { hybrid = NitroModules.createHybridObject<OneAdaptive>('OneAdaptive') } return hybrid } const DEFAULT_SIZE_CLASS: SizeClass = { horizontal: 'unspecified', vertical: 'unspecified', } // synchronous seed at import, mirroring safe-area initialWindowMetrics: // the first render already measures real instead of flashing defaults. let currentSizeClass: SizeClass = DEFAULT_SIZE_CLASS let currentHinge: HingeState | null = null function seedFromNative(): void { const created = native() currentSizeClass = created.getInitialSizeClass() currentHinge = created.getInitialHinge() ?? null } seedFromNative() const sizeClassListeners = new Set<() => void>() const hingeListeners = new Set<() => void>() let sizeClassRemove: (() => void) | undefined let hingeRemove: (() => void) | undefined let sizeClassGeneration = 0 let hingeGeneration = 0 // set when the last observer leaves: native stops watching and the // cached value can go stale, so the next first render reads the seed again. let hingeSeedStale = false let sizeClassSeedStale = false function sizesEqual(a: SizeClass, b: SizeClass): boolean { return a.horizontal === b.horizontal && a.vertical === b.vertical } function hingesEqual(a: HingeState | null, b: HingeState | null): boolean { return ( a === b || (a != null && b != null && a.status === b.status && a.angle === b.angle) ) } function setSizeClass(next: SizeClass) { if (sizesEqual(currentSizeClass, next)) return currentSizeClass = next sizeClassListeners.forEach((listener) => listener()) } function setHinge(next: HingeState | null) { if (hingesEqual(currentHinge, next)) return currentHinge = next hingeListeners.forEach((listener) => listener()) } function subscribeSizeClass(onStoreChange: () => void): () => void { sizeClassListeners.add(onStoreChange) if (sizeClassRemove === undefined) { // the first subscriber starts the native monitor; the newcomer also // gets the current value in case no change lands after subscribing. const created = native() const generation = ++sizeClassGeneration let sawEvent = false sizeClassRemove = created.addSizeClassListener((sizeClass) => { sawEvent = true setSizeClass(sizeClass) }) created.getSizeClass().then((sizeClass) => { if (generation === sizeClassGeneration && !sawEvent) setSizeClass(sizeClass) }) } return () => { sizeClassListeners.delete(onStoreChange) if (sizeClassListeners.size === 0) { sizeClassGeneration++ sizeClassRemove?.() sizeClassRemove = undefined sizeClassSeedStale = true } } } function subscribeHinge(onStoreChange: () => void): () => void { hingeListeners.add(onStoreChange) if (hingeRemove === undefined) { const created = native() const generation = ++hingeGeneration let sawEvent = false hingeRemove = created.addHingeListener((hinge) => { sawEvent = true setHinge(hinge ?? null) }) created.getHinge().then((hinge) => { if (generation === hingeGeneration && !sawEvent) setHinge(hinge ?? null) }) } return () => { hingeListeners.delete(onStoreChange) if (hingeListeners.size === 0) { hingeGeneration++ hingeRemove?.() hingeRemove = undefined hingeSeedStale = true } } } function sizeClassSnapshot(): SizeClass { if (sizeClassSeedStale) { sizeClassSeedStale = false currentSizeClass = native().getInitialSizeClass() } return currentSizeClass } function hingeSnapshot(): HingeState | null { if (hingeSeedStale) { hingeSeedStale = false currentHinge = native().getInitialHinge() ?? null } return currentHinge } /** * Returns the window's horizontal and vertical size class as live React state. * iOS reads the window scene's UIUserInterfaceSizeClass with live * trait-change updates; Android maps the activity window's WindowMetrics * (compact below 600dp wide / 480dp tall, else regular) and updates on * configuration and window-metrics changes. */ export function useSizeClass(): SizeClass { return useSyncExternalStore( subscribeSizeClass, sizeClassSnapshot, () => DEFAULT_SIZE_CLASS ) } export function getSizeClass(): Promise<SizeClass> { return native().getSizeClass() } /** * Returns the current hardware hinge state (angle in radians and status). * The first render already reads the window's hinge once the system has * reported one. null on a device or view hierarchy without a hinge. Use * size class and reserved regions to choose layout. */ export function useHinge(): HingeState | null { return useSyncExternalStore(subscribeHinge, hingeSnapshot, () => null) } export async function getHinge(): Promise<HingeState | null> { return (await native().getHinge()) ?? null } /** * Subscribes to hardware hinge changes. */ export function onHingeChange(callback: (hinge: HingeState | null) => void): () => void { const remove = native().addHingeListener((hinge) => callback(hinge ?? null)) return () => remove() }