TLDR; see draft PR
Problem Outline
Tab navigation in the current Layout system does not work when Layouts are nested
For example:
col $ do
-- row 1
fixed 5 $ row $ do
fixed 1 $ someWidget1
fixed 1 $ someWidget2
fixed 1 $ someWidget3
-- row 2
fixed 5 $ row $ do
fixed 1 $ someWidget4
fixed 1 $ someWidget5
fixed 1 $ someWidget6
assume row1
+ someWidget1
are in focus
The desired behavior is:
- pressing "tab" will focus
row1
and someWidget2
- pressing "tab" will switch focus from
someWidget<n>
to someWidget<n+1 mod 6>
and switch focus between row1
and row2
as necessary
However, instead we have:
- pressing "tab" will focus
row2
and someWidget2
- in addition, pressing
tab
again will focus row1
and someWidget2
Challenges
Broadly speaking, Layout alternates between Layout and VtyWidget and we need to pass data across this boundary in order for tabbing to work as desired.
Using the existing Layout
module as a starting point, there are 2 major challenges to support the desired behavior outlined above:
runLayout
needs to be aware of nested layout nodes in order to focus the correct tile
tile
needs to be aware of when a particular nested node index should be focus
Solution
- is addressed by introducing the new class below. NOTE,
LayoutTree
is discussed later.
class IsLayoutReturn t b a where
getLayoutResult :: b -> a
getLayoutNumChildren :: b -> Int
getLayoutFocussedDyn :: b -> Dynamic t (Maybe Int)
getLayoutTree :: b -> LayoutTree t
instance IsLayoutReturn t (LayouTree t, Dynamic t (Maybe Int), Int, a) a where
getLayoutResult (_,_,_,a) = a
getLayoutNumChildren (_,_,d,_) = d
getLayoutFocussedDyn (_,d,_,_) = d
getLayoutTree (tree,_,_,_) = tree
instance Reflex t => IsLayoutReturn t a a where
getLayoutResult = id
getLayoutNumChildren _ = 1
getLayoutFocussedDyn _ = constDyn Nothing
getLayoutTree _ = emptyLayoutTree
- is address by wrapping the VtyWidget monad with additional contextual information
class IsLayoutVtyWidget l t (m :: * -> *) where
runIsLayoutVtyWidget :: l t m a -> Event t (Maybe Int) -> VtyWidget t m a
newtype LayoutVtyWidget t m a = LayoutVtyWidget {
unLayoutVtyWidget :: ReaderT (Event t (Maybe Int)) (VtyWidget t m) a
} deriving (...)
instance MonadTrans (LayoutVtyWidget t) where
lift x = LayoutVtyWidget $ lift $ lift x
instance IsLayoutVtyWidget VtyWidget t m where
runIsLayoutVtyWidget w _ = w
instance IsLayoutVtyWidget LayoutVtyWidget t m where
runIsLayoutVtyWidget = runReaderT . unLayoutVtyWidget
Note that since we have instance LayoutReturn t a a
in 1. and instance IsLayoutVtyWidget VtyWidget t m
in 2. they work with both layout and non-layout nodes.
Interface Changes
Since focus events now come from the top level layout node, a new method is needed:
beginLayout :: LayoutVtyWidget -> VtyWidget
beginLayout child = do
tab <- tabNavigation
runLayoutVtyWidget child tab
Unfortunately, the interface is not seamless in the case of 1. as the desired monadic return value type is ambiguous as we have both
IsLayoutReturn t (LayoutTree t, Dynamic t (Maybe Int), Int, a) a
IsLayoutReturn t (LayoutTree t, Dynamic t (Maybe Int), Int, a) (LayoutTree t, Dynamic t (Maybe Int), Int, a)
So we introduce new variants fixedD
and stretchD
that make the type deductions above explicit.
Testing
LayoutTree
is an optional returned object of type Dynamic t (Tree Region)
objects of all the children. The intent is to use for automated testing (e.g. get me the absolute coordinates of (5,7) in relative coordinates of the third node of the second node of some LayoutTree
). You can see an example of this here. I imagine it might be useful in non-testing scenarios as well.
Code
I've opened up the following draft PR that implements most of the above for discussion.