vimus-0.1.0: src/Widget/ListWidget.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
module Widget.ListWidget (
ListWidget
, new
, getLength
-- * current element
, getPosition
, select
, breadcrumbs
, selected
, removeSelected
-- * movement
, move
, moveUp
, moveDown
, moveUpWhile
, moveDownWhile
, moveLast
, moveFirst
, moveTo
, setPosition
, noVisual
-- * parent and children
, newChild
, getParent
-- * update
, update
, append
, resize
-- * format
, getElementsFormat
, setElementsFormat
-- * exported, because they have fewer constraints than the Widget variants
, Widget.ListWidget.render
, Widget.ListWidget.searchItem
, Widget.ListWidget.filterItem
, Widget.ListWidget.handleEvent
-- * exported for testing
, getElements
, getViewSize
, getViewPosition
, getVisualStart
, scroll
) where
import Data.List (isInfixOf, intercalate, findIndex)
import Data.Maybe
import Data.Char (toLower)
import Control.Monad (when)
import Data.Foldable (forM_, asum)
import Data.Default
import Widget.Type
import WindowLayout
import Util (clamp)
import Ruler
import Render hiding (getWindowSize)
import Vimus (Widget(..), Event(..), SearchOrder(..))
import Content
data ListWidget f a = ListWidget {
getPosition :: Int -- ^ Cursor position
, getElements :: [a]
, getFormat :: f
, getLength :: Int
, getVisualStart :: Maybe Int -- ^ First element of visual selection
, windowSize :: WindowSize
-- | position of viewport within the list
, getViewPosition :: Int
, getListParent :: Maybe (ListWidget f a)
} deriving (Eq, Show, Functor)
instance Default f => Default (ListWidget f a) where
def = ListWidget def def def def def def def def
instance (f ~ Format a, Searchable a, Renderable a) => Widget (ListWidget f a) where
render = Widget.ListWidget.render (const False)
currentItem = const Nothing
searchItem = Widget.ListWidget.searchItem
filterItem = Widget.ListWidget.filterItem
handleEvent = Widget.ListWidget.handleEvent
handleEvent :: Monad m => ListWidget f a -> Event -> m (ListWidget f a)
handleEvent l@ListWidget{..} ev = return $ case ev of
EvMoveUp -> moveUp l
EvMoveDown -> moveDown l
EvMoveFirst -> moveFirst l
EvMoveLast -> moveLast l
EvScroll n -> scroll n l
EvResize size -> resize l size
EvVisual -> if 0 < getLength then l {getVisualStart = Just getPosition} else l
EvNoVisual -> noVisual True l
_ -> l
noVisual :: Bool -> ListWidget f a -> ListWidget f a
noVisual keepPosition l@ListWidget{..}
| keepPosition = l {getVisualStart = Nothing}
| otherwise = setPosition l {getVisualStart = Nothing} (fromMaybe getPosition getVisualStart)
-- | The number of lines that are available for content.
getViewSize :: ListWidget f a -> Int
getViewSize = windowSizeY . windowSize
getParent :: ListWidget f a -> Maybe (ListWidget f a)
getParent list = fmap (setElementsFormat (getFormat list)) (getListParent list)
new :: Default f => [a] -> ListWidget f a
new = setElements def
newChild :: forall f a. Default f => [a] -> ListWidget f a -> ListWidget f a
newChild list parent = widget {getListParent = Just parent, getFormat = getFormat parent}
where
widget :: ListWidget f a
widget = resize (new list) (windowSize parent)
resize :: ListWidget f a -> WindowSize -> ListWidget f a
resize l@ListWidget{..} size = sanitize $ l {
windowSize = sanitizeWindowSize size
, getListParent = (`resize` size) `fmap` getListParent
}
where
-- make sure that the window height is never < 2
sanitizeWindowSize :: WindowSize -> WindowSize
sanitizeWindowSize s@WindowSize{..} = s {windowSizeY = max windowSizeY 2}
-- | Make sure that position is within the viewport.
--
-- The viewport is moved, if necessary.
sanitize :: ListWidget f a -> ListWidget f a
sanitize l@ListWidget{..} = setPosition l getPosition
update :: (a -> a -> Bool) -> ListWidget f a -> [a] -> ListWidget f a
update eq widget@ListWidget{..} list = setPosition (setElements widget list) (fromMaybe 0 mNewPos)
where
mNewPos = asum $ ys ++ reverse xs
where
(xs, ys) = splitAt getPosition $ map ((`findIndex` list) . eq) getElements
-- IMPORTANT: You must call `setPosition` after `setElements`!
setElements :: ListWidget f a -> [a] -> ListWidget f a
setElements widget list = widget {getElements = list, getLength = length list, getListParent = Nothing}
getElementsFormat :: ListWidget f a -> f
getElementsFormat list = getFormat list
setElementsFormat :: f -> ListWidget f a -> ListWidget f a
setElementsFormat format list = list { getFormat = format }
append :: ListWidget f a -> a -> ListWidget f a
append widget@(ListWidget{..}) x = widget{getElements = ys, getLength = length ys}
where
ys = getElements ++ [x]
------------------------------------------------------------------------
-- search
filterItem :: Searchable a => ListWidget f a -> String -> ListWidget f a
filterItem w t = filter_ (filterPredicate t) w
where
filter_ :: (a -> Bool) -> ListWidget f a -> ListWidget f a
filter_ predicate widget = (setElements widget $ filter predicate $ getElements widget) `setPosition` 0
-- | Rotate elements of given list by given number.
--
-- >>> rotate 3 [0..10]
-- [3,4,5,6,7,8,9,10,0,1,2]
rotate :: Int -> [a] -> [a]
rotate n l = drop n l ++ take n l
searchForward :: (a -> Bool) -> ListWidget f a -> ListWidget f a
searchForward predicate widget = maybe widget (setPosition widget) match
where
match = findFirst predicate shiftedList
-- rotate list, to get next match from current position
shiftedList = rotate n enumeratedList
where
n = getPosition widget + 1
enumeratedList = zip [0..] $ getElements widget
searchBackward :: (a -> Bool) -> ListWidget f a -> ListWidget f a
searchBackward predicate widget = maybe widget (setPosition widget) match
where
match = findFirst predicate shiftedList
-- rotate list, to get next match from current position
shiftedList = reverse $ rotate n enumeratedList
where
n = getPosition widget
enumeratedList = zip [0..] $ getElements widget
findFirst :: (a -> Bool) -> [(Int, a)] -> Maybe Int
findFirst predicate list = case matches of
(n, _):_ -> Just n
_ -> Nothing
where
matches = filter predicate_ list
where
predicate_ (_, y) = predicate y
searchItem :: Searchable a => ListWidget f a -> SearchOrder -> String -> ListWidget f a
searchItem w Forward t = searchForward (searchPredicate t) w
searchItem w Backward t = searchBackward (searchPredicate t) w
-- | Select given element.
moveTo :: Eq a => a -> ListWidget f a -> Maybe (ListWidget f a)
moveTo c lw = setPosition lw `fmap` findFirst (==c) (zip [0..] $ getElements lw)
data SearchPredicate = Search | Filter
searchPredicate :: Searchable a => String -> a -> Bool
searchPredicate = searchPredicate_ Search
filterPredicate :: Searchable a => String -> a -> Bool
filterPredicate = searchPredicate_ Filter
searchPredicate_ :: Searchable a => SearchPredicate -> String -> a -> Bool
searchPredicate_ predicate "" _ = onEmptyTerm predicate
where
onEmptyTerm Search = False
onEmptyTerm Filter = True
searchPredicate_ _ term item = all (\term_ -> any (isInfixOf term_) tags) terms
where
tags = map (map toLower) (searchTags item)
terms = words $ map toLower term
------------------------------------------------------------------------
-- move
setPosition :: ListWidget f a -> Int -> ListWidget f a
setPosition widget pos = widget { getPosition = newPosition, getViewPosition = newViewPosition }
where
newPosition = clamp 0 listLength pos
listLength = getLength widget
viewPosition = getViewPosition widget
minViewPosition = newPosition - (getViewSize widget - 1)
newViewPosition = max minViewPosition $ min viewPosition newPosition
moveFirst :: ListWidget f a -> ListWidget f a
moveFirst l = setPosition l 0
moveLast :: ListWidget f a -> ListWidget f a
moveLast l = setPosition l (getLength l - 1)
moveUp :: ListWidget f a -> ListWidget f a
moveUp = move (-1)
moveDown :: ListWidget f a -> ListWidget f a
moveDown = move 1
move :: Int -> ListWidget f a -> ListWidget f a
move n l = setPosition l (getPosition l + n)
moveUpWhile :: (a -> Bool) -> ListWidget f a -> ListWidget f a
moveUpWhile p l@ListWidget{..} = setPosition l pos
where
pos = getPosition - (length . takeWhile p . reverse . take getPosition) getElements
moveDownWhile :: (a -> Bool) -> ListWidget f a -> ListWidget f a
moveDownWhile p l@ListWidget{..} = setPosition l pos
where
pos = getPosition + (length . takeWhile p . drop getPosition) getElements
scroll :: Int -> ListWidget f a -> ListWidget f a
scroll n l = l {getViewPosition = newViewPosition, getPosition = newPosition}
where
newViewPosition = clamp 0 (getLength l) (getViewPosition l + n)
newPosition = clamp newViewPosition (newViewPosition + getViewSize l) (getPosition l)
select :: ListWidget f a -> Maybe a
select l
| getLength l == 0 = Nothing
| otherwise = Just (getElements l !! getPosition l)
selected :: ListWidget f a -> [a]
selected ListWidget{..}
| getLength == 0 = []
| otherwise = take n $ drop a $ getElements
where
start = fromMaybe getPosition getVisualStart
a = min getPosition start
b = max getPosition start
n = succ (b - a)
removeSelected :: ListWidget f a -> ListWidget f a
removeSelected l@ListWidget{..}
| getLength == 0 = l
| otherwise = (setElements l (take a getElements ++ drop b getElements) `setPosition` a) {getVisualStart = Nothing}
where
start = fromMaybe getPosition getVisualStart
a = min getPosition start
b = succ $ max getPosition start
render :: (f ~ Format a, Renderable a) => (a -> Bool) -> ListWidget f a -> Render Ruler
render isMarked l = do
let listLength = getLength l
viewSize = getViewSize l
viewPosition = getViewPosition l
currentPosition = getPosition l
format = getElementsFormat l
visualStart = fromMaybe currentPosition $ getVisualStart l
when (listLength > 0) $ do
let isSelected y = a <= y && y <= b
a = clamp 0 viewSize $ min currentPosition visualStart - viewPosition
b = clamp 0 viewSize $ max currentPosition visualStart - viewPosition
list = take viewSize $ drop viewPosition $ getElements l
forM_ (zip [0..] list) $ \(y, element) -> do
addLine y 0 (renderItem format element)
case (isMarked element, isSelected y) of
(True, True ) -> chgat y [Reverse, Bold] MainColor
(True, False) -> chgat y [Bold] MainColor
(False, True ) -> chgat y [Reverse] MainColor
(False, False) -> return ()
let positionIndicator
| listLength > 0 = Just (succ currentPosition, listLength)
| otherwise = Nothing
rulerText = maybe "" showBreadcrumbs (getListParent l)
showBreadcrumbs = intercalate " > " . map (toPlainText . renderItem format) . breadcrumbs
return $ Ruler rulerText positionIndicator (visible listLength viewSize viewPosition)
-- | Return path to current element.
breadcrumbs :: ListWidget f a -> [a]
breadcrumbs = reverse . go
where
go l = maybe id (:) (select l) $ case getListParent l of
Just p -> go p
Nothing -> []