diff options
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/Paint.tsx | 156 |
1 files changed, 119 insertions, 37 deletions
diff --git a/src/pages/Paint.tsx b/src/pages/Paint.tsx index 136a347..995bcc3 100644 --- a/src/pages/Paint.tsx +++ b/src/pages/Paint.tsx @@ -1,16 +1,34 @@ import { GridComponent } from '@/components/grid/GridComponent'; +import { ColorSwatch } from '@/components/toolbar/ColorSwatch'; +import { Toolbar } from '@/components/toolbar/Toolbar'; +import { ToolbarItem } from '@/components/toolbar/ToolbarItem'; +import { SaveModal } from '@/components/SaveModal'; import type { AnsiTermColor, Grid, GridCell } from '@/types/grid'; -import { - type IZipper, - ListZipper, -} from '@emprespresso/pengueno'; -import { useCallback, useState } from 'react'; +import { gridToAnsi } from '@/utils/grid'; +import { saveArt } from '@/utils/storage'; +import { type IZipper, ListZipper } from '@emprespresso/pengueno'; +import { useCallback, useEffect, useState } from 'react'; +import { flushSync } from 'react-dom'; export interface ChooseArtProps { grid: Grid; + onGoHome?: () => void; } -export const Paint: React.FC<ChooseArtProps> = ({ grid }) => { +// Gruvbox theme colors converted to ANSI RGB values +const defaultColors: AnsiTermColor[] = [ + { foreground: { r: 5, g: 5, b: 5 }, background: null }, // bright7 #ebdbb2 + { foreground: { r: 5, g: 1, b: 1 }, background: null }, // regular1 #cc241d + { foreground: { r: 4, g: 4, b: 1 }, background: null }, // regular2 #98971a + { foreground: { r: 5, g: 4, b: 1 }, background: null }, // regular3 #d79921 + { foreground: { r: 2, g: 3, b: 4 }, background: null }, // regular4 #458588 + { foreground: { r: 4, g: 2, b: 3 }, background: null }, // regular5 #b16286 + { foreground: { r: 3, g: 4, b: 3 }, background: null }, // regular6 #689d6a + { foreground: { r: 4, g: 4, b: 3 }, background: null }, // regular7 #a89984 +]; + +export const Paint: React.FC<ChooseArtProps> = ({ grid, onGoHome }) => { + const [showSaveModal, setShowSaveModal] = useState(false); const [selectedColor, setSelectedColor] = useState<AnsiTermColor>({ foreground: { r: 5, g: 5, b: 5 }, background: null, @@ -18,49 +36,113 @@ export const Paint: React.FC<ChooseArtProps> = ({ grid }) => { const [history, setHistory] = useState<IZipper<Grid>>( ListZipper.from([grid]), ); + const [isDragging, setIsDragging] = useState(false); + const [workingGrid, setWorkingGrid] = useState<Grid>(grid); + + const handleDragStart = useCallback(() => { + setIsDragging(true); + // Don't reset workingGrid here - let it accumulate changes + }, []); + + const handleDragEnd = useCallback(() => { + if (isDragging) { + // Commit the working grid to history as a single atomic operation + setHistory((currentHistory) => { + return currentHistory.prepend(workingGrid).previous().get(); + }); + setIsDragging(false); + } + }, [isDragging, workingGrid]); const cellInteractionCallback = useCallback( (cell: GridCell) => { - setHistory((currentHistory) => { - const currentGrid = currentHistory.read().get(); - const newGrid = currentGrid.map((row) => [...row]); // Deep copy for current state - newGrid[cell.y][cell.x] = { ...cell, color: selectedColor }; - return currentHistory.prepend(newGrid).previous().get(); + flushSync(() => { + setWorkingGrid((currentGrid) => { + const newGrid = currentGrid.map((row) => [...row]); + newGrid[cell.y][cell.x] = { ...cell, color: selectedColor }; + return newGrid; + }); }); }, [selectedColor], ); + const handleSave = (name: string) => { + const currentGrid = history.read().get(); + saveArt(name, currentGrid); + }; + + const currentGrid = workingGrid; + const ansiOutput = gridToAnsi(currentGrid); + return ( - <div> + <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}> + {showSaveModal && ( + <SaveModal + ansiOutput={ansiOutput} + onSave={handleSave} + onClose={() => setShowSaveModal(false)} + /> + )} + <Toolbar> + <ToolbarItem + renderContent={(onClose) => ( + <ColorSwatch + onSelect={setSelectedColor} + onClose={onClose} + defaultColors={defaultColors} + ></ColorSwatch> + )} + > + 🎨 + </ToolbarItem> + <ToolbarItem + disabled={ + !history + .previous() + .flatMap((it) => it.read()) + .present() + } + onClick={() => { + setHistory((history) => { + const newHistory = history.previous().get(); + setWorkingGrid(newHistory.read().get()); + return newHistory; + }); + }} + > + ↷ + </ToolbarItem> + <ToolbarItem + disabled={ + !history + .next() + .flatMap((it) => it.read()) + .present() + } + onClick={() => { + setHistory((history) => { + const newHistory = history.next().get(); + setWorkingGrid(newHistory.read().get()); + return newHistory; + }); + }} + > + ↶ + </ToolbarItem> + <ToolbarItem onClick={() => setShowSaveModal(true)}> + 💾 + </ToolbarItem> + <ToolbarItem onClick={onGoHome}> + 🏠 + </ToolbarItem> + </Toolbar> <GridComponent - grid={history.read().get()} + grid={currentGrid} onCellInteract={cellInteractionCallback} + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} /> - <button - disabled={ - !history - .next() - .flatMap((it) => it.read()) - .present() - } - onClick={() => setHistory((history) => history.next().get())} - > - Undo - </button> - <button - disabled={ - !history - .previous() - .flatMap((it) => it.read()) - .present() - } - onClick={() => - setHistory((history) => history.previous().get()) - } - > - Redo - </button> </div> ); }; |
