tart-0.2: programs/Draw.hs
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
module Draw
( drawWithCurrentTool
, drawAtPoint
, drawTextAtPoint
, truncateText
, pasteTextAtPoint
, undo
, redo
)
where
import Brick
import Brick.Widgets.Border.Style
import Data.Monoid ((<>))
import Lens.Micro.Platform
import Control.Monad.Trans (liftIO)
import Control.Monad (foldM)
import qualified Data.Text as T
import qualified Graphics.Vty as V
import qualified Data.Vector as Vec
import Data.Maybe (catMaybes)
import Types
import Tart.Canvas
import State
import Draw.Line
import Draw.Box
undo :: AppState -> EventM Name AppState
undo s =
case s^.undoStack of
[] -> return s
(actions:rest) -> do
let go st [] old = return (st, old)
go st (a:as) old = do
(st', old') <- applyAction st a
go st' as (old' <> old)
(finalSt, undoActs) <- go s actions []
return $ finalSt & undoStack .~ rest
& redoStack %~ (undoActs:)
redo :: AppState -> EventM Name AppState
redo s =
case s^.redoStack of
[] -> return s
(actions:rest) -> do
let go st [] old = return (st, old)
go st (a:as) old = do
(st', old') <- applyAction st a
go st' as (old' <> old)
(finalSt, undoActs) <- go s actions []
return $ finalSt & redoStack .~ rest
& undoStack %~ (undoActs:)
applyAction :: AppState -> Action -> EventM Name (AppState, [Action])
applyAction s ClearCanvasDirty = return (s & canvasDirty .~ False, [])
applyAction s (SetPixels idx ps) = do
let old' = (\(p, (ch, attr)) -> (p, ch, attr)) <$> ps
(s', old) <- drawMany old' (layerAt idx) (Just idx) s
return (s', old)
applyAction s (InsertLayer c idx ordIdx name) =
return $ insertLayer c idx ordIdx name s
applyAction s (RemoveLayer idx) =
return $ deleteLayer idx s
applyAction s (ChangeLayerName idx newName) =
return $ renameLayer idx newName s
applyAction s (MoveLayerBy idx up) =
return $ moveLayer idx up s
applyAction s (ToggleLayer idx) =
return $ toggleLayer idx s
applyAction s (SelectLayerIndex idx) =
return $ selectLayer idx s
findFgPaletteEntry :: V.Attr -> AppState -> Int
findFgPaletteEntry a s =
let fgc = case V.attrForeColor a of
V.KeepCurrent -> Nothing
V.Default -> Nothing
V.SetTo c -> Just c
in maybe 0 id $ Vec.findIndex (== fgc) (s^.palette)
findBgPaletteEntry :: V.Attr -> AppState -> Int
findBgPaletteEntry a s =
let bgc = case V.attrBackColor a of
V.KeepCurrent -> Nothing
V.Default -> Nothing
V.SetTo c -> Just c
in maybe 0 id $ Vec.findIndex (== bgc) (s^.palette)
drawWithCurrentTool :: (Int, Int) -> AppState -> EventM Name AppState
drawWithCurrentTool point s =
case s^.tool of
Freehand -> drawAtPoint point s
Eraser -> eraseAtPoint point (s^.eraserSize) s
Repaint -> repaintAtPoint point (s^.repaintSize) s
Restyle -> restyleAtPoint point (s^.restyleSize) s
FloodFill -> floodFillAtPoint point s
TextString -> return $ beginTextEntry point s
Line ->
case s^.dragging of
Nothing -> return s
Just (n, l0, l1) ->
case n of
Canvas -> do
o <- liftIO $ clearCanvas (s^.drawingOverlay)
(s', _) <- drawLine l0 l1 drawingOverlay Nothing $
s & drawingOverlay .~ o
return s'
_ -> return s
Box -> do
case s^.dragging of
Nothing -> return s
Just (n, l0, l1) ->
case n of
Canvas -> do
let bs = snd $ getBoxBorderStyle s
o <- liftIO $ clearCanvas (s^.drawingOverlay)
(s', _) <- drawBox bs l0 l1 drawingOverlay Nothing $
s & drawingOverlay .~ o
return s'
_ -> return s
Eyedropper ->
-- Read the pixel at the canvas location. Set the
-- application state's current drawing character and colors
-- from it.
let (ch, attr) = canvasGetPixel (s^.currentLayer) point
in return $ s & drawCharacter .~ ch
& drawFgPaletteIndex .~ findFgPaletteEntry attr s
& drawBgPaletteIndex .~ findBgPaletteEntry attr s
& drawStyle .~ styleWord (V.attrStyle attr)
styleWord :: V.MaybeDefault V.Style -> V.Style
styleWord V.KeepCurrent = 0
styleWord V.Default = 0
styleWord (V.SetTo s) = s
drawLine :: Location
-> Location
-> Lens' AppState Canvas
-> Maybe Int
-> AppState
-> EventM Name (AppState, [Action])
drawLine (Location p0) (Location p1) which whichIdx s =
let points = plotLine p0 p1
pixels = (, s^.drawCharacter, currentPaletteAttribute s) <$> points
in drawMany pixels which whichIdx s
truncateText :: (Int, Int) -> [(Char, V.Attr)] -> AppState -> [(Char, V.Attr)]
truncateText point t s =
let startCol = point^._1
maxCol = min ((s^.appCanvasSize)^._1 - 1)
(startCol + length t - 1)
safe = take (maxCol - startCol + 1) t
in safe
pasteTextAtPoint :: (Int, Int) -> AppState -> T.Text -> EventM Name AppState
pasteTextAtPoint point s t = do
let ls = T.lines t
(startCol, startRow) = point
pasteWidth = maximum $ T.length <$> ls
pasteHeight = length ls
(oldWidth, oldHeight) = s^.appCanvasSize
newSize = ( max oldWidth pasteWidth
, max oldHeight pasteHeight
)
pairs = zip [startRow..] ls
mkLine line = zip (T.unpack line) $ repeat $ currentPaletteAttribute s
s' <- resizeCanvas s newSize
foldM (\st (row, line) -> drawTextAtPoint (startCol, row) (mkLine line) st) s' pairs
drawTextAtPoint :: (Int, Int) -> [(Char, V.Attr)] -> AppState -> EventM Name AppState
drawTextAtPoint point t s = do
let (startCol, row) = point
pixs = zip ([startCol..]) (truncateText point t s)
many = mkEntry <$> pixs
mkEntry (col, (ch, attr)) = ((col, row), ch, attr)
withUndo <$> drawMany many currentLayer (Just $ s^.selectedLayerIndex) s
floodFillAtPoint :: (Int, Int) -> AppState -> EventM Name AppState
floodFillAtPoint point s = do
let fillAttr = normalizeAttr fillCh $ currentPaletteAttribute s
fillCh = s^.drawCharacter
fillPix = (fillCh, fillAttr)
targetPix = canvasGetPixel (s^.currentLayer) point
(w, h) = s^.appCanvasSize
up = (& _2 %~ (max 0 . pred))
down = (& _2 %~ (min (h-1) . succ))
left = (& _1 %~ (max 0 . pred))
right = (& _1 %~ (min (w-1) . succ))
go :: (Int, Int)
-> (AppState, [((Int, Int), (Char, V.Attr))])
-> EventM Name (AppState, [((Int, Int), (Char, V.Attr))])
go p (st, uBuf) = do
let rawPix = canvasGetPixel (st^.currentLayer) p
pix = rawPix & _2 %~ normalizeAttr (rawPix^._1)
if | pix == fillPix -> return (st, uBuf)
| pix /= targetPix -> return (st, uBuf)
| otherwise -> do
let old = canvasGetPixel (st^.currentLayer) p
d' <- liftIO $ canvasSetPixel (st^.currentLayer) p fillCh fillAttr
let newSt = st & currentLayer .~ d' & canvasDirty .~ True
go (down p) (newSt, (p, old):uBuf) >>=
go (up p) >>=
go (left p) >>=
go (right p)
(finalSt, undoBuf) <- go point (s, [])
let prevDirty = s^.canvasDirty
newDirty = s^.canvasDirty
d = if prevDirty /= newDirty
then [ClearCanvasDirty]
else []
return $ pushUndo (d <> [SetPixels (s^.selectedLayerIndex) undoBuf]) finalSt
drawAtPoint :: (Int, Int) -> AppState -> EventM Name AppState
drawAtPoint point s =
drawAtPoint' point (s^.drawCharacter) (currentPaletteAttribute s) s
drawAtPoint' :: (Int, Int) -> Char -> V.Attr -> AppState -> EventM Name AppState
drawAtPoint' point ch attr s = do
withUndo <$> drawMany [(point, ch, attr)] currentLayer (Just $ s^.selectedLayerIndex) s
drawMany :: [((Int, Int), Char, V.Attr)]
-> Lens' AppState Canvas
-> Maybe Int
-> AppState
-> EventM Name (AppState, [Action])
drawMany pixels which whichIdx s = do
let arr = s^.which
old = getOld <$> pixels
getOld (oldLoc, _, _) = (oldLoc, canvasGetPixel (s^.which) oldLoc)
arr' <- liftIO $ canvasSetMany arr pixels
let prevDirty = s^.canvasDirty
newDirty = not $ null pixels
newSt = s & which .~ arr'
& canvasDirty .~ (prevDirty || newDirty)
return (newSt, catMaybes [ do i <- whichIdx
return $ SetPixels i old
, if prevDirty /= newDirty
then Just ClearCanvasDirty
else Nothing
])
makeBoxAboutPoint :: (Int, Int) -> Int -> [(Int, Int)]
makeBoxAboutPoint point sz =
if sz <= 0
then []
else let len = (sz * 2) - 1
off = negate $ sz - 1
noOffset = [(c, r) | r <- [0..len-1], c <- [0..len-1]]
addOffset (c, r) = (c + off + point^._1
,r + off + point^._2
)
in addOffset <$> noOffset
eraseAtPoint :: (Int, Int) -> Int -> AppState -> EventM Name AppState
eraseAtPoint point sz s = do
let points = makeBoxAboutPoint point sz
pixels = (, ' ', V.defAttr) <$> points
withUndo <$> drawMany pixels currentLayer (Just $ s^.selectedLayerIndex) s
repaintAtPoint :: (Int, Int) -> Int -> AppState -> EventM Name AppState
repaintAtPoint point sz s = do
let points = makeBoxAboutPoint point sz
attr = currentPaletteAttribute s
getPixel p = let old = canvasGetPixel (s^.currentLayer) p
in (p, old^._1, attr { V.attrStyle = V.attrStyle $ old^._2 })
pixels = getPixel <$> points
withUndo <$> drawMany pixels currentLayer (Just $ s^.selectedLayerIndex) s
restyleAtPoint :: (Int, Int) -> Int -> AppState -> EventM Name AppState
restyleAtPoint point sz s = do
let points = makeBoxAboutPoint point sz
attr = currentPaletteAttribute s
getPixel p = let old = canvasGetPixel (s^.currentLayer) p
in (p, old^._1, (old^._2) { V.attrStyle = V.attrStyle attr })
pixels = getPixel <$> points
withUndo <$> drawMany pixels currentLayer (Just $ s^.selectedLayerIndex) s
drawBox :: BorderStyle
-> Location
-> Location
-> Lens' AppState Canvas
-> Maybe Int
-> AppState
-> EventM Name (AppState, [Action])
drawBox bs (Location a) (Location b) which whichIdx s = do
let attr = currentPaletteAttribute s
points = plotBox bs a b
pixels = mkPixel <$> points
mkPixel (p, ch) = (p, ch, attr)
drawMany pixels which whichIdx s