packages feed

tart-0.1.1: programs/State.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
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
  , withUndo
  , 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 Data.Monoid ((<>))
import qualified Graphics.Vty 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)]
styleBindings =
    [ ('!', V.bold)
    , ('@', V.underline)
    , ('#', V.blink)
    , ('$', V.reverseVideo)
    ]

isStyleKey :: V.Event -> Bool
isStyleKey (V.EvKey (V.KChar c) []) =
    isJust $ lookup c styleBindings
isStyleKey _ = False

toggleStyleFromKey :: V.Event -> AppState -> AppState
toggleStyleFromKey e s =
    if isStyleKey e
    then let V.EvKey (V.KChar c) _ = e
             Just sty = lookup c styleBindings
         in s & drawStyle %~ toggleStyle sty
    else s

boxStyles :: [(String, BorderStyle)]
boxStyles =
    [ ("ASCII", ascii)
    , ("Unicode", unicode)
    , ("Unicode rounded", unicodeRounded)
    ]

getBoxBorderStyle :: AppState -> (String, BorderStyle)
getBoxBorderStyle s = boxStyles !! (s^.boxStyleIndex)

increaseToolSize :: AppState -> AppState
increaseToolSize s =
    let f = case s^.tool of
              Repaint -> increaseRepaintSize
              Restyle -> increaseRestyleSize
              Eraser  -> increaseEraserSize
              _ -> id
    in f s

decreaseToolSize :: AppState -> AppState
decreaseToolSize s =
    let f = case s^.tool of
              Repaint -> decreaseRepaintSize
              Restyle -> decreaseRestyleSize
              Eraser  -> decreaseEraserSize
              _ -> id
    in f s

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))

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 :: AppState -> AppState
beginLayerRename s =
    let z = textZipper [line] (Just 1)
        line = T.pack $ s^.layerInfoFor(s^.selectedLayerIndex).layerName
    in pushMode RenameLayer $
        s & layerNameEditor.editContentsL .~ gotoEOL z

toggleCurrentLayer :: AppState -> AppState
toggleCurrentLayer s =
    withUndo $ toggleLayer (s^.selectedLayerIndex) s

toggleLayer :: Int -> AppState -> (AppState, [Action])
toggleLayer idx s =
    ( s & layerInfoFor(idx).layerVisible %~ not
    , [ToggleLayer idx]
    )

renameCurrentLayer :: T.Text -> AppState -> AppState
renameCurrentLayer name s =
    withUndo $ renameLayer (s^.selectedLayerIndex) name s

renameLayer :: Int -> T.Text -> AppState -> (AppState, [Action])
renameLayer idx name s =
    let newName = T.unpack name
        oldName = s^.layerInfoFor(idx).layerName
        act = ChangeLayerName idx (T.pack oldName)
    in if null newName
       then (s, [])
       else if newName == oldName
            then (popMode s, [])
            else (popMode $
                   s & layerInfoFor(idx).layerName .~ T.unpack name
                     & canvasDirty .~ True
                 , [act])

moveCurrentLayerDown :: AppState -> AppState
moveCurrentLayerDown s =
    withUndo $ moveLayer (s^.selectedLayerIndex) False s

moveCurrentLayerUp :: AppState -> AppState
moveCurrentLayerUp s =
    withUndo $ moveLayer (s^.selectedLayerIndex) True s

moveLayer :: Int -> Bool -> AppState -> (AppState, [Action])
moveLayer idx up s =
    if up && idx == (head $ s^.layerOrder)
    then (s, [])
    else if (not up) && idx == (last $ s^.layerOrder)
         then (s, [])
         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 (s & canvasDirty .~ True & layerOrder .~ newOrder, [act])

selectNextLayer :: AppState -> AppState
selectNextLayer s =
    -- 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)
    in s & selectedLayerIndex .~ newSel

selectPrevLayer :: AppState -> AppState
selectPrevLayer s =
    -- 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)
    in s & selectedLayerIndex .~ newSel

selectLayer :: Int -> AppState -> AppState
selectLayer idx s =
    s & selectedLayerIndex .~ idx

cancelDragging :: AppState -> AppState
cancelDragging s =
    s & dragging .~ Nothing

deleteSelectedLayer :: AppState -> AppState
deleteSelectedLayer s =
    withUndo $ deleteLayer (s^.selectedLayerIndex) s

deleteLayer :: Int -> AppState -> (AppState, [Action])
deleteLayer idx s
    | M.size (s^.layers) == 1 = (s, [])
    | otherwise =
        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)

        in (-- Change the selected index
           s & 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
           , [act])

insertLayer :: Canvas -> Int -> Int -> String -> AppState -> (AppState, [Action])
insertLayer c newIdx orderIndex name s =
    let selIdx = s^.selectedLayerIndex
        newSelIndex = if selIdx >= newIdx
                      then selIdx + 1
                      else selIdx
        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)

        act = RemoveLayer newIdx

    in (
       s & selectedLayerIndex .~ newSelIndex
         & layers %~ (M.insert newIdx c . fixNameKeys)
         & layerOrder .~ newOrder
         & layerInfo %~ (M.insert newIdx (LayerInfo name True) . fixNameKeys)
       , [act])

quit :: Bool -> AppState -> EventM Name (Next AppState)
quit ask s = do
    case (s^.canvasDirty) of
        True ->
            case s^.canvasPath of
                Nothing ->
                    case ask of
                        True -> continue $ askToSave s
                        False -> halt s
                Just p ->
                    if ask
                    then continue $ askToSave s
                    else do
                        liftIO $ saveToDisk s p
                        halt s
        False -> halt s

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 :: AppState -> EventM Name AppState
saveAndContinue s = do
    case s^.canvasPath of
        Nothing -> return ()
        Just p -> liftIO $ saveToDisk s p
    return $ s & canvasDirty .~ False

writeCanvasFiles :: FilePath -> [Canvas] -> [Int] -> [String] -> 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 -> AppState -> AppState
askForSaveFilename shouldQuit s =
    pushMode (AskForSaveFilename shouldQuit) $
        s & askToSaveFilenameEdit .~ applyEdit gotoEOL (editor AskToSaveFilenameEdit (Just 1) $
                                     T.pack $ maybe "" id $ s^.canvasPath)

beginTextEntry :: (Int, Int) -> AppState -> AppState
beginTextEntry start s =
    pushMode TextEntry $ s & textEntryStart .~ start
                           & textEntered .~ mempty

handleDragFinished :: AppState -> Name -> EventM Name AppState
handleDragFinished s n =
    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)
                    return $ pushUndo [SetPixels (s^.selectedLayerIndex) old] $
                             s & currentLayer .~ c'
                               & drawingOverlay .~ o'
                False -> return s
        _ -> return s

increaseCanvasSize :: AppState -> EventM Name AppState
increaseCanvasSize s =
    resizeCanvas s $
        (s^.appCanvasSize) & _1 %~ (\w -> if w == 1 then 4 else w + 4)
                           & _2 %~ (\h -> if h == 1 then 2 else h + 2)

decreaseCanvasSize :: AppState -> EventM Name AppState
decreaseCanvasSize s =
    resizeCanvas s $
        (s^.appCanvasSize) & _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 :: AppState -> AppState
beginCanvasSizePrompt s =
    pushMode CanvasSizePrompt $
        s & 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)

canvasMoveDown :: AppState -> AppState
canvasMoveDown s =
    s & canvasOffset._2 %~ pred

canvasMoveUp :: AppState -> AppState
canvasMoveUp s =
    s & canvasOffset._2 %~ succ

canvasMoveLeft :: AppState -> AppState
canvasMoveLeft s =
    s & canvasOffset._1 %~ pred

canvasMoveRight :: AppState -> AppState
canvasMoveRight s =
    s & canvasOffset._1 %~ succ

tryResizeCanvas :: AppState -> EventM Name AppState
tryResizeCanvas s = do
    -- 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
            resizeCanvas (popMode s) (w, h)
        _ -> return s

beginToolSelect :: AppState -> AppState
beginToolSelect = pushMode ToolSelect

beginBoxStyleSelect :: AppState -> AppState
beginBoxStyleSelect = pushMode BoxStyleSelect

beginStyleSelect :: AppState -> AppState
beginStyleSelect = pushMode StyleSelect

beginFgPaletteSelect :: AppState -> AppState
beginFgPaletteSelect = pushMode FgPaletteEntrySelect

beginBgPaletteSelect :: AppState -> AppState
beginBgPaletteSelect = pushMode BgPaletteEntrySelect

setTool :: AppState -> Tool -> AppState
setTool s t = s & tool .~ t

setToolByChar :: Char -> AppState -> AppState
setToolByChar c s =
    let idx = read [c]
    in case filter ((== idx) . snd) tools of
        [(t, _)] -> popMode $ setTool s t
        _ -> s

whenTool :: AppState -> [Tool] -> (AppState -> AppState) -> AppState
whenTool s ts f = if s^.tool `elem` ts then f s else s

setFgPaletteIndex :: AppState -> Int -> AppState
setFgPaletteIndex s i = popMode $ s & drawFgPaletteIndex .~ i

setBgPaletteIndex :: AppState -> Int -> AppState
setBgPaletteIndex s i = popMode $ s & drawBgPaletteIndex .~ i

beginCharacterSelect :: AppState -> AppState
beginCharacterSelect = pushMode CharacterSelect

cancelCharacterSelect :: AppState -> AppState
cancelCharacterSelect = popMode

selectCharacter :: Char -> AppState -> AppState
selectCharacter c s = popMode $ s & drawCharacter .~ c

checkForMouseSupport :: IO ()
checkForMouseSupport = do
    vty <- V.mkVty =<< V.standardIOConfig

    when (not $ V.supportsMode (V.outputIface vty) V.Mouse) $ do
        putStrLn "Error: this terminal does not support mouse interaction"
        exitFailure

    V.shutdown vty

resizeCanvas :: AppState -> (Int, Int) -> EventM n AppState
resizeCanvas s newSz = do
    ls <- liftIO $ forM (M.toList $ s^.layers) $ \(idx, l) ->
        (idx,) <$> resizeFrom l newSz
    o <- liftIO $ resizeFrom (s^.drawingOverlay) newSz
    return $
        recenterCanvas $
            s & layers .~ (M.fromList ls)
              & drawingOverlay .~ o
              & appCanvasSize .~ newSz
              & canvasDirty .~ (s^.appCanvasSize /= newSz)

recenterCanvas :: AppState -> AppState
recenterCanvas s =
    let sz = s^.appCanvasSize
    in s & canvasOffset .~ (Location $ sz & each %~ (`div` 2))

toggleLayerList :: AppState -> AppState
toggleLayerList s =
    s & layerListVisible %~ not

addLayer :: AppState -> EventM Name AppState
addLayer s = do
    let newLayerName = "layer " <> (show $ idx + 1)
        idx = M.size $ s^.layers
        orderIndex = length (s^.layerOrder)

    c <- liftIO $ newCanvas (s^.appCanvasSize)
    return $ withUndo $ insertLayer c idx orderIndex newLayerName s

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)