packages feed

zwirn-0.2.3.1: app/zwirnmill/UI/Window.hs

module UI.Window where

import Animation (changeAnimation, changeFramerate, toggleAnimation)
import Brick (EventM, Extent (..), Location (..), Viewport (..), lookupExtent, lookupViewport)
import Config (getConfigPath, getKeymapPath, setBootPath, setSamplePath)
import Control.Monad (forM_)
import Control.Monad.RWS
import qualified Data.ByteString.Lazy.UTF8 as BL
import Data.List ((!?))
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Text.Zipper (textZipper)
import Docs.Event (copyCodeEvent, evalCodeEvent, gotoLink, gotoStart)
import Editor.Core (esFilePath, esZipper, newEditor)
import Editor.Cursor (moveCursorFileStart, moveCursorPos)
import Editor.Selection (extendSelectionTo)
import Editor.Util (currentCursor, lineNumberWidth)
import Keyboard.Event (selectSound)
import Lens.Micro ((&), (.~), (?~))
import SampleBrowser.Event (sampleBrowserClickAction)
import Session (quitEvent)
import System.File.OsPath as F
import System.OsPath (decodeFS)
import UI.Core

rightClickWindow :: Name -> (Int, Int) -> EventM Name AppState ()
rightClickWindow name (cx, cy) = do
  (mx, my) <- getAbsolutePos (name, parentWindow name) (cx, cy)
  os <- getWindowOptions name
  let opt = OptionWindow (mx, my) name os
  modify $ \as -> as {asOptionWindow = Just opt}

clickOptionWindow :: (Int, Int) -> EventM Name AppState ()
clickOptionWindow (_, my) = do
  mos <- gets asOptionWindow
  case mos of
    Nothing -> return ()
    Just (OptionWindow p name os) -> forM_ (os !? my) (executeOption name p) >> closeOptions

clickWindow :: Name -> (Int, Int) -> EventM Name AppState ()
clickWindow name (cx, cy) = do
  (mx, my) <- getParentCoords name (cx, cy)
  wm <- gets asWindows
  case Map.lookup (parentWindow name) wm of
    Nothing -> return ()
    Just win@(Window _ (sx, sy) _ _ _) -> do
      newWindow <- contentClickAction (name, win) (mx, my)
      modify $ \as -> as {asDragging = Just ((mx, my), name, action), asWindows = Map.insert (parentWindow name) newWindow wm}
      where
        action
          | mx /= cx || my /= cy = Content
          | mx == sx - 1 && my == sy - 1 = ResizeBoth
          | mx == sx - 1 = ResizeRight
          | my == sy - 1 = ResizeBottom
          | mx == 0 || my == 0 = Move
          | otherwise = Content

dragWindow :: Name -> (Int, Int) -> (Int, Int) -> DragAction -> EventM Name AppState ()
dragWindow name (ax, ay) _ Content = do
  wm <- gets asWindows
  case Map.lookup (parentWindow name) wm of
    Just win -> do
      win' <- contentDragAction (name, win) (ax, ay)
      modify $ \as -> as {asWindows = Map.insert (parentWindow name) win' wm}
    Nothing -> return ()
dragWindow name (ax, ay) (lx, ly) Move = do
  wm <- gets asWindows
  modify $ \as -> as {asWindows = Map.adjust (\(Window _ s c h l) -> Window (ax - lx, ay - ly) s c h l) name wm}
dragWindow name (_, ay) _ ResizeBottom = do
  wm <- gets asWindows
  modify $ \as -> as {asWindows = Map.adjust (\(Window (px, py) (sx, _) c h l) -> Window (px, py) (sx, ay - py) c h l) name wm}
dragWindow name (ax, _) _ ResizeRight = do
  wm <- gets asWindows
  modify $ \as -> as {asWindows = Map.adjust (\(Window (px, py) (_, sy) c h l) -> Window (px, py) (ax - px, sy) c h l) name wm}
dragWindow name (ax, ay) _ ResizeBoth = do
  wm <- gets asWindows
  modify $ \as -> as {asWindows = Map.adjust (\(Window (px, py) _ c h l) -> Window (px, py) (ax - px, ay - py) c h l) name wm}

getAbsolutePos :: (Name, Name) -> (Int, Int) -> EventM Name AppState (Int, Int)
getAbsolutePos (name, dragged) c
  | name == dragged = do
      wm <- gets asWindows
      (mx, my) <- getParentCoords name c
      case Map.lookup (parentWindow name) wm of
        Just (Window (px, py) _ _ _ _) -> return (px + mx, py + my)
        Nothing -> return (mx, my)
  | otherwise = do
      wm <- gets asWindows
      (mx, my) <- getParentCoords name c
      doff <- viewportOffset dragged
      off <- viewportOffset name
      case Map.lookup (parentWindow name) wm of
        Just (Window (px, py) _ _ _ _) -> return (px + mx, py + my + doff - off)
        Nothing -> return (mx, my + doff)

viewportOffset :: Name -> EventM Name AppState Int
viewportOffset name = do
  mvp <- lookupViewport name
  return $ maybe 0 (\(VP _ x _ _) -> x) mvp

parentWindow :: Name -> Name
parentWindow (EditorViewport i) = Editor i
parentWindow DocViewport = Documentation
parentWindow (DocLink _) = Documentation
parentWindow (DocCode _) = Documentation
parentWindow SampleBrowserViewport = SampleBrowser
parentWindow EnvBrowserViewport = EnvBrowser
parentWindow x = x

getParentCoords :: Name -> (Int, Int) -> EventM Name AppState (Int, Int)
getParentCoords name (mx, my) = do
  (cx, cy) <- maybe (0, 0) (\(Extent _ (Location u) _) -> u) <$> lookupExtent name
  (px, py) <- maybe (0, 0) (\(Extent _ (Location u) _) -> u) <$> lookupExtent (parentWindow name)
  return (mx + cx - px, my + cy - py)

contentClickAction :: (Name, Window) -> (Int, Int) -> EventM Name AppState Window
contentClickAction (Editor i, Window p s (EditorContent es) h l) (mx, my) = do
  mvp <- lookupViewport (EditorViewport i)
  let ro = maybe 0 (\(VP _ x _ _) -> x) mvp
  return $ Window p s (EditorContent $ moveCursorPos (my + ro - 1, mx - lineNumberWidth es) es) h l
contentClickAction (EditorViewport _, Window p s (EditorContent es) h l) (mx, my) = return $ Window p s (EditorContent $ moveCursorPos (my - 1, mx - lineNumberWidth es) es) h l
contentClickAction (DocLink l, Window p s (DocumentationContent d _ m) h k) _ = gotoLink d l m >>= \(d', f) -> return (Window p s (DocumentationContent d' f m) h k)
contentClickAction (DocCode i, Window p s (DocumentationContent d _ m) h k) _ = evalCodeEvent d i >>= \(_, f) -> return (Window p s (DocumentationContent d f m) h k)
contentClickAction (SampleBrowserViewport, Window p s (SampleBrowserContent sb) h k) pos = sampleBrowserClickAction sb pos >>= \sb' -> return (Window p s (SampleBrowserContent sb') h k)
contentClickAction (_, x) _ = return x

contentDragAction :: (Name, Window) -> (Int, Int) -> EventM Name AppState Window
contentDragAction (EditorViewport _, Window (px, py) s (EditorContent es) h l) (ax, ay) = return $ Window (px, py) s (EditorContent $ extendSelectionTo (currentCursor $ moveCursorPos (ay - py - 1, ax - px - lineNumberWidth es) es) es) h l
contentDragAction (Slider i, Window (px, py) (sx, sy) (SliderContent _) h l) (ax, _) = do
  let new = max 0 $ min 1 $ fromIntegral (ax - px) / fromIntegral sx
  env <- gets asEnvironment
  env' <- liftIO $ updateSlider i new env
  modify (\as -> as {asEnvironment = env'})
  return $ Window (px, py) (sx, sy) (SliderContent new) h l
contentDragAction (_, x) _ = return x

executeOption :: Name -> (Int, Int) -> Option -> EventM Name AppState ()
executeOption name _ Hide = modify (\as -> as {asWindows = Map.adjust (\win -> win {windowHidden = True}) (parentWindow name) $ asWindows as})
executeOption _ _ (Show name) = modify (\as -> as {asWindows = Map.adjust (\win -> win {windowHidden = False}) (parentWindow name) $ asWindows as})
executeOption name _ Close = modify (\as -> as {asWindows = Map.delete (parentWindow name) $ asWindows as})
executeOption _ p AddEditor = addEditorWithFile p Nothing
executeOption _ _ AddSlider = addSlider
executeOption _ _ ToggleAnimation = toggleAnimation
executeOption _ _ ChangeAnimation = changeAnimation
executeOption _ _ ChangeFramerate = changeFramerate
executeOption _ _ SetBootPath = setBootPath
executeOption _ _ SetSamplePath = setSamplePath
executeOption _ _ QuitMill = quitEvent
executeOption name _ ToggleLabel = toggleLabel (parentWindow name)
executeOption (DocCode i) _ Copy = do
  mw <- Map.lookup Documentation <$> gets asWindows
  case mw of
    Just (Window _ _ (DocumentationContent d _ _) _ _) -> copyCodeEvent d i
    _ -> return ()
executeOption _ _ GotoStart = gotoStart
executeOption _ p OpenConfig = openConfig p
executeOption _ p OpenKeymap = openKeymap p
executeOption _ _ SelectAction = selectSound
executeOption _ _ _ = return ()

closeOptions :: EventM Name AppState ()
closeOptions = modify (\as -> as {asOptionWindow = Nothing})

getWindowOptions :: Name -> EventM Name AppState [Option]
getWindowOptions name = case parentWindow name of
  Background -> do
    hs <- gets $ Map.keys . Map.filter windowHidden . asWindows
    return $ AddEditor : AddSlider : OpenConfig : OpenKeymap : map Show hs ++ [QuitMill]
  (Editor _) -> return [Hide, ToggleLabel, Close]
  (Slider _) -> return [Hide, ToggleLabel, Close]
  Animator -> return [ToggleAnimation, ChangeAnimation, ChangeFramerate, Hide, ToggleLabel]
  Status -> return [SetBootPath, Hide, ToggleLabel]
  SampleBrowser -> return [SetSamplePath, Hide, ToggleLabel]
  Keyboard -> return [SelectAction, Hide, ToggleLabel]
  Documentation -> case name of
    DocCode _ -> return [Copy]
    _ -> return [GotoStart, Hide, ToggleLabel]
  _ -> return [Hide, ToggleLabel]

addSlider :: EventM Name AppState ()
addSlider = do
  ss <- gets $ (+ 1) . findMaxSlider . map fst . Map.toList . asWindows
  slider <- newSlider ss
  modify (\as -> as {asWindows = Map.insert (Slider ss) slider $ asWindows as, asActiveWindow = Slider ss})
  where
    findMaxSlider = foldl (\x y -> max x (numbered y)) 0
    numbered (Slider i) = i
    numbered _ = 0

toggleLabel :: Name -> EventM Name AppState ()
toggleLabel name = modify $ \as -> as {asWindows = Map.alter alt name $ asWindows as}
  where
    alt (Just win) = Just win {windowLabel = not $ windowLabel win}
    alt _ = Nothing

addEditorWithFile :: (Int, Int) -> Maybe (FilePath, T.Text) -> EventM Name AppState ()
addEditorWithFile (mx, my) mfile = do
  es <- gets $ (+ 1) . findMaxEditor . map fst . Map.toList . asWindows
  modify (\as -> as {asWindows = Map.insert (Editor es) (Window (mx, my) (40, 20) (EditorContent editor) False False) $ asWindows as, asActiveWindow = Editor es})
  where
    editor = case mfile of
      Just (path, cont) ->
        newEditor
          & esZipper .~ textZipper (T.lines cont) Nothing
          & esFilePath ?~ path
          & moveCursorFileStart
      Nothing -> newEditor
    findMaxEditor = foldl (\x y -> max x (numbered y)) 0
    numbered (Editor i) = i
    numbered _ = 0

openConfig :: (Int, Int) -> EventM Name AppState ()
openConfig (mx, my) = do
  ospath <- liftIO getConfigPath
  c <- liftIO $ F.readFile ospath
  path <- liftIO $ decodeFS ospath
  addEditorWithFile (mx, my) (Just (path, T.pack $ BL.toString c))

openKeymap :: (Int, Int) -> EventM Name AppState ()
openKeymap (mx, my) = do
  ospath <- liftIO getKeymapPath
  c <- liftIO $ F.readFile ospath
  path <- liftIO $ decodeFS ospath
  addEditorWithFile (mx, my) (Just (path, T.pack $ BL.toString c))