import { useCallback, useEffect, useRef, useState } from "react"; import { DownloadSimple, FolderOpen, Sparkle, Stack } from "@phosphor-icons/react"; import type { ImageStudioApp } from "@app/app"; import { commandIds } from "@commands/ids"; import { BottomControlsIsland } from "./BottomControlsIsland"; import { brushUnavailableHint } from "./canvas/brush"; import { CanvasViewport } from "./CanvasViewport"; import { GenerateSheet } from "./GenerateSheet"; import { LayersSheet } from "./LayersSheet"; import { ShortcutsDisplay } from "./ShortcutsDisplay"; import { ToolOverlay } from "./ToolOverlay"; import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets"; import type { AppState } from "@editor/state"; import type { ToolId } from "@editor/tools"; import { handleDeleteSelectionKey, handleHistoryKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index"; import { shallowEqual, useAppState } from "./useAppState"; import { downloadArtboardPng } from "./exportArtboardPng"; import { useImageImport } from "./useImageImport"; import { useViewportActivityIsland } from "./useViewportActivityIsland"; import "./index.css"; export type AppProps = { app: ImageStudioApp; }; export function App({ app }: AppProps) { const shellState = useAppState(app.store, selectAppShellState, shallowEqual); const { document, selection, viewport, tools, generation, transformSession, maskEdit } = shellState; const viewportActivityIsland = useViewportActivityIsland(viewport); const imageImport = useImageImport(app.store); const [layersOpen, setLayersOpen] = useState(false); const previousGenerateTool = useRef("select"); const transformTarget = transformSession?.target ?? selectedTransformTarget(document, selection); const activeArtboard = document.artboards.find((artboard) => artboard.id === selection.artboardId) ?? document.artboards[0]; const generateOpen = tools.activeTool === "generate"; useEffect(() => { if (tools.activeTool !== "generate") previousGenerateTool.current = tools.activeTool; }, [tools.activeTool]); const openGenerate = useCallback(() => { const activeTool = app.store.getState().editor.tools.activeTool; if (activeTool !== "generate") previousGenerateTool.current = activeTool; app.store.dispatch(commandIds.toolSetActive, { tool: "generate" }); setLayersOpen(false); }, [app.store]); const closeGenerate = useCallback(() => { if (app.store.getState().editor.tools.activeTool === "generate") { app.store.dispatch(commandIds.toolSetActive, { tool: previousGenerateTool.current }); } }, [app.store]); const toggleGenerate = useCallback(() => { if (generateOpen) closeGenerate(); else openGenerate(); }, [closeGenerate, generateOpen, openGenerate]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const target = event.target; const editableTarget = target instanceof HTMLElement && (target.isContentEditable || target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement); if (editableTarget) return; const historyConsumed = handleHistoryKey({ event: keybindEventFromKeyboardEvent(event), dispatch: app.store.dispatch }); if (historyConsumed) { event.preventDefault(); return; } if (event.altKey || event.ctrlKey || event.metaKey) return; if (event.key === "Escape" && app.store.getState().editor.maskEdit) { app.store.dispatch(commandIds.toolExitMaskEdit, undefined); event.preventDefault(); return; } const key = event.key.toLowerCase(); const keybindEvent = keybindEventFromKeyboardEvent(event); const toolConsumed = handleToolKey({ event: keybindEvent, dispatch: app.store.dispatch }); if (toolConsumed) { event.preventDefault(); return; } if (key === "l") { closeGenerate(); setLayersOpen((open) => !open); event.preventDefault(); return; } const consumed = handleDeleteSelectionKey({ event: keybindEvent, selection: app.store.getState().editor.selection, dispatch: app.store.dispatch, }); if (consumed) event.preventDefault(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [app.store, closeGenerate, toggleGenerate]); const transformBounds = transformTarget ? resolveTransformTargetBounds(document, transformTarget) : undefined; const brushHint = brushUnavailableHint(document, { selection, tools, maskEdit }); return (
{imageImport.input}
); } function topBarButtonClass(active = false) { const base = "inline-flex size-12 items-center justify-center rounded-full text-sm font-medium transition disabled:pointer-events-none disabled:opacity-35 focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/30"; return active ? `${base} bg-white text-black hover:bg-white hover:text-black` : `${base} text-white/75 hover:bg-white/10 hover:text-white`; } type AppShellState = { document: AppState["document"]; selection: AppState["editor"]["selection"]; viewport: AppState["editor"]["viewport"]; tools: AppState["editor"]["tools"]; generation: AppState["editor"]["generation"]; transformSession: AppState["editor"]["transformSession"]; maskEdit: AppState["editor"]["maskEdit"]; }; function selectAppShellState(state: AppState): AppShellState { return { document: state.document, selection: state.editor.selection, viewport: state.editor.viewport, tools: state.editor.tools, generation: state.editor.generation, transformSession: state.editor.transformSession, maskEdit: state.editor.maskEdit, }; } export default App;