vty-ui-0.1: src/Graphics/Vty/Widgets/List.hs
-- |This module provides a 'List' widget for rendering a list of
-- single-line strings. A 'List' has the following features:
--
-- * A style for the list elements
--
-- * A styled cursor indicating which element is selected
--
-- * A /window size/ indicating how many elements should be visible to
-- the user
--
-- * An internal pointer to the start of the visible window, which
-- automatically shifts as the list is scrolled
--
-- To create a list, see 'mkList'. To modify the list's state, see
-- 'scrollDown' and 'scrollUp'. To inspect the list, see, see
-- 'getSelected' and 'getVisibleItems'.
module Graphics.Vty.Widgets.List
( List
, mkList
, scrollDown
, scrollUp
, getSelected
, getVisibleItems
)
where
import Graphics.Vty ( Attr, (<->) )
import Graphics.Vty.Widgets.Base
( Widget(..)
, text
, GrowthPolicy(Static)
)
-- |The list widget type.
data List = List { normalAttr :: Attr
, selectedAttr :: Attr
, selectedIndex :: Int
, scrollTopIndex :: Int
, scrollWindowSize :: Int
, listItems :: [String]
}
-- |Create a new list. Emtpy lists are not allowed.
mkList :: Attr -- ^The attribute of normal, non-selected items
-> Attr -- ^The attribute of the selected item
-> Int -- ^The scrolling window size, i.e., the number of items
-- which should be visible to the user at any given time
-> [String] -- ^The list items
-> List
mkList _ _ _ [] = error "Lists cannot be empty"
mkList normAttr selAttr swSize contents = List normAttr selAttr 0 0 swSize contents
-- note that !! here will always succeed because selectedIndex will
-- never be out of bounds and the list will always be non-empty.
-- |Get the currently selected list item.
getSelected :: List -> String
getSelected list = (listItems list) !! (selectedIndex list)
-- |Scroll a list down one position and return the new scrolled list.
-- This automatically takes care of managing all list state:
--
-- * Moves the cursor down one position, unless the cursor is already
-- in the last position (in which case this does nothing)
--
-- * Moves the scrolling window position if necessary (i.e., if the
-- cursor moves to an item not currently in view)
scrollDown :: List -> List
scrollDown list
-- If the list is already at the last position, do nothing.
| selectedIndex list == length (listItems list) - 1 = list
-- If the list requires scrolling the visible area, scroll it.
| selectedIndex list == scrollTopIndex list + scrollWindowSize list - 1 =
list { selectedIndex = selectedIndex list + 1
, scrollTopIndex = scrollTopIndex list + 1
}
-- Otherwise, just increment the selectedIndex.
| otherwise = list { selectedIndex = selectedIndex list + 1 }
-- |Scroll a list up one position and return the new scrolled list.
-- This automatically takes care of managing all list state:
--
-- * Moves the cursor up one position, unless the cursor is already
-- in the first position (in which case this does nothing)
--
-- * Moves the scrolling window position if necessary (i.e., if the
-- cursor moves to an item not currently in view)
scrollUp :: List -> List
scrollUp list
-- If the list is already at the first position, do nothing.
| selectedIndex list == 0 = list
-- If the list requires scrolling the visible area, scroll it.
| selectedIndex list == scrollTopIndex list =
list { selectedIndex = selectedIndex list - 1
, scrollTopIndex = scrollTopIndex list - 1
}
-- Otherwise, just decrement the selectedIndex.
| otherwise = list { selectedIndex = selectedIndex list - 1 }
-- |Given a 'List', return the items that are currently visible
-- according to the state of the list. Returns the visible items and
-- flags indicating whether each is selected.
getVisibleItems :: List -> [(String, Bool)]
getVisibleItems list =
let start = scrollTopIndex list
stop = scrollTopIndex list + scrollWindowSize list
adjustedStop = (min stop $ length $ listItems list) - 1
in [ (listItems list !! i, i == selectedIndex list)
| i <- [start..adjustedStop] ]
instance Widget List where
-- Statically sized, because we know how many items should be
-- visible.
growthPolicy _ = Static
render s w =
foldl (<->) (head widgets) (tail widgets)
where
widgets = map (render s . mkWidget) (getVisibleItems w)
mkWidget (str, selected) = let att = if selected
then selectedAttr
else normalAttr
in text (att w) str