Add pan tool toolbar

This commit is contained in:
syntaxbullet
2026-07-03 11:27:48 +02:00
parent b5c7f87a20
commit 8320203f11
9 changed files with 168 additions and 4 deletions

View File

@@ -1,5 +1,7 @@
import type { ImageStudioApp } from "@app/app";
import { CanvasViewport } from "./CanvasViewport";
import { ToolOverlay } from "./ToolOverlay";
import { labelForTool } from "./toolLabels";
import { useAppState } from "./useAppState";
import "./index.css";
@@ -16,9 +18,17 @@ export function App({ app }: AppProps) {
<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} · {zoomPercent}% · {state.editor.viewport.size.w}×{state.editor.viewport.size.h}
{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>
<CanvasViewport store={app.store} />
</main>
);

57
view/ToolOverlay.tsx Normal file
View File

@@ -0,0 +1,57 @@
import { Cursor, Hand } from "@phosphor-icons/react";
import type { AppStore } from "@editor/store";
import type { InteractionMode, ToolId } from "@editor/tools";
import { availableToolIds } from "@editor/tools";
import { labelForTool } from "./toolLabels";
export type ToolOverlayProps = {
activeTool: ToolId;
interactionMode: InteractionMode;
dispatch: AppStore["dispatch"];
};
export function ToolOverlay({ activeTool, interactionMode, dispatch }: ToolOverlayProps) {
return (
<nav
aria-label="Tools"
className="pointer-events-auto flex flex-col gap-1 rounded-lg border border-white/10 bg-black/70 p-1 text-white shadow-xl backdrop-blur"
>
{availableToolIds.map((tool) => {
const active = isToolHighlighted(tool, activeTool, interactionMode);
const Icon = iconForTool(tool);
return (
<button
key={tool}
type="button"
aria-label={labelForTool(tool)}
aria-pressed={active}
title={labelForTool(tool)}
className={buttonClass(active)}
onClick={() => dispatch("tool.setActive", { tool })}
>
<Icon size={22} weight={active ? "fill" : "regular"} />
</button>
);
})}
</nav>
);
}
function iconForTool(tool: ToolId) {
switch (tool) {
case "pan":
return Hand;
case "select":
return Cursor;
}
}
function isToolHighlighted(tool: ToolId, activeTool: ToolId, interactionMode: InteractionMode) {
if (interactionMode.type === "temporary-pan") return tool === "pan";
return activeTool === tool;
}
function buttonClass(active: boolean) {
return `grid size-9 place-items-center rounded-md transition focus:outline-none focus-visible:outline-none ${active ? "bg-white text-black" : "text-white/80 hover:bg-white/10 hover:text-white"}`;
}

View File

@@ -1,8 +1,9 @@
import type { InteractionMode } from "@editor/tools";
import { isPanInteractionMode } from "@editor/tools";
import type { CanvasInputState } from "./useCanvasInput";
export function canvasCursorClass(interactionMode: InteractionMode, input: CanvasInputState) {
if (input.isPanning) return "cursor-grabbing";
if (interactionMode.type === "temporary-pan") return "cursor-grab";
if (isPanInteractionMode(interactionMode)) return "cursor-grab";
return "cursor-default";
}

View File

@@ -1,5 +1,6 @@
import { useEffect, useState, type RefObject } from "react";
import type { AppStore } from "@editor/store";
import { isPanInteractionMode } from "@editor/tools";
import type { GlobalKeybindConsumer, GlobalPointerConsumer, GlobalWheelConsumer } from "@input/index";
import {
createViewportPanInputController,
@@ -35,7 +36,7 @@ export function useCanvasInput(
globalPointerConsumer: options.globalPointerConsumer,
dispatch: store.dispatch,
getCurrentZoom: () => store.getState().editor.viewport.zoom,
isPanMode: () => store.getState().editor.tools.interactionMode.type === "temporary-pan",
isPanMode: () => isPanInteractionMode(store.getState().editor.tools.interactionMode),
});
const handleKeyDown = (event: KeyboardEvent) => {

5
view/toolLabels.ts Normal file
View File

@@ -0,0 +1,5 @@
import type { ToolId } from "@editor/tools";
export function labelForTool(tool: ToolId): string {
return tool === "pan" ? "Pan" : "Select";
}