Implement tree functions
This commit is contained in:
parent
54ec7afa59
commit
3255bfd2ec
3 changed files with 140 additions and 28 deletions
|
|
@ -1,14 +1,26 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Forest.Client.Tree
|
module Forest.Client.Tree
|
||||||
( Tree(..)
|
( Tree
|
||||||
, newTree
|
, newTree
|
||||||
, renderTree
|
|
||||||
, toggleFold
|
|
||||||
, moveFocusUp
|
|
||||||
, moveFocusDown
|
|
||||||
, switchNode
|
, switchNode
|
||||||
|
, renderTree
|
||||||
|
-- * Focused element
|
||||||
|
, getCurrent
|
||||||
|
, moveUp
|
||||||
|
, moveDown
|
||||||
|
-- * Folding
|
||||||
|
, isCurrentFolded
|
||||||
|
, foldCurrent
|
||||||
|
, unfoldCurrent
|
||||||
|
, toggleFold
|
||||||
|
-- * Example values
|
||||||
|
, exampleTree
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Brick
|
import Brick
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import Data.Maybe
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
import Forest.Client.Node
|
import Forest.Client.Node
|
||||||
|
|
@ -16,20 +28,44 @@ import Forest.Client.NodeEditor
|
||||||
import Forest.Client.ResourceName
|
import Forest.Client.ResourceName
|
||||||
import Forest.Client.WidgetTree
|
import Forest.Client.WidgetTree
|
||||||
import Forest.Node
|
import Forest.Node
|
||||||
|
import Forest.Util
|
||||||
|
|
||||||
data Tree = Tree
|
data Tree = Tree
|
||||||
{ treeNode :: Node
|
{ treeNode :: Node
|
||||||
|
-- Invariant: The node pointed to by the focused path must always exist
|
||||||
|
-- Invariant: The node pointed to by the focused path must not be folded away
|
||||||
, treeFocused :: Path
|
, treeFocused :: Path
|
||||||
|
-- Invariant: The nodes pointed to by the unfolded paths must always exist
|
||||||
, treeUnfolded :: Set.Set Path
|
, treeUnfolded :: Set.Set Path
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
newTree :: Node -> Tree
|
-- | Find the focus path closest to the input path that still corresponds to a
|
||||||
newTree node = Tree
|
-- node in the input tree.
|
||||||
{ treeNode = node
|
findNearestFocus :: Node -> Path -> Path
|
||||||
, treeFocused = Path []
|
findNearestFocus _ (Path []) = Path []
|
||||||
, treeUnfolded = Set.empty
|
findNearestFocus node (Path (x:xs)) = case applyId x node of
|
||||||
}
|
Nothing -> Path []
|
||||||
|
Just child ->
|
||||||
|
let (Path childPath) = findNearestFocus child $ Path xs
|
||||||
|
in Path (x:childPath)
|
||||||
|
|
||||||
|
-- | Create a new tree, ensuring that all required invariants hold.
|
||||||
|
newTree :: Node -> Path -> Set.Set Path -> Tree
|
||||||
|
newTree node focused unfolded = Tree
|
||||||
|
{ treeNode = node
|
||||||
|
, treeFocused = safeFocused
|
||||||
|
, treeUnfolded = safeUnfolded
|
||||||
|
}
|
||||||
|
where
|
||||||
|
safeUnfolded = Set.filter (isValidPath node) unfolded
|
||||||
|
safeFocused = findNearestFocus (applyFolds safeUnfolded node) focused
|
||||||
|
|
||||||
|
-- | Switch out a tree's node, keeping as much of the focus and folding
|
||||||
|
-- information as the type's invariants allow.
|
||||||
|
switchNode :: Node -> Tree -> Tree
|
||||||
|
switchNode node tree = newTree node (treeFocused tree) (treeUnfolded tree)
|
||||||
|
|
||||||
|
-- | Render a 'Tree' into a widget.
|
||||||
renderTree :: IndentOptions -> Maybe NodeEditor -> Tree -> Widget ResourceName
|
renderTree :: IndentOptions -> Maybe NodeEditor -> Tree -> Widget ResourceName
|
||||||
renderTree opts maybeEditor tree =
|
renderTree opts maybeEditor tree =
|
||||||
renderWidgetTree opts $ nodeToTree drawState $ treeNode tree
|
renderWidgetTree opts $ nodeToTree drawState $ treeNode tree
|
||||||
|
|
@ -40,34 +76,97 @@ renderTree opts maybeEditor tree =
|
||||||
, dsUnfolded = treeUnfolded tree
|
, dsUnfolded = treeUnfolded tree
|
||||||
}
|
}
|
||||||
|
|
||||||
isCurrentFolded :: Tree -> Bool
|
{- Focused element -}
|
||||||
isCurrentFolded tree = treeFocused tree `Set.member` treeUnfolded tree
|
|
||||||
|
|
||||||
|
-- | Get the currently focused node.
|
||||||
|
getCurrent :: Tree -> Node
|
||||||
|
-- We rely on the invariant that the focused node always exists
|
||||||
|
getCurrent tree = fromJust $ applyPath (treeFocused tree) (treeNode tree)
|
||||||
|
|
||||||
|
-- | Attempt to find the path of the node that is above the input path.
|
||||||
|
findPrevNode :: Node -> Path -> Maybe Path
|
||||||
|
findPrevNode _ (Path []) = Nothing
|
||||||
|
findPrevNode node (Path [x]) =
|
||||||
|
let childIds = Map.keys $ nodeChildren node
|
||||||
|
prevId = findPrev (==x) childIds
|
||||||
|
in case prevId of
|
||||||
|
Nothing -> Just $ Path []
|
||||||
|
Just nodeId -> Just $ Path [nodeId]
|
||||||
|
findPrevNode node (Path (x:xs)) = case applyId x node of
|
||||||
|
Nothing -> Just $ Path [] -- This should not happen normally
|
||||||
|
Just childNode -> case findPrevNode childNode (Path xs) of
|
||||||
|
Nothing -> Just $ Path []
|
||||||
|
Just path -> Just path
|
||||||
|
|
||||||
|
-- | Attempt to find the path of the node that is below the input path.
|
||||||
|
findNextNode :: Node -> Path -> Maybe Path
|
||||||
|
findNextNode node (Path []) = case Map.keys $ nodeChildren node of
|
||||||
|
(x:_) -> Just $ Path [x]
|
||||||
|
_ -> Nothing
|
||||||
|
findNextNode node (Path [x]) =
|
||||||
|
let childIds = Map.keys $ nodeChildren node
|
||||||
|
nextId = findNext (==x) childIds
|
||||||
|
in case nextId of
|
||||||
|
Nothing -> Nothing
|
||||||
|
Just nodeId -> Just $ Path [nodeId]
|
||||||
|
findNextNode node (Path (x:xs)) = case applyId x node of
|
||||||
|
Nothing -> Just $ Path [] -- This should not happen normally
|
||||||
|
Just childNode -> case findPrevNode childNode (Path xs) of
|
||||||
|
Just path -> Just path
|
||||||
|
Nothing -> findNextNode node (Path [x])
|
||||||
|
|
||||||
|
-- | Move the focus upward by one node, if possible. Otherwise, do nothing.
|
||||||
|
moveUp :: Tree -> Tree
|
||||||
|
moveUp tree@Tree{treeFocused=focused} = tree{treeFocused = fromMaybe focused prevNode}
|
||||||
|
where
|
||||||
|
folded = applyFolds (treeUnfolded tree) (treeNode tree)
|
||||||
|
prevNode = findPrevNode folded focused
|
||||||
|
|
||||||
|
-- | Move the focus downward by one node, if possible. Otherwise, do nothing.
|
||||||
|
moveDown :: Tree -> Tree
|
||||||
|
moveDown tree@Tree{treeFocused=focused} = tree{treeFocused = fromMaybe focused nextNode}
|
||||||
|
where
|
||||||
|
folded = applyFolds (treeUnfolded tree) (treeNode tree)
|
||||||
|
nextNode = findNextNode folded focused
|
||||||
|
|
||||||
|
{- Folding -}
|
||||||
|
|
||||||
|
-- | Check if the currently focused node is folded.
|
||||||
|
isCurrentFolded :: Tree -> Bool
|
||||||
|
isCurrentFolded tree = not $ treeFocused tree `Set.member` treeUnfolded tree
|
||||||
|
|
||||||
|
-- | Fold the currently focused node. Does nothing if it is already folded.
|
||||||
foldCurrent :: Tree -> Tree
|
foldCurrent :: Tree -> Tree
|
||||||
foldCurrent tree@Tree {treeFocused = f, treeUnfolded = u} =
|
foldCurrent tree@Tree {treeFocused = f, treeUnfolded = u} =
|
||||||
tree {treeUnfolded = Set.delete f u}
|
let foldedTree = withFolds tree {treeUnfolded = Set.delete f u}
|
||||||
|
in foldedTree {treeNode = treeNode tree}
|
||||||
|
|
||||||
|
-- | Unfold the currently focused node. Does nothing if it is already unfolded.
|
||||||
unfoldCurrent :: Tree -> Tree
|
unfoldCurrent :: Tree -> Tree
|
||||||
unfoldCurrent tree@Tree {treeFocused = f, treeUnfolded = u} =
|
unfoldCurrent tree@Tree {treeFocused = f, treeUnfolded = u} =
|
||||||
tree {treeUnfolded = Set.insert f u}
|
tree {treeUnfolded = Set.insert f u}
|
||||||
|
|
||||||
|
-- | Toggle whether the currently focused node is folded.
|
||||||
toggleFold :: Tree -> Tree
|
toggleFold :: Tree -> Tree
|
||||||
toggleFold tree
|
toggleFold tree
|
||||||
| isCurrentFolded tree = unfoldCurrent tree
|
| isCurrentFolded tree = unfoldCurrent tree
|
||||||
| otherwise = foldCurrent tree
|
| otherwise = foldCurrent tree
|
||||||
|
|
||||||
moveFocusUp :: Tree -> Tree
|
-- | Remove all nodes that would not be visible due to the folding.
|
||||||
moveFocusUp = id -- TODO implement properly
|
applyFolds :: Set.Set Path -> Node -> Node
|
||||||
|
applyFolds unfolded node
|
||||||
|
| localPath `Set.member` unfolded = node {nodeChildren = foldedChildren}
|
||||||
|
| otherwise = node {nodeChildren = Map.empty}
|
||||||
|
where
|
||||||
|
foldedChildren = Map.fromList $ mapChildren applyFoldsToChild node
|
||||||
|
applyFoldsToChild nid n = (nid, applyFolds (narrowSet nid unfolded) n)
|
||||||
|
|
||||||
moveFocusDown :: Tree -> Tree
|
-- | Apply folds to a whole 'Tree' (see 'applyFolds').
|
||||||
moveFocusDown = id -- TODO implement properly
|
withFolds :: Tree -> Tree
|
||||||
|
withFolds tree = newTree
|
||||||
|
(applyFolds (treeUnfolded tree) (treeNode tree))
|
||||||
|
(treeFocused tree)
|
||||||
|
(treeUnfolded tree)
|
||||||
|
|
||||||
findNearestFocus :: Node -> Path -> Path
|
exampleTree :: Tree
|
||||||
findNearestFocus _ _ = localPath -- TODO implement properly
|
exampleTree = newTree exampleNode (Path ["hammer"]) Set.empty
|
||||||
|
|
||||||
switchNode :: Node -> Tree -> Tree
|
|
||||||
switchNode node tree = Tree
|
|
||||||
{ treeNode = node
|
|
||||||
, treeFocused = findNearestFocus node $ treeFocused tree
|
|
||||||
, treeUnfolded = Set.filter (isValidPath node) $ treeUnfolded tree
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ module Forest.Node
|
||||||
, initialNode
|
, initialNode
|
||||||
, applyId
|
, applyId
|
||||||
, applyPath
|
, applyPath
|
||||||
|
, mapChildren
|
||||||
-- * Path
|
-- * Path
|
||||||
, Path(..)
|
, Path(..)
|
||||||
, localPath
|
, localPath
|
||||||
|
|
@ -66,6 +67,9 @@ applyId nodeId node = nodeChildren node Map.!? nodeId
|
||||||
applyPath :: Path -> Node -> Maybe Node
|
applyPath :: Path -> Node -> Maybe Node
|
||||||
applyPath (Path ids) node = foldM (flip applyId) node ids
|
applyPath (Path ids) node = foldM (flip applyId) node ids
|
||||||
|
|
||||||
|
mapChildren :: (NodeId -> Node -> a) -> Node -> [a]
|
||||||
|
mapChildren f node = map (uncurry f) $ Map.toAscList $ nodeChildren node
|
||||||
|
|
||||||
{- Path -}
|
{- Path -}
|
||||||
|
|
||||||
newtype Path = Path
|
newtype Path = Path
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
module Forest.Util
|
module Forest.Util
|
||||||
( withThread
|
( findPrev
|
||||||
|
, findNext
|
||||||
|
, withThread
|
||||||
, sendPacket
|
, sendPacket
|
||||||
, receivePacket
|
, receivePacket
|
||||||
, closeWithErrorMessage
|
, closeWithErrorMessage
|
||||||
|
|
@ -9,9 +11,16 @@ module Forest.Util
|
||||||
|
|
||||||
import Control.Concurrent.Async
|
import Control.Concurrent.Async
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
|
import Data.List
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Network.WebSockets as WS
|
import qualified Network.WebSockets as WS
|
||||||
|
|
||||||
|
findPrev :: (a -> Bool) -> [a] -> Maybe a
|
||||||
|
findPrev f as = fst <$> find (f . snd) (zip as $ tail as)
|
||||||
|
|
||||||
|
findNext :: (a -> Bool) -> [a] -> Maybe a
|
||||||
|
findNext f as = snd <$> find (f . fst) (zip as $ tail as)
|
||||||
|
|
||||||
withThread :: IO () -> IO () -> IO ()
|
withThread :: IO () -> IO () -> IO ()
|
||||||
withThread thread main = withAsync thread $ const main
|
withThread thread main = withAsync thread $ const main
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue