brick-tabular-list-2.1.0.0: src/Brick/Widgets/TabularList/Grid.hs
{-# OPTIONS_HADDOCK show-extensions #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeApplications #-}
-- | Grid tabular list is a uniform grid that supports cell-by-cell navigation.
--
--   
--
-- Because this list is designed to show arbitrary numbers of columns, horizontal scrolling is supported through
-- cell-by-cell navigation.
--
-- Grid tabular list tries to show the current column in the center. If it can't show the current column in the center,
-- it shows the first column in the left corner or the last column in the right corner.
--
-- It should be fast enough to handle a large spreadsheet. It is also suitable for an interface to a database table.
module Brick.Widgets.TabularList.Grid (
-- * Data types
GridRowCtxt(..)
, GridColCtxt(..)
, GridCtxt(..)
, GridColHdr(..)
, GridRenderers(..)
, GridTabularList(..)
-- * List construction
, gridTabularList
-- * Rendering
, renderGridTabularList
-- * Column navigation #ColumnNavigation#
, gridMoveLeft
, gridMoveRight
, gridMoveTo
, gridMoveToBeginning
, gridMoveToEnd
, gridMovePageUp
, gridMovePageDown
-- * Event handlers
, handleGridListEvent
, handleGridListEventVi
-- * Shared types
, module Brick.Widgets.TabularList.Types
) where
import Brick.Widgets.TabularList.Types
import Brick.Widgets.TabularList.Internal.Common
import Brick.Widgets.TabularList.Internal.Lens
-- base
import GHC.Generics (Generic)
import Data.Foldable (toList)
import Control.Monad (unless)
-- Third party libraries
import Optics.Core ( (&), (%), (%~), (.~), (^.), coercedTo )
import qualified Data.Sequence as S
import Data.Sequence (Seq(..))
import Data.Generics.Labels
-- Brick & Vty
import qualified Brick.Widgets.List as L
import Brick.Types
import Brick.Widgets.Core
import Graphics.Vty (Event(..), Key(..), Modifier(..))
import Brick.Main (lookupViewport)
-- | Grid row context
data GridRowCtxt = GRowC {
index :: Index
, selected :: Selected
} deriving (Eq, Generic, Show)
-- | Grid column context
data GridColCtxt = GColC {
index :: Index
, selected :: Selected
} deriving (Eq, Generic, Show)
-- | Context for grid cells
data GridCtxt = GrdCtxt {
row :: GridRowCtxt
, col :: GridColCtxt
} deriving (Eq, Generic, Show)
-- | Grid column header
--
-- * [Type Variables]("Brick.Widgets.TabularList#g:TypeVariables")
-- * [Rendering]("Brick.Widgets.TabularList#g:Rendering")
data GridColHdr n = GridColHdr {
draw :: ListFocused -> WidthDeficit -> GridColCtxt -> Widget n
, height :: ColHdrHeight
} deriving Generic
-- | Rendering functions for components of grid tabular list
--
-- * [Type Variables]("Brick.Widgets.TabularList#g:TypeVariables")
-- * [Rendering]("Brick.Widgets.TabularList#g:Rendering")
data GridRenderers n e r = GridRenderers {
cell :: ListFocused -> WidthDeficit -> GridCtxt -> e -> Widget n
, rowHdr :: Maybe (RowHdr n e r)
, colHdr :: Maybe (GridColHdr n)
, colHdrRowHdr :: Maybe (ColHdrRowHdr n)
} deriving Generic
-- | * [Type Variables]("Brick.Widgets.TabularList#g:TypeVariables")
data GridTabularList n e = GridTabularList {
-- | The underlying primitive list that comes from brick
list :: L.GenericList n Seq e
, widths :: Seq ColWidth
-- | Manipulating this field directly is unsafe. Use
-- [column navigation]("Brick.Widgets.TabularList.Grid#g:ColumnNavigation") functions to manipulate this. If you still
-- want to manipulate this directly, create a function that manipulates it, and test the function properly.
, currentColumn :: Index
} deriving Generic
-- | Create a grid tabular list
gridTabularList
:: n -- ^ The list name (must be unique)
-> Seq e -- ^ The initial list elements
-> ListItemHeight
-> Seq ColWidth
-> GridTabularList n e
gridTabularList n rows (LstItmH h) widths = GridTabularList {
list = L.list n rows h
, widths = widths
, currentColumn = Ix 0
}
-- | Width accumulated in the process of traversing columns
newtype AccWidth = AccW Int deriving (Eq, Show)
data VisibleColumns =
-- | No column is visible
NoColumn |
-- | Only current column is visible
CurrentColumn |
-- | The first column is shown at the left corner.
AnchoredLeft {
right :: Index -- ^ The rightmost column that is visible
} |
-- | The current column is shown in the center
MiddleColumns {
-- | The leftmost visible column
left :: Index,
-- | The rightmost visible column
right :: Index,
-- | Slide the columns to the left by this offset to show the current column in the center
offset :: Int,
-- | Total widths of all visible columns.
tW :: AccWidth
} |
-- | The last column is shown at the right corner.
AnchoredRight {
-- | The leftmost visible column
left :: Index,
-- | Slide the columns to the left by this offset to show the last column at the right corner.
offset :: Int,
-- | Total widths of all visible columns.
tW :: AccWidth
}
deriving (Eq, Show)
-- | Calculate visible columns with the width available for columns. It tries to show the current column in the center.
-- If it can't show the current column in the center, the first column is shown at the left corner, or the last column
-- is shown at the right corner.
visibleColumns :: GridTabularList n e -> AvailWidth -> VisibleColumns
visibleColumns l (AvlW aW) = let
(Ix curCol) = l ^. #currentColumn
ws = l ^. #widths
in case S.splitAt curCol ws of
-- If current column is outside the boundary of row columns, return `NoColumn`.
(_, Empty) -> NoColumn
(left, ColW cW :<| right) -> if aW <= 0
-- If the available width is 0 or less than 0,
then NoColumn
-- If the available width is equal to or less than the current column's width,
else if aW <= cW
then CurrentColumn
-- Otherwise
else let
-- The amount of space to the left of the current column shown in the center.
lW = (aW - cW) `div` 2
-- The amount of space to the right of the current column shown in the center.
rW = aW - lW - cW
-- Calculate the leftmost visible column for the current column shown in the center.
leftForMiddle (l :|> ColW w) (Ix idx) (AccW accW) = if accW+w < lW
-- If the leftmost visible column hasn't been reached, go to the left by one column.
then leftForMiddle l (Ix $ idx-1) (AccW $ accW+w)
-- If the leftmost visible column has been reached, calculate the rightmost visible column.
else rightForMiddle (Ix idx) (AccW $ accW+w) right (Ix $ curCol + 1) (AccW 0)
-- If there aren't enough columns to the left of the current column shown in the center, calculate the rightmost
-- visible column for the first column shown at the left corner.
leftForMiddle Empty _ (AccW accW) = rightForLeft right (Ix $ curCol+1) (AccW $ accW+cW)
-- Calculate the rightmost visible column for the current column shown in the center.
rightForMiddle (Ix li) (AccW lAccW) (ColW w :<| r) (Ix ri) (AccW accW) = if accW+w < rW
-- If the rightmost visible column hasn't been reached, go to the right by one column.
then rightForMiddle (Ix li) (AccW lAccW) r (Ix $ ri+1) (AccW $ accW+w)
-- If the rightmost visible column has been reached, return 'MiddleColumns'.
else MiddleColumns { left = Ix li, right = Ix ri, offset = lAccW-lW, tW = AccW $ lAccW+cW+accW+w }
-- If there aren't enough columns to the right of the current column shown in the center, calculate the leftmost
-- visible column for the last column shown at the right corner.
rightForMiddle _ _ Empty _ (AccW accW) = leftForRight left (Ix $ curCol-1) (AccW $ accW+cW)
-- Calculate the rightmost visible column for the first column shown at the left corner.
rightForLeft (ColW w :<| r) (Ix idx) (AccW accW) = if accW+w < aW
-- If the rightmost visible column hasn't been reached, go to the right by one column.
then rightForLeft r (Ix $ idx+1) (AccW $ accW+w)
-- If the rightmost visible column has been reached, return 'AnchoredLeft'.
else AnchoredLeft $ Ix idx
-- If there aren't enough columns to fill the available width with the first column at the left corner, return
-- 'AnchoredLeft' with the last column as the rightmost visible column.
rightForLeft Empty (Ix idx) _ = AnchoredLeft (Ix $ idx-1)
-- Calculate the leftmost visible column for the last column shown at the right corner.
leftForRight (l :|> ColW w) (Ix idx) (AccW accW) = if accW+w < aW
-- If the leftmost visible column hasn't been reached, go to the left by one column.
then leftForRight l (Ix $ idx-1) (AccW $ accW+w)
-- If the leftmost visible column has been reached, return 'AnchoredRight'.
else AnchoredRight { left = Ix idx, offset = accW+w-aW, tW = AccW $ accW+w }
-- If there aren't enough columns to fill the available width with the last column at the right corner, return
-- 'AnchoredLeft' with the last column as the rightmost visible column.
leftForRight Empty _ _ = AnchoredLeft $ Ix $ length ws - 1
in leftForMiddle left (Ix $ curCol-1) (AccW 0)
-- | Height for tabular list components
newtype Height = H Int deriving (Eq, Show)
renderColumns
:: GridTabularList n e
-> VisibleColumns
-> (WidthDeficit -> Index -> ColWidth -> Widget n)
-> Height
-> Widget n
renderColumns l vCs dC (H h) = Widget Greedy Fixed $ do
c <- getContext
let cWs = l ^. #widths
Ix curCol = l ^. #currentColumn
aW = c^^.availWidthL
render $ case vCs of
NoColumn -> emptyWidget
CurrentColumn -> case S.lookup curCol cWs of
Nothing -> error $ "Current column, " <> show curCol <> " is outside the boundary of column widths."
Just (ColW cW) -> dC (WdthD $ max 0 $ cW - aW) (Ix curCol) (ColW aW)
AnchoredLeft {right=Ix r} -> hBox $ zipWith (dC $ WdthD 0) [Ix 0..] $ toList $ S.take (r+1) cWs
MiddleColumns {left=Ix l, right=Ix r, offset, tW=AccW tw} -> cropLeftBy offset $ sz (tw, h) $
hBox $ zipWith (dC $ WdthD 0) [Ix l..] $ toList $ S.take (r-l+1) $ S.drop l cWs
AnchoredRight {left=Ix l, offset, tW=AccW tw} -> cropLeftBy offset $ sz (tw, h) $
hBox $ zipWith (dC $ WdthD 0) [Ix l..] $ toList $ S.drop l cWs
-- | Render grid tabular list
renderGridTabularList :: (Ord n, Show n)
=> GridRenderers n e r
-> ListFocused
-> GridTabularList n e -- ^ The list
-> Widget n
renderGridTabularList r (LstFcs f) l = Widget Greedy Greedy $ do
c <- getContext
let aW = c^^.availWidthL
aH = c^^.availHeightL
cell = r ^. #cell
GridTabularList {list=l', currentColumn=curCol} = l
iH = l' ^. #listItemHeight
colHdrRow vCs (RowHdrW rhw) (WdthD rhwd) = case r ^. #colHdr of
Nothing -> emptyWidget
Just (GridColHdr {draw, height=ColHdrH chh}) -> let
col wd c (ColW w) = sz (w, chh) $ draw (LstFcs f) wd $ GColC c $ Sel (c == curCol)
chrw = case r ^. #colHdrRowHdr of
Nothing -> fill ' '
Just (CHRH chrh) -> chrh (LstFcs f) (WdthD rhwd)
in sz (rhw, chh) chrw <+> renderColumns l vCs col (H chh)
row vCs i f r = let
col wd c (ColW w) = let gc = GrdCtxt (GRowC (Ix i) (Sel f)) $ GColC c $ Sel (c == curCol)
in sz (w, iH) $ cell (LstFcs f) wd gc r
in renderColumns l vCs col (H iH)
lst = let vCs = visibleColumns l (AvlW aW) in
render $ colHdrRow vCs (RowHdrW 0) (WdthD 0) <=> L.renderListWithIndex (row vCs) f l'
hdrLst (RowHdr {draw=drw, width, toRH=tR}) = let
RowHdrW rhw' = width (AvlW aW) $ uncurry (zipWith tR) $ visibleRowIdx l' (AvlH aH)
rhw = min rhw' aW
rhwd = WdthD $ max 0 $ rhw' - aW
vCs = visibleColumns l $ AvlW $ aW - rhw
hdrRow i f r = sz (rhw, iH) (drw (LstFcs f) rhwd (RowHdrCtxt $ Sel f) $ tR r (Ix i)) <+> row vCs i f r
in render $ colHdrRow vCs (RowHdrW rhw) rhwd <=> L.renderListWithIndex hdrRow f l'
maybe lst hdrLst $ r ^. #rowHdr
-- | Move to the left by one column.
gridMoveLeft
:: GridTabularList n e -- ^ The list
-> GridTabularList n e
gridMoveLeft gl = if null $ gl ^. #list % #listSelected
then gl
else gl & #currentColumn % coercedTo @Int %~ max 0 . subtract 1
-- | Move to the right by one column.
gridMoveRight
:: GridTabularList n e -- ^ The list
-> GridTabularList n e
gridMoveRight gl = if null $ gl ^. #list % #listSelected
then gl
else gl & #currentColumn % coercedTo @Int %~ min (length (gl ^. #widths) - 1) . (+1)
-- | Move to the given column index
gridMoveTo
:: Index
-> GridTabularList n e -- ^ The list
-> GridTabularList n e
gridMoveTo (Ix n) gl = if null $ gl ^. #list % #listSelected
then gl
else gl & #currentColumn % coercedTo @Int .~ max 0 (min (length (gl ^. #widths) - 1) n)
-- | Move to the first column.
gridMoveToBeginning
:: GridTabularList n e -- ^ The list
-> GridTabularList n e
gridMoveToBeginning gl = if null $ gl ^. #list % #listSelected
then gl
else gl & #currentColumn % coercedTo @Int .~ 0
-- | Move to the last column.
gridMoveToEnd
:: GridTabularList n e -- ^ The list
-> GridTabularList n e
gridMoveToEnd gl = if null $ gl ^. #list % #listSelected
then gl
else gl & #currentColumn % coercedTo @Int .~ length (gl ^. #widths) - 1
-- | 'GridRenderers' are needed because if row header renderer doesn't exist, width calculation is affected.
gridMovePage :: Ord n
=> GridRenderers n e r
-> (VisibleColumns -> EventM n (GridTabularList n e) ())
-> EventM n (GridTabularList n e) ()
gridMovePage r f = do
l <- get
let l' = l ^. #list
unless (null $ l' ^^. L.listSelectedL) $ do
v <- lookupViewport $ l' ^^. L.listNameL
case v of
Nothing -> return ()
Just vp -> let
(aW, aH) = vp ^. #_vpSize
RowHdrW rhw = case r ^. #rowHdr of
Nothing -> RowHdrW 0
Just (RowHdr {width, toRH}) -> width (AvlW aW) $ uncurry (zipWith toRH) $ visibleRowIdx l' (AvlH aH)
in f $ visibleColumns l $ AvlW $ aW - rhw
-- | Move to the previous page of columns.
--
-- 'GridRenderers' are needed because if row header doesn't exist, width calculation is affected.
gridMovePageUp :: Ord n
=> GridRenderers n e r -- ^ Renderers
-> EventM n (GridTabularList n e) ()
gridMovePageUp r = gridMovePage r $ \case
NoColumn -> return ()
CurrentColumn -> modify gridMoveLeft
AnchoredLeft _ -> modify gridMoveToBeginning
MiddleColumns {left} -> modify $ gridMoveTo left
AnchoredRight {left} -> modify $ gridMoveTo left
-- | Move to the next page of columns.
--
-- 'GridRenderers' are needed because if row header doesn't exist, width calculation is affected.
gridMovePageDown :: Ord n
=> GridRenderers n e r -- ^ Renderers
-> EventM n (GridTabularList n e) ()
gridMovePageDown r = gridMovePage r $ \case
NoColumn -> return ()
CurrentColumn -> modify gridMoveRight
AnchoredLeft {right} -> modify $ gridMoveTo right
MiddleColumns {right} -> modify $ gridMoveTo right
AnchoredRight {} -> modify gridMoveToEnd
-- | Handle events for grid tabular list with navigation keys.
--
-- It adds the following keyboard shortcuts to 'L.handleListEvent'.
--
-- * Move to the left by one column (Left arrow key)
-- * Move to the right by one column (Right arrow key)
-- * Go to the first column (Ctrl+Home)
-- * Go to the last column (Ctrl+End)
-- * Move to the previous page of columns (Ctrl+PageUp)
-- * Move to the next page of columns (Ctrl+PageDown)
--
-- 'GridRenderers' are needed because if row header doesn't exist, width calculation is affected.
handleGridListEvent :: Ord n
=> GridRenderers n e r -- ^ Renderers
-> Event -> EventM n (GridTabularList n e) ()
handleGridListEvent r e = case e of
EvKey KLeft [] -> modify gridMoveLeft
EvKey KRight [] -> modify gridMoveRight
EvKey KHome [MCtrl] -> modify gridMoveToBeginning
EvKey KEnd [MCtrl] -> modify gridMoveToEnd
EvKey KPageUp [MCtrl] -> gridMovePageUp r
EvKey KPageDown [MCtrl] -> gridMovePageDown r
_ -> zoom #list (L.handleListEvent e)
-- | Handle events for grid tabular list with vim keys.
--
-- It adds the following keyboard shortcuts to 'L.handleListEventVi'.
--
-- * Move to the left by one column (h)
-- * Move to the right by one column (l)
-- * Go to the first column (H)
-- * Go to the last column (L)
-- * Move to the previous page of columns (Alt+h)
-- * Move to the next page of columns (Alt+l)
--
-- 'GridRenderers' are needed because if row header doesn't exist, width calculation is affected.
handleGridListEventVi :: Ord n
=> GridRenderers n e r -- ^ Renderers
-> Event -> EventM n (GridTabularList n e) ()
handleGridListEventVi r e = case e of
EvKey (KChar 'h') [] -> modify gridMoveLeft
EvKey (KChar 'l') [] -> modify gridMoveRight
EvKey (KChar 'H') [] -> modify gridMoveToBeginning
EvKey (KChar 'L') [] -> modify gridMoveToEnd
EvKey (KChar 'h') [MMeta] -> gridMovePageUp r
EvKey (KChar 'l') [MMeta] -> gridMovePageDown r
EvKey (KChar 'h') [MAlt] -> gridMovePageUp r
EvKey (KChar 'l') [MAlt] -> gridMovePageDown r
_ -> zoom #list (L.handleListEventVi (\_ -> return ()) e)