import React from "react";
import type {} from "@shopify/react-native-skia/lib/typescript/src/renderer/HostComponents";
import SkiaDomViewNativeComponent from "./SkiaDomView";
import type { NativeProps } from "@shopify/react-native-skia/lib/typescript/src/specs/SkiaDomViewNativeComponent";
const { Skia, clamp } =
require("@shopify/react-native-skia/src/") as typeof import("@shopify/react-native-skia/lib/typescript/src/");
const { SkiaRoot } =
require("@shopify/react-native-skia/src/renderer/Reconciler") as typeof import("@shopify/react-native-skia/lib/typescript/src/renderer/Reconciler");
import { GestureDetector, ScrollView } from "react-native-gesture-handler";
import { useSkiaScrollView, type SkiaScrollViewProps, type SkiaScrollViewState } from "./State";
import { runOnUI, runOnJS } from "react-native-reanimated";
import {
Platform,
View,
type LayoutRectangle,
type NativeMethods,
type ScrollViewProps,
type ViewStyle,
} from "react-native";
import { forwardRef, useEffect, useImperativeHandle, useRef, useState, type ReactNode } from "react";
import type { BaseGestureHandlerProps } from "react-native-gesture-handler/lib/typescript/handlers/gestureHandlerCommon";
/**
*/
export type SkiaScrollViewElementProps = {
/**
* Manage the list state yourself using `useSkiaScrollView`:
*
* ```tsx
* const list = useSkiaScrollView({ height: 1000 });
*
* // do something with the list, e.g. skew the list content:
* list.matrix.value[1] = 0.1;
*
*
* ```
*/
list?: SkiaScrollViewState;
/**
* The [view style](https://reactnative.dev/docs/view-style-props) of the canvas style.
* You should specify `flex: 1` to ensure the canvas fills the screen.
*
* ```tsx
*
* ```
*/
style?: ViewStyle;
/**
* Use children to render skia elements.
* :::info
* You must manually specify the `height` prop of [ScrollGestureProps](#scrollgestureprops) to make the list scrollable.
* :::
*
* ```tsx
*
*
*
* ```
*
* :::info
* If you are updating the children very fequently, you should consider using `useSkiaScrollView`
* to improve performance by imperatively updating the list content and therefore avoiding the React reconciler.
*
* ```tsx
* const state = useSkiaScrollView({ height: 1000 });
* const content = content.value;
*
* let previous = null;
* // do something with the list content, e.g. add a new rect every 10ms
* setInterval(() => {
* if (previous) content.removeChild(previous);
*
* previous = SkiaDomApi.RectNode({
* width: 100, height: 100,
* x: Math.random() * 1000,
* y: Math.random() * 1000
* });
*
* content.addChild(previous);
* }, 10);
* ```
* :::
*/
children?: ReactNode;
/**
* Use `fixedChildren` to render skia elements that are displayed fixed on top of the list content and are not scrollable.
*
* ```tsx
* } />
* ```
*/
fixedChildren?: ReactNode;
/**
* Enable debug mode to show the FPS count and render time.
*/
debug?: boolean;
/**
* Determines whether the keyboard gets dismissed in response to a drag.
* - `none` (the default) drags do not dismiss the keyboard.
* - `onDrag` the keyboard is dismissed when a drag begins.
* - `interactive` the keyboard is dismissed interactively with the drag
* and moves in synchrony with the touch; dragging upwards cancels the
* dismissal.
*/
keyboardDismissMode?: "none" | "interactive" | "on-drag" | undefined;
/**
* Determines when the keyboard should stay visible after a tap.
* - `never` (the default), tapping outside of the focused text input when the keyboard is up dismisses the keyboard. When this happens, children won`t receive the tap.
* - `always`, the keyboard will not dismiss automatically, and the scroll view will not catch taps, but children of the scroll view can catch taps.
* - `false`, deprecated, use `never` instead
* - `true`, deprecated, use `always` instead
*/
keyboardShouldPersistTaps?: boolean | "always" | "never" | undefined;
} & SkiaScrollViewProps;
/**
*
* Use `` as a replacement for the React Native `` component.
*
* :::info
* It uses the Skia rendering engine to render the content so you can't use React Native components inside it.
* :::
*
* :::note
* You must specify the `height` prop of [ScrollGestureProps](#scrollgestureprops) to make the list scrollable.
* :::
*
* ### Example
* ```tsx
* const paint = Skia.Paint();
* paint.setColor(Skia.Color("rgb(91, 128, 218)"));
*
* const circleCount = 100;
*
*
* {Array.from({ length: circleCount }, (_, i) => (
*
* ))}
*
* ```
*
* ### Example with `useSkiaScrollView`
*
* You can manage the list state yourself by using `useSkiaScrollView`: \
* This is useful when you need to build custom behavior on top of the list, e.g. custom gestures/renderer.
*
* ```tsx
* const state = useSkiaScrollView({ height: 1000 });
* const content = content.value;
*
* content.addChild(SkiaDomApi.RectNode({ width: 100, height: 100, x: 0, y: 0 }));
*
*
* ```
*/
export function SkiaScrollView(props: SkiaScrollViewElementProps) {
var { list, style, children, debug, fixedChildren, keyboardDismissMode, keyboardShouldPersistTaps, ...p } = props;
const ref = useRef<(React.Component & Readonly) | null>(null);
const scrollViewRef = useRef(null);
var state = list!;
if (!state) {
state = useSkiaScrollView(p) as any;
}
const { _nativeId, gesture, layout, safeArea, maxHeight, content, root, Scrollbar, mode, scrollY } = state!;
useEffect(() => {
if (Platform.OS !== "web") return;
function onWheel(event: any) {
// event.preventDefault();
// event.stopPropagation();
scrollY.value = clamp(scrollY.value - event.deltaY, 0, maxHeight.value);
// @ts-ignore
ref.current?.redraw();
}
globalThis.window.addEventListener("wheel", onWheel, true);
return () => {
globalThis.window.removeEventListener("wheel", onWheel);
};
}, []);
useState(() => {
function setMode(value: string) {
if (!ref.current) return;
if (Platform.OS === "web") {
// @ts-ignore
ref.current._mode = value;
// @ts-ignore
ref.current.redraw();
} else {
ref.current.setNativeProps?.({ mode: value });
}
}
if (Platform.OS === "web") {
globalThis.SkiaViewApi.requestRedraw = () => {
// @ts-ignore
ref.current?.redraw();
};
}
if (!props.mode) {
runOnUI(() => {
mode.addListener(1, (value) => {
runOnJS(setMode)(value);
});
})();
}
});
const [fixedReconciler] = useState(() => {
const reconciler = new SkiaRoot(Skia, !!global.SkiaDomApi, state.redraw);
root.value.insertChildBefore(reconciler.dom, content.value);
return reconciler;
});
const [contentReconciler] = useState(() => {
const reconciler = new SkiaRoot(Skia, !!global.SkiaDomApi, state.redraw);
content.value.addChild(reconciler.dom);
return reconciler;
});
contentReconciler.render(children);
fixedReconciler.render(
<>
{fixedChildren}
>
);
return (
<>
{
ref.current = x;
if (Platform.OS === "web") {
// x.props = { ...x.props };
}
}}
onLayout={(e) => {
scrollViewRef.current?.setLayout(e.nativeEvent.layout);
runOnUI((rect: LayoutRectangle) => {
"worklet";
layout.value = rect;
if (p.height) {
maxHeight.value = Math.max(
p.height - rect.height + safeArea.value.top + safeArea.value.bottom,
1
);
}
})(e.nativeEvent.layout);
}}
nativeID={`${_nativeId}`}
// mode={"continuous"}
mode={props.mode || mode.value}
debug={debug}
style={style || { flex: 1 }}
root={state.root.value}
/>
>
);
}
type InteractiveScrollViewProps = {
simultaneousHandlers: BaseGestureHandlerProps["simultaneousHandlers"];
keyboardDismissMode?: ScrollViewProps["keyboardDismissMode"];
keyboardShouldPersistTaps?: ScrollViewProps["keyboardShouldPersistTaps"];
scrollToStart: (opts: { animated?: boolean }) => void;
};
type InteractiveScrollViewRef = {
setLayout: (layout: LayoutRectangle) => void;
};
const InteractiveScrollView = forwardRef(
function InteractiveScrollView(props, ref) {
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
useImperativeHandle(ref, () => {
return {
setLayout,
};
});
return (
{}}
children={}
contentContainerStyle={{
width: layout.width,
height: layout.height * 100,
}}
contentOffset={{ x: 0, y: layout.height * 50 }}
persistentScrollbar={false}
showsHorizontalScrollIndicator={false}
showsVerticalScrollIndicator={false}
decelerationRate={0}
directionalLockEnabled
scrollsToTop
disableScrollViewPanResponder
onScrollToTop={() => {
props.scrollToStart({ animated: true });
}}
pinchGestureEnabled={false}
snapToInterval={1}
disableIntervalMomentum
style={{
zIndex: 1,
position: "absolute",
width: layout.width,
height: layout.height,
top: layout.y,
left: layout.x,
}}
/>
);
}
);