'use client'; import { useEffect, useRef, useState, useCallback } from 'react'; import { useTextStore } from '@/store/text-store'; import { useCanvasStore } from '@/store/canvas-store'; import { useLayerStore } from '@/store/layer-store'; import { useHistoryStore } from '@/store/history-store'; import { TextTool } from '@/tools/text-tool'; import { renderText } from '@/lib/text-utils'; import { DrawCommand } from '@/core/commands/draw-command'; export function OnCanvasTextEditor() { const { isOnCanvasEditorActive, editorPosition, editorText, editingTextId, settings, updateEditorText, updateEditorPosition, deactivateOnCanvasEditor, addTextObject, updateTextObject, setText, } = useTextStore(); const { zoom, offsetX, offsetY, width: canvasWidth, height: canvasHeight } = useCanvasStore(); const { getActiveLayer } = useLayerStore(); const { executeCommand } = useHistoryStore(); const [screenPosition, setScreenPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0, initialCanvasX: 0, initialCanvasY: 0 }); const [textBounds, setTextBounds] = useState({ width: 0, height: 0 }); const textareaRef = useRef(null); const previewCanvasRef = useRef(null); const containerRef = useRef(null); const commitTextRef = useRef<(() => void) | null>(null); // Calculate screen position from canvas position const calculateScreenPosition = useCallback(() => { if (!editorPosition || !containerRef.current) return { x: 0, y: 0 }; const container = containerRef.current.parentElement; if (!container) return { x: 0, y: 0 }; const rect = container.getBoundingClientRect(); // Apply canvas transformation: canvas coords -> screen coords // The canvas uses: translate(offsetX + width/2, offsetY + height/2), scale(zoom), translate(-canvasWidth/2, -canvasHeight/2) const transformedX = (editorPosition.x - canvasWidth / 2) * zoom + (rect.width / 2) + offsetX; const transformedY = (editorPosition.y - canvasHeight / 2) * zoom + (rect.height / 2) + offsetY; return { x: transformedX, y: transformedY, }; }, [editorPosition, zoom, offsetX, offsetY, canvasWidth, canvasHeight]); // Update screen position when canvas transforms or position changes useEffect(() => { if (isOnCanvasEditorActive && editorPosition) { const pos = calculateScreenPosition(); setScreenPosition(pos); } }, [isOnCanvasEditorActive, editorPosition, calculateScreenPosition]); // Measure text bounds const measureTextBounds = useCallback((text: string) => { if (!previewCanvasRef.current) { return { width: 200, height: settings.fontSize * settings.lineHeight }; } const canvas = previewCanvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) { return { width: 200, height: settings.fontSize * settings.lineHeight }; } // Measure text ctx.font = `${settings.fontStyle} ${settings.fontWeight} ${settings.fontSize}px ${settings.fontFamily}`; const lines = text ? text.split('\n') : ['']; let maxWidth = 0; lines.forEach((line) => { const width = ctx.measureText(line || ' ').width; // Measure space if empty if (width > maxWidth) maxWidth = width; }); // Add padding for better visibility and ensure minimum size const width = Math.max(maxWidth + 40, 200); const height = Math.max(lines.length * settings.fontSize * settings.lineHeight + 10, settings.fontSize * settings.lineHeight + 10); return { width, height }; }, [settings]); // Update text bounds when text or settings change useEffect(() => { const bounds = measureTextBounds(editorText); setTextBounds(bounds); }, [editorText, settings, measureTextBounds]); // Render text preview on canvas useEffect(() => { if (!previewCanvasRef.current) return; const canvas = previewCanvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; // Set canvas size to match text bounds (scaled) canvas.width = textBounds.width * zoom; canvas.height = textBounds.height * zoom; // Clear canvas ctx.clearRect(0, 0, canvas.width, canvas.height); if (!editorText) return; // Scale context for rendering ctx.save(); ctx.scale(zoom, zoom); // Render text at origin with 'top' baseline so it's visible // Add small padding from top edge renderText(ctx, 10, 0, { ...settings, text: editorText, baseline: 'top' }); ctx.restore(); }, [editorText, settings, textBounds, zoom]); // Handle commit text const commitText = useCallback(() => { if (!editorPosition) return; if (!editorText.trim()) { // Empty text, just close editor deactivateOnCanvasEditor(); return; } const activeLayer = getActiveLayer(); if (!activeLayer) { deactivateOnCanvasEditor(); return; } if (editingTextId) { // Update existing text object - update position if moved updateTextObject(editingTextId, { x: editorPosition.x + 10, y: editorPosition.y, text: editorText, fontFamily: settings.fontFamily, fontSize: settings.fontSize, fontStyle: settings.fontStyle, fontWeight: settings.fontWeight, color: settings.color, align: settings.align, baseline: 'top', lineHeight: settings.lineHeight, letterSpacing: settings.letterSpacing, }); } else { // Create new text object addTextObject({ layerId: activeLayer.id, x: editorPosition.x + 10, y: editorPosition.y, text: editorText, fontFamily: settings.fontFamily, fontSize: settings.fontSize, fontStyle: settings.fontStyle, fontWeight: settings.fontWeight, color: settings.color, align: settings.align, baseline: 'top', lineHeight: settings.lineHeight, letterSpacing: settings.letterSpacing, }); } // Update settings text for future use setText(editorText); // Close editor deactivateOnCanvasEditor(); }, [editorPosition, editorText, editingTextId, settings, deactivateOnCanvasEditor, addTextObject, updateTextObject, setText, getActiveLayer]); // Store commitText in ref for click outside handler useEffect(() => { commitTextRef.current = commitText; }, [commitText]); // Handle keyboard shortcuts const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); commitText(); } else if (e.key === 'Escape') { e.preventDefault(); deactivateOnCanvasEditor(); } }, [commitText, deactivateOnCanvasEditor] ); // Handle drag start const handleMouseDown = useCallback( (e: React.MouseEvent) => { if (!editorPosition) return; setIsDragging(true); setDragStart({ x: e.clientX, y: e.clientY, initialCanvasX: editorPosition.x, initialCanvasY: editorPosition.y, }); e.preventDefault(); e.stopPropagation(); }, [editorPosition] ); // Handle drag move const handleMouseMove = useCallback( (e: globalThis.MouseEvent) => { if (!isDragging) return; const deltaX = e.clientX - dragStart.x; const deltaY = e.clientY - dragStart.y; // Convert screen delta to canvas delta const canvasDeltaX = deltaX / zoom; const canvasDeltaY = deltaY / zoom; updateEditorPosition( dragStart.initialCanvasX + canvasDeltaX, dragStart.initialCanvasY + canvasDeltaY ); }, [isDragging, dragStart, zoom, updateEditorPosition] ); // Handle drag end const handleMouseUp = useCallback(() => { setIsDragging(false); }, []); // Attach global mouse listeners for dragging useEffect(() => { if (isDragging) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; } }, [isDragging, handleMouseMove, handleMouseUp]); // No separate click-outside handler needed - using overlay approach // Focus textarea when editor activates useEffect(() => { if (isOnCanvasEditorActive) { setTimeout(() => { textareaRef.current?.focus(); }, 10); } }, [isOnCanvasEditorActive]); if (!isOnCanvasEditorActive || !editorPosition) return null; const scaledWidth = textBounds.width * zoom; const scaledHeight = textBounds.height * zoom; return ( <> {/* Full-screen overlay to capture outside clicks */}
{ e.preventDefault(); e.stopPropagation(); commitText(); }} />
{/* Bounding box - visual only */}
{/* Transform handles - draggable */}
{/* Text preview canvas */} {/* Transparent textarea for input */}