{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module IOTree
( IOTree
, IOTreePath
, RowState(..)
, RowCtx(..)
, ioTree
, setIOTreeRoots
, getIOTreeRoots
, renderIOTree
, handleIOTreeEvent
, ioTreeSelection
, ioTreeToggle
, ioTreeViewSelection
, unViewTree
, viewPath
, viewSelect
, viewUp
, viewUp'
, viewUnsafeDown
, viewPrevSibling
, viewNextSibling
, viewCollapse
, viewExpand
, viewIsCollapsed
) where
import Brick
import Control.Applicative
import Control.Monad.IO.Class
import Data.Maybe (fromMaybe)
import qualified Data.List as List
import GHC.Stack
import qualified Graphics.Vty.Input.Events as Vty
import Graphics.Vty.Input.Events (Key(..))
-- A tree style list where items can be expanded and collapsed
data IOTree node name = IOTree
{ _name :: name
, _roots :: [IOTreeNode node name]
, _getChildren :: (node -> IO [node])
, _renderRow :: RowState -- is row expanded?
-> Bool -- is row selected?
-> RowCtx -- is current node last in subtree?
-> [RowCtx] -- per level of tree depth, are parent nodes last in subtree?
-> node -- the node to render
-> Widget name
-- Render some extra info as the first child of each node
, _selection :: [Int]
-- ^ Indices along the path to the current selection. Empty list means no
-- selection.
}
data RowState = Expanded Bool | Collapsed
data RowCtx = NotLastRow | LastRow
setIOTreeRoots :: [node] -> IOTree node name -> IOTree node name
setIOTreeRoots newRoots iot = iot { _roots = (nodeToTreeNode (_getChildren iot) <$> newRoots) }
getIOTreeRoots :: IOTree node name -> [node]
getIOTreeRoots iot = map _node (_roots iot)
type IOTreePath node = [(Int, node)]
data IOTreeNode node name
= IOTreeNode
{ _node :: node
-- ^ Current node
, _children :: Either
(IO [IOTreeNode node name]) -- Node is collapsed
[IOTreeNode node name] -- Node is expanded
}
ioTree
:: forall node name
. name
-- ^ Name of the tree
-> [node]
-- ^ Root nodes
-> (node -> IO [node])
-- ^ Get child nodes of a node
-> (RowState -- is row expanded or collapsed?
-> Bool -- Is row selected
-> RowCtx -- innermost context
-> [RowCtx] -- Tree depth
-> node -- the node to render
-> Widget name)
-- ^ Row renderer (should add it's own indent based on depth)
-> IOTree node name
ioTree name rootNodes getChildrenIO renderRow
= IOTree
{ _name = name
, _roots = nodeToTreeNode getChildrenIO <$> rootNodes
, _getChildren = getChildrenIO
, _renderRow = renderRow
, _selection = if null rootNodes then [] else [0]
-- ^ TODO we could take the initial path but we'd have to expand through to
-- that path with IO
}
where
nodeToTreeNode :: (node -> IO [node]) -> node -> IOTreeNode node name
nodeToTreeNode k n = IOTreeNode n (Left (fmap (nodeToTreeNode k) <$> k n))
renderIOTree :: (Show name, Ord name) => IOTree node name -> Widget name
renderIOTree (IOTree widgetName rs _ renderRow pathTop)
= viewport widgetName Vertical $ vBox $ renderTree 0 [] rs pathTop
where
-- Render the tree of nodes
renderTree _ _ [] _ = []
renderTree minorIx depth (IOTreeNode node' csE : ns) path = case csE of
-- Collapsed
Left _ -> row Collapsed : rowsRest
-- Expanded
Right cs -> row (Expanded (null cs))
: renderTree 0 (rowCtx : depth) cs (if childIsSelected then drop 1 path else [])
++ rowsRest
where
childIsSelected = case path of
x:_ -> x == minorIx
_ -> False
selected = path == [minorIx]
rowCtx = if null ns then LastRow else NotLastRow
row state = (if selected then visible else id) $ renderRow state selected rowCtx depth node'
rowsRest = renderTree (minorIx + 1) depth ns path
handleIOTreeEvent :: Vty.Event -> IOTree node name -> EventM name s (IOTree node name)
handleIOTreeEvent e tree
= liftIO
$ forIOTreeViewSelection tree
$ \view -> fmap viewSelect $ case e of
Vty.EvKey KRight _ -> do
(view', cs) <- viewExpand view
return $ if null cs then view' else viewUnsafeDown view' 0
Vty.EvKey KDown _ -> return $ next view
Vty.EvKey KLeft [Vty.MShift] -> return $ viewCollapseAll view
Vty.EvKey KLeft _ -> return $ viewCollapse $ fromMaybe view (viewUp' view)
Vty.EvKey KUp _ -> return $ prev view
Vty.EvKey KPageDown _ -> return $ List.foldl' (flip ($)) view (replicate 15 next)
Vty.EvKey KPageUp _ -> return $ List.foldl' (flip ($)) view (replicate 15 prev)
_ -> return view
where
next v = fromMaybe v (viewNextVisible v)
prev v = fromMaybe v (viewPrevVisible v)
-- | Toggle (expanded/collapsed) at the current selection.
ioTreeToggle :: IOTree node name -> IO (IOTree node name)
ioTreeToggle t = forIOTreeViewSelection t $ \view ->
if viewIsCollapsed view
then fst <$> viewExpand view
else return (viewCollapse view)
-- | A view (or Zipper) used to navigate the tree
data IOTreeView node name
= Root (IOTree node name)
| Node
(IOTreeNode node name -> IOTreeView node name) -- reconstruct the parent given this node
Int -- The index in the parent
(IOTreeNode node name) -- This node
forIOTreeViewSelection
:: IOTree node name
-> (IOTreeView node name -> IO (IOTreeView node name))
-> IO (IOTree node name)
forIOTreeViewSelection t f = unViewTree <$> f (ioTreeViewSelection t)
ioTreeViewSelection :: IOTree node name -> IOTreeView node name
ioTreeViewSelection t = List.foldl' viewUnsafeDown (Root t) (_selection t)
ioTreeSelection :: IOTree node name -> Maybe node
ioTreeSelection t = case ioTreeViewSelection t of
Root{} -> Nothing
Node _ _ n -> Just (_node n)
unViewTree :: IOTreeView node name -> IOTree node name
unViewTree t = case t of
Root t' -> t'
Node mkParent _ t' -> unViewTree (mkParent t')
-- | Current path in the tree
viewPath :: IOTreeView node name -> [Int]
viewPath tTop = reverse $ go tTop
where
go t = case t of
Root _ -> []
Node mkParent i t' -> i : go (mkParent t')
-- | Select the current path
viewSelect :: IOTreeView node name -> IOTreeView node name
viewSelect t = ioTreeViewSelection newTree
where
newTree = oldTree { _selection = newSelection }
oldTree = unViewTree t
newSelection = viewPath t
-- | move up the tree
viewUp :: IOTreeView node name -> Maybe (IOTreeView node name)
viewUp t = case t of
Root{} -> Nothing
Node mkParent _ t' -> Just (mkParent t')
-- | move up the tree, but never to the root
viewUp' :: IOTreeView node name -> Maybe (IOTreeView node name)
viewUp' t = case viewUp t of
Just Root{} -> Nothing
x -> x
-- | Move down to a child in the tree. Index must be in range. Must be expanded.
viewUnsafeDown :: HasCallStack => IOTreeView node name -> Int -> IOTreeView node name
viewUnsafeDown view i
| viewIsCollapsed view = error "viewUnsafeDown: view must be expanded"
| otherwise = case view of
Root t -> Node (\c -> Root t{ _roots = listSet i c (_roots t) }) i (t !. i)
Node mkParent ixInParent t -> Node
(\c -> Node mkParent ixInParent (unsafeSetChild c i t))
i
(t ! i)
viewPrevVisible :: HasCallStack => IOTreeView node name -> Maybe (IOTreeView node name)
viewPrevVisible view = case viewPrevSibling view of
Nothing -> viewUp' view
Just nextSib -> Just (viewLastVisibleChild nextSib)
where
viewLastVisibleChild view' = if viewIsCollapsed view'
then view'
else let
n = case view' of
Root t -> length (_roots t) - 1
Node _ _ t -> either (error "Impossible! view' is expanded") length (_children t)
in if n == 0 then view' else viewLastVisibleChild $ viewUnsafeDown view' (n-1)
viewNextVisible :: HasCallStack => IOTreeView node name -> Maybe (IOTreeView node name)
viewNextVisible view = let
upwardNext v = case viewNextSibling v of
Nothing -> upwardNext =<< viewUp v
Just s -> Just s
in viewFirstVisibleChild view <|> upwardNext view
where
viewFirstVisibleChild view' = if viewIsCollapsed view'
then Nothing
else let
nullChildren = case view' of
Root t -> null (_roots t)
Node _ _ t -> either (error "Impossible! view' is expanded") null (_children t)
in if nullChildren then Nothing else Just (viewUnsafeDown view' 0)
-- | Move to the previous sibling within the parent node
viewPrevSibling :: HasCallStack => IOTreeView node name -> Maybe (IOTreeView node name)
viewPrevSibling t = case t of
Root{} -> Nothing
Node mkParent ixInParent t' -> if ixInParent == 0
then Nothing
else Just $ viewUnsafeDown (mkParent t') (ixInParent - 1)
-- | Move to the next sibling within the parent node
viewNextSibling :: HasCallStack => IOTreeView node name -> Maybe (IOTreeView node name)
viewNextSibling t = case t of
Root{} -> Nothing
Node mkParent ixInParent t' -> let
parent = mkParent t'
nSiblings = case parent of
Root t'' -> length (_roots t'')
Node _ _ t'' -> length (either (error "Impossible! syblings must be expanded") id (_children t''))
in if ixInParent + 1 == nSiblings
then Nothing
else Just (viewUnsafeDown parent (ixInParent + 1))
-- | Collapse the current node.
viewCollapse :: HasCallStack => IOTreeView node name -> IOTreeView node name
viewCollapse t = case t of
Root _ -> t -- Can't collapse the root
Node mkParent i t' -> case _children t' of
Left _ -> t
Right cs -> Node mkParent i t'{_children = Left (return cs)}
-- | Collapse the current node and all the nodes in the subtree rooted at
-- the current node.
viewCollapseAll :: HasCallStack => IOTreeView node name -> IOTreeView node name
viewCollapseAll tv = case tv of
Root t -> Root (t {_roots = fmap go (_roots t)})
Node mkParent i t -> case _children t of
Left cs -> Node mkParent i t {_children = Left $ fmap go <$> cs}
Right cs -> Node mkParent i t {_children = Left . pure $ fmap go cs }
where
go :: IOTreeNode node name -> IOTreeNode node name
go tn = case _children tn of
Left cs -> tn {_children = Left $ fmap go <$> cs }
Right cs -> tn {_children = Left . pure $ fmap go cs}
-- | Expand the current node. Returns the children of the current node.
viewExpand :: HasCallStack => IOTreeView node name -> IO (IOTreeView node name, [IOTreeNode node name])
viewExpand t = case t of
Root t' -> return (t, _roots t')
Node mkParent i t' -> case _children t' of
Left getChildren -> do
cs <- getChildren
return (Node mkParent i t'{_children=Right cs}, cs)
Right cs -> return (t, cs)
viewIsCollapsed :: HasCallStack => IOTreeView node name -> Bool
viewIsCollapsed t = case t of
Root{} -> False
Node _ _ t' -> case _children t' of
Left{} -> True
Right{} -> False
(!.) :: IOTree node name -> Int -> IOTreeNode node name
t !. i = _roots t !! i
(!) :: IOTreeNode node name -> Int -> IOTreeNode node name
t ! i = case _children t of
Right xs -> xs !! i
Left _ -> error "(!): tree node not expanded"
unsafeSetChild :: HasCallStack => IOTreeNode node name -> Int -> IOTreeNode node name -> IOTreeNode node name
unsafeSetChild newChild i t = case _children t of
Right xs -> t { _children = Right (listSet i newChild xs) }
Left _ -> error "(!): tree node not expanded"
listSet :: HasCallStack => Int -> a -> [a] -> [a]
listSet i a as
| i >= length as = error $ "listSet: index (" ++ show i ++ ") out of bounds [0 - " ++ show (length as) ++ ")"
| otherwise = take i as ++ [a] ++ drop (i+1) as