{-# LANGUAGE ExistentialQuantification, MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances, DeriveDataTypeable #-} -- |This module provides a table layout widget capable of laying out -- columns of widgets with various padding and alignment properties. -- For complete details, please see the Vty-ui User's Manual. module Graphics.Vty.Widgets.Table ( Table , TableCell , ColumnSize(..) , BorderStyle(..) , BorderFlag(..) , RowLike(..) , TableError(..) , ColumnSpec(..) , Alignment(..) , Alignable(..) , (.|.) , newTable , setDefaultCellAlignment , setDefaultCellPadding , addRow , addHeadingRow , addHeadingRow_ , column , customCell , emptyCell ) where import Data.Monoid import Data.Typeable import Data.Word import Data.List import Control.Applicative hiding ((<|>)) import Control.Exception import Control.Monad import Graphics.Vty import Graphics.Vty.Widgets.Core import Graphics.Vty.Widgets.Text import Graphics.Vty.Widgets.Centering import Graphics.Vty.Widgets.Padding import Graphics.Vty.Widgets.Borders import Graphics.Vty.Widgets.Skins import Graphics.Vty.Widgets.Util import Graphics.Vty.Widgets.Fills import Graphics.Vty.Widgets.Box data TableError = ColumnCountMismatch -- ^A row added to the table did not have the same -- number of widgets as the table has columns. | CellImageTooBig -- ^The image rendered by a cell widget exceeded the -- size permitted by the cell. | BadTableWidgetSizePolicy Int -- ^A table cell contains a widget which grows -- vertically, which is not permitted. deriving (Show, Typeable) instance Exception TableError -- |Column alignment values. data Alignment = AlignCenter | AlignLeft | AlignRight deriving (Show) -- |The class of types whose values can be aligned. class Alignable a where align :: a -> Alignment -> a -- |The wrapper type for all table cells; stores the widgets -- themselves in addition to alignment and padding settings. -- Alignment and padding settings on a cell override the column- and -- table-wide defaults. data TableCell = forall a. (Show a) => TableCell (Widget a) (Maybe Alignment) (Maybe Padding) | EmptyCell instance Show TableCell where show EmptyCell = "EmptyCell" show (TableCell _ mAl mPad) = concat [ "TableCell { " , "alignment = " , show mAl , ", padding = " , show mPad , ", ... " , "}" ] data TableRow = TableRow [TableCell] -- |The types of borders we can have in a table. data BorderFlag = Rows -- ^Borders between rows. | Columns -- ^Borders between columns. | Edges -- ^Borders around the outside edges of the table. deriving (Eq, Show) -- |The border configuration of a table. data BorderStyle = BorderPartial [BorderFlag] -- |A partial set of border flags. | BorderFull -- |Draw borders everywhere we support them. | BorderNone -- ^Don't draw any borders anywhere. deriving (Eq, Show) -- |The type of column size policies. data ColumnSize = ColFixed Int -- ^The column has the specified fixed width in -- columns. | ColAuto -- ^The column's width is a function of space -- available to the table at rendering time. deriving (Eq, Show) -- |The specification of a column's settings. The alignment and -- padding of a column specification override the table-wide default. data ColumnSpec = ColumnSpec { columnSize :: ColumnSize , columnAlignment :: Maybe Alignment , columnPadding :: Maybe Padding } deriving (Show) instance Paddable ColumnSpec where pad c p = c { columnPadding = Just p } instance Alignable ColumnSpec where align c a = c { columnAlignment = Just a } instance Paddable TableCell where pad (TableCell w a _) p = TableCell w a (Just p) pad EmptyCell _ = EmptyCell instance Alignable TableCell where align (TableCell w _ p) a = TableCell w (Just a) p align EmptyCell _ = EmptyCell -- |The class of types whose values can be used to construct table -- rows. class RowLike a where mkRow :: a -> TableRow instance RowLike TableRow where mkRow = id instance RowLike TableCell where mkRow c = TableRow [c] instance (Show a) => RowLike (Widget a) where mkRow w = TableRow [TableCell w Nothing Nothing] instance (RowLike a) => RowLike [a] where mkRow rs = TableRow cs where cs = concat $ map (\(TableRow cells) -> cells) rs' rs' = map mkRow rs -- |Row constructor using 'RowLike' instances. (.|.) :: (RowLike a, RowLike b) => a -> b -> TableRow (.|.) a b = TableRow (cs ++ ds) where (TableRow cs) = mkRow a (TableRow ds) = mkRow b instance Monoid TableRow where mempty = TableRow [] (TableRow as) `mappend` (TableRow bs) = TableRow $ as ++ bs infixl 2 .|. data Table = Table { rows :: [TableRow] , numColumns :: Int , columnSpecs :: [ColumnSpec] , borderStyle :: BorderStyle , borderAttr :: Attr , defaultCellAlignment :: Alignment , defaultCellPadding :: Padding } instance HasBorderAttr (Widget Table) where setBorderAttribute t a = updateWidgetState t $ \s -> s { borderAttr = mergeAttr a $ borderAttr s } instance Show Table where show t = concat [ "Table { " , "rows = <", show $ length $ rows t, " rows>" , ", numColumns = ", show $ numColumns t , ", columnSpecs = ", show $ columnSpecs t , ", borderStyle = ", show $ borderStyle t , ", borderAttr = ", show $ borderAttr t , ", defaultCellAlignment = ", show $ defaultCellAlignment t , ", defaultCellPadding = ", show $ defaultCellPadding t , " }" ] -- |Create a custom 'TableCell' to set its alignment and/or padding -- settings. customCell :: (Show a) => Widget a -> TableCell customCell w = TableCell w Nothing Nothing -- |Create an empty table cell. emptyCell :: TableCell emptyCell = EmptyCell -- |Set the default table-wide cell alignment. setDefaultCellAlignment :: Widget Table -> Alignment -> IO () setDefaultCellAlignment t a = updateWidgetState t $ \s -> s { defaultCellAlignment = a } -- |Set the default table-wide cell padding. setDefaultCellPadding :: Widget Table -> Padding -> IO () setDefaultCellPadding t p = updateWidgetState t $ \s -> s { defaultCellPadding = p } -- |Create a column. column :: ColumnSize -> ColumnSpec column sz = ColumnSpec sz Nothing Nothing -- |Create a table widget using a list of column specifications and a -- border style. newTable :: [ColumnSpec] -> BorderStyle -> IO (Widget Table) newTable specs borderSty = do t <- newWidget $ \w -> w { state = Table { rows = [] , columnSpecs = specs , borderStyle = borderSty , numColumns = length specs , borderAttr = def_attr , defaultCellAlignment = AlignLeft , defaultCellPadding = padNone } , growHorizontal_ = \st -> do return $ any (== ColAuto) (map columnSize $ columnSpecs st) , render_ = \this sz ctx -> do rs <- rows <~~ this let sk = skin ctx rowImgs <- mapM (\(TableRow r) -> renderRow this sz r ctx) rs rowBorder <- mkRowBorder this sz ctx $ skinIntersectionFull sk topBorder <- mkTopBottomBorder this sz ctx $ skinIntersectionT sk bottomBorder <- mkTopBottomBorder this sz ctx $ skinIntersectionB sk sideBorderL <- mkSideBorder this ctx True sideBorderR <- mkSideBorder this ctx False let body = vert_cat $ intersperse rowBorder rowImgs withTBBorders = vert_cat [topBorder, body, bottomBorder] withSideBorders = horiz_cat [sideBorderL, withTBBorders, sideBorderR] -- Ideally, we would only display rows that we have room -- to render, but this is a much easier cop-out. :) if ((region_width sz < image_width withSideBorders) || (region_height sz < image_height withSideBorders)) then return empty_image else return withSideBorders , setCurrentPosition_ = \this pos -> do sz <- getCurrentSize this if region_width sz == 0 || region_height sz == 0 then return () else do bs <- borderStyle <~~ this rs <- rows <~~ this let edgeOffset = if edgeBorders bs then 1 else 0 positionRows _ [] = return () positionRows height ((TableRow row):rest) = do -- Compute the position for this row based on -- border settings let rowPos = pos `plusWidth` edgeOffset `withHeight` height -- Get the maximum cell height cellPhysSizes <- forM row $ \cell -> case cell of TableCell cw _ _ -> getCurrentSize cw EmptyCell -> return $ DisplayRegion 0 1 -- Include 1 as a possible height to -- prevent zero-height images from -- breaking position computations. This -- won't hurt in the case where other -- cells are bigger, since their heights -- will be chosen instead. let maxSize = maximum $ 1 : map region_height cellPhysSizes borderOffset = if rowBorders bs then 1 else 0 -- Position the individual row widgets -- (again, based on border settings) positionRow this bs rowPos row positionRows (height + maxSize + borderOffset) rest positionRows (region_height pos + edgeOffset) rs } return t getCellAlignment :: Widget Table -> Int -> TableCell -> IO Alignment getCellAlignment _ _ (TableCell _ (Just p) _) = return p getCellAlignment t columnNumber _ = do -- If the column for this cell has properties, use those; otherwise -- default to table-wide properties. specs <- columnSpecs <~~ t let spec = specs !! columnNumber case columnAlignment spec of Nothing -> defaultCellAlignment <~~ t Just p -> return p getCellPadding :: Widget Table -> Int -> TableCell -> IO Padding getCellPadding _ _ (TableCell _ _ (Just p)) = return p getCellPadding t columnNumber _ = do -- If the column for this cell has properties, use those; otherwise -- default to table-wide properties. specs <- columnSpecs <~~ t let spec = specs !! columnNumber case columnPadding spec of Nothing -> defaultCellPadding <~~ t Just p -> return p mkRowBorder :: Widget Table -> DisplayRegion -> RenderContext -> Char -> IO Image mkRowBorder t sz ctx intChar = do bs <- borderStyle <~~ t if not $ rowBorders bs then return empty_image else mkRowBorder_ t sz ctx intChar -- Make a row border that matches the width of each row but does not -- include outermost edge characters. mkRowBorder_ :: Widget Table -> DisplayRegion -> RenderContext -> Char -> IO Image mkRowBorder_ t sz ctx intChar = do bs <- borderStyle <~~ t bAttr <- borderAttr <~~ t specs <- columnSpecs <~~ t aw <- autoWidth t sz let sk = skin ctx bAttr' = mergeAttrs [ overrideAttr ctx , bAttr , normalAttr ctx ] szs = map columnSize specs intersection = string bAttr' [intChar] imgs = (flip map) szs $ \s -> case s of ColFixed n -> char_fill bAttr' (skinHorizontal sk) n 1 ColAuto -> char_fill bAttr' (skinHorizontal sk) aw 1 imgs' = if colBorders bs then intersperse intersection imgs else imgs return $ horiz_cat imgs' mkTopBottomBorder :: Widget Table -> DisplayRegion -> RenderContext -> Char -> IO Image mkTopBottomBorder t sz ctx intChar = do bs <- borderStyle <~~ t if edgeBorders bs then mkRowBorder_ t sz ctx intChar else return empty_image -- Make vertical side borders for the table, including row border -- intersections if necessary. mkSideBorder :: Widget Table -> RenderContext -> Bool -> IO Image mkSideBorder t ctx isLeft = do bs <- borderStyle <~~ t if edgeBorders bs then mkSideBorder_ t ctx isLeft else return empty_image mkSideBorder_ :: Widget Table -> RenderContext -> Bool -> IO Image mkSideBorder_ t ctx isLeft = do bs <- borderStyle <~~ t bAttr <- borderAttr <~~ t rs <- rows <~~ t let sk = skin ctx intersection = string bAttr' [ if isLeft then skinIntersectionL sk else skinIntersectionR sk ] topCorner = string bAttr' [ if isLeft then skinCornerTL sk else skinCornerTR sk ] bottomCorner = string bAttr' [ if isLeft then skinCornerBL sk else skinCornerBR sk ] bAttr' = mergeAttrs [ overrideAttr ctx , bAttr , normalAttr ctx ] rowHeights <- forM rs $ \(TableRow row) -> do hs <- forM row $ \cell -> case cell of TableCell cw _ _ -> region_height <$> getCurrentSize cw EmptyCell -> return 1 return $ maximum hs let borderImgs = (flip map) rowHeights $ \h -> char_fill bAttr' (skinVertical sk) 1 h withIntersections = if rowBorders bs then intersperse intersection borderImgs else borderImgs return $ vert_cat $ topCorner : withIntersections ++ [bottomCorner] positionRow :: Widget Table -> BorderStyle -> DisplayRegion -> [TableCell] -> IO () positionRow t bs pos cells = do -- Position each cell widget based on the base position of the row -- (which starts from the origin of the leftmost widget, NOT the -- leftmost cell border) oldSize <- getCurrentSize t aw <- autoWidth t oldSize specs <- columnSpecs <~~ t let szs = map columnSize specs offset = if colBorders bs then 1 else 0 cellWidth ColAuto = aw cellWidth (ColFixed n) = toEnum n doPositioning _ [] = return () doPositioning width ((szPolicy, cell):ws) = do case cell of TableCell w _ _ -> setCurrentPosition w $ pos `plusWidth` width EmptyCell -> return () doPositioning (width + cellWidth szPolicy + offset) ws doPositioning 0 $ zip szs cells autoWidth :: Widget Table -> DisplayRegion -> IO Word autoWidth t sz = do specs <- columnSpecs <~~ t bs <- borderStyle <~~ t let sizes = map columnSize specs numAuto = length $ filter (== ColAuto) sizes totalFixed = sum $ (flip map) sizes $ \s -> case s of ColAuto -> 0 ColFixed n -> n edgeWidth = if edgeBorders bs then 2 else 0 colWidth = if colBorders bs then (toEnum $ length sizes - 1) else 0 return $ toEnum ((max 0 ((fromEnum $ region_width sz) - totalFixed - edgeWidth - colWidth)) `div` numAuto) -- |Add a heading row to a table. Adds a row using the specified -- |labels and attribute. Returns the widgets it constructed as a -- |side-effect in case you want to do something with them. addHeadingRow :: Widget Table -> Attr -> [String] -> IO [Widget FormattedText] addHeadingRow tbl attr labels = do ws <- mapM (\s -> plainText s >>= withNormalAttribute attr) labels addRow tbl ws return ws -- |Add a heading row to a table. Adds a row using the specified -- |labels and attribute. addHeadingRow_ :: Widget Table -> Attr -> [String] -> IO () addHeadingRow_ tbl attr labels = addHeadingRow tbl attr labels >> return () applyCellAlignment :: Alignment -> TableCell -> IO TableCell applyCellAlignment _ EmptyCell = return EmptyCell applyCellAlignment al (TableCell w a p) = do case al of AlignLeft -> return $ TableCell w a p AlignCenter -> do -- This check really belongs in the centering code... grow <- growHorizontal w case grow of False -> do w' <- hCentered w return $ TableCell w' a p True -> return $ TableCell w a p AlignRight -> do grow <- growHorizontal w case grow of False -> do w' <- (hFill ' ' 1) <++> (return w) return $ TableCell w' a p True -> return $ TableCell w a p applyCellPadding :: Padding -> TableCell -> IO TableCell applyCellPadding _ EmptyCell = return EmptyCell applyCellPadding padding (TableCell w a p) = do w' <- padded w padding return $ TableCell w' a p -- |Add a row to the table. Use 'RowLike' instances to populate the -- row. Throws 'BadTableWidgetSizePolicy' if any widgets in the row -- grow vertically; throws 'ColumnCountMismatch' if the row's number -- of columns does not match the table's column count. addRow :: (RowLike a) => Widget Table -> a -> IO () addRow t row = do let (TableRow cells_) = mkRow row cells <- forM (zip [1..] cells_) $ \(i, c) -> do case c of EmptyCell -> return () TableCell w _ _ -> do v <- growVertical w when (v) $ throw $ BadTableWidgetSizePolicy i -- Apply cell properties to the widget in this cell. alignment <- getCellAlignment t (i - 1) c padding <- getCellPadding t (i - 1) c applyCellAlignment alignment c >>= applyCellPadding padding nc <- numColumns <~~ t when (length cells /= nc) $ throw ColumnCountMismatch updateWidgetState t $ \s -> s { rows = rows s ++ [TableRow cells] } renderCell :: DisplayRegion -> TableCell -> RenderContext -> IO Image renderCell region EmptyCell ctx = do w <- plainText "" render w region ctx renderCell region (TableCell w _ _) ctx = render w region ctx colBorders :: BorderStyle -> Bool colBorders (BorderPartial fs) = Columns `elem` fs colBorders BorderFull = True colBorders _ = False edgeBorders :: BorderStyle -> Bool edgeBorders (BorderPartial fs) = Edges `elem` fs edgeBorders BorderFull = True edgeBorders _ = False rowBorders :: BorderStyle -> Bool rowBorders (BorderPartial fs) = Rows `elem` fs rowBorders BorderFull = True rowBorders _ = False rowHeight :: [Image] -> Word rowHeight = maximum . map image_height renderRow :: Widget Table -> DisplayRegion -> [TableCell] -> RenderContext -> IO Image renderRow tbl sz cells ctx = do specs <- columnSpecs <~~ tbl borderSty <- borderStyle <~~ tbl bAttr <- borderAttr <~~ tbl aw <- autoWidth tbl sz let sk = skin ctx sizes = map columnSize specs att = mergeAttrs [ overrideAttr ctx , normalAttr ctx ] newDefault = normalAttr ctx cellImgs <- forM (zip cells sizes) $ \(cellW, sizeSpec) -> do let cellSz = DisplayRegion cellWidth (region_height sz) cellWidth = case sizeSpec of ColFixed n -> toEnum n ColAuto -> aw img <- renderCell cellSz cellW $ ctx { normalAttr = newDefault } -- Right-pad the image if it isn't big enough to fill the -- cell. case compare (image_width img) (region_width cellSz) of EQ -> return img LT -> return $ img <|> char_fill att ' ' (max 0 (region_width cellSz - image_width img)) (max (image_height img) 1) GT -> throw CellImageTooBig let maxHeight = rowHeight cellImgs cellImgsBottomPadded = (flip map) cellImgs $ \img -> img <-> char_fill att ' ' (image_width img) (maxHeight - image_height img) -- If we need to draw borders in between columns, do that. let bAttr' = mergeAttrs [ overrideAttr ctx , bAttr , normalAttr ctx ] withBorders = case colBorders borderSty of False -> cellImgsBottomPadded True -> intersperse (char_fill bAttr' (skinVertical sk) 1 maxHeight) cellImgsBottomPadded return $ horiz_cat withBorders