Files
paseo/packages/app/src/contexts/horizontal-scroll-context.tsx
T
Mohamed Boudra c7b103a2cd fix(app): stabilize canvas diff rendering
Keep diff headers and painted content on the same scroll clock while preserving native scrolling and independent per-file horizontal offsets.

Complete review, selection, typography, tree, and palette parity without returning line rendering to React.
2026-08-17 21:14:38 +02:00

100 lines
3.1 KiB
TypeScript

import { createContext, useContext, useCallback, useMemo, useRef, type ReactNode } from "react";
import { useSharedValue, type SharedValue } from "react-native-reanimated";
interface HorizontalScrollContextValue {
// Shared value indicating if any registered horizontal scroll is scrolled (offset > 0)
isAnyScrolledRight: SharedValue<boolean>;
// Captures whether the horizontal surface under the current touch started
// away from its leading edge. The value stays true for the whole gesture so
// returning to zero cannot also close a mobile panel in the same swipe.
activeGestureStartedScrolled: SharedValue<boolean>;
beginHorizontalGesture: (offset: number) => void;
endHorizontalGesture: () => void;
// Register a scroll view's offset - returns an unregister function
registerScrollOffset: (id: string, offset: number) => void;
unregisterScrollOffset: (id: string) => void;
}
const HorizontalScrollContext = createContext<HorizontalScrollContextValue | null>(null);
export function HorizontalScrollProvider({ children }: { children: ReactNode }) {
const isAnyScrolledRight = useSharedValue(false);
const activeGestureStartedScrolled = useSharedValue(false);
const scrollOffsetsRef = useRef<Map<string, number>>(new Map());
const beginHorizontalGesture = useCallback(
(offset: number) => {
activeGestureStartedScrolled.value = offset > 1;
},
[activeGestureStartedScrolled],
);
const endHorizontalGesture = useCallback(() => {
activeGestureStartedScrolled.value = false;
}, [activeGestureStartedScrolled]);
const updateIsAnyScrolled = useCallback(() => {
let anyScrolled = false;
for (const offset of scrollOffsetsRef.current.values()) {
if (offset > 1) {
anyScrolled = true;
break;
}
}
isAnyScrolledRight.value = anyScrolled;
}, [isAnyScrolledRight]);
const registerScrollOffset = useCallback(
(id: string, offset: number) => {
scrollOffsetsRef.current.set(id, offset);
updateIsAnyScrolled();
},
[updateIsAnyScrolled],
);
const unregisterScrollOffset = useCallback(
(id: string) => {
scrollOffsetsRef.current.delete(id);
updateIsAnyScrolled();
},
[updateIsAnyScrolled],
);
const contextValue = useMemo(
() => ({
activeGestureStartedScrolled,
beginHorizontalGesture,
endHorizontalGesture,
isAnyScrolledRight,
registerScrollOffset,
unregisterScrollOffset,
}),
[
activeGestureStartedScrolled,
beginHorizontalGesture,
endHorizontalGesture,
isAnyScrolledRight,
registerScrollOffset,
unregisterScrollOffset,
],
);
return (
<HorizontalScrollContext.Provider value={contextValue}>
{children}
</HorizontalScrollContext.Provider>
);
}
export function useHorizontalScroll() {
const context = useContext(HorizontalScrollContext);
if (!context) {
throw new Error("useHorizontalScroll must be used within HorizontalScrollProvider");
}
return context;
}
export function useHorizontalScrollOptional() {
return useContext(HorizontalScrollContext);
}