vty-ui-0.2: src/Graphics/Vty/Widgets/Base.hs
{-# LANGUAGE ExistentialQuantification #-}
-- |A collection of primitive user interface widgets for composing and
-- laying out 'Graphics.Vty' user interfaces. This module provides
-- basic static and box layout widgets and a type class for rendering
-- widgets to Vty 'Graphics.Vty.Image's.
--
-- Each widget type supplied by this library is exported as a type and
-- an associated constructor function (e.g., 'Text' and 'text', 'Box'
-- and 'vBox' / 'hBox').
module Graphics.Vty.Widgets.Base
( Widget(..)
, mkImage
, AnyWidget
, Text
, Box
, Fill
, (<++>)
, (<-->)
, anyWidget
, text
, hBox
, vBox
, hFill
, vFill
)
where
import GHC.Word ( Word )
import Graphics.Vty ( DisplayRegion(DisplayRegion), Vty, Image, Attr
, string, char_fill, image_width, image_height
, region_width, region_height, terminal
, display_bounds, vert_cat, horiz_cat )
-- |The class of user interface widgets. Note that the growth
-- properties 'growHorizontal' and 'growVertical' are used to control
-- rendering order; if a widget /can/ grow to fill available space,
-- then neighboring fixed-size widgets will be rendered first so
-- remaining space can be computed. Then, variable-sized (growable)
-- widgets will be rendered last to consume that space.
class Widget w where
-- |Given a widget, render it with the given dimensions. The
-- resulting Image should not be larger than the specified
-- dimensions, but may be smaller.
render :: DisplayRegion -> w -> Image
-- |Will this widget expand to take advantage of available
-- horizontal space?
growHorizontal :: w -> Bool
-- |Will this widget expand to take advantage of available
-- vertical space?
growVertical :: w -> Bool
-- |The primary attribute of this widget, used when composing
-- widgets. For example, if you want to compose a widget /A/ with
-- a space-filling widget /B/, you probably want /B/'s text
-- attributes to be identical to those of /A/.
primaryAttribute :: w -> Attr
-- |Apply the specified attribute to this widget.
withAttribute :: w -> Attr -> w
-- |A wrapper for all widget types used in normalizing heterogeneous
-- lists of widgets. See 'anyWidget'.
data AnyWidget = forall a. (Widget a) => AnyWidget a
instance Widget AnyWidget where
growHorizontal (AnyWidget w) = growHorizontal w
growVertical (AnyWidget w) = growVertical w
render s (AnyWidget w) = render s w
primaryAttribute (AnyWidget w) = primaryAttribute w
withAttribute (AnyWidget w) att = AnyWidget (withAttribute w att)
-- |A text widget consisting of a string rendered using an
-- attribute. See 'text'.
data Text = Text Attr String
instance Widget Text where
growHorizontal _ = False
growVertical _ = False
render _ (Text att content) = string att content
primaryAttribute (Text att _) = att
withAttribute (Text _ content) att = Text att content
-- |A fill widget for filling available vertical or horizontal space
-- in a box layout. See 'vFill' and 'hFill'.
data Fill = VFill Attr Char
| HFill Attr Char Int
instance Widget Fill where
growHorizontal (HFill _ _ _) = True
growHorizontal (VFill _ _) = False
growVertical (VFill _ _) = True
growVertical (HFill _ _ _) = False
primaryAttribute (HFill att _ _) = att
primaryAttribute (VFill att _) = att
withAttribute (HFill _ c h) att = HFill att c h
withAttribute (VFill _ c) att = VFill att c
render s (VFill att c) = char_fill att c (region_width s) (region_height s)
render s (HFill att c h) = char_fill att c (region_width s) (toEnum h)
data Orientation = Horizontal | Vertical
-- |A box layout widget capable of containing two 'Widget's
-- horizontally or vertically. See 'hBox' and 'vBox'. Boxes lay out
-- their children as follows:
--
-- * If both children are expandable in the same dimension (i.e., both
-- vertically or both horizontally), the children are each given
-- half of the parent container's available space
--
-- * If one of the children is expandable and the other is static, the
-- static child is rendered first and the remaining space is given
-- to the expandable child
--
-- * Otherwise, both children are rendered in top-to-bottom or
-- left-to-right order and the resulting container uses only as much
-- space as its children combined
data Box = forall a b. (Widget a, Widget b) => Box Orientation a b
instance Widget Box where
growHorizontal (Box _ a b) =
growHorizontal a || growHorizontal b
growVertical (Box _ a b) =
growVertical a || growVertical b
withAttribute (Box o top bottom) att = Box o
(withAttribute top att)
(withAttribute bottom att)
-- Not the best way to choose this, but it seems like anything
-- here is going to be arbitrary.
primaryAttribute (Box _ top _) = primaryAttribute top
render s (Box Vertical top bottom) =
renderBox s (top, bottom) growVertical vert_cat region_height image_height withHeight
render s (Box Horizontal left right) =
renderBox s (left, right) growHorizontal horiz_cat region_width image_width withWidth
-- Box layout rendering implementation. This is generalized over the
-- two dimensions in which box layout can be performed; it takes lot
-- of functions, but mostly those are to query and update the correct
-- dimensions on regions and images as they are manipulated by the
-- layout algorithm.
renderBox :: (Widget a, Widget b) =>
DisplayRegion
-> (a, b)
-> (AnyWidget -> Bool) -- growth comparison function
-> ([Image] -> Image) -- concatenation function
-> (DisplayRegion -> Word) -- region dimension fetch function
-> (Image -> Word) -- image dimension fetch function
-> (DisplayRegion -> Word -> DisplayRegion) -- dimension modification function
-> Image
renderBox s (first, second) grow concatenate regDimension imgDimension withDim =
concatenate ws
where
ws = case (grow $ anyWidget first, grow $ anyWidget second) of
(True, True) -> renderHalves
(False, _) -> renderOrdered first second
(_, False) -> let [a, b] = renderOrdered second first
in [b, a]
renderHalves = let half = s `withDim` div (regDimension s) 2
half' = if regDimension s `mod` 2 == 0
then half
else half `withDim` (regDimension half + 1)
in [ render half first
, render half' second ]
renderOrdered a b = let renderedA = render s a
renderedB = render s' b
remaining = regDimension s - imgDimension renderedA
s' = s `withDim` remaining
in if imgDimension renderedA >= regDimension s
then [renderedA]
else [renderedA, renderedB]
withWidth :: DisplayRegion -> Word -> DisplayRegion
withWidth (DisplayRegion _ h) w = DisplayRegion w h
withHeight :: DisplayRegion -> Word -> DisplayRegion
withHeight (DisplayRegion w _) h = DisplayRegion w h
-- |Given a 'Widget' and a 'Vty' object, render the widget using the
-- current size of the terminal controlled by Vty. Returns the
-- rendered 'Widget' as an 'Image'.
mkImage :: (Widget a) => Vty -> a -> IO Image
mkImage vty w = do
size <- display_bounds $ terminal vty
return $ render size w
-- |Wrap a 'Widget' in the 'AnyWidget' type for normalization
-- purposes.
anyWidget :: (Widget a) => a -> AnyWidget
anyWidget = AnyWidget
-- |Create a 'Text' widget.
text :: Attr -- ^The attribute to use to render the text
-> String -- ^The text to display
-> Text
text = Text
-- |Create an horizonal fill widget.
hFill :: Attr -- ^The attribute to use to render the fill
-> Char -- ^The character to fill
-> Int -- ^The height, in rows, of the filled area; width of the
-- fill depends on available space
-> Fill
hFill = HFill
-- |Create a vertical fill widget. The dimensions of the widget will
-- depend on available space.
vFill :: Attr -- ^The attribute to use to render the fill
-> Char -- ^The character to fill
-> Fill
vFill = VFill
-- |Create a horizontal box layout widget containing two widgets side
-- by side. Space consumed by the box will depend on its contents and
-- the available space.
hBox :: (Widget a, Widget b) => a -- ^The left widget
-> b -- ^The right widget
-> Box
hBox = Box Horizontal
-- |An alias for 'hBox' intended as sugar to chain widgets
-- horizontally.
(<++>) :: (Widget a, Widget b) => a -> b -> Box
(<++>) = hBox
-- |Create a vertical box layout widget containing two widgets. Space
-- consumed by the box will depend on its contents and the available
-- space.
vBox :: (Widget a, Widget b) => a -- ^The top widget
-> b -- ^The bottom widget
-> Box
vBox = Box Vertical
-- |An alias for 'vBox' intended as sugar to chain widgets vertically.
(<-->) :: (Widget a, Widget b) => a -> b -> Box
(<-->) = vBox