-- |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