97 lines
4.1 KiB
TypeScript
97 lines
4.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import type { ImageStudioApp } from "@app/app";
|
||
import { BottomControlsIsland } from "./BottomControlsIsland";
|
||
import { CanvasViewport } from "./CanvasViewport";
|
||
import { LayersSheet } from "./LayersSheet";
|
||
import { ToolOverlay } from "./ToolOverlay";
|
||
import { labelForTool } from "./toolLabels";
|
||
import { resolveTransformTargetBounds, selectedTransformTarget } from "@editor/transform-targets";
|
||
import { handleDeleteSelectionKey, handleHistoryKey, keybindEventFromKeyboardEvent } from "@input/index";
|
||
import { useAppState } from "./useAppState";
|
||
import { useImageImport } from "./useImageImport";
|
||
import { useViewportActivityIsland } from "./useViewportActivityIsland";
|
||
import "./index.css";
|
||
|
||
export type AppProps = {
|
||
app: ImageStudioApp;
|
||
};
|
||
|
||
export function App({ app }: AppProps) {
|
||
const state = useAppState(app.store);
|
||
const zoomPercent = Math.round(state.editor.viewport.zoom * 100);
|
||
const viewportActivityIsland = useViewportActivityIsland(state.editor.viewport);
|
||
const imageImport = useImageImport(app.store);
|
||
const [layersOpen, setLayersOpen] = useState(false);
|
||
const transformTarget = state.editor.transformSession?.target ?? selectedTransformTarget(state.document, state.editor.selection);
|
||
|
||
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.toLowerCase() === "l") {
|
||
setLayersOpen(true);
|
||
event.preventDefault();
|
||
return;
|
||
}
|
||
|
||
const consumed = handleDeleteSelectionKey({
|
||
event: keybindEventFromKeyboardEvent(event),
|
||
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]);
|
||
const transformBounds = transformTarget ? resolveTransformTargetBounds(state.document, transformTarget) : undefined;
|
||
|
||
return (
|
||
<main className="relative h-full overflow-hidden bg-background text-foreground">
|
||
{imageImport.input}
|
||
<header className="pointer-events-none absolute inset-x-0 top-0 z-10 flex h-8 items-center justify-between px-3 text-white">
|
||
<h1 className="text-sm font-medium">Image Studio</h1>
|
||
<div className="text-xs">
|
||
{state.document.name} · {labelForTool(state.editor.tools.activeTool)} · {zoomPercent}% · {state.editor.viewport.size.w}×
|
||
{state.editor.viewport.size.h}
|
||
</div>
|
||
</header>
|
||
<div className="absolute left-3 top-1/2 z-10 -translate-y-1/2">
|
||
<ToolOverlay
|
||
activeTool={state.editor.tools.activeTool}
|
||
interactionMode={state.editor.tools.interactionMode}
|
||
dispatch={app.store.dispatch}
|
||
/>
|
||
</div>
|
||
<LayersSheet document={state.document} selection={state.editor.selection} open={layersOpen} dispatch={app.store.dispatch} onClose={() => setLayersOpen(false)} />
|
||
<div className="absolute inset-x-0 bottom-4 z-10 flex justify-center">
|
||
<BottomControlsIsland
|
||
viewport={state.editor.viewport}
|
||
visible={Boolean(transformBounds) || viewportActivityIsland.visible}
|
||
action={viewportActivityIsland.action}
|
||
transformBounds={viewportActivityIsland.visible ? undefined : transformBounds}
|
||
transformTarget={viewportActivityIsland.visible ? undefined : transformTarget}
|
||
dispatch={app.store.dispatch}
|
||
/>
|
||
</div>
|
||
<CanvasViewport store={app.store} />
|
||
</main>
|
||
);
|
||
}
|
||
|
||
export default App;
|