import { useCallback, useEffect } from "react"; import { DownloadSimple, FloppyDisk, FolderOpen, FolderSimple, Sparkle, Stack } from "@phosphor-icons/react"; import type { ImageStudioApp } from "@app/app"; import { commandIds } from "@commands/ids"; import { BottomControlsIsland } from "./BottomControlsIsland"; import { brushUnavailableHint } from "@operations/paint/brush"; import { CanvasViewport } from "./CanvasViewport"; import { CommandPalette } from "./CommandPalette"; import { GenerateSheet } from "./GenerateSheet"; import { GenerationJobStatus } from "./GenerationJobStatus"; import { LayersSheet } from "./LayersSheet"; import { ToolOverlay } from "./ToolOverlay"; import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets"; import type { AppState } from "@editor/state"; import { handleCommandPaletteKey, handleDeleteSelectionKey, handleHistoryKey, handleOperationKey, handleToolKey, keybindEventFromKeyboardEvent } from "@input/index"; import { shallowEqual, useAppState } from "./useAppState"; import { useImageImport } from "./useImageImport"; import { useViewportActivityIsland } from "./useViewportActivityIsland"; import { useProjectLifecycle } from "./useProjectLifecycle"; 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, commandPalette, transformSession, maskEdit, workspace } = shellState; const viewportActivityIsland = useViewportActivityIsland(viewport); const imageImport = useImageImport(app.store); const project = useProjectLifecycle(app.store); const transformTarget = transformSession?.target ?? selectedTransformTarget(document, selection); const activeArtboard = document.artboards.find((artboard) => artboard.id === selection.artboardId) ?? document.artboards[0]; const generateOpen = workspace.panel === "generate"; const chromaKeyOpen = workspace.panel === "chromaKey"; const layersOpen = workspace.panel === "layers"; useEffect(() => { window.document.title = `${document.name} — Image Studio`; }, [document.name]); useEffect(() => { if (generateOpen) void app.workflows.generation.loadResources(); }, [app.workflows.generation, generateOpen]); const openGenerate = useCallback(() => { app.actions.document.openGenerate(); }, [app.actions.document]); const closeGenerate = useCallback(() => { app.store.dispatch(commandIds.workspaceSetPanel, { panel: "none" }); }, [app.store]); const toggleGenerate = useCallback(() => { if (generateOpen) closeGenerate(); else openGenerate(); }, [closeGenerate, generateOpen, openGenerate]); const openLayers = useCallback(() => { app.store.dispatch(commandIds.workspaceSetPanel, { panel: "layers" }); }, [app.store]); const closeLayers = useCallback(() => { app.store.dispatch(commandIds.workspaceSetPanel, { panel: "none" }); }, [app.store]); const toggleLayers = useCallback(() => { app.store.dispatch(commandIds.workspaceSetPanel, { panel: layersOpen ? "none" : "layers" }); }, [app.store, layersOpen]); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const keybindEvent = keybindEventFromKeyboardEvent(event); const paletteConsumed = handleCommandPaletteKey({ event: keybindEvent, dispatch: app.store.dispatch }); if (paletteConsumed) { event.preventDefault(); return; } if (app.store.getState().editor.commandPalette.open) { if (event.key === "Escape") { app.store.dispatch(commandIds.commandPaletteClose, undefined); event.preventDefault(); } return; } 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: keybindEvent, actions: app.actions.document }); 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 operationConsumed = handleOperationKey({ event: keybindEvent, dispatch: app.store.dispatch }); if (operationConsumed) { event.preventDefault(); return; } const toolConsumed = handleToolKey({ event: keybindEvent, dispatch: app.store.dispatch }); if (toolConsumed) { event.preventDefault(); return; } if (key === "l") { toggleLayers(); 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.actions.document, app.store, toggleLayers]); const transformBounds = transformTarget ? resolveTransformTargetBounds(document, transformTarget) : undefined; const brushHint = brushUnavailableHint(document, { selection, tools, maskEdit }); return (
{imageImport.input} {project.input}

Image Studio

{document.name}

); } function topBarButtonClass(active = false) { const base = "inline-flex size-8 items-center justify-center rounded-lg text-sm font-medium transition disabled:pointer-events-none disabled:opacity-35 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-300/50"; return active ? `${base} bg-sky-300 text-slate-950` : `${base} text-white/60 hover:bg-white/[0.07] hover:text-white`; } type AppShellState = { document: AppState["document"]; selection: AppState["editor"]["selection"]; viewport: AppState["editor"]["viewport"]; tools: AppState["editor"]["tools"]; generation: AppState["editor"]["generation"]; commandPalette: AppState["editor"]["commandPalette"]; transformSession: AppState["editor"]["transformSession"]; maskEdit: AppState["editor"]["maskEdit"]; workspace: AppState["editor"]["workspace"]; }; 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, commandPalette: state.editor.commandPalette, transformSession: state.editor.transformSession, maskEdit: state.editor.maskEdit, workspace: state.editor.workspace, }; } export default App;