flaw-ui/Flaw/UI/ListBox.hs

Summary

Maintainability
Test Coverage
{-|
Module: Flaw.UI.ListBox
Description: List box.
License: MIT
-}

{-# LANGUAGE GADTs, RankNTypes #-}

module Flaw.UI.ListBox
  ( ListBox(..)
  , ListBoxColumn(..)
  , newListBox
  , addListBoxItem
  , removeListBoxItem
  , changeListBoxItem
  , clearListBox
  , reorderListBox
  , getListBoxSelectedValues
  , newListBoxTextColumnDesc
  ) where

import Control.Concurrent.STM
import Control.Monad
import Control.Monad.Fix
import Data.Bits
import qualified Data.IntMap.Strict as IM
import qualified Data.IntSet as IS
import Data.Maybe
import qualified Data.Set as S
import qualified Data.Text as T

import Flaw.Graphics
import Flaw.Graphics.Canvas
import Flaw.Input.Keyboard
import Flaw.Input.Mouse
import Flaw.Math
import Flaw.UI
import Flaw.UI.Drawer
import Flaw.UI.Label
import Flaw.UI.Metrics
import Flaw.UI.Panel
import Flaw.UI.PileBox
import Flaw.UI.ScrollBox
import Flaw.UI.VisualElement

-- | `ListBox` is an extendable element allowing user to work with multiple items.
-- It creates and caches temporary element for every cell, sends input events
-- and renders them as a scrollable list efficiently.
data ListBox v = ListBox
  { listBoxPanel :: !Panel
  , listBoxColumnHeaderHeight :: {-# UNPACK #-} !Metric
  , listBoxItemHeight :: {-# UNPACK #-} !Metric
  -- | Values ordered by item index.
  , listBoxValuesVar :: !(TVar (IM.IntMap v))
  -- | Items ordered by current sort function.
  , listBoxItemsVar :: !(TVar (ListBoxItems v))
  -- | Selected values.
  , listBoxSelectedValuesVar :: {-# UNPACK #-} !(TVar IS.IntSet)
  -- | Index to assign next added item.
  , listBoxNextItemIndexVar :: !(TVar Int)
  -- | Columns.
  , listBoxColumns :: [ListBoxColumn v]
  -- | Change handler.
  , listBoxChangeHandlerVar :: {-# UNPACK #-} !(TVar (STM ()))
  }

-- | Handle for list box item.
-- Allows to remove item from list.
newtype ListBoxItemHandle v = ListBoxItemHandle Int

-- | Wrapper for list box item sorted by some key.
data ListBoxItem k v = ListBoxItem Int (v -> k) v

instance Eq (ListBoxItem k v) where
  {-# INLINE (==) #-}
  ListBoxItem i1 _f1 _v1 == ListBoxItem i2 _f2 _v2 = i1 == i2

instance Ord k => Ord (ListBoxItem k v) where
  {-# INLINE compare #-}
  compare (ListBoxItem i1 f1 v1) (ListBoxItem i2 f2 v2) = case compare (f1 v1) (f2 v2) of
    LT -> LT
    EQ -> compare i1 i2
    GT -> GT

-- | List box items sorted by some key.
data ListBoxItems v where
  ListBoxItems :: Ord k => (v -> k) -> S.Set (ListBoxItem k v) -> ListBoxItems v

-- | Column (also works as column header element).
data ListBoxColumn v = ListBoxColumn
  { listBoxColumnParent :: !(ListBox v)
  , listBoxColumnDesc :: !(ListBoxColumnDesc v)
  , listBoxColumnElementsCacheVar :: {-# UNPACK #-} !(TVar (IM.IntMap SomeElement))
  , listBoxColumnWidthVar :: {-# UNPACK #-} !(TVar Metric)
  , listBoxColumnMousedVar :: {-# UNPACK #-} !(TVar Bool)
  , listBoxColumnPressedVar :: {-# UNPACK #-} !(TVar Bool)
  }

-- | Immutable column descrition.
data ListBoxColumnDesc v where
  ListBoxColumnDesc :: Ord k =>
    { listBoxColumnDescVisual :: !SomeVisual
    , listBoxColumnDescWidth :: {-# UNPACK #-} !Metric
    , listBoxColumnDescKeyFunc :: !(v -> k)
    , listBoxColumnDescElementFunc :: !(v -> STM SomeElement)
    } -> ListBoxColumnDesc v

data ListBoxContent v = ListBoxContent
  { listBoxContentParent :: !(ListBox v)
  , listBoxContentScrollBarVar :: {-# UNPACK #-} !(TVar ScrollBar)
  , listBoxContentSizeVar :: {-# UNPACK #-} !(TVar Size)
  , listBoxContentFocusedVar :: {-# UNPACK #-} !(TVar Bool)
  , listBoxContentLastMousePositionVar :: {-# UNPACK #-} !(TVar (Maybe Position))
  , listBoxContentLastMousedCellVar :: {-# UNPACK #-} !(TVar (Maybe (Int, Int)))
  }

newListBox :: Metrics -> [ListBoxColumnDesc v] -> STM (ListBox v)
newListBox metrics@Metrics
  { metricsListBoxColumnHeaderHeight = columnHeaderHeight
  , metricsListBoxItemHeight = itemHeight
  , metricsScrollBarWidth = scrollBarWidth
  } columnDescs = do
  panel <- newPanel False
  listBox@ListBox
    { listBoxColumns = columns
    } <- mfix $ \listBox -> do
    columns <- forM columnDescs $ \columnDesc -> do
      elementsCacheVar <- newTVar IM.empty
      widthVar <- newTVar 0
      mousedVar <- newTVar False
      pressedVar <- newTVar False
      return ListBoxColumn
        { listBoxColumnParent = listBox
        , listBoxColumnDesc = columnDesc
        , listBoxColumnElementsCacheVar = elementsCacheVar
        , listBoxColumnWidthVar = widthVar
        , listBoxColumnMousedVar = mousedVar
        , listBoxColumnPressedVar = pressedVar
        }

    valuesVar <- newTVar IM.empty
    itemsVar <- newTVar $ ListBoxItems (const (0 :: Int)) S.empty
    selectedValuesVar <- newTVar IS.empty
    nextItemIndexVar <- newTVar 0
    changeHandlerVar <- newTVar $ return ()
    return ListBox
      { listBoxPanel = panel
      , listBoxColumnHeaderHeight = columnHeaderHeight
      , listBoxItemHeight = itemHeight
      , listBoxValuesVar = valuesVar
      , listBoxItemsVar = itemsVar
      , listBoxSelectedValuesVar = selectedValuesVar
      , listBoxNextItemIndexVar = nextItemIndexVar
      , listBoxColumns = columns
      , listBoxChangeHandlerVar = changeHandlerVar
      }

  -- pile box for column headers
  pileBox <- newPileBox metrics $ flip map columns $ \column@ListBoxColumn
    { listBoxColumnDesc = ListBoxColumnDesc
      { listBoxColumnDescWidth = columnWidth
      }
    } -> PileBoxItemDesc
    { pileBoxItemDescElement = SomeElement column
    , pileBoxItemDescWidth = columnWidth
    }
  pileBoxChild <- addFreeChild panel pileBox

  -- content element
  scrollBarVar <- newTVar undefined
  contentSizeVar <- newTVar $ Vec2 0 0
  focusedVar <- newTVar False
  lastMousePositionVar <- newTVar Nothing
  lastMousedCellVar <- newTVar Nothing
  let
    content = ListBoxContent
      { listBoxContentParent = listBox
      , listBoxContentScrollBarVar = scrollBarVar
      , listBoxContentSizeVar = contentSizeVar
      , listBoxContentFocusedVar = focusedVar
      , listBoxContentLastMousePositionVar = lastMousePositionVar
      , listBoxContentLastMousedCellVar = lastMousedCellVar
      }

  -- scroll box
  scrollBox <- newScrollBox content
  scrollBoxChild <- addFreeChild panel scrollBox

  -- scroll bar
  scrollBar <- newVerticalScrollBar scrollBox
  scrollBarChild <- addFreeChild panel scrollBar

  writeTVar scrollBarVar scrollBar

  setLayoutHandler panel $ \(Vec2 sx sy) -> do
    placeFreeChild panel pileBoxChild $ Vec2 1 1
    layoutElement pileBox $ Vec2 (sx - 2) columnHeaderHeight
    placeFreeChild panel scrollBoxChild $ Vec2 1 (1 + columnHeaderHeight)
    layoutElement scrollBox $ Vec2 (sx - 1 - scrollBarWidth) (sy - 2 - columnHeaderHeight)
    placeFreeChild panel scrollBarChild $ Vec2 (sx - scrollBarWidth) (1 + columnHeaderHeight)
    layoutElement scrollBar $ Vec2 scrollBarWidth (sy - 1 - columnHeaderHeight)

  return listBox

-- | Add new list item to listbox.
addListBoxItem :: ListBox v -> v -> STM (ListBoxItemHandle v)
addListBoxItem ListBox
  { listBoxValuesVar = valuesVar
  , listBoxItemsVar = itemsVar
  , listBoxNextItemIndexVar = nextItemIndexVar
  } value = do
  itemIndex <- readTVar nextItemIndexVar
  writeTVar nextItemIndexVar $ itemIndex + 1
  modifyTVar' valuesVar $ IM.insert itemIndex value
  modifyTVar' itemsVar $ \(ListBoxItems keyFunc items) ->
    ListBoxItems keyFunc $ S.insert (ListBoxItem itemIndex keyFunc value) items
  return $ ListBoxItemHandle itemIndex

-- | Remove list item by handle.
removeListBoxItem :: ListBox v -> ListBoxItemHandle v -> STM ()
removeListBoxItem ListBox
  { listBoxValuesVar = valuesVar
  , listBoxItemsVar = itemsVar
  , listBoxSelectedValuesVar = selectedValuesVar
  , listBoxChangeHandlerVar = changeHandlerVar
  } (ListBoxItemHandle itemIndex) = do
  values <- readTVar valuesVar
  case IM.lookup itemIndex values of
    Just value -> do
      writeTVar valuesVar $ IM.delete itemIndex values
      modifyTVar' itemsVar $ \(ListBoxItems keyFunc items) ->
        ListBoxItems keyFunc $ S.delete (ListBoxItem itemIndex keyFunc value) items
    Nothing -> return ()
  selectedValues <- readTVar selectedValuesVar
  when (IS.member itemIndex selectedValues) $ do
    writeTVar selectedValuesVar $ IS.delete itemIndex selectedValues
    join $ readTVar changeHandlerVar

-- | Change list item by handle.
-- List item's handle remains valid.
changeListBoxItem :: ListBox v -> ListBoxItemHandle v -> v -> STM ()
changeListBoxItem ListBox
  { listBoxValuesVar = valuesVar
  , listBoxItemsVar = itemsVar
  , listBoxSelectedValuesVar = selectedValuesVar
  , listBoxChangeHandlerVar = changeHandlerVar
  } (ListBoxItemHandle itemIndex) newValue = do
  values <- readTVar valuesVar
  case IM.lookup itemIndex values of
    Just oldValue -> do
      writeTVar valuesVar $ IM.insert itemIndex newValue values
      modifyTVar' itemsVar $ \(ListBoxItems keyFunc items) ->
        ListBoxItems keyFunc
        $ S.insert (ListBoxItem itemIndex keyFunc newValue)
        $ S.delete (ListBoxItem itemIndex keyFunc oldValue) items
      selectedValues <- readTVar selectedValuesVar
      when (IS.member itemIndex selectedValues) $ join $ readTVar changeHandlerVar
    Nothing -> return ()

-- | Remove all items from list box.
clearListBox :: ListBox v -> STM ()
clearListBox ListBox
  { listBoxValuesVar = valuesVar
  , listBoxItemsVar = itemsVar
  , listBoxSelectedValuesVar = selectedValuesVar
  , listBoxChangeHandlerVar = changeHandlerVar
  } = do
  writeTVar valuesVar IM.empty
  modifyTVar' itemsVar $ \(ListBoxItems keyFunc _items) -> ListBoxItems keyFunc S.empty
  selectionWasEmpty <- IS.null <$> readTVar selectedValuesVar
  writeTVar selectedValuesVar IS.empty
  unless selectionWasEmpty $ join $ readTVar changeHandlerVar

-- | Reorder list box using new sort function.
reorderListBox :: Ord k => ListBox v -> (v -> k) -> STM ()
reorderListBox ListBox
  { listBoxItemsVar = itemsVar
  } newSortFunc = modifyTVar' itemsVar $ \(ListBoxItems _oldSortFunc items) ->
  ListBoxItems newSortFunc $ S.fromList $ flip map (S.toList items)
  $ \(ListBoxItem itemIndex _oldSortFunc value) -> ListBoxItem itemIndex newSortFunc value

-- | Get list of selected values from list box.
getListBoxSelectedValues :: ListBox v -> STM [v]
getListBoxSelectedValues ListBox
  { listBoxValuesVar = valuesVar
  , listBoxSelectedValuesVar = selectedValuesVar
  } = do
  values <- readTVar valuesVar
  selectedValues <- readTVar selectedValuesVar
  return $ map (fromJust . flip IM.lookup values) $ IS.toList selectedValues

instance Element (ListBox v) where
  layoutElement = layoutElement . listBoxPanel
  dabElement = dabElement . listBoxPanel
  elementMouseCursor = elementMouseCursor . listBoxPanel
  renderElement ListBox
    { listBoxPanel = panel@Panel
      { panelSizeVar = sizeVar
      }
    } drawer@Drawer
    { drawerCanvas = canvas
    , drawerStyles = DrawerStyles
      { drawerLoweredStyleVariant = StyleVariant
        { styleVariantNormalStyle = Style
          { styleFillColor = fillColor
          , styleBorderColor = borderColor
          }
        }
      }
    } position@(Vec2 px py) = do
    size <- readTVar sizeVar
    let Vec2 sx sy = size
    r <- renderElement panel drawer position
    return $ do
      drawBorderedRectangle canvas
        (Vec4 px (px + 1) (px + sx - 1) (px + sx))
        (Vec4 py (py + 1) (py + sy - 1) (py + sy))
        fillColor borderColor
      renderIntersectScissor $ Vec4 (px + 1) (py + 1) (px + sx - 2) (py + sy - 2)
      r
  processInputEvent = processInputEvent . listBoxPanel
  focusElement = focusElement . listBoxPanel
  unfocusElement = unfocusElement . listBoxPanel

instance HasChangeHandler (ListBox v) where
  setChangeHandler = writeTVar . listBoxChangeHandlerVar

instance Element (ListBoxContent v) where

  layoutElement ListBoxContent
    { listBoxContentSizeVar = sizeVar
    } = writeTVar sizeVar

  dabElement _ _ = return True

  renderElement _ _ _ = return $ return ()

  processInputEvent ListBoxContent
    { listBoxContentParent = ListBox
      { listBoxItemHeight = itemHeight
      , listBoxValuesVar = valuesVar
      , listBoxItemsVar = itemsVar
      , listBoxSelectedValuesVar = selectedValuesVar
      , listBoxColumns = columns
      , listBoxChangeHandlerVar = changeHandlerVar
      }
    , listBoxContentScrollBarVar = scrollBarVar
    , listBoxContentLastMousePositionVar = lastMousePositionVar
    , listBoxContentLastMousedCellVar = lastMousedCellVar
    } inputEvent inputState@InputState
    { inputStateKeyboard = keyboardState
    } = do
    scrollBar@ScrollBar
      { scrollBarScrollBox = scrollBox@ScrollBox
        { scrollBoxSizeVar = boxSizeVar
        }
      } <- readTVar scrollBarVar

    let
      moveSelection getEdgeItemIndex adjustItemOrderIndex = do
        selectedValues <- readTVar selectedValuesVar
        if IS.null selectedValues then selectByItemOrderIndex 0 else do
          values <- readTVar valuesVar
          ListBoxItems keyFunc items <- readTVar itemsVar
          let
            itemIndex = getEdgeItemIndex selectedValues
            value = fromJust $ IM.lookup itemIndex values
            itemOrderIndex = S.findIndex (ListBoxItem itemIndex keyFunc value) items
            itemOrderIndexToSelect = adjustItemOrderIndex itemOrderIndex
          selectByItemOrderIndex itemOrderIndexToSelect
      -- select by item order index, possibly unselecting currently selected items
      selectByItemOrderIndex itemOrderIndex = do
        ListBoxItems _keyFunc items <- readTVar itemsVar
        when (itemOrderIndex >= 0 && itemOrderIndex < S.size items) $ do
          shiftLPressed <- getKeyState keyboardState KeyShiftL
          shiftRPressed <- getKeyState keyboardState KeyShiftR
          ctrlLPressed <- getKeyState keyboardState KeyControlL
          ctrlRPressed <- getKeyState keyboardState KeyControlR
          -- clear selection if shift or ctrl is not pressed
          selectedValues <- if shiftLPressed || shiftRPressed || ctrlLPressed || ctrlRPressed then readTVar selectedValuesVar else return IS.empty
          writeTVar selectedValuesVar $
            let ListBoxItem itemIndex _keyFunc _value = S.elemAt itemOrderIndex items
            in IS.insert itemIndex selectedValues
          -- ensure selected item is visible
          let itemY = itemOrderIndex * itemHeight
          ensureVisibleScrollBoxArea scrollBox $ Vec4 0 itemY 0 (itemY + itemHeight)
          -- call change handler
          join $ readTVar changeHandlerVar
      getCellElement (itemIndex, columnIndex) = let
        ListBoxColumn
          { listBoxColumnElementsCacheVar = elementsCacheVar
          } = columns !! columnIndex
        in IM.lookup itemIndex <$> readTVar elementsCacheVar
      passInputEventToLastMousedCell = do
        maybeLastMousedCell <- readTVar lastMousedCellVar
        case maybeLastMousedCell of
          Just lastMousedCell -> do
            maybeCellElement <- getCellElement lastMousedCell
            case maybeCellElement of
              Just (SomeElement cellElement) -> processInputEvent cellElement inputEvent inputState
              Nothing -> return False
          Nothing -> return False

    processedByScrollBar <- processScrollBarEvent scrollBar inputEvent inputState
    if processedByScrollBar then return True else do
      processedByContainer <- case inputEvent of
        KeyboardInputEvent keyboardEvent -> case keyboardEvent of
          KeyDownEvent KeyDown -> do
            moveSelection IS.findMax (+ 1)
            return True
          KeyDownEvent KeyUp -> do
            moveSelection IS.findMin (+ (-1))
            return True
          KeyDownEvent KeyPageDown -> do
            boxSize <- readTVar boxSizeVar
            let Vec2 _sx sy = boxSize
            ListBoxItems _keyFunc items <- readTVar itemsVar
            moveSelection IS.findMax $ \i -> min (S.size items - 1) $ i + sy `quot` itemHeight
            return True
          KeyDownEvent KeyPageUp -> do
            boxSize <- readTVar boxSizeVar
            let Vec2 _sx sy = boxSize
            moveSelection IS.findMin $ \i -> max 0 $ i - sy `quot` itemHeight
            return True
          KeyDownEvent KeyHome -> do
            selectByItemOrderIndex 0
            return True
          KeyDownEvent KeyEnd -> do
            ListBoxItems _keyFunc items <- readTVar itemsVar
            unless (S.null items) $ selectByItemOrderIndex (S.size items - 1)
            return True
          _ -> return False
        MouseInputEvent mouseEvent -> case mouseEvent of
          MouseDownEvent LeftMouseButton -> do
            maybeLastMousePosition <- readTVar lastMousePositionVar
            case maybeLastMousePosition of
              Just (Vec2 _x y) -> do
                selectByItemOrderIndex $ y `quot` itemHeight
                return True
              Nothing -> return False
          CursorMoveEvent x y -> do
            writeTVar lastMousePositionVar $ Just $ Vec2 x y
            return True
          _ -> return False
        MouseLeaveEvent -> do
          writeTVar lastMousePositionVar Nothing
          return True
      processedByItem <- case inputEvent of
        MouseInputEvent mouseEvent -> case mouseEvent of
          MouseDownEvent {} -> passInputEventToLastMousedCell
          MouseUpEvent {} -> passInputEventToLastMousedCell
          RawMouseMoveEvent {} -> passInputEventToLastMousedCell
          CursorMoveEvent x y -> do
            let (itemOrderIndex, yy) = y `quotRem` itemHeight
            ListBoxItems _keyFunc items <- readTVar itemsVar
            maybeMousedElementAndXAndCell <-
              if itemOrderIndex >= 0 && itemOrderIndex < S.size items then do
                let ListBoxItem itemIndex _keyFunc _value = S.elemAt itemOrderIndex items

                -- find currently moused cell and column
                let
                  findCell xx i (ListBoxColumn
                    { listBoxColumnWidthVar = widthVar
                    , listBoxColumnElementsCacheVar = elementsCacheVar
                    } : restColumns) = do
                    width <- readTVar widthVar
                    if xx < width then do
                      elementsCache <- readTVar elementsCacheVar
                      return $ case IM.lookup itemIndex elementsCache of
                        Just mousedElement -> Just (mousedElement, xx, (itemIndex, i))
                        Nothing -> Nothing
                    else findCell (xx - width) (i + 1) restColumns
                  findCell _ _ [] = return Nothing

                findCell x 0 columns
              else return Nothing

            let
              (maybeMousedElementAndX, maybeMousedCell) = case maybeMousedElementAndXAndCell of
                Just (mousedElement, xx, mousedCell) -> (Just (mousedElement, xx), Just mousedCell)
                Nothing -> (Nothing, Nothing)

            -- get last moused cell
            maybeLastMousedCell <- readTVar lastMousedCellVar
            maybeLastMousedElement <- maybe (return Nothing) getCellElement maybeLastMousedCell

            -- if current cell is not the same as before
            when (maybeMousedCell /= maybeLastMousedCell) $ do
              -- remember new cell
              writeTVar lastMousedCellVar maybeMousedCell
              -- send mouse leave event to previously moused element
              case maybeLastMousedElement of
                Just (SomeElement lastMousedElement) -> void $ processInputEvent lastMousedElement MouseLeaveEvent inputState
                Nothing -> return ()
            -- send event to currently moused element
            case maybeMousedElementAndX of
              Just (SomeElement mousedElement, xx) -> processInputEvent mousedElement (MouseInputEvent (CursorMoveEvent xx yy)) inputState
              Nothing -> return False

        MouseLeaveEvent -> do
          maybeLastMousedCell <- readTVar lastMousedCellVar
          case maybeLastMousedCell of
            Just (lastMousedItemIndex, lastMousedColumnIndex) -> do
              let
                ListBoxColumn
                  { listBoxColumnElementsCacheVar = elementsCacheVar
                  } = columns !! lastMousedColumnIndex
              elementsCache <- readTVar elementsCacheVar
              case IM.lookup lastMousedItemIndex elementsCache of
                Just (SomeElement element) -> processInputEvent element inputEvent inputState
                Nothing -> return False
            Nothing -> return False
        _ -> return False
      return $ processedByContainer || processedByItem

  focusElement ListBoxContent
    { listBoxContentFocusedVar = focusedVar
    } = do
    writeTVar focusedVar True
    return True

  unfocusElement ListBoxContent
    { listBoxContentFocusedVar = focusedVar
    } = writeTVar focusedVar False

instance Scrollable (ListBoxContent v) where
  renderScrollableElement ListBoxContent
    { listBoxContentParent = listBox@ListBox
      { listBoxItemHeight = itemHeight
      , listBoxItemsVar = itemsVar
      , listBoxSelectedValuesVar = selectedValuesVar
      , listBoxColumns = columns
      }
    , listBoxContentFocusedVar = focusedVar
    } drawer@Drawer
    { drawerCanvas = canvas
    , drawerStyles = DrawerStyles
      { drawerLoweredStyleVariant = StyleVariant
        { styleVariantNormalStyle = normalStyle
        , styleVariantMousedStyle = mousedStyle
        , styleVariantSelectedFocusedStyle = selectedFocusedStyle
        , styleVariantSelectedUnfocusedStyle = selectedUnfocusedStyle
        }
      }
    } (Vec2 px py) (Vec4 left top right bottom) = do
    -- get state
    focused <- readTVar focusedVar
    let
      unselectedStyle = if focused then mousedStyle else normalStyle
      selectedStyle = if focused then selectedFocusedStyle else selectedUnfocusedStyle
    selectedValues <- readTVar selectedValuesVar
  
    -- calculate rendering of items
    renderItemColumns <- let
      f x (column@ListBoxColumn
        { listBoxColumnWidthVar = widthVar
        } : restColumns) = do
        width <- readTVar widthVar
        ((x, width, column) :) <$> f (x + width) restColumns
      f _ [] = return []
      in f px columns

    ListBoxItems _keyFunc items <- readTVar itemsVar

    let
      renderItems _i y _ | y >= py + bottom = return (IS.empty, return ())
      renderItems _i _y [] = return (IS.empty, return ())
      renderItems i y (ListBoxItem itemIndex _keyFunc value : restItems) = do
        let
          selected = IS.member itemIndex selectedValues
          isOdd = (i .&. 1) > 0
          style = if selected then selectedStyle else unselectedStyle
        r <- (sequence_ <$>) . forM renderItemColumns $ \(x, width, column) -> do
          SomeElement cellElement <- getItemElement listBox column value itemIndex
          r <- renderElement cellElement drawer (Vec2 (x + 1) (y + 1))
          return $ renderScope $ do
            renderIntersectScissor $ Vec4 (x + 1) (y + 1) (x + width - 2) (y + itemHeight - 2)
            r
        let
          itemRender =
            if selected then do
              drawBorderedRectangle canvas
                (Vec4 (px + left) (px + left + 1) (px + right - 1) (px + right))
                (Vec4 y (y + 1) (y + itemHeight - 1) (y + itemHeight))
                (styleFillColor style) (styleBorderColor style)
              r
            else if isOdd then do
              let evenColor = styleFillColor selectedUnfocusedStyle * Vec4 1 1 1 0.05
              drawBorderedRectangle canvas
                (Vec4 (px + left) (px + left) (px + right) (px + right))
                (Vec4 y y (y + itemHeight) (y + itemHeight))
                evenColor evenColor
              r
            else r
        (visibleItemIndices, restItemsRender) <- renderItems (i + 1) (y + itemHeight) restItems
        return (IS.insert itemIndex visibleItemIndices, itemRender >> restItemsRender)
      topOrderedIndex = top `quot` itemHeight
      (visibleItems, firstVisibleItemOrderedIndex) =
        if topOrderedIndex <= 0 then (items, 0)
        else if topOrderedIndex >= S.size items then (S.empty, 0)
        else let
          ListBoxItem firstVisibleItemIndex firstVisibleItemKeyFunc firstVisibleItemValue = S.elemAt topOrderedIndex items
          -- split by special non-existent item which will be just before first item
          in (snd $ S.split (ListBoxItem (firstVisibleItemIndex - 1) firstVisibleItemKeyFunc firstVisibleItemValue) items, topOrderedIndex)

    (visibleItemIndices, itemsRender) <- renderItems firstVisibleItemOrderedIndex (py + firstVisibleItemOrderedIndex * itemHeight) $ S.toAscList visibleItems

    -- filter out invisible items from column caches
    forM_ columns $ \ListBoxColumn
      { listBoxColumnElementsCacheVar = elementsCacheVar
      } -> modifyTVar' elementsCacheVar $ IM.filterWithKey $ \itemIndex _element -> IS.member itemIndex visibleItemIndices

    return itemsRender

  scrollableElementSize ListBoxContent
    { listBoxContentParent = ListBox
      { listBoxItemHeight = itemHeight
      , listBoxValuesVar = valuesVar
      }
    , listBoxContentSizeVar = sizeVar
    } = do
    size <- readTVar sizeVar
    let Vec2 sx _sy = size
    height <- (itemHeight *) . IM.size <$> readTVar valuesVar
    return $ Vec2 sx height

-- | Get element representing item for the given column.
-- Gets an element either from cache, or creates new one.
getItemElement :: ListBox v -> ListBoxColumn v -> v -> Int -> STM SomeElement
getItemElement ListBox
  { listBoxItemHeight = itemHeight
  } ListBoxColumn
  { listBoxColumnDesc = ListBoxColumnDesc
    { listBoxColumnDescElementFunc = elementFunc
    }
  , listBoxColumnElementsCacheVar = elementsCacheVar
  , listBoxColumnWidthVar = widthVar
  } value itemIndex = do
  -- get element from cache or create new
  elementsCache <- readTVar elementsCacheVar
  someElement@(SomeElement element) <- case IM.lookup itemIndex elementsCache of
    Just someElement -> return someElement
    Nothing -> do
      someElement <- elementFunc value
      writeTVar elementsCacheVar $! IM.insert itemIndex someElement elementsCache
      return someElement
  -- layout element
  width <- readTVar widthVar
  layoutElement element $ Vec2 (width - 2) (itemHeight - 2)
  return someElement

instance Element (ListBoxColumn v) where

  layoutElement ListBoxColumn
    { listBoxColumnWidthVar = widthVar
    } (Vec2 sx _sy) = writeTVar widthVar sx

  dabElement ListBoxColumn
    { listBoxColumnParent = ListBox
      { listBoxColumnHeaderHeight = columnHeaderHeight
      }
    , listBoxColumnWidthVar = widthVar
    } (Vec2 x y) = do
    if x < 0 || y < 0 || y >= columnHeaderHeight then return False
    else (x <) <$> readTVar widthVar

  renderElement ListBoxColumn
    { listBoxColumnParent = ListBox
      { listBoxColumnHeaderHeight = columnHeaderHeight
      }
    , listBoxColumnDesc = ListBoxColumnDesc
      { listBoxColumnDescVisual = SomeVisual visual
      }
    , listBoxColumnWidthVar = widthVar
    , listBoxColumnMousedVar = mousedVar
    , listBoxColumnPressedVar = pressedVar
    } drawer@Drawer
    { drawerCanvas = canvas
    , drawerStyles = DrawerStyles
      { drawerRaisedStyleVariant = StyleVariant
        { styleVariantNormalStyle = normalStyle
        , styleVariantMousedStyle = mousedStyle
        , styleVariantPressedStyle = pressedStyle
        }
      }
    } (Vec2 px py) = do
    -- get state
    sx <- readTVar widthVar
    let sy = columnHeaderHeight
    moused <- readTVar mousedVar
    pressed <- readTVar pressedVar
    -- get style
    let
      style
        | pressed = pressedStyle
        | moused = mousedStyle
        | otherwise = normalStyle
    -- calculate visual rendering
    visualRender <- renderVisual visual drawer (Vec2 (px + 1) (py + 1)) (Vec2 sx sy) style
    -- return rendering
    return $ do
      drawBorderedRectangle canvas
        (Vec4 px px (px + sx - 1) (px + sx))
        (Vec4 py py (py + sy - 1) (py + sy))
        (styleFillColor style) (styleBorderColor style)
      renderIntersectScissor $ Vec4 (px + 1) (py + 1) (px + sx - 1) (py + sy - 1)
      renderScope visualRender

  processInputEvent ListBoxColumn
    { listBoxColumnParent = parent
    , listBoxColumnDesc = ListBoxColumnDesc
      { listBoxColumnDescKeyFunc = keyFunc
      }
    , listBoxColumnMousedVar = mousedVar
    , listBoxColumnPressedVar = pressedVar
    } inputEvent _inputState = case inputEvent of
    MouseInputEvent mouseEvent -> case mouseEvent of
      MouseDownEvent LeftMouseButton -> do
        writeTVar pressedVar True
        reorderListBox parent keyFunc
        return True
      MouseUpEvent LeftMouseButton -> do
        writeTVar pressedVar False
        return True
      CursorMoveEvent _x _y -> do
        writeTVar mousedVar True
        return True
      _ -> return False
    MouseLeaveEvent -> do
      writeTVar mousedVar False
      writeTVar pressedVar False
      return True
    _ -> return False

-- | Description of most normal column: text column title, item is shown as text.
newListBoxTextColumnDesc
  :: Ord k
  => T.Text -- ^ Column title.
  -> Metric -- ^ Column width.
  -> (v -> k) -- ^ Key function, returns key to sort by.
  -> (v -> T.Text) -- ^ Display text function, returns text to display for item.
  -> STM (ListBoxColumnDesc v)
newListBoxTextColumnDesc title width keyFunc textFunc = do
  columnLabel <- newTextLabel
  setText columnLabel title
  return ListBoxColumnDesc
    { listBoxColumnDescVisual = SomeVisual columnLabel
    , listBoxColumnDescWidth = width
    , listBoxColumnDescKeyFunc = keyFunc
    , listBoxColumnDescElementFunc = \value -> do
      cellLabel <- newTextLabel
      setText cellLabel $ textFunc value
      SomeElement <$> newVisualElement cellLabel
    }