packages feed

brick 0.35.1 → 0.36

raw patch · 15 files changed

+1243/−58 lines, 15 filesdep +QuickCheckdep ~basedep ~microlensnew-component:exe:brick-dynamic-border-demoPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck

Dependency ranges changed: base, microlens

API changes (from Hackage documentation)

+ Brick.BorderMap: Edges :: a -> Edges a
+ Brick.BorderMap: [eTop, eBottom, eLeft, eRight] :: Edges a -> a
+ Brick.BorderMap: bounds :: BorderMap a -> Edges (Int, Int)
+ Brick.BorderMap: coordinates :: BorderMap a -> Edges Int
+ Brick.BorderMap: crop :: Edges Int -> BorderMap a -> BorderMap a
+ Brick.BorderMap: data BorderMap a
+ Brick.BorderMap: data Edges a
+ Brick.BorderMap: eBottomL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.BorderMap: eLeftL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.BorderMap: eRightL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.BorderMap: eTopL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.BorderMap: empty :: BorderMap a
+ Brick.BorderMap: emptyCoordinates :: Edges Int -> BorderMap a
+ Brick.BorderMap: expand :: Edges Int -> BorderMap a -> BorderMap a
+ Brick.BorderMap: insert :: Location -> a -> BorderMap a -> BorderMap a
+ Brick.BorderMap: insertH :: Location -> Run a -> BorderMap a -> BorderMap a
+ Brick.BorderMap: insertV :: Location -> Run a -> BorderMap a -> BorderMap a
+ Brick.BorderMap: instance GHC.Base.Functor Brick.BorderMap.BorderMap
+ Brick.BorderMap: instance GHC.Classes.Eq a => GHC.Classes.Eq (Brick.BorderMap.BorderMap a)
+ Brick.BorderMap: instance GHC.Classes.Ord a => GHC.Classes.Ord (Brick.BorderMap.BorderMap a)
+ Brick.BorderMap: instance GHC.Show.Show a => GHC.Show.Show (Brick.BorderMap.BorderMap a)
+ Brick.BorderMap: lookup :: Location -> BorderMap a -> Maybe a
+ Brick.BorderMap: lookupCol :: Int -> BorderMap a -> IMap a
+ Brick.BorderMap: lookupH :: Location -> Run ignored -> BorderMap a -> IMap a
+ Brick.BorderMap: lookupRow :: Int -> BorderMap a -> IMap a
+ Brick.BorderMap: lookupV :: Location -> Run ignored -> BorderMap a -> IMap a
+ Brick.BorderMap: setCoordinates :: Edges Int -> BorderMap a -> BorderMap a
+ Brick.BorderMap: singleton :: Location -> a -> BorderMap a
+ Brick.BorderMap: translate :: Location -> BorderMap a -> BorderMap a
+ Brick.BorderMap: unsafeUnion :: BorderMap a -> BorderMap a -> BorderMap a
+ Brick.BorderMap: values :: BorderMap a -> Edges (IMap a)
+ Brick.Types: BorderSegment :: Bool -> Bool -> Bool -> BorderSegment
+ Brick.Types: DynBorder :: BorderStyle -> Attr -> Edges BorderSegment -> DynBorder
+ Brick.Types: Edges :: a -> Edges a
+ Brick.Types: [borders] :: Result n -> BorderMap DynBorder
+ Brick.Types: [bsAccept] :: BorderSegment -> Bool
+ Brick.Types: [bsDraw] :: BorderSegment -> Bool
+ Brick.Types: [bsOffer] :: BorderSegment -> Bool
+ Brick.Types: [dbAttr] :: DynBorder -> Attr
+ Brick.Types: [dbSegments] :: DynBorder -> Edges BorderSegment
+ Brick.Types: [dbStyle] :: DynBorder -> BorderStyle
+ Brick.Types: [eTop, eBottom, eLeft, eRight] :: Edges a -> a
+ Brick.Types: bordersL :: forall n_aBMP. Lens' (Result n_aBMP) (BorderMap DynBorder)
+ Brick.Types: bsAcceptL :: Lens' BorderSegment Bool
+ Brick.Types: bsDrawL :: Lens' BorderSegment Bool
+ Brick.Types: bsOfferL :: Lens' BorderSegment Bool
+ Brick.Types: ctxDynBordersL :: Lens' Context Bool
+ Brick.Types: data BorderSegment
+ Brick.Types: data DynBorder
+ Brick.Types: data Edges a
+ Brick.Types: dbAttrL :: Lens' DynBorder Attr
+ Brick.Types: dbSegmentsL :: Lens' DynBorder (Edges BorderSegment)
+ Brick.Types: dbStyleL :: Lens' DynBorder BorderStyle
+ Brick.Types: eBottomL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.Types: eLeftL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.Types: eRightL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.Types: eTopL :: forall a_aebK. Lens' (Edges a_aebK) a_aebK
+ Brick.Widgets.Border: joinableBorder :: Edges Bool -> Widget n
+ Brick.Widgets.Core: freezeBorders :: Widget n -> Widget n
+ Brick.Widgets.Core: joinBorders :: Widget n -> Widget n
+ Brick.Widgets.Core: separateBorders :: Widget n -> Widget n
+ Data.IMap: Run :: !Int -> !a -> Run a
+ Data.IMap: [len] :: Run a -> !Int
+ Data.IMap: [val] :: Run a -> !a
+ Data.IMap: addToKeys :: Int -> IMap a -> IMap a
+ Data.IMap: data IMap a
+ Data.IMap: data Run a
+ Data.IMap: delete :: Int -> Run ignored -> IMap a -> IMap a
+ Data.IMap: empty :: IMap a
+ Data.IMap: fromList :: [(Int, Run a)] -> IMap a
+ Data.IMap: insert :: Int -> Run a -> IMap a -> IMap a
+ Data.IMap: instance Data.Foldable.Foldable Data.IMap.Run
+ Data.IMap: instance Data.Traversable.Traversable Data.IMap.Run
+ Data.IMap: instance GHC.Base.Applicative Data.IMap.IMap
+ Data.IMap: instance GHC.Base.Functor Data.IMap.IMap
+ Data.IMap: instance GHC.Base.Functor Data.IMap.Run
+ Data.IMap: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.IMap.IMap a)
+ Data.IMap: instance GHC.Classes.Eq a => GHC.Classes.Eq (Data.IMap.Run a)
+ Data.IMap: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.IMap.IMap a)
+ Data.IMap: instance GHC.Classes.Ord a => GHC.Classes.Ord (Data.IMap.Run a)
+ Data.IMap: instance GHC.Read.Read a => GHC.Read.Read (Data.IMap.Run a)
+ Data.IMap: instance GHC.Show.Show a => GHC.Show.Show (Data.IMap.IMap a)
+ Data.IMap: instance GHC.Show.Show a => GHC.Show.Show (Data.IMap.Run a)
+ Data.IMap: intersectionWith :: (a -> b -> c) -> IMap a -> IMap b -> IMap c
+ Data.IMap: lookup :: Int -> IMap a -> Maybe a
+ Data.IMap: mapMaybe :: (a -> Maybe b) -> IMap a -> IMap b
+ Data.IMap: null :: IMap a -> Bool
+ Data.IMap: restrict :: Int -> Run ignored -> IMap a -> IMap a
+ Data.IMap: singleton :: Int -> Run a -> IMap a
+ Data.IMap: splitLE :: Int -> IMap a -> (IMap a, IMap a)
+ Data.IMap: unsafeRuns :: IMap a -> IntMap (Run a)
+ Data.IMap: unsafeToAscList :: IMap a -> [(Int, Run a)]
+ Data.IMap: unsafeUnion :: IMap a -> IMap a -> IMap a
- Brick.Types: Result :: Image -> [CursorLocation n] -> [VisibilityRequest] -> [Extent n] -> Result n
+ Brick.Types: Result :: Image -> [CursorLocation n] -> [VisibilityRequest] -> [Extent n] -> BorderMap DynBorder -> Result n
- Brick.Types: cursorLocationL :: forall n_asXv. Lens' (CursorLocation n_asXv) Location
+ Brick.Types: cursorLocationL :: forall n_aBdm. Lens' (CursorLocation n_aBdm) Location
- Brick.Types: cursorLocationNameL :: forall n_asXv n_atrB. Lens (CursorLocation n_asXv) (CursorLocation n_atrB) (Maybe n_asXv) (Maybe n_atrB)
+ Brick.Types: cursorLocationNameL :: forall n_aBdm n_aC8N. Lens (CursorLocation n_aBdm) (CursorLocation n_aC8N) (Maybe n_aBdm) (Maybe n_aC8N)
- Brick.Types: cursorsL :: forall n_asXu. Lens' (Result n_asXu) [CursorLocation n_asXu]
+ Brick.Types: cursorsL :: forall n_aBMP. Lens' (Result n_aBMP) [CursorLocation n_aBMP]
- Brick.Types: extentsL :: forall n_asXu. Lens' (Result n_asXu) [Extent n_asXu]
+ Brick.Types: extentsL :: forall n_aBMP. Lens' (Result n_aBMP) [Extent n_aBMP]
- Brick.Types: imageL :: forall n_asXu. Lens' (Result n_asXu) Image
+ Brick.Types: imageL :: forall n_aBMP. Lens' (Result n_aBMP) Image
- Brick.Types: visibilityRequestsL :: forall n_asXu. Lens' (Result n_asXu) [VisibilityRequest]
+ Brick.Types: visibilityRequestsL :: forall n_aBMP. Lens' (Result n_aBMP) [VisibilityRequest]
- Brick.Widgets.Dialog: dialogButtonsL :: forall a_aQxG a_aQym. Lens (Dialog a_aQxG) (Dialog a_aQym) [(String, a_aQxG)] [(String, a_aQym)]
+ Brick.Widgets.Dialog: dialogButtonsL :: forall a_a14be a_a14bU. Lens (Dialog a_a14be) (Dialog a_a14bU) [(String, a_a14be)] [(String, a_a14bU)]
- Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_aQxG. Lens' (Dialog a_aQxG) (Maybe Int)
+ Brick.Widgets.Dialog: dialogSelectedIndexL :: forall a_a14be. Lens' (Dialog a_a14be) (Maybe Int)
- Brick.Widgets.Dialog: dialogTitleL :: forall a_aQxG. Lens' (Dialog a_aQxG) (Maybe String)
+ Brick.Widgets.Dialog: dialogTitleL :: forall a_a14be. Lens' (Dialog a_a14be) (Maybe String)
- Brick.Widgets.Dialog: dialogWidthL :: forall a_aQxG. Lens' (Dialog a_aQxG) Int
+ Brick.Widgets.Dialog: dialogWidthL :: forall a_a14be. Lens' (Dialog a_a14be) Int
- Brick.Widgets.Edit: editContentsL :: forall t_aMAg n_aMAh t_aMAY. Lens (Editor t_aMAg n_aMAh) (Editor t_aMAY n_aMAh) (TextZipper t_aMAg) (TextZipper t_aMAY)
+ Brick.Widgets.Edit: editContentsL :: forall t_aZHO n_aZHP t_aZIw. Lens (Editor t_aZHO n_aZHP) (Editor t_aZIw n_aZHP) (TextZipper t_aZHO) (TextZipper t_aZIw)
- Brick.Widgets.List: listElementsL :: forall n_aYMC e_aYMD e_aYXi. Lens (List n_aYMC e_aYMD) (List n_aYMC e_aYXi) (Vector e_aYMD) (Vector e_aYXi)
+ Brick.Widgets.List: listElementsL :: forall n_a18No e_a18Np e_a18XB. Lens (List n_a18No e_a18Np) (List n_a18No e_a18XB) (Vector e_a18Np) (Vector e_a18XB)
- Brick.Widgets.List: listItemHeightL :: forall n_aYMC e_aYMD. Lens' (List n_aYMC e_aYMD) Int
+ Brick.Widgets.List: listItemHeightL :: forall n_a18No e_a18Np. Lens' (List n_a18No e_a18Np) Int
- Brick.Widgets.List: listNameL :: forall n_aYMC e_aYMD n_aYXj. Lens (List n_aYMC e_aYMD) (List n_aYXj e_aYMD) n_aYMC n_aYXj
+ Brick.Widgets.List: listNameL :: forall n_a18No e_a18Np n_a18XC. Lens (List n_a18No e_a18Np) (List n_a18XC e_a18Np) n_a18No n_a18XC
- Brick.Widgets.List: listSelectedL :: forall n_aYMC e_aYMD. Lens' (List n_aYMC e_aYMD) (Maybe Int)
+ Brick.Widgets.List: listSelectedL :: forall n_a18No e_a18Np. Lens' (List n_a18No e_a18Np) (Maybe Int)

Files

CHANGELOG.md view
@@ -2,6 +2,16 @@ Brick changelog --------------- +0.36+----++New features:+ * Dynamic border support: adjacent widgets that use borders can make+   those borders seamlessly connect to each other! Thanks+   so much to Daniel Wagner for this feature! Please see+   `programs/DynamicBorderDemo.hs` for a demonstraton. Also see the+   "Joinable Borders" section of the User Guide.+ 0.35.1 ------ 
README.md view
@@ -62,7 +62,7 @@  * `matterhorn`, a client for [Mattermost](https://about.mattermost.com/): https://github.com/matterhorn-chat/matterhorn  * `viewprof`, a GHC profile viewer: https://github.com/maoe/viewprof  * `tart`, a mouse-driven ASCII art drawing program: https://github.com/jtdaugherty/tart- * `silly-joy`, an interpreter for Joy in Haskell: https://github.com/rootmos/silly-joy+ * `silly-joy`, an interpreter for Joy: https://github.com/rootmos/silly-joy  * `herms`, a command-line tool for managing kitchen recipes: https://github.com/jackkiefer/herms  * `purebred`, a mail user agent: https://github.com/purebred-mua/purebred  * `2048Haskell`, an implementation of the 2048 game: https://github.com/8Gitbrix/2048Haskell@@ -98,8 +98,6 @@  `brick` comes with a bunch of batteries included: -Low-level stuff:-  * Vertical and horizontal box layout widgets  * Basic single- and multi-line text editor widgets  * List widget@@ -113,6 +111,7 @@  * Extensible widget-building API  * User-customizable attribute themes  * Type-safe, validated input form API (see the `Brick.Forms` module)+ * Borders can be configured to automatically connect!  Brick-Users Discussion ----------------------
brick.cabal view
@@ -1,5 +1,5 @@ name:                brick-version:             0.35.1+version:             0.36 synopsis:            A declarative terminal user interface library description:   Write terminal applications painlessly with 'brick'! You write an@@ -34,7 +34,7 @@ copyright:           (c) Jonathan Daugherty 2015-2018 category:            Graphics build-type:          Simple-cabal-version:       >=1.18+cabal-version:       1.18 Homepage:            https://github.com/jtdaugherty/brick/ Bug-reports:         https://github.com/jtdaugherty/brick/issues tested-with:         GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.1@@ -62,6 +62,7 @@     Brick     Brick.AttrMap     Brick.BChan+    Brick.BorderMap     Brick.Focus     Brick.Forms     Brick.Main@@ -77,8 +78,10 @@     Brick.Widgets.Edit     Brick.Widgets.List     Brick.Widgets.ProgressBar+    Data.IMap     Data.Text.Markup   other-modules:+    Brick.Types.Common     Brick.Types.TH     Brick.Types.Internal     Brick.Widgets.Internal@@ -392,6 +395,20 @@                        text,                        microlens +executable brick-dynamic-border-demo+  if !flag(demos)+    Buildable: False+  hs-source-dirs:      programs+  ghc-options:         -threaded -Wall -fno-warn-unused-do-bind -O3+  default-extensions:  CPP+  default-language:    Haskell2010+  main-is:             DynamicBorderDemo.hs+  build-depends:       base <= 5,+                       brick,+                       vty >= 5.18.1,+                       text,+                       microlens+ executable brick-progressbar-demo   if !flag(demos)     Buildable: False@@ -405,3 +422,16 @@                        vty >= 5.18.1,                        text,                        microlens++test-suite brick-tests+  type:                exitcode-stdio-1.0+  hs-source-dirs:      tests+  ghc-options:         -Wall -O2+  if impl(ghc >= 8)+    ghc-options:       -Wno-orphans+  default-language:    Haskell2010+  main-is:             Main.hs+  build-depends:       base <=5,+                       brick,+                       containers,+                       QuickCheck
docs/guide.rst view
@@ -714,6 +714,25 @@    let w = withBorderStyle Brick.Widgets.Border.Style.ascii $              Brick.Widgets.Border.border $ str "Hello, world!" +By default, borders in adjacent widgets do not connect to each other.+This can lead to visual oddities, for example, when horizontal borders+are drawn next to vertical borders by leaving a small gap like this:++.. code:: text++    │─++You can request that adjacent borders connect to each other with+``Brick.Widgets.Core.joinBorders``. Two borders drawn with the+same attribute and border style, and both under the influence of+``joinBorders``, will produce a border like this instead:++.. code:: text++    ├─++See `Joining Borders`_ for further details.+ How Attributes Work =================== @@ -771,7 +790,7 @@  Customizations are written in an INI-style file. Here's an example: -.. code::+.. code:: ini     [default]    default.fg = blue@@ -1454,6 +1473,192 @@ the ``FormFieldState`` and ``FormField`` data types along with the implementations of the built-in form field types. +Joining Borders+===============++Brick supports a feature called "joinable borders" which means that+borders drawn in adjacent widgets can be configured to automatically+"join" with each other using the appropriate intersection characters.+This feature is helpful for creating seamless connected borders without+the need for manual calculations to determine where to draw intersection+characters.++Under normal circumstances, widgets are self-contained in that their+renderings do not interact with the appearance of adjacent widgets. This+is unfortunate for borders: one often wants to draw a T-shaped character+at the intersection of a vertical and horizontal border, for example.+To facilitate automatically adding such characters, ``brick`` offers+some border-specific capabilities for widgets to re-render themselves+as information about neighboring widgets becomes available during the+rendering process.++Border-joining works by iteratively *redrawing* the edges of widgets as+those edges come into contact with other widgets during rendering. If+the adjacent edge locations of two widgets both use joinable borders,+the Brick will re-draw one of the characters to so that it connects+seamlessly with the adjacent border.++How Joining Works+-----------------++When a widget is rendered, it can report supplementary information+about each position on its edges. Each position has four notional line+segments extending from its center, arranged like this:++.. code:: text++            top+             |+             |+    left ----+---- right+             |+             |+           bottom++These segments can independently be *drawn*, *accepting*, and+*offering*, as captured in the ``Brick.Types.BorderSegment`` type:++.. code:: haskell++    data BorderSegment = BorderSegment+        { bsAccept :: Bool+        , bsOffer :: Bool+        , bsDraw :: Bool+        }++If no information is reported for a position, it assumed that it is+not drawn, not accepting, and not offering -- and so it will never+be rewritten. This situation is the ordinary situation where an edge+location is not a border at all, or is a border that we don't want to+join to other borders.++Line segments that are *drawn* are used for deciding which part of the+``BorderStyle`` to use if this position needs to be updated. (See also+`The Active Border Style`_.) For example, suppose a position needs to+be redrawn, and already has the left and bottom segments drawn; then it+will replace the current character with the upper-right corner drawing+character ``bsCornerTR`` from its border style.++The *accepting* and *offering* properties are used to perform a small+handshake between neighboring widgets; when the handshake is successful,+one segment will transition to being drawn. For example, suppose a+horizontal and vertical border widget are drawn next to each other:++.. code:: text++            top+         (offering)                 top+             |+             |+    left     +     right    left ----+---- right+             |           (offering)     (offering)+             |+           bottom                  bottom+         (offering)++These borders are accepting in all directions, drawn in the directions+signified by visible lines, and offering in the directions written.+Since the horizontal border on the right is offering towards the+vertical border, and the vertical border is accepting from the direction+towards the horizontal border, the right segment of the vertical+border will transition to being drawn. This will trigger an update of+the ``Image`` associated with the left widget, overwriting whatever+character is there currently with a ``bsIntersectL`` character instead.+The state of the segments afterwards will be the same, but the fact that+there is one more segment drawn will be recorded:++.. code:: text++            top+         (offering)                 top+             |+             |+    left     +---- right    left ----+---- right+             |           (offering)     (offering)+             |+           bottom                  bottom+         (offering)++It is important that this be recorded: we may later place this combined+widget to the right of another horizontal border, in which case we+would want to transition again from a ``bsIntersectL`` character to a+``bsIntersectFull`` character that represents all four segments being+drawn.++Because this involves an interaction between multiple widgets, we+may find that the two widgets involved were rendered under different+rendering contexts. To avoid mixing and matching border styles and+drawing attributes, each location records not just the state of its+four segments but also the border style and attribute that were active+at the time the border was drawn. This information is stored in+``Brick.Types.DynBorder``.++.. code:: haskell++    data DynBorder = DynBorder+        { dbStyle :: BorderStyle+        , dbAttr :: Attr+        , dbSegments :: Edges BorderSegment+        }++The ``Brick.Types.Edges`` type has one field for each direction:++.. code:: haskell++    data Edges a = Edges { eTop, eBottom, eLeft, eRight :: a }++In addition to the offer/accept handshake described above, segments also+check that their neighbor's ``BorderStyle`` and ``Attr`` match their own+before transitioning from undrawn to drawn to avoid visual glitches from+trying to connect e.g. ``unicode`` borders to ``ascii`` ones or green+borders to red ones.++The above description applies to a single location; any given widget's+result may report information about any location on its border using the+``Brick.BorderMap.BorderMap`` type. A ``BorderMap a`` is close kin to a+``Data.Map.Map Location a`` except that each ``BorderMap`` has a fixed+rectangle on which keys are retained. Values inserted at other keys are+silently discarded.++For backwards compatibility, all the widgets that ship with ``brick``+avoid reporting any border information by default, but ``brick`` offers+three ways of modifying the border-joining behavior of a widget.++* ``Brick.Widgets.Core.joinBorders`` instructs any borders drawn in its+  child widget to report their edge information. It does this+  by setting a flag in the rendering context that tells the+  ``Brick.Widgets.Border`` widgets to report the information described+  above. Consequently, widgets drawn in this context will join their+  borders with neighbors.+* ``Brick.Widgets.Core.separateBorders`` does the opposite of+  ``joinBorders`` by unsetting the same context flag, preventing border+  widgets from attempting to connect.+* ``Brick.Widgets.Core.freezeBorders`` lets its child widget connect its+  borders internally but prevents it from connecting with anything+  outside the ``freezeBorders`` call. It does this by deleting the edge+  metadata about its child widget. This means that any connections+  already made within the child widget will stay as they are but no new+  connections will be made to adjacent widgets. For example, one might+  use this to create a box with internal but no external connections:++  .. code:: haskell++      joinBorders . freezeBorders . border . hBox $+          [str "left", vBorder, str "right"]++  Or to create a box that allows external connections but not internal+  ones:++  .. code:: haskell++      joinBorders . border . freezeBorders . hBox $+          [str "left", vBorder, str "right"]++When creating new widgets, if you would like ``joinBorders`` and+``separateBorders`` to affect the behavior of your widget, you may do+so by consulting the ``ctxDynBorders`` field of the rendering context+before writing to your ``Result``'s ``borders`` field.+ The Rendering Cache =================== @@ -1539,6 +1744,7 @@                , cursors            :: [Brick.Types.CursorLocation n]                , visibilityRequests :: [Brick.Types.VisibilityRequest]                , extents            :: [Extent n]+               , borders            :: BorderMap DynBorder                }  The rendering function runs in the ``RenderM`` monad, which gives us@@ -1555,6 +1761,7 @@                 , availHeight    :: Int                 , ctxBorderStyle :: BorderStyle                 , ctxAttrMap     :: AttrMap+                , ctxDynBorders  :: Bool                 }  and has lens fields exported as described in `Conventions`_.
+ programs/DynamicBorderDemo.hs view
@@ -0,0 +1,71 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Brick.Main as M+import Brick.Types+  ( Widget+  )+import qualified Brick.Widgets.Center as C+import qualified Brick.Widgets.Core as C+import qualified Brick.Widgets.Border as B+import qualified Brick.Widgets.Border.Style as BS++doubleHorizontal :: BS.BorderStyle+doubleHorizontal = BS.BorderStyle+    { BS.bsCornerTL = '╒'+    , BS.bsCornerTR = '╕'+    , BS.bsCornerBR = '╛'+    , BS.bsCornerBL = '╘'+    , BS.bsIntersectL = '╞'+    , BS.bsIntersectR = '╡'+    , BS.bsIntersectT = '╤'+    , BS.bsIntersectB = '╧'+    , BS.bsIntersectFull = '╪'+    , BS.bsHorizontal = '═'+    , BS.bsVertical = '│'+    }++box1 :: Widget ()+box1+    = C.withBorderStyle doubleHorizontal . B.border+    . C.withBorderStyle BS.unicodeRounded . B.border+    $ C.str "25 kg"++weights :: Widget ()+weights = C.withBorderStyle doubleHorizontal $ C.hBox+    [ box1+    , C.str "\n\n" C.<=> B.hBorder+    , box1+    ]++box2 :: Widget ()+box2 = C.freezeBorders $ C.vBox+    [ C.hBox+        [ C.vLimit 3 B.vBorder+        , C.str "Resize horizontally to\nmove across the label\nbelow"+        , C.vLimit 3 B.vBorder+        ]+    , B.borderWithLabel (B.vBorder C.<+> C.str " Label " C.<+> B.vBorder) $ C.hBox+        [ C.str "               "+        , C.vBox [B.vBorder, C.str "L\na\nb\ne\nl", C.vLimit 3 B.vBorder]+        , C.str "\n\n\n Resize vertically to\n move across the label\n to the left\n\n\n\n\n" C.<=> B.hBorder+        ]+    ]++-- BYOB: build your own border+byob :: Widget ()+byob = C.vBox+    [              C.hBox [ corner   , top      , corner   ]+    , C.vLimit 6 $ C.hBox [ B.vBorder, mid      , B.vBorder]+    ,              C.hBox [ corner   , B.hBorder, corner   ]+    ]+    where+    top = B.hBorderWithLabel (C.str "BYOB")+    mid = C.center (C.str "If `border` is too easy,\nyou can build it yourself")+    corner = B.joinableBorder (pure False)++ui :: Widget ()+ui = C.vBox [weights, box2, byob]++main :: IO ()+main = M.simpleMain (C.joinBorders ui)
+ src/Brick/BorderMap.hs view
@@ -0,0 +1,211 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+module Brick.BorderMap+    ( BorderMap+    , Edges(..)+    , eTopL, eBottomL, eRightL, eLeftL+    , empty, emptyCoordinates, singleton+    , insertH, insertV, insert+    , unsafeUnion+    , coordinates, bounds+    , values+    , lookupRow, lookupCol, lookupH, lookupV, lookup+    , setCoordinates, crop, expand+    , translate+    ) where++import Brick.Types.Common (Edges(..), Location(..), eTopL, eBottomL, eRightL, eLeftL, origin)+import Control.Applicative (liftA2)+import Data.IMap (IMap, Run(Run))+import Prelude hiding (lookup)+import qualified Data.IMap as IM++-- | Internal use only.+neighbors :: Edges a -> Edges (a, a)+neighbors (Edges vt vb vl vr) = Edges horiz horiz vert vert where+    horiz = (vl, vr)+    vert  = (vt, vb)++-- Invariant: corner values are present on all the edges incident on that+-- corner. Widthless or heightless rectangles replicate the IMaps exactly on+-- the two coincident edges.+--+-- Practically speaking, this means for lookup you can look on any edge that+-- could contain the key you care about, while for insertion you must insert on+-- every edge that could contain the keys being inserted.++-- | A @BorderMap a@ is like a @Map Location a@, except that there is a+-- rectangle, and only 'Location's on the border of this rectangle are+-- retained. The 'BorderMap' can be queried for the position and size of the+-- rectangle. There are also efficient bulk query and bulk update operations+-- for adjacent positions on the border.+data BorderMap a = BorderMap+    { _coordinates :: Edges Int+    , _values :: Edges (IMap a)+    } deriving (Eq, Ord, Show, Functor)++-- | Given a rectangle (specified as the coordinates of the top, left, bottom,+-- and right sides), initialize an empty 'BorderMap'.+emptyCoordinates :: Edges Int -> BorderMap a+emptyCoordinates cs = BorderMap { _coordinates = cs, _values = pure IM.empty }++-- | An empty 'BorderMap' that only tracks the point (0,0).+empty :: BorderMap a+empty = emptyCoordinates (pure 0)++-- | A 'BorderMap' that tracks only the given the point (and initially maps it+-- to the given value).+singleton :: Location -> a -> BorderMap a+singleton l v = translate l . insert origin v $ empty++{-# INLINE coordinates #-}+-- | The positions of the edges of the rectangle whose border is retained in a+-- 'BorderMap'. For example, if @coordinates m = e@, then the top border+-- contains the 'Location's on row @eTop e@ and between columns @eLeft e@ to+-- @eRight e@ inclusive.+coordinates :: BorderMap a -> Edges Int+coordinates = _coordinates++-- | A complementary way to query the edges of the rectangle whose border is+-- retained in a 'BorderMap'. For example, if @bounds m = b@, then a+-- 'Location'\'s column must be between @fst (eTop b)@ and @snd (eTop b)@ to be+-- retained. See also 'coordinates', which is in most cases a more natural+-- border query.+bounds :: BorderMap a -> Edges (Int, Int)+bounds = neighbors . coordinates++{-# INLINE values #-}+-- | Maps giving the values along each edge. Corner values are replicated in+-- all relevant edges.+values :: BorderMap a -> Edges (IMap a)+values = _values++-- | Bulk insertion of horizontally-adjacent values. The 'Location' gives the+-- start point, and the 'Run' extends in the "larger columns" direction.+insertH :: Location -> Run a -> BorderMap a -> BorderMap a+insertH = insertDirAgnostic (Edges insertPar insertPar insertPerp insertPerp) . swapLoc+    where+    swapLoc (Location (col, row)) = Location (row, col)++-- | Bulk insertion of vertically-adjacent values. The 'Location' gives the+-- start point, and the 'Run' extends in the "larger rows" direction.+insertV :: Location -> Run a -> BorderMap a -> BorderMap a+insertV = insertDirAgnostic (Edges insertPerp insertPerp insertPar insertPar)++insertDirAgnostic+    :: Edges (Location -> Run a -> Int -> (Int, Int) -> IMap a -> IMap a)+    -> Location -> Run a -> BorderMap a -> BorderMap a+insertDirAgnostic insertions l r m =+    m { _values = insertions <*> pure l <*> pure r <*> coordinates m <*> bounds m <*> _values m }++insertPar, insertPerp :: Location -> Run a -> Int -> (Int, Int) -> IMap a -> IMap a+insertPar (Location (kPar, kPerp)) r herePar (loPerp, hiPerp)+    | kPar == herePar && loPerp <= kPerp + IM.len r - 1 && kPerp <= hiPerp+        = IM.insert beg r { IM.len = end - beg + 1 }+    | otherwise = id+    where+    beg = max kPerp loPerp+    end = min (kPerp + IM.len r - 1) hiPerp+insertPerp (Location (kPar, kPerp)) r herePerp (loPar, hiPar)+    | loPar <= kPar && kPar <= hiPar && kPerp <= herePerp && herePerp <= kPerp + IM.len r - 1+        = IM.insert kPar r { IM.len = 1 }+    | otherwise = id++-- | Insert a single value at the given location.+insert :: Location -> a -> BorderMap a -> BorderMap a+insert l = insertV l . Run 1++-- | Look up all values on a given row. The 'IMap' returned maps columns to+-- values.+lookupRow :: Int -> BorderMap a -> IMap a+lookupRow row m+    | row == eTop    (coordinates m) = eTop    (_values m)+    | row == eBottom (coordinates m) = eBottom (_values m)+    | otherwise = IM.fromList+        $  [(eLeft   (coordinates m), Run 1 a) | Just a <- [IM.lookup row (eLeft   (_values m))]]+        ++ [(eRight  (coordinates m), Run 1 a) | Just a <- [IM.lookup row (eRight  (_values m))]]++-- | Look up all values on a given column. The 'IMap' returned maps rows to+-- values.+lookupCol :: Int -> BorderMap a -> IMap a+lookupCol col m+    | col == eLeft   (coordinates m) = eLeft   (_values m)+    | col == eRight  (coordinates m) = eRight  (_values m)+    | otherwise = IM.fromList+        $  [(eTop    (coordinates m), Run 1 a) | Just a <- [IM.lookup col (eTop    (_values m))]]+        ++ [(eBottom (coordinates m), Run 1 a) | Just a <- [IM.lookup col (eBottom (_values m))]]++-- | Bulk lookup of horizontally-adjacent values. The 'Location' gives the+-- starting point, and the 'Run' extends in the "larger columns" direction. The+-- 'IMap' returned maps columns to values.+lookupH :: Location -> Run ignored -> BorderMap a -> IMap a+lookupH (Location (col, row)) r = IM.restrict col r . lookupRow row++-- | Bulk lookup of vertically-adjacent values. The 'Location' gives the+-- starting point, and the 'Run' extends in the "larger rows" direction. The+-- 'IMap' returned maps rows to values.+lookupV :: Location -> Run ignored -> BorderMap a -> IMap a+lookupV (Location (col, row)) r = IM.restrict row r . lookupCol col++-- | Look up a single position.+lookup :: Location -> BorderMap a -> Maybe a+lookup (Location (col, row)) = IM.lookup row . lookupCol col++-- | Set the rectangle being tracked by this 'BorderMap', throwing away any+-- values that do not lie on this new rectangle.+setCoordinates :: Edges Int -> BorderMap a -> BorderMap a+setCoordinates coordinates' m = BorderMap+    { _values = values'+    , _coordinates = coordinates'+    }+    where+    bounds' = neighbors coordinates'+    values' = pure gc+        <*> _coordinates m+        <*> coordinates'+        <*> bounds'+        <*> _values m+        <*> Edges { eTop = lookupRow, eBottom = lookupRow, eLeft = lookupCol, eRight = lookupCol }+    gc oldPar newPar (loPerp, hiPerp) imPar lookupPerp+        | oldPar == newPar = IM.restrict loPerp (Run (hiPerp-loPerp+1) ()) imPar+        | otherwise = lookupPerp newPar m++-- | Ensure that the rectangle being tracked by this 'BorderMap' extends no+-- farther than the given one.+crop :: Edges Int -> BorderMap a -> BorderMap a+crop cs m = setCoordinates (shrink <*> cs <*> coordinates m) m where+    shrink = Edges+        { eTop    = max+        , eBottom = min+        , eLeft   = max+        , eRight  = min+        }++-- | Ensure that the rectangle being tracked by this 'BorderMap' extends at+-- least as far as the given one.+expand :: Edges Int -> BorderMap a -> BorderMap a+expand cs m = setCoordinates (grow <*> cs <*> coordinates m) m where+    grow = Edges+        { eTop    = min+        , eBottom = max+        , eLeft   = min+        , eRight  = max+        }++-- | Move a 'BorderMap' by adding the given 'Location' to all keys in the map.+translate :: Location -> BorderMap a -> BorderMap a+-- fast path: do nothing for (0,0)+translate (Location (0, 0)) m = m+translate (Location (c, r)) m = BorderMap+    { _coordinates = liftA2 (+)          cOffsets (_coordinates m)+    , _values      = liftA2 IM.addToKeys vOffsets (_values      m)+    }+    where+    cOffsets = Edges { eTop = r, eBottom = r, eLeft = c, eRight = c }+    vOffsets = Edges { eTop = c, eBottom = c, eLeft = r, eRight = r }++-- | Assumes the two 'BorderMap's are tracking the same rectangles, but have+-- disjoint keys. This property is not checked.+unsafeUnion :: BorderMap a -> BorderMap a -> BorderMap a+unsafeUnion m m' = m { _values = liftA2 IM.unsafeUnion (_values m) (_values m') }
src/Brick/Types.hs view
@@ -33,13 +33,14 @@   , getContext    -- ** The rendering context-  , Context(ctxAttrName, availWidth, availHeight, ctxBorderStyle, ctxAttrMap)+  , Context(ctxAttrName, availWidth, availHeight, ctxBorderStyle, ctxAttrMap, ctxDynBorders)   , attrL   , availWidthL   , availHeightL   , ctxAttrMapL   , ctxAttrNameL   , ctxBorderStyleL+  , ctxDynBordersL    -- ** Rendering results   , Result(..)@@ -60,6 +61,15 @@    -- * Making lenses   , suffixLenses++  -- * Dynamic borders+  , bordersL+  , DynBorder(..)+  , dbStyleL, dbAttrL, dbSegmentsL+  , BorderSegment(..)+  , bsAcceptL, bsOfferL, bsDrawL+  , Edges(..)+  , eTopL, eBottomL, eRightL, eLeftL    -- * Miscellaneous   , Size(..)
+ src/Brick/Types/Common.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}++module Brick.Types.Common+  ( Location(..)+  , locL+  , origin+  , Edges(..)+  , eTopL, eBottomL, eRightL, eLeftL+  ) where++import Brick.Types.TH (suffixLenses)+import qualified Data.Semigroup as Sem+import Lens.Micro (_1, _2)+import Lens.Micro.Internal (Field1, Field2)++-- | A terminal screen location.+data Location = Location { loc :: (Int, Int)+                         -- ^ (Column, Row)+                         }+                deriving (Show, Eq, Ord)++suffixLenses ''Location++instance Field1 Location Location Int Int where+    _1 = locL._1++instance Field2 Location Location Int Int where+    _2 = locL._2++-- | The origin (upper-left corner).+origin :: Location+origin = Location (0, 0)++instance Sem.Semigroup Location where+    (Location (w1, h1)) <> (Location (w2, h2)) = Location (w1+w2, h1+h2)++instance Monoid Location where+    mempty = origin+    mappend = (Sem.<>)++data Edges a = Edges { eTop, eBottom, eLeft, eRight :: a }+    deriving (Eq, Ord, Read, Show, Functor)++suffixLenses ''Edges++instance Applicative Edges where+    pure a = Edges a a a a+    Edges ft fb fl fr <*> Edges vt vb vl vr =+        Edges (ft vt) (fb vb) (fl vl) (fr vr)++instance Monad Edges where+    Edges vt vb vl vr >>= f = Edges+        (eTop    (f vt))+        (eBottom (f vb))+        (eLeft   (f vl))+        (eRight  (f vr))
src/Brick/Types/Internal.hs view
@@ -23,6 +23,12 @@   , Next(..)   , Result(..)   , Extent(..)+  , Edges(..)+  , eTopL, eBottomL, eRightL, eLeftL+  , BorderSegment(..)+  , bsAcceptL, bsOfferL, bsDrawL+  , DynBorder(..)+  , dbStyleL, dbAttrL, dbSegmentsL   , CacheInvalidateRequest(..)   , BrickEvent(..) @@ -37,6 +43,7 @@   , imageL   , cursorsL   , extentsL+  , bordersL   , visibilityRequestsL   , emptyResult   )@@ -48,12 +55,13 @@  import Lens.Micro (_1, _2, Lens') import Lens.Micro.TH (makeLenses)-import Lens.Micro.Internal (Field1, Field2) import qualified Data.Set as S import qualified Data.Map as M-import qualified Data.Semigroup as Sem-import Graphics.Vty (Vty, Event, Button, Modifier, DisplayRegion, Image, emptyImage)+import Graphics.Vty (Vty, Event, Button, Modifier, DisplayRegion, Image, Attr, emptyImage) +import Brick.BorderMap (BorderMap)+import qualified Brick.BorderMap as BM+import Brick.Types.Common import Brick.Types.TH import Brick.AttrMap (AttrName, AttrMap) import Brick.Widgets.Border.Style (BorderStyle)@@ -130,20 +138,6 @@                -- ^ Down/right                deriving (Show, Eq) --- | A terminal screen location.-data Location = Location { loc :: (Int, Int)-                         -- ^ (Column, Row)-                         }-                deriving (Show, Eq, Ord)--suffixLenses ''Location--instance Field1 Location Location Int Int where-    _1 = locL._1--instance Field2 Location Location Int Int where-    _2 = locL._2- -- | The class of types that behave like terminal locations. class TerminalLocation a where     -- | Get the column out of the value@@ -160,17 +154,6 @@     locationRowL = _2     locationRow (Location t) = snd t --- | The origin (upper-left corner).-origin :: Location-origin = Location (0, 0)--instance Sem.Semigroup Location where-    (Location (w1, h1)) <> (Location (w2, h2)) = Location (w1+w2, h1+h2)--instance Monoid Location where-    mempty = origin-    mappend = (Sem.<>)- -- | A cursor location.  These are returned by the rendering process. data CursorLocation n =     CursorLocation { cursorLocation :: !Location@@ -180,6 +163,36 @@                    }                    deriving Show +-- | A border character has four segments, one extending in each direction+-- (horizontally and vertically) from the center of the character.+data BorderSegment = BorderSegment+    { bsAccept :: Bool+    -- ^ Would this segment be willing to be drawn if a neighbor wanted to+    -- connect to it?+    , bsOffer :: Bool+    -- ^ Does this segment want to connect to its neighbor?+    , bsDraw :: Bool+    -- ^ Should this segment be represented visually?+    } deriving (Eq, Ord, Read, Show)++suffixLenses ''BorderSegment++-- | Information about how to redraw a dynamic border character when it abuts+-- another dynamic border character.+data DynBorder = DynBorder+    { dbStyle :: BorderStyle+    -- ^ The 'Char's to use when redrawing the border. Also used to filter+    -- connections: only dynamic borders with equal 'BorderStyle's will connect+    -- to each other.+    , dbAttr :: Attr+    -- ^ What 'Attr' to use to redraw the border character. Also used to filter+    -- connections: only dynamic borders with equal 'Attr's will connect to+    -- each other.+    , dbSegments :: Edges BorderSegment+    } deriving (Eq, Read, Show)++suffixLenses ''DynBorder+ -- | The type of result returned by a widget's rendering function. The -- result provides the image, cursor positions, and visibility requests -- that resulted from the rendering process.@@ -193,13 +206,28 @@            -- ^ The list of visibility requests made by widgets rendered            -- while rendering this one (used by viewports)            , extents :: [Extent n]+           -- Programmer's note: we don't try to maintain the invariant that+           -- the size of the borders closely matches the size of the 'image'+           -- field. Most widgets don't need to care about borders, and so they+           -- use the empty 'BorderMap' that has a degenerate rectangle. Only+           -- border-drawing widgets and the hbox/vbox stuff try to set this+           -- carefully. Even then, in the boxes, we only make sure that the+           -- 'BorderMap' is no larger than the entire concatenation of boxes,+           -- and it's certainly possible for it to be smaller. (Resizing+           -- 'BorderMap's is lossy, so we try to do it as little as possible.)+           -- If you're writing a widget, this should make it easier for you to+           -- do so; but beware this lack of invariant if you are consuming+           -- widgets.+           , borders :: BorderMap DynBorder+           -- ^ Places where we may rewrite the edge of the image when+           -- placing this widget next to another one.            }            deriving Show  suffixLenses ''Result  emptyResult :: Result n-emptyResult = Result emptyImage [] [] []+emptyResult = Result emptyImage [] [] [] BM.empty  -- | The type of events. data BrickEvent n e = VtyEvent Event@@ -234,6 +262,7 @@             , availHeight :: Int             , ctxBorderStyle :: BorderStyle             , ctxAttrMap :: AttrMap+            , ctxDynBorders :: Bool             }             deriving Show 
src/Brick/Widgets/Border.hs view
@@ -20,6 +20,9 @@    -- * Attribute names   , borderAttr++  -- * Utility+  , joinableBorder   ) where @@ -27,14 +30,16 @@ import Control.Applicative ((<$>)) #endif -import Lens.Micro ((^.), to)+import Lens.Micro ((^.), (&), (.~), to) import Graphics.Vty (imageHeight, imageWidth)  import Brick.AttrMap import Brick.Types import Brick.Widgets.Core-import Brick.Widgets.Center (hCenterWith) import Brick.Widgets.Border.Style (BorderStyle(..))+import Brick.Widgets.Internal (renderDynBorder)+import Data.IMap (Run(..))+import qualified Brick.BorderMap as BM  -- | The top-level border attribute name. borderAttr :: AttrName@@ -42,6 +47,10 @@  -- | Draw the specified border element using the active border style -- using 'borderAttr'.+--+-- Does not participate in dynamic borders (due to the difficulty of+-- introspecting on the first argument); consider using 'joinableBorder'+-- instead. borderElem :: (BorderStyle -> Char) -> Widget n borderElem f =     Widget Fixed Fixed $ do@@ -77,8 +86,12 @@                              $ vLimit (c^.availHeightL - 2)                              $ wrapped -      let top = (borderElem bsCornerTL) <+> hBorder_ label <+> (borderElem bsCornerTR)-          bottom = (borderElem bsCornerBL) <+> hBorder <+> (borderElem bsCornerBR)+      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)+          top = tl <+> maybe hBorder hBorderWithLabel label <+> tr+          bottom = bl <+> hBorder <+> br           middle = vBorder <+> (Widget Fixed Fixed $ return middleResult) <+> vBorder           total = top <=> middle <=> bottom @@ -88,25 +101,70 @@  -- | A horizontal border.  Fills all horizontal space. hBorder :: Widget n-hBorder = hBorder_ Nothing+hBorder =+    withAttr borderAttr $ Widget Greedy Fixed $ do+      ctx <- getContext+      let bs = ctxBorderStyle ctx+          w = availWidth ctx+      db <- dynBorderFromDirections (Edges False False True True)+      let dynBorders = BM.insertH mempty (Run w db)+                     $ BM.emptyCoordinates (Edges 0 0 0 (w-1))+      setDynBorders dynBorders $ render $ vLimit 1 $ fill (bsHorizontal bs)  -- | A horizontal border with a label placed in the center of the -- border. Fills all horizontal space. hBorderWithLabel :: Widget n                  -- ^ The label widget                  -> Widget n-hBorderWithLabel label = hBorder_ (Just label)--hBorder_ :: Maybe (Widget n) -> Widget n-hBorder_ label =+hBorderWithLabel label =     Widget Greedy Fixed $ do-      bs <- ctxBorderStyle <$> getContext-      let msg = maybe (str [bsHorizontal bs]) id label-      render $ vLimit 1 $ withAttr borderAttr $ hCenterWith (Just $ bsHorizontal bs) msg+      res <- render $ vLimit 1 label+      render $ hBox [hBorder, Widget Fixed Fixed (return res), hBorder]  -- | A vertical border.  Fills all vertical space. vBorder :: Widget n vBorder =-    Widget Fixed Greedy $ do-      bs <- ctxBorderStyle <$> getContext-      render $ hLimit 1 $ withAttr borderAttr $ fill (bsVertical bs)+    withAttr borderAttr $ Widget Fixed Greedy $ do+      ctx <- getContext+      let bs = ctxBorderStyle ctx+          h = availHeight ctx+      db <- dynBorderFromDirections (Edges True True False False)+      let dynBorders = BM.insertV mempty (Run h db)+                     $ BM.emptyCoordinates (Edges 0 (h-1) 0 0)+      setDynBorders dynBorders $ render $ hLimit 1 $ fill (bsVertical bs)++-- | Initialize a 'DynBorder'. It will be 'bsDraw'n and 'bsOffer'ing+-- in the given directions to begin with, and accept join offers from+-- all directions. We consult the context to choose the 'dbStyle' and+-- 'dbAttr'.+--+-- This is likely to be useful only for custom widgets that need more+-- complicated dynamic border behavior than 'border', 'vBorder', or+-- 'hBorder' offer.+dynBorderFromDirections :: Edges Bool -> RenderM n DynBorder+dynBorderFromDirections dirs = do+    ctx <- getContext+    return DynBorder+        { dbStyle = ctxBorderStyle ctx+        , dbAttr = attrMapLookup (ctxAttrName ctx) (ctxAttrMap ctx)+        , dbSegments = (\draw -> BorderSegment True draw draw) <$> dirs+        }++-- | Replace the 'Result'\'s dynamic borders with the given one,+-- provided the context says to use dynamic borders at all.+setDynBorders :: BM.BorderMap DynBorder -> RenderM n (Result n) -> RenderM n (Result n)+setDynBorders newBorders act = do+    dyn <- ctxDynBorders <$> getContext+    res <- act+    return $ if dyn+        then res & bordersL .~ newBorders+        else res++-- | A single-character dynamic border that will react to neighboring+-- borders, initially connecting in the given directions.+joinableBorder :: Edges Bool -> Widget n+joinableBorder dirs = withAttr borderAttr . Widget Fixed Fixed $ do+    db <- dynBorderFromDirections dirs+    setDynBorders+        (BM.singleton mempty db)+        (render (raw (renderDynBorder db)))
src/Brick/Widgets/Core.hs view
@@ -3,6 +3,7 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies #-} -- | This module provides the core widget combinators and rendering -- routines. Everything this library does is in terms of these basic -- primitives.@@ -50,6 +51,9 @@    -- * Border style management   , withBorderStyle+  , joinBorders+  , separateBorders+  , freezeBorders    -- * Cursor placement   , showCursor@@ -103,6 +107,7 @@ import qualified Data.DList as DL import qualified Data.Map as M import qualified Data.Set as S+import qualified Data.IMap as I import qualified Data.Function as DF import Data.List (sortBy, partition) import qualified Graphics.Vty as V@@ -116,6 +121,7 @@ import Brick.Util (clOffset, clamp) import Brick.AttrMap import Brick.Widgets.Internal+import qualified Brick.BorderMap as BM  -- | The class of text types that have widths measured in terminal -- columns. NEVER use 'length' etc. to measure the length of a string if@@ -140,6 +146,29 @@ withBorderStyle :: BorderStyle -> Widget n -> Widget n withBorderStyle bs p = Widget (hSize p) (vSize p) $ withReaderT (& ctxBorderStyleL .~ bs) (render p) +-- | When rendering the specified widget, create borders that respond+-- dynamically to their neighbors to form seamless connections.+joinBorders :: Widget n -> Widget n+joinBorders p = Widget (hSize p) (vSize p) $ withReaderT (& ctxDynBordersL .~ True) (render p)++-- | When rendering the specified widget, use static borders. This+-- may be marginally faster, but will introduce a small gap between+-- neighboring orthogonal borders.+--+-- This is the default for backwards compatibility.+separateBorders :: Widget n -> Widget n+separateBorders p = Widget (hSize p) (vSize p) $ withReaderT (&ctxDynBordersL .~ False) (render p)++-- | After the specified widget has been rendered, freeze its borders. A+-- frozen border will not be affected by neighbors, nor will it affect+-- neighbors. Compared to 'separateBorders', 'freezeBorders' will not+-- affect whether borders connect internally to a widget (whereas+-- 'separateBorders' prevents them from connecting).+--+-- Frozen borders cannot be thawed.+freezeBorders :: Widget n -> Widget n+freezeBorders p = Widget (hSize p) (vSize p) $ (bordersL .~ BM.empty) <$> render p+ -- | The empty widget. emptyWidget :: Widget n emptyWidget = raw V.emptyImage@@ -156,7 +185,8 @@ addResultOffset :: Location -> Result n -> Result n addResultOffset off = addCursorOffset off .                       addVisibilityOffset off .-                      addExtentOffset off+                      addExtentOffset off .+                      addDynBorderOffset off  addVisibilityOffset :: Location -> Result n -> Result n addVisibilityOffset off r = r & visibilityRequestsL.each.vrPositionL %~ (off <>)@@ -164,6 +194,9 @@ addExtentOffset :: Location -> Result n -> Result n addExtentOffset off r = r & extentsL.each %~ (\(Extent n l sz o) -> Extent n (off <> l) sz o) +addDynBorderOffset :: Location -> Result n -> Result n+addDynBorderOffset off r = r & bordersL %~ BM.translate off+ -- | Render the specified widget and record its rendering extent using -- the specified name (see also 'lookupExtent'). reportExtent :: n -> Widget n -> Widget n@@ -412,8 +445,20 @@                 , limitSecondary :: Int -> Widget n -> Widget n                 , primaryWidgetSize :: Widget n -> Size                 , concatenatePrimary :: [V.Image] -> V.Image+                , concatenateSecondary :: [V.Image] -> V.Image                 , locationFromOffset :: Int -> Location                 , padImageSecondary :: Int -> V.Image -> V.Attr -> V.Image+                , loPrimary :: forall a. Lens' (Edges a) a -- lo: towards smaller coordinates in that dimension+                , hiPrimary :: forall a. Lens' (Edges a) a -- hi: towards larger  coordinates in that dimension+                , loSecondary :: forall a. Lens' (Edges a) a+                , hiSecondary :: forall a. Lens' (Edges a) a+                , locationFromPrimarySecondary :: Int -> Int -> Location+                , splitLoPrimary :: Int -> V.Image -> V.Image+                , splitHiPrimary :: Int -> V.Image -> V.Image+                , splitLoSecondary :: Int -> V.Image -> V.Image+                , splitHiSecondary :: Int -> V.Image -> V.Image+                , lookupPrimary :: Int -> BM.BorderMap DynBorder -> I.IMap DynBorder+                , insertSecondary :: Location -> I.Run DynBorder -> BM.BorderMap DynBorder -> BM.BorderMap DynBorder                 }  vBoxRenderer :: BoxRenderer n@@ -426,10 +471,22 @@                 , limitSecondary = hLimit                 , primaryWidgetSize = vSize                 , concatenatePrimary = V.vertCat+                , concatenateSecondary = V.horizCat                 , locationFromOffset = Location . (0 ,)                 , padImageSecondary = \amt img a ->                     let p = V.charFill a ' ' amt (V.imageHeight img)                     in V.horizCat [img, p]+                , loPrimary = eTopL+                , hiPrimary = eBottomL+                , loSecondary = eLeftL+                , hiSecondary = eRightL+                , locationFromPrimarySecondary = \r c -> Location (c, r)+                , splitLoPrimary = V.cropBottom+                , splitHiPrimary = \n img -> V.cropTop (V.imageHeight img-n) img+                , splitLoSecondary = V.cropRight+                , splitHiSecondary = \n img -> V.cropLeft (V.imageWidth img-n) img+                , lookupPrimary = BM.lookupRow+                , insertSecondary = BM.insertH                 }  hBoxRenderer :: BoxRenderer n@@ -442,10 +499,22 @@                 , limitSecondary = vLimit                 , primaryWidgetSize = hSize                 , concatenatePrimary = V.horizCat+                , concatenateSecondary = V.vertCat                 , locationFromOffset = Location . (, 0)                 , padImageSecondary = \amt img a ->                     let p = V.charFill a ' ' (V.imageWidth img) amt                     in V.vertCat [img, p]+                , loPrimary = eLeftL+                , hiPrimary = eRightL+                , loSecondary = eTopL+                , hiSecondary = eBottomL+                , locationFromPrimarySecondary = \c r -> Location (c, r)+                , splitLoPrimary = V.cropRight+                , splitHiPrimary = \n img -> V.cropLeft (V.imageWidth img-n) img+                , splitLoSecondary = V.cropBottom+                , splitHiSecondary = \n img -> V.cropTop (V.imageHeight img-n) img+                , lookupPrimary = BM.lookupCol+                , insertSecondary = BM.insertV                 }  -- | Render a series of widgets in a box layout in the order given.@@ -551,12 +620,129 @@           maxSecondary = maximum $ imageSecondary br <$> allImages           padImage img = padImageSecondary br (maxSecondary - imageSecondary br img)                          img (c^.attrL)-          paddedImages = padImage <$> allImages+          (imageRewrites, newBorders) = catAllBorders br (borders <$> allTranslatedResults)+          rewrittenImages = zipWith (rewriteImage br) imageRewrites allImages+          paddedImages = padImage <$> rewrittenImages        cropResultToContext $ Result (concatenatePrimary br paddedImages)                             (concat $ cursors <$> allTranslatedResults)                             (concat $ visibilityRequests <$> allTranslatedResults)                             (concat $ extents <$> allTranslatedResults)+                            newBorders++catDynBorder+    :: Lens' (Edges BorderSegment) BorderSegment+    -> Lens' (Edges BorderSegment) BorderSegment+    -> DynBorder+    -> DynBorder+    -> Maybe DynBorder+catDynBorder towardsA towardsB a b+    -- Currently, we check if the 'BorderStyle's are exactly the same. In the+    -- future, it might be nice to relax this restriction. For example, if a+    -- horizontal border is being rewritten to accomodate a neighboring+    -- vertical border, all we care about is that the two 'bsVertical's line up+    -- sanely. After all, if the horizontal border's 'bsVertical' is the same+    -- as the vertical one's, and the horizontal border's 'BorderStyle' is+    -- self-consistent, then it will look "right" to rewrite according to the+    -- horizontal border's 'BorderStyle'.+    |  dbStyle a == dbStyle b+    && dbAttr  a == dbAttr  b+    && a ^. dbSegmentsL.towardsB.bsAcceptL+    && b ^. dbSegmentsL.towardsA.bsOfferL+    && not (a ^. dbSegmentsL.towardsB.bsDrawL) -- don't bother doing an update if we don't need to+    = Just (a & dbSegmentsL.towardsB.bsDrawL .~ True)+    | otherwise = Nothing++catDynBorders+    :: Lens' (Edges BorderSegment) BorderSegment+    -> Lens' (Edges BorderSegment) BorderSegment+    -> I.IMap DynBorder+    -> I.IMap DynBorder+    -> I.IMap DynBorder+catDynBorders towardsA towardsB am bm = I.mapMaybe id+    $ I.intersectionWith (catDynBorder towardsA towardsB) am bm++-- | Given borders that should be placed next to each other (the first argument+-- on the right or bottom, and the second argument on the left or top), compute+-- new borders and the rewrites that should be done along the edges of the two+-- images to keep the image in synch with the border information.+--+-- The input borders are assumed to be disjoint. This property is not checked.+catBorders+    :: (border ~ BM.BorderMap DynBorder, rewrite ~ I.IMap V.Image)+    => BoxRenderer n -> border -> border -> ((rewrite, rewrite), border)+catBorders br r l = if lCoord + 1 == rCoord+    then ((lRe, rRe), lr')+    else ((I.empty, I.empty), lr)+    where+    lr     = BM.expand (BM.coordinates r) l `BM.unsafeUnion`+             BM.expand (BM.coordinates l) r+    lr'    = id+           . mergeIMap lCoord lIMap'+           . mergeIMap rCoord rIMap'+           $ lr+    lCoord = BM.coordinates l ^. hiPrimary br+    rCoord = BM.coordinates r ^. loPrimary br+    lIMap  = lookupPrimary br lCoord l+    rIMap  = lookupPrimary br rCoord r+    lIMap' = catDynBorders (loPrimary br) (hiPrimary br) lIMap rIMap+    rIMap' = catDynBorders (hiPrimary br) (loPrimary br) rIMap lIMap+    lRe    = renderDynBorder <$> lIMap'+    rRe    = renderDynBorder <$> rIMap'+    mergeIMap p imap bm = F.foldl'+        (\bm' (s,v) -> insertSecondary br (locationFromPrimarySecondary br p s) v bm')+        bm+        (I.unsafeToAscList imap)++-- | Given a direction to concatenate borders in, and the border information+-- itself (which list is assumed to be already shifted so that borders do not+-- overlap and are strictly increasing in the primary direction), produce: a+-- list of rewrites for the lo and hi directions of each border, respectively,+-- and the borders describing the fully concatenated object.+catAllBorders ::+    BoxRenderer n ->+    [BM.BorderMap DynBorder] ->+    ([(I.IMap V.Image, I.IMap V.Image)], BM.BorderMap DynBorder)+catAllBorders _ [] = ([], BM.empty)+catAllBorders br (bm:bms) = (zip ([I.empty]++los) (his++[I.empty]), bm') where+    (rewrites, bm') = runState (traverse (state . catBorders br) bms) bm+    (his, los) = unzip rewrites++rewriteEdge ::+    (Int -> V.Image -> V.Image) ->+    (Int -> V.Image -> V.Image) ->+    ([V.Image] -> V.Image) ->+    I.IMap V.Image -> V.Image -> V.Image+rewriteEdge splitLo splitHi combine = (combine .) . go . offsets 0 . I.unsafeToAscList where++    -- convert absolute positions into relative ones+    offsets _ [] = []+    offsets n ((n', r):nrs) = (n'-n, r) : offsets (n'+I.len r) nrs++    go [] old = [old]+    -- TODO: might be nice to construct this image with fill rather than+    -- replicate+char+    go ((lo, I.Run len new):nrs) old+        =  [splitLo lo old]+        ++ replicate len new+        ++ go nrs (splitHi (lo+len) old)++rewriteImage :: BoxRenderer n -> (I.IMap V.Image, I.IMap V.Image) -> V.Image -> V.Image+rewriteImage br (loRewrite, hiRewrite) old = rewriteHi . rewriteLo $ old where+    size = imagePrimary br old+    go = rewriteEdge (splitLoSecondary br) (splitHiSecondary br) (concatenateSecondary br)+    rewriteLo img+        | I.null loRewrite = img+        | otherwise = concatenatePrimary br+            [ go loRewrite (splitLoPrimary br 1 img)+            , splitHiPrimary br 1 img+            ]+    rewriteHi img+        | I.null hiRewrite = img+        | otherwise = concatenatePrimary br+            [ splitLoPrimary br (size-1) img+            , go hiRewrite (splitHiPrimary br (size-1) img)+            ]  -- | Limit the space available to the specified widget to the specified -- number of columns. This is important for constraining the horizontal
src/Brick/Widgets/Internal.hs view
@@ -3,6 +3,7 @@   ( renderFinal   , cropToContext   , cropResultToContext+  , renderDynBorder   ) where @@ -20,6 +21,8 @@ import Brick.Types.Internal import Brick.AttrMap import Brick.Widgets.Border.Style+import Brick.BorderMap (BorderMap, Edges(..))+import qualified Brick.BorderMap as BM  renderFinal :: AttrMap             -> [Widget n]@@ -32,7 +35,7 @@         (layerResults, !newRS) = flip runState rs $ sequence $             (\p -> runReaderT p ctx) <$>             (render <$> cropToContext <$> layerRenders)-        ctx = Context mempty (fst sz) (snd sz) defaultBorderStyle aMap+        ctx = Context mempty (fst sz) (snd sz) defaultBorderStyle aMap False         pic = V.picForLayers $ uncurry V.resize sz <$> (^.imageL) <$> layerResults         -- picWithBg is a workaround for runaway attributes.         -- See https://github.com/coreyoconnor/vty/issues/95@@ -53,6 +56,7 @@     return $ result & imageL   %~ cropImage   c                     & cursorsL %~ cropCursors c                     & extentsL %~ cropExtents c+                    & bordersL %~ cropBorders c  cropImage :: Context -> V.Image -> V.Image cropImage c = V.crop (max 0 $ c^.availWidthL) (max 0 $ c^.availHeightL)@@ -105,3 +109,27 @@             in if w' < 0 || h' < 0                then Nothing                else Just e++cropBorders :: Context -> BorderMap DynBorder -> BorderMap DynBorder+cropBorders ctx = BM.crop Edges+    { eTop = 0+    , eBottom = availHeight ctx - 1+    , eLeft = 0+    , eRight = availWidth ctx - 1+    }++renderDynBorder :: DynBorder -> V.Image+renderDynBorder db = V.char (dbAttr db) . ($dbStyle db) $ case bsDraw <$> dbSegments db of+    --    top   bot   left  right+    Edges False False False False -> const ' ' -- dunno lol (but should never happen, so who cares)+    Edges False False _     _     -> bsHorizontal+    Edges _     _     False False -> bsVertical+    Edges False True  False True  -> bsCornerTL+    Edges False True  True  False -> bsCornerTR+    Edges True  False False True  -> bsCornerBL+    Edges True  False True  False -> bsCornerBR+    Edges False True  True  True  -> bsIntersectT+    Edges True  False True  True  -> bsIntersectB+    Edges True  True  False True  -> bsIntersectL+    Edges True  True  True  False -> bsIntersectR+    Edges True  True  True  True  -> bsIntersectFull
src/Brick/Widgets/List.hs view
@@ -127,8 +127,8 @@         EvKey (KChar 'G') [] -> return $ listMoveTo (V.length $ listElements theList) theList         EvKey (KChar 'f') [MCtrl] -> listMovePageDown theList         EvKey (KChar 'b') [MCtrl] -> listMovePageUp theList-        EvKey (KChar 'd') [MCtrl] -> listMoveByPages 0.5 theList-        EvKey (KChar 'u') [MCtrl] -> listMoveByPages (-0.5) theList+        EvKey (KChar 'd') [MCtrl] -> listMoveByPages (0.5::Double) theList+        EvKey (KChar 'u') [MCtrl] -> listMoveByPages (-0.5::Double) theList         _ -> fallback e theList  -- | The top-level attribute used for the entire list.@@ -274,7 +274,7 @@  -- | Move the list selected index up by one page. listMovePageUp :: (Ord n) => List n e -> EventM n (List n e)-listMovePageUp theList = listMoveByPages (-1) theList+listMovePageUp theList = listMoveByPages (-1::Double) theList  -- | Move the list selected index down by one. (Moves the cursor down, -- adds one to the index.)@@ -283,7 +283,7 @@  -- | Move the list selected index down by one page. listMovePageDown :: (Ord n) => List n e -> EventM n (List n e)-listMovePageDown theList = listMoveByPages 1 theList+listMovePageDown theList = listMoveByPages (1::Double) theList  -- | Move the list selected index by some (fractional) number of pages. listMoveByPages :: (Ord n, RealFrac m) => m -> List n e -> EventM n (List n e)
+ src/Data/IMap.hs view
@@ -0,0 +1,181 @@+{-# LANGUAGE DeriveFunctor #-}+module Data.IMap+    ( IMap+    , Run(..)+    , empty+    , Data.IMap.null+    , singleton+    , insert+    , delete+    , restrict+    , lookup+    , splitLE+    , intersectionWith+    , mapMaybe+    , addToKeys+    , unsafeUnion+    , fromList+    , unsafeRuns+    , unsafeToAscList+    ) where++import Data.List (foldl')+import Data.Monoid+import Data.IntMap.Strict (IntMap)+import Prelude hiding (lookup)+import qualified Data.IntMap.Strict as IM++-- | Semantically, 'IMap' and 'IntMap' are identical; but 'IMap' is more+-- efficient when large sequences of contiguous keys are mapped to the same+-- value.+newtype IMap a = IMap { _runs :: IntMap (Run a) } deriving (Show, Functor)++{-# INLINE unsafeRuns #-}+-- | This function is unsafe because 'IMap's that compare equal may split their+-- runs into different chunks; consumers must promise that they do not treat+-- run boundaries specially.+unsafeRuns :: IMap a -> IntMap (Run a)+unsafeRuns = _runs++instance Eq a => Eq (IMap a) where+    IMap m == IMap m' = go (IM.toAscList m) (IM.toAscList m') where+        go ((k, Run n a):kvs) ((k', Run n' a'):kvs')+            = k == k' && a == a' && case compare n n' of+                LT -> go kvs ((k'+n, Run (n'-n) a'):kvs')+                EQ -> go kvs kvs'+                GT -> go ((k+n', Run (n-n') a):kvs) kvs'+        go [] [] = True+        go _ _ = False++instance Ord a => Ord (IMap a) where+    compare (IMap m) (IMap m') = go (IM.toAscList m) (IM.toAscList m') where+        go [] [] = EQ+        go [] _  = LT+        go _  [] = GT+        go ((k, Run n a):kvs) ((k', Run n' a'):kvs')+            = compare k k' <> compare a a' <> case compare n n' of+                LT -> go kvs ((k'+n, Run (n'-n) a'):kvs')+                EQ -> go kvs kvs'+                GT -> go ((k+n', Run (n-n') a):kvs) kvs'++-- | Zippy: '(<*>)' combines values at equal keys, discarding any values whose+-- key is in only one of its two arguments.+instance Applicative IMap where+    pure a = IMap . IM.fromDistinctAscList $+        [ (minBound, Run maxBound a)+        , (-1, Run maxBound a)+        , (maxBound-1, Run 2 a)+        ]+    (<*>) = intersectionWith ($)++-- | @Run n a@ represents @n@ copies of the value @a@.+data Run a = Run+    { len :: !Int+    , val :: !a+    } deriving (Eq, Ord, Read, Show, Functor)++instance Foldable    Run where foldMap f r = f (val r)+instance Traversable Run where sequenceA (Run n v) = Run n <$> v++empty :: IMap a+empty = IMap IM.empty++null :: IMap a -> Bool+null = IM.null . _runs++singleton :: Int -> Run a -> IMap a+singleton k r+    | len r >= 1 = IMap (IM.singleton k r)+    | otherwise = empty++insert :: Int -> Run a -> IMap a -> IMap a+insert k r m+    | len r < 1 = m+    | otherwise = m { _runs = IM.insert k r (_runs (delete k r m)) }++{-# INLINE delete #-}+delete :: Int -> Run ignored -> IMap a -> IMap a+delete k r m+    | len r < 1 = m+    | otherwise = m { _runs = IM.union (_runs lt) (_runs gt) }+    where+    (lt, ge) = splitLE (k-1) m+    (_ , gt) = splitLE (k+len r-1) ge++-- | Given a range of keys (as specified by a starting key and a length for+-- consistency with other functions in this module), restrict the map to keys+-- in that range. @restrict k r m@ is equivalent to @intersectionWith const m+-- (insert k r empty)@ but potentially more efficient.+restrict :: Int -> Run ignored -> IMap a -> IMap a+restrict k r = id+    . snd+    . splitLE (k-1)+    . fst+    . splitLE (k+len r-1)++lookup :: Int -> IMap a -> Maybe a+lookup k m = case IM.lookupLE k (_runs m) of+    Just (k', Run n a) | k < k'+n -> Just a+    _ -> Nothing++-- | @splitLE n m@ produces a tuple @(le, gt)@ where @le@ has all the+-- associations of @m@ where the keys are @<= n@ and @gt@ has all the+-- associations of @m@ where the keys are @> n@.+splitLE :: Int -> IMap a -> (IMap a, IMap a)+splitLE k m = case IM.lookupLE k (_runs m) of+    Nothing -> (empty, m)+    Just (k', r@(Run n _)) -> case (k' + n - 1 <= k, k' == k) of+        (True , False) -> (m { _runs = lt }, m { _runs = gt })+        (True , True ) -> (m { _runs = IM.insert k r lt }, m { _runs = gt })+        (False, _    ) -> ( m { _runs = IM.insert k'    r { len =     1 + k - k' } lt' }+                          , m { _runs = IM.insert (k+1) r { len = n - 1 - k + k' } gt' }+                          )+        where+        (lt', gt') = IM.split k' (_runs m)+    where+    (lt, gt) = IM.split k (_runs m)++-- | Increment all keys by the given amount. This is like+-- 'IM.mapKeysMonotonic', but restricted to partially-applied addition.+addToKeys :: Int -> IMap a -> IMap a+addToKeys n m = m { _runs = IM.mapKeysMonotonic (n+) (_runs m) }++-- TODO: This is pretty inefficient. IntMap offers some splitting functions+-- that should make it possible to be more efficient here (though the+-- implementation would be significantly messier).+intersectionWith :: (a -> b -> c) -> IMap a -> IMap b -> IMap c+intersectionWith f (IMap runsa) (IMap runsb)+    = IMap . IM.fromDistinctAscList $ merge (IM.toAscList runsa) (IM.toAscList runsb)+    where+    merge as@((ka, ra):at) bs@((kb, rb):bt)+        | ka' < kb = merge at bs+        | kb' < ka = merge as bt+        | otherwise = (kc, Run (kc' - kc + 1) vc) : case compare ka' kb' of+            LT -> merge at bs+            EQ -> merge at bt+            GT -> merge as bt+        where+        ka' = ka + len ra - 1+        kb' = kb + len rb - 1+        kc  = max ka  kb+        kc' = min ka' kb'+        vc  = f (val ra) (val rb)+    merge _ _ = []++mapMaybe :: (a -> Maybe b) -> IMap a -> IMap b+mapMaybe f (IMap runs) = IMap (IM.mapMaybe (traverse f) runs)++fromList :: [(Int, Run a)] -> IMap a+fromList = foldl' (\m (k, r) -> insert k r m) empty++-- | This function is unsafe because 'IMap's that compare equal may split their+-- runs into different chunks; consumers must promise that they do not treat+-- run boundaries specially.+unsafeToAscList :: IMap a -> [(Int, Run a)]+unsafeToAscList = IM.toAscList . _runs++-- | This function is unsafe because it assumes there is no overlap between its+-- arguments. That is, in the call @unsafeUnion a b@, the caller must guarantee+-- that if @lookup k a = Just v@ then @lookup k b = Nothing@ and vice versa.+unsafeUnion :: IMap a -> IMap a -> IMap a+unsafeUnion a b = IMap { _runs = _runs a `IM.union` _runs b }
+ tests/Main.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++import Control.Applicative+import Data.IMap (IMap, Run(Run))+import Data.IntMap (IntMap)+import Test.QuickCheck+import qualified Data.IMap as IMap+import qualified Data.IntMap as IntMap++instance Arbitrary v => Arbitrary (Run v) where+    arbitrary = liftA2 (\(Positive n) -> Run n) arbitrary arbitrary++instance Arbitrary v => Arbitrary (IMap v) where+    arbitrary = IMap.fromList <$> arbitrary++instance (a ~ Ordering, Show b) => Show (a -> b) where+    show f = show [f x | x <- [minBound .. maxBound]]++lower :: IMap v -> IntMap v+lower m = IntMap.fromDistinctAscList+    [ (base+offset, v)+    | (base, Run n v) <- IMap.unsafeToAscList m+    , offset <- [0..n-1]+    ]++raise :: Eq v => IntMap v -> IMap v+raise = IMap.fromList . rle . map singletonRun . IntMap.toAscList where+    singletonRun (k, v) = (k, Run 1 v)++    rle ((k, Run n v):(k', Run n' v'):kvs)+        | k+n == k' && v == v' = rle ((k, Run (n+n') v):kvs)+    rle (kv:kvs) = kv:rle kvs+    rle [] = []++lowerRun :: Int -> Run v -> IntMap v+lowerRun k r = IntMap.fromAscList [(k+offset, IMap.val r) | offset <- [0..IMap.len r-1]]++type O = Ordering+type I = IMap Ordering++-- These next two probably have overflow bugs that QuickCheck can't reasonably+-- notice. Hopefully they don't come up in real use cases...+prop_raiseLowerFaithful :: IntMap O -> Bool+prop_raiseLowerFaithful m = m == lower (raise m)++prop_equalityReflexive :: I -> Bool+prop_equalityReflexive m = m == raise (lower m)++prop_equality :: I -> I -> Bool+prop_equality l r = (l == r) == (lower l == lower r)++prop_compare :: I -> I -> Bool+prop_compare l r = compare l r == compare (lower l) (lower r)++prop_applicativeIdentity :: I -> Bool+prop_applicativeIdentity v = (pure id <*> v) == v++prop_applicativeComposition :: IMap (O -> O) -> IMap (O -> O) -> IMap O -> Bool+prop_applicativeComposition u v w = (pure (.) <*> u <*> v <*> w) == (u <*> (v <*> w))++prop_applicativeHomomorphism :: (O -> O) -> O -> Bool+prop_applicativeHomomorphism f x = (pure f <*> pure x :: I) == pure (f x)++prop_applicativeInterchange :: IMap (O -> O) -> O -> Bool+prop_applicativeInterchange u y = (u <*> pure y) == (pure ($ y) <*> u)++prop_empty :: Bool+prop_empty = lower (IMap.empty :: I) == IntMap.empty++prop_singleton :: Int -> Run O -> Bool+prop_singleton k r = lower (IMap.singleton k r) == lowerRun k r++prop_insert :: Int -> Run O -> I -> Bool+prop_insert k r m = lower (IMap.insert k r m) == IntMap.union (lowerRun k r) (lower m)++prop_delete :: Int -> Run () -> I -> Bool+prop_delete k r m = lower (IMap.delete k r m) == lower m IntMap.\\ lowerRun k r++prop_splitLE :: Int -> I -> Bool+prop_splitLE k m = (lower le, lower gt) == (le', gt') where+    (le, gt) = IMap.splitLE k m+    (lt, eq, gt') = IntMap.splitLookup k (lower m)+    le' = maybe id (IntMap.insert k) eq lt++prop_intersectionWith :: (O -> O -> O) -> I -> I -> Bool+prop_intersectionWith f l r = lower (IMap.intersectionWith f l r) == IntMap.intersectionWith f (lower l) (lower r)++prop_addToKeys :: Int -> I -> Bool+prop_addToKeys n m = lower (IMap.addToKeys n m) == IntMap.mapKeysMonotonic (n+) (lower m)++prop_lookup :: Int -> I -> Bool+prop_lookup k m = IMap.lookup k m == IntMap.lookup k (lower m)++prop_restrict :: Int -> Run () -> I -> Bool+prop_restrict k r m = lower (IMap.restrict k r m) == IntMap.intersection (lower m) (lowerRun k r)++prop_mapMaybe :: (O -> Maybe O) -> I -> Bool+prop_mapMaybe f m = lower (IMap.mapMaybe f m) == IntMap.mapMaybe f (lower m)++prop_null :: I -> Bool+prop_null m = IMap.null m == IntMap.null (lower m)++return []++main :: IO Bool+main = $quickCheckAll