tart-0.4: 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, void)
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 :: EventM Name AppState ()
undo = do
s <- get
case s^.undoStack of
[] -> return ()
(actions:rest) -> do
let go [] old = return old
go (a:as) old = do
old' <- applyAction a
go as (old' <> old)
undoActs <- go actions []
undoStack .= rest
redoStack %= (undoActs:)
redo :: EventM Name AppState ()
redo = do
s <- get
case s^.redoStack of
[] -> return ()
(actions:rest) -> do
let go [] old = return old
go (a:as) old = do
old' <- applyAction a
go as (old' <> old)
undoActs <- go actions []
redoStack .= rest
undoStack %= (undoActs:)
applyAction :: Action -> EventM Name AppState [Action]
applyAction ClearCanvasDirty = do
canvasDirty .= False
return []
applyAction (SetPixels idx ps) = do
let old' = (\(p, (ch, attr)) -> (p, ch, attr)) <$> ps
drawMany old' (layerAt idx) (Just idx)
applyAction (InsertLayer c idx ordIdx name) =
insertLayer c idx ordIdx name
applyAction (RemoveLayer idx) =
deleteLayer idx
applyAction (ChangeLayerName idx newName) =
renameLayer idx newName
applyAction (MoveLayerBy idx up) =
moveLayer idx up
applyAction (ToggleLayer idx) =
toggleLayer idx
applyAction (SelectLayerIndex idx) =
selectLayer idx
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) -> EventM Name AppState ()
drawWithCurrentTool point = do
s <- get
case s^.tool of
Freehand -> drawAtPoint point
Eraser -> eraseAtPoint point (s^.eraserSize)
Repaint -> repaintAtPoint point (s^.repaintSize)
Restyle -> restyleAtPoint point (s^.restyleSize)
FloodFill -> floodFillAtPoint point
TextString -> modify $ beginTextEntry point
Line ->
case s^.dragging of
Nothing -> return ()
Just (n, l0, l1) ->
case n of
Canvas -> do
o <- liftIO $ clearCanvas (s^.drawingOverlay)
drawingOverlay .= o
void $ drawLine l0 l1 drawingOverlay Nothing
_ -> return ()
Box -> do
case s^.dragging of
Nothing -> return ()
Just (n, l0, l1) ->
case n of
Canvas -> do
let bs = snd $ getBoxBorderStyle s
o <- liftIO $ clearCanvas (s^.drawingOverlay)
drawingOverlay .= o
void $ drawBox bs l0 l1 drawingOverlay Nothing
_ -> return ()
Eyedropper -> do
-- 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
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
-> EventM Name AppState [Action]
drawLine (Location p0) (Location p1) which whichIdx = do
s <- get
let points = plotLine p0 p1
pixels = (, s^.drawCharacter, currentPaletteAttribute s) <$> points
drawMany pixels which whichIdx
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) -> T.Text -> EventM Name AppState ()
pasteTextAtPoint point t = do
s <- get
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
resizeCanvas newSize
mapM_ (\(row, line) -> drawTextAtPoint (startCol, row) (mkLine line)) pairs
drawTextAtPoint :: (Int, Int) -> [(Char, V.Attr)] -> EventM Name AppState ()
drawTextAtPoint point t = do
s <- get
let (startCol, row) = point
pixs = zip ([startCol..]) (truncateText point t s)
many = mkEntry <$> pixs
mkEntry (col, (ch, attr)) = ((col, row), ch, attr)
withUndoM $ drawMany many currentLayer (Just $ s^.selectedLayerIndex)
floodFillAtPoint :: (Int, Int) -> EventM Name AppState ()
floodFillAtPoint point = do
s <- get
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)
-> [((Int, Int), (Char, V.Attr))]
-> EventM Name AppState [((Int, Int), (Char, V.Attr))]
go p uBuf = do
curL <- use currentLayer
let rawPix = canvasGetPixel curL p
pix = rawPix & _2 %~ normalizeAttr (rawPix^._1)
if | pix == fillPix -> return uBuf
| pix /= targetPix -> return uBuf
| otherwise -> do
let old = canvasGetPixel curL p
d' <- liftIO $ canvasSetPixel curL p fillCh fillAttr
currentLayer .= d'
canvasDirty .= True
go (down p) ((p, old):uBuf) >>=
go (up p) >>=
go (left p) >>=
go (right p)
undoBuf <- go point []
let prevDirty = s^.canvasDirty
newDirty = s^.canvasDirty
d = if prevDirty /= newDirty
then [ClearCanvasDirty]
else []
modify $ pushUndo (d <> [SetPixels (s^.selectedLayerIndex) undoBuf])
drawAtPoint :: (Int, Int) -> EventM Name AppState ()
drawAtPoint point = do
s <- get
drawAtPoint' point (s^.drawCharacter) (currentPaletteAttribute s)
drawAtPoint' :: (Int, Int) -> Char -> V.Attr -> EventM Name AppState ()
drawAtPoint' point ch attr = do
s <- get
withUndoM $ drawMany [(point, ch, attr)] currentLayer (Just $ s^.selectedLayerIndex)
drawMany :: [((Int, Int), Char, V.Attr)]
-> Lens' AppState Canvas
-> Maybe Int
-> EventM Name AppState [Action]
drawMany pixels which whichIdx = do
s <- get
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)
put newSt
return (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 -> EventM Name AppState ()
eraseAtPoint point sz = do
s <- get
let points = makeBoxAboutPoint point sz
pixels = (, ' ', V.defAttr) <$> points
withUndoM $ drawMany pixels currentLayer (Just $ s^.selectedLayerIndex)
repaintAtPoint :: (Int, Int) -> Int -> EventM Name AppState ()
repaintAtPoint point sz = do
s <- get
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
withUndoM $ drawMany pixels currentLayer (Just $ s^.selectedLayerIndex)
restyleAtPoint :: (Int, Int) -> Int -> EventM Name AppState ()
restyleAtPoint point sz = do
s <- get
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
withUndoM $ drawMany pixels currentLayer (Just $ s^.selectedLayerIndex)
drawBox :: BorderStyle
-> Location
-> Location
-> Lens' AppState Canvas
-> Maybe Int
-> EventM Name AppState [Action]
drawBox bs (Location a) (Location b) which whichIdx = do
s <- get
let attr = currentPaletteAttribute s
points = plotBox bs a b
pixels = mkPixel <$> points
mkPixel (p, ch) = (p, ch, attr)
drawMany pixels which whichIdx