import { Check, ChevronDown, Eye, Palette } from 'lucide-react'; import { type CSSProperties, type Dispatch, type ReactNode, type SetStateAction, useCallback, useRef, useState, } from 'react'; import { DEFAULT_EDITOR_SCENE_STYLE_PRESET, EDITOR_SCENE_STYLE_PRESET_OPTIONS, EDITOR_SCENE_STYLE_PRESETS, type EditorSceneStylePreset, getEditorSceneStylePresetLabel, } from '../../../packages/shared/src/contracts/editorScene'; import animePreview from '../../assets/image-editor/scene-styles/style-anime.png'; import flatPreview from '../../assets/image-editor/scene-styles/style-flat.png'; import stopMotionPreview from '../../assets/image-editor/scene-styles/style-stop-motion.png'; import watercolorPreview from '../../assets/image-editor/scene-styles/style-watercolor.png'; import { PlatformFloatingMenu } from '../common/PlatformFloatingMenu'; import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import type { GenerateDialogState } from './ImageCanvasEditorTypes'; import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss'; const SCENE_STYLE_PREVIEWS: Partial> = { [EDITOR_SCENE_STYLE_PRESETS.ANIME.value]: animePreview, [EDITOR_SCENE_STYLE_PRESETS.WATERCOLOR.value]: watercolorPreview, [EDITOR_SCENE_STYLE_PRESETS.FLAT.value]: flatPreview, [EDITOR_SCENE_STYLE_PRESETS.STOP_MOTION.value]: stopMotionPreview, }; type ImageCanvasSceneStyleControlProps = { dialog: GenerateDialogState; setGenerateDialog: Dispatch>; renderEditorPortal: (node: ReactNode) => ReactNode; buildPortalMenuStyle: ( anchor: HTMLElement | null, placement: 'above' | 'below', ) => CSSProperties; }; export function ImageCanvasSceneStyleControl({ dialog, setGenerateDialog, renderEditorPortal, buildPortalMenuStyle, }: ImageCanvasSceneStyleControlProps) { const [isOpen, setIsOpen] = useState(false); const [hoveredPreview, setHoveredPreview] = useState(null); const [focusedPreview, setFocusedPreview] = useState(null); const [clickedPreview, setClickedPreview] = useState(null); const triggerRef = useRef(null); const dismiss = useCallback(() => { setIsOpen(false); setHoveredPreview(null); setFocusedPreview(null); setClickedPreview(null); }, []); const selectedValue = dialog.sceneStylePreset ?? DEFAULT_EDITOR_SCENE_STYLE_PRESET; const selectedLabel = getEditorSceneStylePresetLabel(selectedValue); const activePreview = hoveredPreview ?? focusedPreview ?? clickedPreview; useImageCanvasFloatingOptionDismiss({ isOpen, boundaryRefs: [triggerRef], onDismiss: dismiss, }); const selectPreset = (sceneStylePreset: EditorSceneStylePreset) => { setGenerateDialog((currentDialog) => currentDialog ? { ...currentDialog, status: currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: currentDialog.status === 'failed' ? undefined : currentDialog.errorMessage, sceneStylePreset, } : currentDialog, ); dismiss(); }; return (
} onClick={() => { if (isOpen) { dismiss(); return; } setIsOpen(true); }} > {isOpen ? renderEditorPortal(
画风预设
{EDITOR_SCENE_STYLE_PRESET_OPTIONS.map((option) => { const selected = option.value === selectedValue; const preview = SCENE_STYLE_PREVIEWS[option.value]; const previewActive = activePreview === option.value; return (
{preview ? ( setHoveredPreview(option.value)} onPointerLeave={() => setHoveredPreview((current) => current === option.value ? null : current, ) } onFocus={() => setFocusedPreview(option.value)} onBlur={(event) => { if ( !event.currentTarget.contains(event.relatedTarget) ) { setFocusedPreview((current) => current === option.value ? null : current, ); } }} > {previewActive ? (
{`${option.label}固定画风预览`}
{option.label}
) : null}
) : null}
); })}
, ) : null}
); }