{-# 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