tart-0.4: programs/State.hs
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ScopedTypeVariables #-}
module State
( checkForMouseSupport
, setTool
, setToolByChar
, whenTool
, setFgPaletteIndex
, setBgPaletteIndex
, beginFgPaletteSelect
, beginBgPaletteSelect
, beginToolSelect
, selectNextLayer
, selectPrevLayer
, selectLayer
, pushMode
, popMode
, toolSize
, askForSaveFilename
, increaseCanvasSize
, decreaseCanvasSize
, increaseToolSize
, decreaseToolSize
, beginCanvasSizePrompt
, toggleCurrentLayer
, toggleLayer
, beginTextEntry
, tryResizeCanvas
, quit
, beginLayerRename
, renameCurrentLayer
, renameLayer
, deleteLayer
, deleteSelectedLayer
, insertLayer
, currentPaletteAttribute
, handleDragFinished
, getBoxBorderStyle
, beginBoxStyleSelect
, beginStyleSelect
, writeCanvasFiles
, resizeCanvas
, increaseEraserSize
, decreaseEraserSize
, increaseRepaintSize
, decreaseRepaintSize
, increaseRestyleSize
, decreaseRestyleSize
, pushUndo
, withUndoM
, toggleStyleFromKey
, isStyleKey
, styleBindings
, recenterCanvas
, addLayer
, moveLayer
, moveCurrentLayerDown
, moveCurrentLayerUp
, cancelDragging
, toggleLayerList
, saveAndContinue
, canvasMoveDown
, canvasMoveUp
, canvasMoveLeft
, canvasMoveRight
, tools
, charTools
, boxStyles
, beginCharacterSelect
, cancelCharacterSelect
, selectCharacter
)
where
import Control.Monad (when, forM, forM_)
import Control.Monad.Trans (liftIO)
import qualified Control.Exception as E
import Data.Monoid ((<>))
import qualified Graphics.Vty as V
import qualified Graphics.Vty.CrossPlatform as V
import qualified Data.Vector as Vec
import qualified Data.Text as T
import qualified Data.Map as M
import Data.List (sortOn, elemIndex)
import System.Exit (exitFailure)
import Lens.Micro.Platform
import Data.Text.Zipper (gotoEOL, textZipper)
import Text.Read (readMaybe)
import Data.Maybe (isJust, fromJust, catMaybes)
import Brick
import Brick.Focus
import Brick.Widgets.Edit (editor, applyEdit, getEditContents, editContentsL)
import Brick.Widgets.Border.Style
import Types
import Tart.Canvas
import Tart.Format
tools :: [(Tool, Int)]
tools =
[ (Freehand , 1)
, (Box , 2)
, (Line , 3)
, (FloodFill , 4)
, (TextString, 5)
, (Repaint , 6)
, (Restyle , 7)
, (Eyedropper, 8)
, (Eraser , 0)
]
charTools :: [Tool]
charTools =
[ Freehand
, Line
, FloodFill
]
styleBindings :: [(Char, (V.Style, T.Text))]
styleBindings =
[ ('!', (V.bold, "Bold"))
, ('@', (V.underline, "Underline"))
, ('#', (V.blink, "Blink"))
, ('$', (V.reverseVideo, "Reverse"))
]
isStyleKey :: V.Event -> Bool
isStyleKey (V.EvKey (V.KChar c) []) =
isJust $ lookup c styleBindings
isStyleKey _ = False
toggleStyleFromKey :: V.Event -> EventM Name AppState ()
toggleStyleFromKey e =
when (isStyleKey e) $ do
let V.EvKey (V.KChar c) _ = e
Just (sty, _) = lookup c styleBindings
drawStyle %= toggleStyle sty
boxStyles :: [(T.Text, BorderStyle)]
boxStyles =
[ ("ASCII", ascii)
, ("Unicode", unicode)
, ("Unicode rounded", unicodeRounded)
]
getBoxBorderStyle :: AppState -> (T.Text, BorderStyle)
getBoxBorderStyle s = boxStyles !! (s^.boxStyleIndex)
increaseToolSize :: EventM Name AppState ()
increaseToolSize = do
s <- get
let f = case s^.tool of
Repaint -> increaseRepaintSize
Restyle -> increaseRestyleSize
Eraser -> increaseEraserSize
_ -> id
modify f
decreaseToolSize :: EventM Name AppState ()
decreaseToolSize = do
s <- get
let f = case s^.tool of
Repaint -> decreaseRepaintSize
Restyle -> decreaseRestyleSize
Eraser -> decreaseEraserSize
_ -> id
modify f
toolSize :: AppState -> Maybe Int
toolSize s =
case s^.tool of
Repaint -> Just $ s^.repaintSize
Restyle -> Just $ s^.restyleSize
Eraser -> Just $ s^.eraserSize
_ -> Nothing
increaseEraserSize :: AppState -> AppState
increaseEraserSize = (& eraserSize %~ succ)
decreaseEraserSize :: AppState -> AppState
decreaseEraserSize = (& eraserSize %~ (max 1 . pred))
increaseRepaintSize :: AppState -> AppState
increaseRepaintSize = (& repaintSize %~ succ)
decreaseRepaintSize :: AppState -> AppState
decreaseRepaintSize = (& repaintSize %~ (max 1 . pred))
increaseRestyleSize :: AppState -> AppState
increaseRestyleSize = (& restyleSize %~ succ)
decreaseRestyleSize :: AppState -> AppState
decreaseRestyleSize = (& restyleSize %~ (max 1 . pred))
withUndoM :: EventM Name AppState [Action] -> EventM Name AppState ()
withUndoM act = do
as <- act
modify $ pushUndo as
withUndo :: (AppState, [Action]) -> AppState
withUndo (s, as) = pushUndo as s
pushUndo :: [Action] -> AppState -> AppState
pushUndo [] s = s
pushUndo l s = s & undoStack %~ (l:)
& redoStack .~ []
beginLayerRename :: EventM Name AppState ()
beginLayerRename = do
s <- get
let z = textZipper [line] (Just 1)
line = s^.layerInfoFor(s^.selectedLayerIndex).layerName
layerNameEditor.editContentsL .= gotoEOL z
modify $ pushMode RenameLayer
toggleCurrentLayer :: EventM Name AppState ()
toggleCurrentLayer = do
idx <- use selectedLayerIndex
withUndoM $ toggleLayer idx
toggleLayer :: Int -> EventM Name AppState [Action]
toggleLayer idx = do
layerInfoFor(idx).layerVisible %= not
return [ToggleLayer idx]
renameCurrentLayer :: T.Text -> EventM Name AppState ()
renameCurrentLayer name = do
idx <- use selectedLayerIndex
withUndoM $ renameLayer idx name
renameLayer :: Int -> T.Text -> EventM Name AppState [Action]
renameLayer idx newName = do
s <- get
let oldName = s^.layerInfoFor(idx).layerName
act = ChangeLayerName idx oldName
if T.null newName
then return []
else if newName == oldName
then do
modify popMode >> return []
else do
modify popMode
layerInfoFor(idx).layerName .= newName
canvasDirty .= True
return [act]
moveCurrentLayerDown :: EventM Name AppState ()
moveCurrentLayerDown = do
idx <- use selectedLayerIndex
withUndoM $ moveLayer idx False
moveCurrentLayerUp :: EventM Name AppState ()
moveCurrentLayerUp = do
idx <- use selectedLayerIndex
withUndoM $ moveLayer idx True
moveLayer :: Int -> Bool -> EventM Name AppState [Action]
moveLayer idx up = do
s <- get
if up && idx == (head $ s^.layerOrder)
then return []
else if (not up) && idx == (last $ s^.layerOrder)
then return []
else let Just orderIndex = elemIndex idx $ s^.layerOrder
newIndex = if up then orderIndex - 1
else orderIndex + 1
dropped = filter (/= idx) $ s^.layerOrder
newOrder = take newIndex dropped <>
[idx] <>
drop newIndex dropped
act = MoveLayerBy idx (not up)
in do
canvasDirty .= True
layerOrder .= newOrder
return [act]
selectNextLayer :: EventM Name AppState ()
selectNextLayer = do
s <- get
-- Find the selected layer in the layer ordering.
let Just selIndex = elemIndex (s^.selectedLayerIndex) (s^.layerOrder)
-- Then select the next layer, if any.
newSel = if selIndex == length (s^.layerOrder) - 1
then s^.selectedLayerIndex
else (s^.layerOrder) !! (selIndex + 1)
selectedLayerIndex .= newSel
selectPrevLayer :: EventM Name AppState ()
selectPrevLayer = do
s <- get
-- Find the selected layer in the layer ordering.
let Just selIndex = elemIndex (s^.selectedLayerIndex) (s^.layerOrder)
-- Then select the previous layer, if any.
newSel = if selIndex == 0
then s^.selectedLayerIndex
else (s^.layerOrder) !! (selIndex - 1)
selectedLayerIndex .= newSel
selectLayer :: Int -> EventM Name AppState [Action]
selectLayer idx = do
oldIdx <- use selectedLayerIndex
selectedLayerIndex .= idx
return [SelectLayerIndex oldIdx]
cancelDragging :: EventM Name AppState ()
cancelDragging =
dragging .= Nothing
deleteSelectedLayer :: EventM Name AppState ()
deleteSelectedLayer = do
idx <- use selectedLayerIndex
withUndoM $ deleteLayer idx
deleteLayer :: Int -> EventM Name AppState [Action]
deleteLayer idx = do
s <- get
if M.size (s^.layers) == 1
then return []
else do
let Just orderIndex = elemIndex idx (s^.layerOrder)
Just selOrderIndex = elemIndex (s^.selectedLayerIndex) (s^.layerOrder)
newSelIndex = if selOrderIndex == orderIndex
then newOrder !! (min (length newOrder - 1) selOrderIndex)
else s^.selectedLayerIndex
newOrder = catMaybes $ fixOrder <$> s^.layerOrder
fixOrder i = if idx == i
then Nothing
else Just $ if i > idx
then i - 1
else i
fixNameKeys m = M.fromList $ catMaybes $ fixPair <$> M.toList m
fixPair (i, n) = if idx == i
then Nothing
else (, n) <$> fixOrder i
act = InsertLayer (s^.layerAt idx)
idx
orderIndex
(_layerName $ fromJust $ s^.layerInfo.at idx)
-- Change the selected index
selectedLayerIndex .= newSelIndex
-- Remove the layer from the layer map, fix indices
layers %= fixNameKeys
-- Reassign all higher indices in name map, ordering list,
-- layer map
layerOrder .= newOrder
-- Remove the layer from the layer visibility map, fix
-- indices
layerInfo %= fixNameKeys
return [act]
insertLayer :: Canvas -> Int -> Int -> T.Text -> EventM Name AppState [Action]
insertLayer c newIdx orderIndex name = do
s <- get
let newOrderNoInsert = (\i -> if i >= newIdx then i + 1 else i) <$> s^.layerOrder
newOrder = take orderIndex newOrderNoInsert <>
[newIdx] <>
drop orderIndex newOrderNoInsert
fixNameKeys m = M.fromList $ fixPair <$> M.toList m
fixPair (i, n) = if i >= newIdx
then (i + 1, n)
else (i, n)
removeAct = RemoveLayer newIdx
selAct = SelectLayerIndex (s^.selectedLayerIndex)
selectedLayerIndex .= (length (s^.layerOrder))
layers %= (M.insert newIdx c . fixNameKeys)
layerOrder .= newOrder
layerInfo %= (M.insert newIdx (LayerInfo name True) . fixNameKeys)
return [removeAct, selAct]
quit :: Bool -> EventM Name AppState ()
quit ask = do
s <- get
case (s^.canvasDirty) of
True ->
case s^.canvasPath of
Nothing ->
case ask of
True -> modify askToSave
False -> halt
Just p ->
if ask
then modify askToSave
else do
result <- liftIO $ E.try $ saveToDisk s p
case result of
Left (e::E.SomeException) -> do
saveError .= (Just $ T.pack $ show e)
askForSaveFilename True
Right () -> halt
False -> halt
saveToDisk :: AppState -> FilePath -> IO ()
saveToDisk s p = do
let ls = snd <$> (sortOn fst $ M.toList $ s^.layers)
writeCanvasFiles p ls (s^.layerOrder)
(_layerName <$> snd <$> (sortOn fst $ M.toList $ s^.layerInfo))
saveAndContinue :: EventM Name AppState ()
saveAndContinue = do
s <- get
case s^.canvasPath of
Nothing -> return ()
Just p -> do
liftIO $ saveToDisk s p
put $ s & canvasDirty .~ False
writeCanvasFiles :: FilePath -> [Canvas] -> [Int] -> [T.Text] -> IO ()
writeCanvasFiles path cs order names = do
let tf = TartFile cs names order
tfp = toTartFilepath path
formats = [FormatBinary, FormatPlain, FormatAnsiColor]
forM_ formats $ \f -> writeTartFile f tf tfp
askToSave :: AppState -> AppState
askToSave s =
pushMode AskToSave s
askForSaveFilename :: Bool -> EventM Name AppState ()
askForSaveFilename shouldQuit = do
s <- get
askToSaveFilenameEdit .= applyEdit gotoEOL (editor AskToSaveFilenameEdit (Just 1) $
T.pack $ maybe "" id $ s^.canvasPath)
modify $ pushMode (AskForSaveFilename shouldQuit)
beginTextEntry :: (Int, Int) -> AppState -> AppState
beginTextEntry start s =
pushMode TextEntry $ s & textEntryStart .~ start
& textEntered .~ mempty
handleDragFinished :: Name -> EventM Name AppState ()
handleDragFinished n = do
s <- get
case n of
Canvas ->
case s^.tool `elem` [Box, Line] of
True -> do
(c', old) <- liftIO $ merge (s^.currentLayer) (s^.drawingOverlay)
o' <- liftIO $ clearCanvas (s^.drawingOverlay)
put $ pushUndo [SetPixels (s^.selectedLayerIndex) old] $
s & currentLayer .~ c'
& drawingOverlay .~ o'
False -> return ()
_ -> return ()
increaseCanvasSize :: EventM Name AppState ()
increaseCanvasSize = do
sz <- use appCanvasSize
resizeCanvas $
sz & _1 %~ (\w -> if w == 1 then 4 else w + 4)
& _2 %~ (\h -> if h == 1 then 2 else h + 2)
decreaseCanvasSize :: EventM Name AppState ()
decreaseCanvasSize = do
sz <- use appCanvasSize
resizeCanvas $
sz & _1 %~ (max 1 . (subtract 4))
& _2 %~ (max 1 . (subtract 2))
pushMode :: Mode -> AppState -> AppState
pushMode m s =
if isSelectionMode m && isSelectionMode (currentMode s)
then s & modes %~ ((m:) . tail)
& dragging .~ Nothing
else s & modes %~ (m:)
& dragging .~ Nothing
popMode :: AppState -> AppState
popMode s = s & modes %~ (\m -> if length m == 1 then m else tail m)
& dragging .~ Nothing
beginCanvasSizePrompt :: EventM Name AppState ()
beginCanvasSizePrompt = do
s <- get
canvasSizeFocus .= focusRing [ CanvasSizeWidthEdit
, CanvasSizeHeightEdit
]
canvasSizeWidthEdit .= applyEdit gotoEOL (editor CanvasSizeWidthEdit (Just 1) $
T.pack $ show $ fst $ s^.appCanvasSize)
canvasSizeHeightEdit .= applyEdit gotoEOL (editor CanvasSizeHeightEdit (Just 1) $
T.pack $ show $ snd $ s^.appCanvasSize)
modify $ pushMode CanvasSizePrompt
canvasMoveDown :: EventM Name AppState ()
canvasMoveDown =
canvasOffset._2 %= pred
canvasMoveUp :: EventM Name AppState ()
canvasMoveUp =
canvasOffset._2 %= succ
canvasMoveLeft :: EventM Name AppState ()
canvasMoveLeft =
canvasOffset._1 %= pred
canvasMoveRight :: EventM Name AppState ()
canvasMoveRight =
canvasOffset._1 %= succ
tryResizeCanvas :: EventM Name AppState ()
tryResizeCanvas = do
s <- get
-- If the canvas size prompt inputs are valid, resize the canvas and
-- exit prompt mode. Otherwise stay in prompt mode.
let [wStr] = getEditContents $ s^.canvasSizeWidthEdit
[hStr] = getEditContents $ s^.canvasSizeHeightEdit
result = (,) <$> (readMaybe $ T.unpack wStr)
<*> (readMaybe $ T.unpack hStr)
case result of
Just (w, h) | w > 0 && h > 0 -> do
modify popMode
resizeCanvas (w, h)
_ -> return ()
beginToolSelect :: EventM Name AppState ()
beginToolSelect = modify $ pushMode ToolSelect
beginBoxStyleSelect :: EventM Name AppState ()
beginBoxStyleSelect = modify $ pushMode BoxStyleSelect
beginStyleSelect :: EventM Name AppState ()
beginStyleSelect = modify $ pushMode StyleSelect
beginFgPaletteSelect :: EventM Name AppState ()
beginFgPaletteSelect = modify $ pushMode FgPaletteEntrySelect
beginBgPaletteSelect :: EventM Name AppState ()
beginBgPaletteSelect = modify $ pushMode BgPaletteEntrySelect
setTool :: Tool -> EventM Name AppState ()
setTool t = tool .= t
setToolByChar :: Char -> EventM Name AppState ()
setToolByChar c =
let idx = read [c]
in case filter ((== idx) . snd) tools of
[(t, _)] -> do
setTool t
modify popMode
_ -> return ()
whenTool :: [Tool] -> EventM Name AppState () -> EventM Name AppState ()
whenTool ts act = do
t <- use tool
when (t `elem` ts) act
setFgPaletteIndex :: Int -> EventM Name AppState ()
setFgPaletteIndex i = do
drawFgPaletteIndex .= i
modify popMode
setBgPaletteIndex :: Int -> EventM Name AppState ()
setBgPaletteIndex i = do
drawBgPaletteIndex .= i
modify popMode
beginCharacterSelect :: EventM Name AppState ()
beginCharacterSelect = modify $ pushMode CharacterSelect
cancelCharacterSelect :: EventM Name AppState ()
cancelCharacterSelect = modify popMode
selectCharacter :: Char -> EventM Name AppState ()
selectCharacter c = do
drawCharacter .= c
modify popMode
checkForMouseSupport :: IO ()
checkForMouseSupport = do
vty <- V.mkVty V.defaultConfig
when (not $ V.supportsMode (V.outputIface vty) V.Mouse) $ do
putStrLn "Error: this terminal does not support mouse interaction"
exitFailure
V.shutdown vty
resizeCanvas :: (Int, Int) -> EventM n AppState ()
resizeCanvas newSz = do
s <- get
ls <- liftIO $ forM (M.toList $ s^.layers) $ \(idx, l) ->
(idx,) <$> resizeFrom l newSz
o <- liftIO $ resizeFrom (s^.drawingOverlay) newSz
layers .= (M.fromList ls)
drawingOverlay .= o
appCanvasSize .= newSz
canvasDirty .= (s^.appCanvasSize /= newSz)
recenterCanvas
recenterCanvas :: EventM n AppState ()
recenterCanvas = do
sz <- use appCanvasSize
canvasOffset .= (Location $ sz & each %~ (`div` 2))
toggleLayerList :: EventM Name AppState ()
toggleLayerList =
layerListVisible %= not
addLayer :: EventM Name AppState ()
addLayer = do
s <- get
let newLayerName = T.pack $ "layer " <> (show $ idx + 1)
idx = M.size $ s^.layers
c <- liftIO $ newCanvas (s^.appCanvasSize)
withUndoM $ insertLayer c idx 0 newLayerName
currentPaletteAttribute :: AppState -> V.Attr
currentPaletteAttribute s =
let fgEntry = Vec.unsafeIndex (s^.palette) (s^.drawFgPaletteIndex)
bgEntry = Vec.unsafeIndex (s^.palette) (s^.drawBgPaletteIndex)
applyFg Nothing = id
applyFg (Just c) = (`V.withForeColor` c)
applyBg Nothing = id
applyBg (Just c) = (`V.withBackColor` c)
in (applyFg fgEntry $ applyBg bgEntry V.defAttr) `V.withStyle` (s^.drawStyle)