brick 0.70.1 → 0.71
raw patch · 10 files changed
+260/−94 lines, 10 filesdep ~vtyPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: vty
API changes (from Hackage documentation)
+ Brick.Main: renderWidget :: Ord n => Maybe AttrMap -> [Widget n] -> DisplayRegion -> Picture
- Brick.BorderMap: eBottomL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.BorderMap: eBottomL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.BorderMap: eLeftL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.BorderMap: eLeftL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.BorderMap: eRightL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.BorderMap: eRightL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.BorderMap: eTopL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.BorderMap: eTopL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.Types: availHeightL :: forall n_awD8. Lens' (Context n_awD8) Int
+ Brick.Types: availHeightL :: forall n_awEp. Lens' (Context n_awEp) Int
- Brick.Types: availWidthL :: forall n_awD8. Lens' (Context n_awD8) Int
+ Brick.Types: availWidthL :: forall n_awEp. Lens' (Context n_awEp) Int
- Brick.Types: bordersL :: forall n_awDc. Lens' (Result n_awDc) (BorderMap DynBorder)
+ Brick.Types: bordersL :: forall n_awEt. Lens' (Result n_awEt) (BorderMap DynBorder)
- Brick.Types: ctxAttrMapL :: forall n_awD8. Lens' (Context n_awD8) AttrMap
+ Brick.Types: ctxAttrMapL :: forall n_awEp. Lens' (Context n_awEp) AttrMap
- Brick.Types: ctxAttrNameL :: forall n_awD8. Lens' (Context n_awD8) AttrName
+ Brick.Types: ctxAttrNameL :: forall n_awEp. Lens' (Context n_awEp) AttrName
- Brick.Types: ctxBorderStyleL :: forall n_awD8. Lens' (Context n_awD8) BorderStyle
+ Brick.Types: ctxBorderStyleL :: forall n_awEp. Lens' (Context n_awEp) BorderStyle
- Brick.Types: ctxDynBordersL :: forall n_awD8. Lens' (Context n_awD8) Bool
+ Brick.Types: ctxDynBordersL :: forall n_awEp. Lens' (Context n_awEp) Bool
- Brick.Types: ctxHScrollBarOrientationL :: forall n_awD8. Lens' (Context n_awD8) (Maybe HScrollBarOrientation)
+ Brick.Types: ctxHScrollBarOrientationL :: forall n_awEp. Lens' (Context n_awEp) (Maybe HScrollBarOrientation)
- Brick.Types: ctxHScrollBarRendererL :: forall n_awD8. Lens' (Context n_awD8) (Maybe (ScrollbarRenderer n_awD8))
+ Brick.Types: ctxHScrollBarRendererL :: forall n_awEp. Lens' (Context n_awEp) (Maybe (ScrollbarRenderer n_awEp))
- Brick.Types: ctxVScrollBarOrientationL :: forall n_awD8. Lens' (Context n_awD8) (Maybe VScrollBarOrientation)
+ Brick.Types: ctxVScrollBarOrientationL :: forall n_awEp. Lens' (Context n_awEp) (Maybe VScrollBarOrientation)
- Brick.Types: ctxVScrollBarRendererL :: forall n_awD8. Lens' (Context n_awD8) (Maybe (ScrollbarRenderer n_awD8))
+ Brick.Types: ctxVScrollBarRendererL :: forall n_awEp. Lens' (Context n_awEp) (Maybe (ScrollbarRenderer n_awEp))
- Brick.Types: cursorLocationL :: forall n_awDd. Lens' (CursorLocation n_awDd) Location
+ Brick.Types: cursorLocationL :: forall n_awEu. Lens' (CursorLocation n_awEu) Location
- Brick.Types: cursorLocationNameL :: forall n_awDd n_ayNl. Lens (CursorLocation n_awDd) (CursorLocation n_ayNl) (Maybe n_awDd) (Maybe n_ayNl)
+ Brick.Types: cursorLocationNameL :: forall n_awEu n_ayOC. Lens (CursorLocation n_awEu) (CursorLocation n_ayOC) (Maybe n_awEu) (Maybe n_ayOC)
- Brick.Types: cursorsL :: forall n_awDc. Lens' (Result n_awDc) [CursorLocation n_awDc]
+ Brick.Types: cursorsL :: forall n_awEt. Lens' (Result n_awEt) [CursorLocation n_awEt]
- Brick.Types: eBottomL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.Types: eBottomL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.Types: eLeftL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.Types: eLeftL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.Types: eRightL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.Types: eRightL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.Types: eTopL :: forall a_acJW. Lens' (Edges a_acJW) a_acJW
+ Brick.Types: eTopL :: forall a_acLd. Lens' (Edges a_acLd) a_acLd
- Brick.Types: extentsL :: forall n_awDc. Lens' (Result n_awDc) [Extent n_awDc]
+ Brick.Types: extentsL :: forall n_awEt. Lens' (Result n_awEt) [Extent n_awEt]
- Brick.Types: imageL :: forall n_awDc. Lens' (Result n_awDc) Image
+ Brick.Types: imageL :: forall n_awEt. Lens' (Result n_awEt) Image
- Brick.Types: visibilityRequestsL :: forall n_awDc. Lens' (Result n_awDc) [VisibilityRequest]
+ Brick.Types: visibilityRequestsL :: forall n_awEt. Lens' (Result n_awEt) [VisibilityRequest]
- Brick.Types: windowHeightL :: forall n_awD8. Lens' (Context n_awD8) Int
+ Brick.Types: windowHeightL :: forall n_awEp. Lens' (Context n_awEp) Int
- Brick.Types: windowWidthL :: forall n_awD8. Lens' (Context n_awD8) Int
+ Brick.Types: windowWidthL :: forall n_awEp. Lens' (Context n_awEp) Int
- Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1fGn a_a1fH1. Lens (Dialog a_a1fGn) (Dialog a_a1fH1) [(String, a_a1fGn)] [(String, a_a1fH1)]
+ Brick.Widgets.Dialog: dialogButtonsL :: forall a_a1fNW a_a1fOA. Lens (Dialog a_a1fNW) (Dialog a_a1fOA) [(String, a_a1fNW)] [(String, a_a1fOA)]
- Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1fGn. Lens' (Dialog a_a1fGn) (Maybe Int)
+ Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a1fNW. Lens' (Dialog a_a1fNW) (Maybe Int)
- Brick.Widgets.Dialog: dialogTitleL :: forall a_a1fGn. Lens' (Dialog a_a1fGn) (Maybe String)
+ Brick.Widgets.Dialog: dialogTitleL :: forall a_a1fNW. Lens' (Dialog a_a1fNW) (Maybe String)
- Brick.Widgets.Dialog: dialogWidthL :: forall a_a1fGn. Lens' (Dialog a_a1fGn) Int
+ Brick.Widgets.Dialog: dialogWidthL :: forall a_a1fNW. Lens' (Dialog a_a1fNW) Int
- Brick.Widgets.Edit: editContentsL :: forall t_a19Qx n_a19Qy t_a19Rc. Lens (Editor t_a19Qx n_a19Qy) (Editor t_a19Rc n_a19Qy) (TextZipper t_a19Qx) (TextZipper t_a19Rc)
+ Brick.Widgets.Edit: editContentsL :: forall t_a19Y8 n_a19Y9 t_a19YN. Lens (Editor t_a19Y8 n_a19Y9) (Editor t_a19YN n_a19Y9) (TextZipper t_a19Y8) (TextZipper t_a19YN)
- Brick.Widgets.FileBrowser: fileBrowserEntriesG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) (List n_a1q2f FileInfo)
+ Brick.Widgets.FileBrowser: fileBrowserEntriesG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) (List n_a1q9L FileInfo)
- Brick.Widgets.FileBrowser: fileBrowserEntryFilterG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) (Maybe (FileInfo -> Bool))
+ Brick.Widgets.FileBrowser: fileBrowserEntryFilterG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) (Maybe (FileInfo -> Bool))
- Brick.Widgets.FileBrowser: fileBrowserExceptionG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) (Maybe IOException)
+ Brick.Widgets.FileBrowser: fileBrowserExceptionG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) (Maybe IOException)
- Brick.Widgets.FileBrowser: fileBrowserLatestResultsG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) [FileInfo]
+ Brick.Widgets.FileBrowser: fileBrowserLatestResultsG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) [FileInfo]
- Brick.Widgets.FileBrowser: fileBrowserNameG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) n_a1q2f
+ Brick.Widgets.FileBrowser: fileBrowserNameG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) n_a1q9L
- Brick.Widgets.FileBrowser: fileBrowserSearchStringG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) (Maybe Text)
+ Brick.Widgets.FileBrowser: fileBrowserSearchStringG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) (Maybe Text)
- Brick.Widgets.FileBrowser: fileBrowserSelectableG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) (FileInfo -> Bool)
+ Brick.Widgets.FileBrowser: fileBrowserSelectableG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) (FileInfo -> Bool)
- Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1q2f. Lens' (FileBrowser n_a1q2f) (FileInfo -> Bool)
+ Brick.Widgets.FileBrowser: fileBrowserSelectableL :: forall n_a1q9L. Lens' (FileBrowser n_a1q9L) (FileInfo -> Bool)
- Brick.Widgets.FileBrowser: fileBrowserSelectedFilesG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) (Set String)
+ Brick.Widgets.FileBrowser: fileBrowserSelectedFilesG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) (Set String)
- Brick.Widgets.FileBrowser: fileBrowserWorkingDirectoryG :: forall n_a1q2f. SimpleGetter (FileBrowser n_a1q2f) FilePath
+ Brick.Widgets.FileBrowser: fileBrowserWorkingDirectoryG :: forall n_a1q9L. SimpleGetter (FileBrowser n_a1q9L) FilePath
- Brick.Widgets.List: listElementsL :: forall n_a1kQn t_a1kQo e_a1kQp t_a1l4t e_a1l4u. Lens (GenericList n_a1kQn t_a1kQo e_a1kQp) (GenericList n_a1kQn t_a1l4t e_a1l4u) (t_a1kQo e_a1kQp) (t_a1l4t e_a1l4u)
+ Brick.Widgets.List: listElementsL :: forall n_a1kXT t_a1kXU e_a1kXV t_a1lbZ e_a1lc0. Lens (GenericList n_a1kXT t_a1kXU e_a1kXV) (GenericList n_a1kXT t_a1lbZ e_a1lc0) (t_a1kXU e_a1kXV) (t_a1lbZ e_a1lc0)
- Brick.Widgets.List: listItemHeightL :: forall n_a1kQn t_a1kQo e_a1kQp. Lens' (GenericList n_a1kQn t_a1kQo e_a1kQp) Int
+ Brick.Widgets.List: listItemHeightL :: forall n_a1kXT t_a1kXU e_a1kXV. Lens' (GenericList n_a1kXT t_a1kXU e_a1kXV) Int
- Brick.Widgets.List: listNameL :: forall n_a1kQn t_a1kQo e_a1kQp n_a1l4v. Lens (GenericList n_a1kQn t_a1kQo e_a1kQp) (GenericList n_a1l4v t_a1kQo e_a1kQp) n_a1kQn n_a1l4v
+ Brick.Widgets.List: listNameL :: forall n_a1kXT t_a1kXU e_a1kXV n_a1lc1. Lens (GenericList n_a1kXT t_a1kXU e_a1kXV) (GenericList n_a1lc1 t_a1kXU e_a1kXV) n_a1kXT n_a1lc1
- Brick.Widgets.List: listSelectedL :: forall n_a1kQn t_a1kQo e_a1kQp. Lens' (GenericList n_a1kQn t_a1kQo e_a1kQp) (Maybe Int)
+ Brick.Widgets.List: listSelectedL :: forall n_a1kXT t_a1kXU e_a1kXV. Lens' (GenericList n_a1kXT t_a1kXU e_a1kXV) (Maybe Int)
Files
- CHANGELOG.md +15/−0
- README.md +2/−1
- brick.cabal +5/−4
- programs/ReadmeDemo.hs +2/−1
- src/Brick/Forms.hs +3/−2
- src/Brick/Main.hs +1/−0
- src/Brick/Widgets/Internal.hs +36/−0
- src/Brick/Widgets/Table.hs +132/−85
- tests/Main.hs +2/−1
- tests/Render.hs +62/−0
CHANGELOG.md view
@@ -2,6 +2,21 @@ Brick changelog --------------- +0.71+----++Package changes:+ * Increased `vty` lower bound to `5.36`.++API changes:+ * Added `tests/Render.hs` to provide a simple test of+ `Brick.Main.renderWidget` (thanks @valyagolev)+ * Added `Brick.Main.renderWidget` to help in golden testing contexts+ (thanks @valyagolev)++Other changes:+ * Various `table` documentation improvements.+ 0.70.1 ------
README.md view
@@ -20,6 +20,7 @@ Here's an example interface (see `programs/ReadmeDemo.hs`): ```+joinBorders $ withBorderStyle unicode $ borderWithLabel (str "Hello!") $ (center (str "Left") <+> vBorder <+> center (str "Right"))@@ -34,7 +35,7 @@ │ Left │ Right │ │ │ │ │ │ │-└────────────────────────┘+└───────────┴────────────┘ ``` Featured Projects
brick.cabal view
@@ -1,5 +1,5 @@ name: brick-version: 0.70.1+version: 0.71 synopsis: A declarative terminal user interface library description: Write terminal user interfaces (TUIs) painlessly with 'brick'! You@@ -32,7 +32,7 @@ license-file: LICENSE author: Jonathan Daugherty <cygnus@foobox.com> maintainer: Jonathan Daugherty <cygnus@foobox.com>-copyright: (c) Jonathan Daugherty 2015-2021+copyright: (c) Jonathan Daugherty 2015-2022 category: Graphics build-type: Simple cabal-version: 1.18@@ -112,7 +112,7 @@ Brick.Widgets.Internal build-depends: base >= 4.9.0.0 && < 4.17.0.0,- vty >= 5.33,+ vty >= 5.36, transformers, data-clist >= 0.1, directory >= 1.2.5.0,@@ -511,10 +511,11 @@ ghc-options: -Wall -Wcompat -Wno-orphans -O2 default-language: Haskell2010 main-is: Main.hs- other-modules: List+ other-modules: List Render build-depends: base <=5, brick, containers, microlens, vector,+ vty, QuickCheck
programs/ReadmeDemo.hs view
@@ -1,12 +1,13 @@ module Main where -import Brick (Widget, simpleMain, (<+>), str, withBorderStyle)+import Brick (Widget, simpleMain, (<+>), str, withBorderStyle, joinBorders) import Brick.Widgets.Center (center) import Brick.Widgets.Border (borderWithLabel, vBorder) import Brick.Widgets.Border.Style (unicode) ui :: Widget () ui =+ joinBorders $ withBorderStyle unicode $ borderWithLabel (str "Hello!") $ (center (str "Left") <+> vBorder <+> center (str "Right"))
src/Brick/Forms.hs view
@@ -30,8 +30,9 @@ -- button sequence). -- -- To use a 'Form', you must include it within your application state--- type. You can use 'formState' to access the underlying s whenever you--- need it. See @programs/FormDemo.hs@ for a complete working example.+-- type. You can use 'formState' to access the underlying state whenever+-- you need it. See @programs/FormDemo.hs@ for a complete working+-- example. -- -- Also note that, by default, forms and their field inputs are -- concatenated together in a 'vBox'. This can be customized on a
src/Brick/Main.hs view
@@ -47,6 +47,7 @@ , renderFinal , getRenderState , resetRenderState+ , renderWidget ) where
src/Brick/Widgets/Internal.hs view
@@ -4,6 +4,7 @@ , cropToContext , cropResultToContext , renderDynBorder+ , renderWidget ) where @@ -12,7 +13,9 @@ import Control.Monad (forM_) import Control.Monad.Trans.State.Lazy import Control.Monad.Trans.Reader+import Data.Maybe (fromMaybe) import qualified Data.Map as M+import qualified Data.Set as S import Data.Maybe (catMaybes) import qualified Graphics.Vty as V @@ -154,3 +157,36 @@ Edges True True False True -> bsIntersectL Edges True True True False -> bsIntersectR Edges True True True True -> bsIntersectFull++-- | This function provides a simplified interface to rendering a list+-- of 'Widget's as a 'V.Picture' outside of the context of an 'App'.+-- This can be useful in a testing setting but isn't intended to be used+-- for normal application rendering. The API is deliberately narrower+-- than the main interactive API and is not yet stable. Use at your own+-- risk.+--+-- Consult the [Vty library documentation](https://hackage.haskell.org/package/vty)+-- for details on how to output the resulting 'V.Picture'.+renderWidget :: (Ord n)+ => Maybe AttrMap+ -- ^ Optional attribute map used to render. If omitted,+ -- an empty attribute map with the terminal's default+ -- attribute will be used.+ -> [Widget n]+ -- ^ The widget layers to render, topmost first.+ -> V.DisplayRegion+ -- ^ The size of the display region in which to render the+ -- layers.+ -> V.Picture+renderWidget mAttrMap layerRenders region = pic+ where+ initialRS = RS { viewportMap = M.empty+ , rsScrollRequests = []+ , observedNames = S.empty+ , renderCache = mempty+ , clickableNames = []+ , requestedVisibleNames_ = S.empty+ , reportedExtents = mempty+ }+ am = fromMaybe (attrMap V.defAttr []) mAttrMap+ (_, pic, _, _) = renderFinal am layerRenders region (const Nothing) initialRS
src/Brick/Widgets/Table.hs view
@@ -1,4 +1,8 @@--- | Support for basic table drawing.+{-# LANGUAGE MultiWayIf #-}+-- | This module provides a table widget that can draw other widgets+-- in a table layout, draw borders between rows and columns, and allow+-- configuration of row and column alignment. To get started, see+-- 'table'. module Brick.Widgets.Table ( -- * Types@@ -45,7 +49,8 @@ import Brick.Widgets.Center import Brick.Widgets.Border --- | Column alignment modes.+-- | Column alignment modes. Use these modes with the alignment+-- functions in this module to configure column alignment behavior. data ColumnAlignment = AlignLeft -- ^ Align all cells to the left.@@ -55,7 +60,8 @@ -- ^ Align all cells to the right. deriving (Eq, Show, Read) --- | Row alignment modes.+-- | Row alignment modes. Use these modes with the alignment functions+-- in this module to configure row alignment behavior. data RowAlignment = AlignTop -- ^ Align all cells to the top.@@ -76,7 +82,8 @@ instance E.Exception TableException where --- | A table data structure.+-- | A table data structure for widgets of type 'Widget' @n@. Create a+-- table with 'table'. data Table n = Table { columnAlignments :: M.Map Int ColumnAlignment , rowAlignments :: M.Map Int RowAlignment@@ -90,34 +97,49 @@ -- | Construct a new table. ----- The argument is the list of rows, with each element of the argument--- list being the columns of the respective row.+-- The argument is the list of rows with the topmost row first, with+-- each element of the argument list being the contents of the cells in+-- in each column of the respective row, with the leftmost cell first. ----- By default, all columns are left-aligned. Use the alignment functions--- in this module to change that behavior.+-- Each row's height is determined by the height of the tallest cell+-- in that row, and each column's width is determined by the width of+-- the widest cell in that column. This means that control over row+-- and column dimensions is a matter of controlling the size of the+-- individual cells, such as by wrapping cell contents in padding,+-- 'fill' and 'hLimit' or 'vLimit', etc. This also means that it is not+-- necessary to explicitly set the width of most table cells because+-- the table will determine the per-row and per-column dimensions by+-- looking at the largest cell contents. In particular, this means+-- that the table's alignment logic only has an effect when a given+-- cell's contents are smaller than the maximum for its row and column,+-- thus giving the table some way to pad the contents to result in the+-- desired alignment. ----- By default, all rows are top-aligned. Use the alignment functions in--- this module to change that behavior.+-- By default: ----- By default, the table will draw borders between columns, between--- rows, and around the outside of the table. Border-drawing behavior--- can be configured with the API in this module. Note that tables--- always draw with 'joinBorders' enabled.+-- * All columns are left-aligned. Use the alignment functions in this+-- module to change that behavior.+-- * All rows are top-aligned. Use the alignment functions in this+-- module to change that behavior.+-- * The table will draw borders between columns, between rows, and+-- around the outside of the table. Border-drawing behavior can be+-- configured with the API in this module. Note that tables always draw+-- with 'joinBorders' enabled. If a cell's contents has smart borders+-- but you don't want those borders to connect to the surrounding table+-- borders, wrap the cell's contents with 'freezeBorders'. -- -- All cells of all rows MUST use the 'Fixed' growth policy for both--- horizontal and vertical growth. If the argument list contains--- any cells that use the 'Greedy' policy, this will raise a+-- horizontal and vertical growth. If the argument list contains any+-- cells that use the 'Greedy' policy, this function will raise a -- 'TableException'. ----- All rows must have the same number of cells. If not, this will raise--- a 'TableException'.+-- All rows MUST have the same number of cells. If not, this function+-- will raise a 'TableException'. table :: [[Widget n]] -> Table n table rows =- if not allFixed- then E.throw TEInvalidCellSizePolicy- else if not allSameLength- then E.throw TEUnequalRowSizes- else t+ if | not allFixed -> E.throw TEInvalidCellSizePolicy+ | not allSameLength -> E.throw TEUnequalRowSizes+ | otherwise -> t where allSameLength = length (nub (length <$> rows)) <= 1 allFixed = all fixedRow rows@@ -149,43 +171,49 @@ t { drawColumnBorders = b } -- | Align the specified column to the right. The argument is the column--- index, starting with zero.+-- index, starting with zero. Silently does nothing if the index is out+-- of range. alignRight :: Int -> Table n -> Table n alignRight = setColAlignment AlignRight -- | Align the specified column to the left. The argument is the column--- index, starting with zero.+-- index, starting with zero. Silently does nothing if the index is out+-- of range. alignLeft :: Int -> Table n -> Table n alignLeft = setColAlignment AlignLeft -- | Align the specified column to center. The argument is the column--- index, starting with zero.+-- index, starting with zero. Silently does nothing if the index is out+-- of range. alignCenter :: Int -> Table n -> Table n alignCenter = setColAlignment AlignCenter -- | Align the specified row to the top. The argument is the row index,--- starting with zero.+-- starting with zero. Silently does nothing if the index is out of+-- range. alignTop :: Int -> Table n -> Table n alignTop = setRowAlignment AlignTop -- | Align the specified row to the middle. The argument is the row--- index, starting with zero.+-- index, starting with zero. Silently does nothing if the index is out+-- of range. alignMiddle :: Int -> Table n -> Table n alignMiddle = setRowAlignment AlignMiddle -- | Align the specified row to bottom. The argument is the row index,--- starting with zero.+-- starting with zero. Silently does nothing if the index is out of+-- range. alignBottom :: Int -> Table n -> Table n alignBottom = setRowAlignment AlignBottom -- | Set the alignment for the specified column index (starting at--- zero).+-- zero). Silently does nothing if the index is out of range. setColAlignment :: ColumnAlignment -> Int -> Table n -> Table n setColAlignment a col t = t { columnAlignments = M.insert col a (columnAlignments t) } -- | Set the alignment for the specified row index (starting at--- zero).+-- zero). Silently does nothing if the index is out of range. setRowAlignment :: RowAlignment -> Int -> Table n -> Table n setRowAlignment a row t = t { rowAlignments = M.insert row a (rowAlignments t) }@@ -208,9 +236,10 @@ joinBorders $ Widget Fixed Fixed $ do ctx <- getContext- let rows = tableRows t- cellResults <- forM rows $ mapM render- let rowHeights = rowHeight <$> cellResults+ cellResults <- forM (tableRows t) $ mapM render++ let maybeIntersperse f v = if f t then intersperse v else id+ rowHeights = rowHeight <$> cellResults colWidths = colWidth <$> byColumn allRowAligns = (\i -> M.findWithDefault (defaultRowAlignment t) i (rowAlignments t)) <$> [0..length rowHeights - 1]@@ -220,65 +249,83 @@ colWidth = maximum . fmap (imageWidth . image) byColumn = transpose cellResults toW = Widget Fixed Fixed . return- applyColAlignment align width w =- Widget Fixed Fixed $ do- result <- render w- case align of- AlignLeft -> render $ hLimit width $ padRight Max $ toW result- AlignCenter -> render $ hLimit width $ hCenter $ toW result- AlignRight -> render $- padLeft (Pad (width - imageWidth (image result))) $- toW result- applyRowAlignment rHeight align result =- case align of- AlignTop -> vLimit rHeight $ padBottom Max $ toW result- AlignMiddle -> vLimit rHeight $ vCenter $ toW result- AlignBottom -> vLimit rHeight $ padTop Max $ toW result- fixEmtpyCell w h result =+ fillEmptyCell w h result = if imageWidth (image result) == 0 && imageHeight (image result) == 0 then result { image = charFill (ctx^.attrL) ' ' w h } else result- mkColumn (hAlign, width, colCells) = do+ mkColumn (hAlign, width, colCells) = let paddedCells = flip map (zip3 allRowAligns rowHeights colCells) $ \(vAlign, rHeight, cell) ->- applyColAlignment hAlign width $+ applyColAlignment width hAlign $ applyRowAlignment rHeight vAlign $- fixEmtpyCell width rHeight cell- maybeRowBorders = if drawRowBorders t- then intersperse (hLimit width hBorder)- else id- render $ vBox $ maybeRowBorders paddedCells- columns <- mapM mkColumn $ zip3 allColAligns colWidths byColumn+ toW $+ fillEmptyCell width rHeight cell+ maybeRowBorders = maybeIntersperse drawRowBorders (hLimit width hBorder)+ in vBox $ maybeRowBorders paddedCells - let tl = joinableBorder (Edges False True False True)- tr = joinableBorder (Edges False True True False)- bl = joinableBorder (Edges True False False True)- br = joinableBorder (Edges True False True False)- cross = joinableBorder (Edges True True True True)- leftT = joinableBorder (Edges True True False True)- rightT = joinableBorder (Edges True True True False)- topT = joinableBorder (Edges False True True True)- bottomT = joinableBorder (Edges True False True True) vBorders = mkVBorder <$> rowHeights hBorders = mkHBorder <$> colWidths mkHBorder w = hLimit w hBorder mkVBorder h = vLimit h vBorder- topBorder = hBox $ (if drawColumnBorders t then intersperse topT else id) hBorders- bottomBorder = hBox $ (if drawColumnBorders t then intersperse bottomT else id) hBorders- leftBorder = vBox $ tl : (if drawRowBorders t then intersperse leftT else id) vBorders <> [bl]- rightBorder = vBox $ tr : (if drawRowBorders t then intersperse rightT else id) vBorders <> [br]- maybeAddSurroundingBorder body =- if not $ drawSurroundingBorder t- then body- else leftBorder <+> (topBorder <=> body <=> bottomBorder) <+> rightBorder- maybeAddColumnBorders =- if not $ drawColumnBorders t- then id- else let maybeAddCrosses = if drawRowBorders t- then intersperse cross- else id- columnBorder = vBox $ maybeAddCrosses vBorders- in intersperse columnBorder+ topBorder =+ hBox $ maybeIntersperse drawColumnBorders topT hBorders+ bottomBorder =+ hBox $ maybeIntersperse drawColumnBorders bottomT hBorders+ leftBorder =+ vBox $ topLeftCorner : maybeIntersperse drawRowBorders leftT vBorders <> [bottomLeftCorner]+ rightBorder =+ vBox $ topRightCorner : maybeIntersperse drawRowBorders rightT vBorders <> [bottomRightCorner] - render $ maybeAddSurroundingBorder $- hBox $- maybeAddColumnBorders $ toW <$> columns+ maybeWrap check f =+ if check t then f else id+ addSurroundingBorder body =+ leftBorder <+> (topBorder <=> body <=> bottomBorder) <+> rightBorder+ addColumnBorders =+ let maybeAddCrosses = maybeIntersperse drawRowBorders cross+ columnBorder = vBox $ maybeAddCrosses vBorders+ in intersperse columnBorder++ let columns = mkColumn <$> zip3 allColAligns colWidths byColumn+ body = hBox $+ maybeWrap drawColumnBorders addColumnBorders columns+ render $ maybeWrap drawSurroundingBorder addSurroundingBorder body++topLeftCorner :: Widget n+topLeftCorner = joinableBorder $ Edges False True False True++topRightCorner :: Widget n+topRightCorner = joinableBorder $ Edges False True True False++bottomLeftCorner :: Widget n+bottomLeftCorner = joinableBorder $ Edges True False False True++bottomRightCorner :: Widget n+bottomRightCorner = joinableBorder $ Edges True False True False++cross :: Widget n+cross = joinableBorder $ Edges True True True True++leftT :: Widget n+leftT = joinableBorder $ Edges True True False True++rightT :: Widget n+rightT = joinableBorder $ Edges True True True False++topT :: Widget n+topT = joinableBorder $ Edges False True True True++bottomT :: Widget n+bottomT = joinableBorder $ Edges True False True True++applyColAlignment :: Int -> ColumnAlignment -> Widget n -> Widget n+applyColAlignment width align w =+ hLimit width $ case align of+ AlignLeft -> padRight Max w+ AlignCenter -> hCenter w+ AlignRight -> padLeft Max w++applyRowAlignment :: Int -> RowAlignment -> Widget n -> Widget n+applyRowAlignment rHeight align w =+ vLimit rHeight $ case align of+ AlignTop -> padBottom Max w+ AlignMiddle -> vCenter w+ AlignBottom -> padTop Max w
tests/Main.hs view
@@ -13,6 +13,7 @@ import qualified Data.IntMap as IntMap import qualified List+import qualified Render instance Arbitrary v => Arbitrary (Run v) where arbitrary = liftA2 (\(Positive n) -> Run n) arbitrary arbitrary@@ -111,5 +112,5 @@ main :: IO () main =- (all id <$> sequenceA [$quickCheckAll, List.main])+ (all id <$> sequenceA [$quickCheckAll, List.main, Render.main]) >>= bool exitFailure exitSuccess
+ tests/Render.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TypeApplications #-}+module Render+ ( main+ )+where++import Brick+import Control.Monad (when)+#if !(MIN_VERSION_base(4,11,0))+import Data.Monoid+#endif+import qualified Graphics.Vty as V+import Brick.Widgets.Border (hBorder)+import Control.Exception (try)++region :: V.DisplayRegion+region = (30, 10)++renderDisplay :: Ord n => [Widget n] -> IO ()+renderDisplay ws = do+ outp <- V.outputForConfig V.defaultConfig+ ctx <- V.displayContext outp region+ V.outputPicture ctx (renderWidget Nothing ws region)+ V.releaseDisplay outp++myWidget :: Widget ()+myWidget = str "Why" <=> hBorder <=> str "not?"++-- Since you can't Read a Picture, we have to compare the result with+-- the Shown one+expectedResult :: String+expectedResult = "Picture {picCursor = NoCursor, picLayers = [VertJoin {partTop = VertJoin {partTop = HorizText {attr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}, displayText = \"Why \", outputWidth = 30, charWidth = 30}, partBottom = VertJoin {partTop = HorizText {attr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}, displayText = \"\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\\9472\", outputWidth = 30, charWidth = 30}, partBottom = HorizText {attr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}, displayText = \"not? \", outputWidth = 30, charWidth = 30}, outputWidth = 30, outputHeight = 2}, outputWidth = 30, outputHeight = 3}, partBottom = BGFill {outputWidth = 30, outputHeight = 7}, outputWidth = 30, outputHeight = 10}], picBackground = Background {backgroundChar = ' ', backgroundAttr = Attr {attrStyle = Default, attrForeColor = Default, attrBackColor = Default, attrURL = Default}}}"++main :: IO Bool+main = do+ result <- try (renderDisplay [myWidget]) :: IO (Either V.VtyConfigurationError ())+ case result of+ Left _ -> do+ putStrLn "Terminal is not available, skipping test"+ -- Even though we could not actually run the test, we return+ -- True here to prevent the absence of a terminal from+ -- causing a test suite failure in an automated context.+ -- This means that this test effectively doesn't get+ -- considered at all in the automated context.+ return True+ Right () -> do+ let matched = actualResult == expectedResult+ actualResult = show (renderWidget Nothing [myWidget] region)+ msg = if matched then "rendering match" else "rendering mismatch"++ putStrLn ""+ putStrLn $ "renderWidget test outcome: " <> msg++ when (not matched) $ do+ putStrLn "Expected result:"+ putStrLn expectedResult++ putStrLn "Actual result:"+ putStrLn actualResult++ return matched