diff --git a/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx b/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx new file mode 100644 index 00000000..d23e8818 --- /dev/null +++ b/apps/native/src/features/student/problem/components/floating-toolbar/PointingDrawingToolbar.tsx @@ -0,0 +1,255 @@ +import { View } from 'react-native'; +import { GestureDetector } from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; +import { Undo2, Redo2 } from 'lucide-react-native'; + +import { AnimatedPressable } from '@components/common'; +import { EraserFilledIcon, PencilFilledIcon } from '@components/system/icons'; +import { colors } from '@theme/tokens'; + +import { + BUTTON_RADIUS, + BUTTON_SIZE, + COLLAPSED_RADIUS, + COLLAPSED_W, + COLOR_GRID_GAP, + COLOR_GRID_W, + ColorSwatch, + type Corner, + DIVIDER_WIDTH, + EXPANDED_RADIUS, + GAP, + ICON_SIZE, + PADDING, + SHADOW, + TOOLBAR_H, + ToolbarButton, + ToolbarDivider, +} from './shared'; +import { useFloatingToolbarSnap } from './useFloatingToolbarSnap'; + +export const POINTING_BRUSH_COLORS = ['#000000', '#FF0900', '#0004FF', '#1BB52A'] as const; + +const EXPANDED_W = + PADDING + + BUTTON_SIZE + + GAP + + BUTTON_SIZE + + GAP + + DIVIDER_WIDTH + + GAP + + BUTTON_SIZE + + GAP + + BUTTON_SIZE + + GAP + + DIVIDER_WIDTH + + GAP + + COLOR_GRID_W + + PADDING; + +interface PointingDrawingToolbarProps { + canUndo: boolean; + canRedo: boolean; + onUndo: () => void; + onRedo: () => void; + isEraserMode: boolean; + onPenModePress: () => void; + onEraserModePress: () => void; + collapsed: boolean; + onCollapsedChange: (collapsed: boolean) => void; + containerWidth: number; + containerHeight: number; + initialCorner?: Corner; + selectedBrushColor: string; + onSelectBrushColor: (color: string) => void; +} + +export const PointingDrawingToolbar = ({ + canUndo, + canRedo, + onUndo, + onRedo, + isEraserMode, + onPenModePress, + onEraserModePress, + collapsed, + onCollapsedChange, + containerWidth, + containerHeight, + initialCorner = 'bottom-right', + selectedBrushColor, + onSelectBrushColor, +}: PointingDrawingToolbarProps) => { + const toolbarWidth = collapsed ? COLLAPSED_W : EXPANDED_W; + const { composedGesture, animatedStyle, ready } = useFloatingToolbarSnap({ + containerWidth, + containerHeight, + toolbarWidth, + initialCorner, + }); + + return ( + + + {collapsed ? ( + onCollapsedChange(false)} /> + ) : ( + + )} + + + ); +}; + +const ExpandedToolbar = ({ + canUndo, + canRedo, + onUndo, + onRedo, + isEraserMode, + onPenModePress, + onEraserModePress, + selectedBrushColor, + onSelectBrushColor, +}: Pick< + PointingDrawingToolbarProps, + | 'canUndo' + | 'canRedo' + | 'onUndo' + | 'onRedo' + | 'isEraserMode' + | 'onPenModePress' + | 'onEraserModePress' + | 'selectedBrushColor' + | 'onSelectBrushColor' +>) => ( + + + } + isActive={canUndo} + /> + + } + isActive={canRedo} + /> + + + } + isActive={!isEraserMode} + /> + + } + isActive={isEraserMode} + /> + + + {POINTING_BRUSH_COLORS.map((c) => ( + onSelectBrushColor(c)} + /> + ))} + + +); + +const CollapsedToolbar = ({ + isEraserMode, + onPress, +}: { + isEraserMode: boolean; + onPress: () => void; +}) => ( + + + {isEraserMode ? ( + + ) : ( + + )} + + +); diff --git a/apps/native/src/features/student/problem/components/floating-toolbar/index.ts b/apps/native/src/features/student/problem/components/floating-toolbar/index.ts index fc4e8576..e0b6dabb 100644 --- a/apps/native/src/features/student/problem/components/floating-toolbar/index.ts +++ b/apps/native/src/features/student/problem/components/floating-toolbar/index.ts @@ -1 +1,2 @@ export { ProblemDrawingToolbar } from './ProblemDrawingToolbar'; +export { PointingDrawingToolbar, POINTING_BRUSH_COLORS } from './PointingDrawingToolbar'; diff --git a/apps/native/src/features/student/problem/screens/PointingScreen.tsx b/apps/native/src/features/student/problem/screens/PointingScreen.tsx index c3e5b040..43d29217 100644 --- a/apps/native/src/features/student/problem/screens/PointingScreen.tsx +++ b/apps/native/src/features/student/problem/screens/PointingScreen.tsx @@ -1,7 +1,7 @@ -import { Alert, View } from 'react-native'; +import { Alert, type LayoutChangeEvent, StyleSheet, View } from 'react-native'; import { XIcon } from 'lucide-react-native'; import { type NativeStackScreenProps } from '@react-navigation/native-stack'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { AnswerEventPayload } from '@repo/pointer-content-renderer'; import { useShallow } from 'zustand/react/shallow'; @@ -30,6 +30,11 @@ import { toChatScenario, toUserAnswers, } from '../transforms/contentRendererTransforms'; +import { POINTING_BRUSH_COLORS, PointingDrawingToolbar } from '../components/floating-toolbar'; +import { DrawingCanvas, type DrawingCanvasRef } from '../../scrap/utils/skia'; +import { useDrawingState } from '../../scrap/hooks/useDrawingState'; + +const CONTENT_MAX_WIDTH = 720; const PointingScreen = ({ navigation, @@ -158,6 +163,41 @@ const PointingScreen = ({ const { leftWidth, rightWidth } = useSplitPanelLayout(); + const canvasRef = useRef(null); + const drawingState = useDrawingState(); + const [toolbarCollapsed, setToolbarCollapsed] = useState(false); + const [toolbarArea, setToolbarArea] = useState({ width: 0, height: 0 }); + const [brushColor, setBrushColor] = useState(POINTING_BRUSH_COLORS[0]); + + const handleToolbarAreaLayout = useCallback(({ nativeEvent }: LayoutChangeEvent) => { + const { width, height } = nativeEvent.layout; + setToolbarArea((prev) => + prev.width === width && prev.height === height ? prev : { width, height } + ); + }, []); + + const handlePenModePress = useCallback(() => { + drawingState.setPenMode(); + }, [drawingState]); + + const handleEraserModePress = useCallback(() => { + if (drawingState.isEraserMode) { + drawingState.setPenMode(); + } else { + drawingState.setEraserMode(); + } + }, [drawingState]); + + const handleSelectBrushColor = useCallback( + (color: string) => { + setBrushColor(color); + if (drawingState.isEraserMode) { + drawingState.setPenMode(); + } + }, + [drawingState] + ); + const redirectToHome = useCallback(() => { resetSession(); navigation?.navigate('StudentTabs', { screen: 'Home' }); @@ -234,11 +274,47 @@ const PointingScreen = ({ badge={badgeStatus} paddingHorizontal={0} /> - + + + + + + setToolbarCollapsed(true)} + /> + + + + + canvasRef.current?.undo()} + onRedo={() => canvasRef.current?.redo()} + isEraserMode={drawingState.isEraserMode} + onPenModePress={handlePenModePress} + onEraserModePress={handleEraserModePress} + collapsed={toolbarCollapsed} + onCollapsedChange={setToolbarCollapsed} + containerWidth={toolbarArea.width} + containerHeight={toolbarArea.height} + selectedBrushColor={brushColor} + onSelectBrushColor={handleSelectBrushColor} + /> + +