matsuri-0.0.1: Widgets/Tree.hs
{-# LANGUAGE RecordWildCards #-}
-- | Tree.hs
-- Tree widget.
module Widgets.Tree (
Tree(..),
NodeData(..),
defaultTree,
moveUp,
moveDn,
cur
) where
import Prelude hiding (sequence, mapM)
import Control.Monad.State.Lazy hiding (sequence, mapM)
import Data.Traversable (sequence, mapM)
import Data.Tree hiding (Tree)
import Data.Maybe
import Graphics.Vty
import Graphics.Vty.Widgets.Base
-- | Дерево хранится в виде Forest (NodeData a).
-- Поскольку используется готовое дерево, приходится вручную везде
-- проверять инвариант "если дочерних узлов нет, то узел считается
-- развёрнутым".
data Tree
= Tree
{ selectedAttr :: Attr
, nodeAttr :: Attr
, selectedIndex :: Int
, windowStart :: Int
, treeWidth :: Maybe Int
, treeHeight :: Maybe Int
, nodes :: Forest NodeData
}
data NodeData = NodeData {collapsed :: Bool, value :: (String, Value)}
type Value = String
defaultTree tree
= Tree
{ selectedAttr = def_attr `with_back_color` cyan `with_fore_color` blue
, nodeAttr = def_attr
, selectedIndex = 0
, windowStart = 0
, treeWidth = Just 30
, treeHeight = Nothing
, nodes = tree
}
----------------------------------------------------------------------
-- moves
-- TODO: first, end, pageup, pagedn
moveUp t@(Tree {windowStart = ws, selectedIndex = sIx})
| sIx == ws && ws == 0 = t
| sIx == ws = t { windowStart = ws-1, selectedIndex = sIx-1 }
| otherwise = t { selectedIndex = sIx-1 }
moveDn t@(Tree {selectedIndex = sIx})
| sIx == treeSize - 1 = t
| otherwise = t { selectedIndex = sIx+1 }
where
treeSize = length $ concatMap (flatten . filterVisible) $ nodes t
----------------------------------------------------------------------
-- collapse/expand
-- | If tree expanded then collapse it, else expand.
collapseOrExpand t
= if collapsed $ current t then expand t
else collapse t
-- | Collapse or expand only current node.
collapse t@(Tree {..}) = t {nodes = setCollapsed True selectedIndex nodes}
expand t@(Tree {..}) = t {nodes = setCollapsed False selectedIndex nodes}
-- FIXME: работает совсем неправильно
-- | Collapse current node or if already collapsed then collapse
-- parent.
collapse' t@(Tree {..}) = t
{ selectedIndex = parentIx
, nodes = setCollapsed True parentIx nodes
}
where
-- ищем узел, который нужно свернуть. Это может быть:
-- - выбранный узел, если у него есть вложенные узлы и он сам
-- развёрнут
-- - иначе, сворачиваем родителя выбранного узла
parentIx = fst $ foldl getParent1 (0, 0) nodes
-- для самого верхнего уровня логика немного отличается:
-- если узел свёрнут или он лист, то ничего не происходит (потому
-- как родительского узла нет)
getParent1 (p,ix) n = if ix <= selectedIndex then getParent' (ix,ix) n else (p,ix)
-- у вложенных узлов всегда есть родитель которого можно свернуть
getParent2 (p,ix) n = if ix <= selectedIndex then getParent' (p,ix) n else (p,ix)
getParent' (p,ix) (Node n ns)
-- если выбран лист дерева или свёрнутый узел, нужно сворачивать
-- ветку на которой он висит
-- если выбран развёрнутый узел дерева, значит его самого и
-- сворачиваем
| ix == selectedIndex = if null ns || collapsed n then (p,ix+1) else (ix,ix+1)
-- до выбранного узла ещё не добрались, встретили развёрнутый,
-- запоминаем его как родительский и заходим внутрь
| (not.collapsed) n && (not.null) ns = foldl getParent2 (ix,ix+1) ns
-- встретили лист или свёрнутый узел, проходим мимо
| otherwise = (p,ix+1)
setCollapsed value nodeIx nodes = evalState (mapM setCollapsed' nodes) 0
where
setCollapsed' (Node n ns) = do
ix <- get
put $ ix + 1
if ix == nodeIx
-- проверяем null ns, чтобы не было крестиков около листьев
then return $ Node (n {collapsed = value && (not.null) ns}) ns
else if collapsed n || ix > nodeIx
then return $ Node n ns
else Node n `fmap` mapM setCollapsed' ns
{- TODO: сделать monad transformer
appendIndex nodes = evalState (mapM (mapNode index) nodes) 0
where
index = getIndex >>= \ix -> put (Index $ ix+1) >> return ix
noIndex = return $ Index (-1)
mapNode indexProvider (Node n ns) = do
ix <- indexProvider
ns' <- mapM (mapNode $ if collapsed n then noIndex else index) ns
return $ Node (ix,n) ns'
-}
----------------------------------------------------------------------
-- rendering
instance Widget Tree where
growHorizontal t = isNothing (treeWidth t)
growVertical t = isNothing (treeHeight t)
primaryAttribute _ = def_attr
withAttribute w _ = w
render rgn t@(Tree {..})
= vert_cat
$ map (renderNode width nodeAttr) a
++ [renderNode width selectedAttr n]
++ map (renderNode width nodeAttr) b
where
width = maybe (fromIntegral $ region_width rgn) id treeWidth
height = maybe (fromIntegral $ region_height rgn) id treeHeight
Tree {windowStart = start} = resize width height t
(a, n:b) = splitAt (selectedIndex - start) $ window start height t
renderNode width attr (depth, n) = string attr $ take' width ' ' $ fst $ value n
resize _ h t@(Tree {windowStart = ws, selectedIndex = sIx})
-- current node is out of view => scroll up until it is visible
| sIx > ws + h - 1 = t { windowStart = sIx - h + 1 }
-- we have free space after last node => scroll down to fill it
| wl < h = t { windowStart = max 0 $ ws - (h - wl) }
| otherwise = t
where
wl = length $ window ws h t
---
cur = snd . value . current
current t = (concatMap flatten $ map filterVisible $ nodes t) !! selectedIndex t
window start sz = take sz . drop start . concatMap (flatten . appendDepths 0 . filterVisible) . nodes
appendDepths d (Node n ns) = Node (d,n) $ map (appendDepths (d+1)) ns
filterVisible (Node n ns) = Node n $ if collapsed n then [] else map filterVisible ns
take' n add lst = if length lst < n
then lst ++ replicate (n - length lst) add
else take n lst