one
Version:
One is a React framework for web, iOS, and Android apps.
188 lines (165 loc) • 5.85 kB
text/typescript
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()
}