Files
zui/frontend/src/app/canvas/ViewportDisplayContext.tsx
2026-03-12 21:01:43 +01:00

56 lines
1.9 KiB
TypeScript

/**
* Single subscriber for viewport zoom: one component (ViewportDisplayProvider)
* subscribes to useViewport() and provides displayMode so contextual nodes
* don't each subscribe to viewport and re-render on every pan/zoom.
*/
import React, { useLayoutEffect, useRef, useState } from 'react'
import { useViewport } from '@xyflow/react'
import { CONTEXTUAL_ZOOM_THRESHOLD } from '@/app/canvas/ContextualZoomNode'
export type ViewportDisplayMode = 'compact' | 'full'
const HYSTERESIS = 0.02
const ViewportDisplayContext = React.createContext<ViewportDisplayMode>('full')
export { ViewportDisplayContext }
/**
* Must be rendered inside ReactFlowProvider. Subscribes to viewport once,
* maps zoom to displayMode with hysteresis. Throttles updates via rAF to avoid
* re-rendering all contextual nodes on every zoom tick.
*/
export function ViewportDisplayProvider({ children }: { children: React.ReactNode }) {
const { zoom } = useViewport()
const [displayMode, setDisplayMode] = useState<ViewportDisplayMode>(() =>
zoom <= CONTEXTUAL_ZOOM_THRESHOLD ? 'compact' : 'full'
)
const lastModeRef = useRef(displayMode)
const zoomRef = useRef(zoom)
const rafRef = useRef<number | null>(null)
zoomRef.current = zoom
useLayoutEffect(() => {
if (rafRef.current !== null) return
rafRef.current = requestAnimationFrame(() => {
rafRef.current = null
const z = zoomRef.current
const low = CONTEXTUAL_ZOOM_THRESHOLD - HYSTERESIS
const high = CONTEXTUAL_ZOOM_THRESHOLD + HYSTERESIS
let next: ViewportDisplayMode = lastModeRef.current
if (z <= low) next = 'compact'
else if (z >= high) next = 'full'
if (next !== lastModeRef.current) {
lastModeRef.current = next
setDisplayMode(next)
}
})
}, [zoom])
return (
<ViewportDisplayContext.Provider value={displayMode}>
{children}
</ViewportDisplayContext.Provider>
)
}