// The documentation mode's sidebar (Java's GUIDocumentationSidebar): „Text“ // and „Gliederungsfläche“ with Java's icons. An item is dragged onto the // surface, where it lands with its middle under the pointer, as Java's does; // tapped, it lands in the middle of the view (Java's tap makes nothing). // „Exportieren“ saves the surface as an image (F4.8c); if that fails, Java's // „Fehler beim Speichern aufgetreten!“ shows below it. import { useRef, useState, type CSSProperties } from 'react'; import type { DocumentationItemKind } from '@mdg-filius/core'; import { DRAG_THRESHOLD } from '../canvas/Canvas.js'; import { useTranslate } from '../i18n/i18n.js'; import addRectangle from '../assets/documentation/add_small.png'; import addText from '../assets/documentation/add_text_small.png'; import exportIcon from '../assets/documentation/download_small.png'; export interface DocumentationSidebarProps { /** An item dropped at a point of the screen, or tapped (no point). */ readonly onItem: (kind: DocumentationItemKind, at: { readonly clientX: number; readonly clientY: number } | null) => void; /** „Exportieren“: the surface saved as an image; rejected, it failed. */ readonly onExport: () => Promise; } const ITEMS: readonly { readonly kind: DocumentationItemKind; readonly icon: string; readonly label: string }[] = [ { kind: 'text', icon: addText, label: 'docu.text' }, { kind: 'rectangle', icon: addRectangle, label: 'docu.rectangle' }, ]; const item: CSSProperties = { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2, minHeight: 64, padding: '6px 4px', border: '2px solid transparent', borderRadius: 8, background: 'transparent', color: 'var(--text-primary)', font: 'inherit', fontSize: 12, textAlign: 'center', overflowWrap: 'anywhere', cursor: 'grab', touchAction: 'none', }; export function DocumentationSidebar({ onItem, onExport }: DocumentationSidebarProps) { const t = useTranslate(); const [ghost, setGhost] = useState<{ kind: DocumentationItemKind; x: number; y: number } | null>(null); const press = useRef<{ kind: DocumentationItemKind; x: number; y: number; dragging: boolean } | null>(null); /** The click the browser sends after a drag ends on the button: not a tap. */ const afterDrag = useRef(false); const [exporting, setExporting] = useState(false); const [failed, setFailed] = useState(false); const exportImage = () => { setExporting(true); setFailed(false); onExport().then( () => setExporting(false), () => { setExporting(false); setFailed(true); }, ); }; const onPointerDown = (kind: DocumentationItemKind) => (event: React.PointerEvent) => { press.current = { kind, x: event.clientX, y: event.clientY, dragging: false }; // The button is the click's own target: capturing at once takes no click from anything inside it. event.currentTarget.setPointerCapture(event.pointerId); }; const onPointerMove = (event: React.PointerEvent) => { const now = press.current; if (now === null) return; if (!now.dragging) { if (Math.hypot(event.clientX - now.x, event.clientY - now.y) < DRAG_THRESHOLD) return; now.dragging = true; } setGhost({ kind: now.kind, x: event.clientX, y: event.clientY }); }; const onPointerUp = (event: React.PointerEvent) => { const now = press.current; press.current = null; setGhost(null); if (now?.dragging !== true) return; afterDrag.current = true; onItem(now.kind, { clientX: event.clientX, clientY: event.clientY }); }; return ( ); }