{-# LANGUAGE DeriveDataTypeable #-} -- |This module provides vertical and horizontal box layout widgets -- using the 'Box' type. Box widgets use their child widgets' size -- policies and their space allocation settings to determine layout. -- -- Box widgets propagate key and focus events to their children. -- -- For more details, see the Vty-ui User's Manual. module Graphics.Vty.Widgets.Box ( Box , ChildSizePolicy(..) , IndividualPolicy(..) , BoxError(..) -- * Box Constructors , hBox , vBox , (<++>) , (<-->) -- * Box Configuration , setBoxSpacing , withBoxSpacing , defaultChildSizePolicy , setBoxChildSizePolicy , getBoxChildSizePolicy -- * Child Widget References , getFirstChild , getSecondChild ) where import GHC.Word ( Word ) import Data.Typeable import Control.Exception import Control.Monad import Graphics.Vty.Widgets.Core import Graphics.Vty import Graphics.Vty.Widgets.Util data BoxError = BadPercentage -- ^Indicates that a given percentage value was -- invalid. deriving (Eq, Show, Typeable) instance Exception BoxError data Orientation = Horizontal | Vertical deriving (Eq, Show) -- |Individual child widget policy applied to a child widget contained -- in a box. data IndividualPolicy = BoxAuto -- ^The child's growth policy will be used to -- determine layout. The child widget layout -- will also be affected by the policy of the -- other widget in the box. | BoxFixed Int -- ^A fixed number of rows or columns, -- depending on box type, will be allocated to -- the child. deriving (Show, Eq) -- |Child size policy applied to a box. data ChildSizePolicy = PerChild IndividualPolicy IndividualPolicy -- ^A per-child policy. | Percentage Int -- ^Percentage, p, of space given to first -- child, which implies that (100 - p) percent -- given to the second. deriving (Show, Eq) data Box a b = Box { boxChildSizePolicy :: ChildSizePolicy , boxOrientation :: Orientation , boxSpacing :: Int , boxFirst :: Widget a , boxSecond :: Widget b -- Box layout functions -- growth comparison function , firstGrows :: IO Bool -- growth comparison function , secondGrows :: IO Bool -- region dimension fetch function , regDimension :: DisplayRegion -> Word -- image dimension fetch function , imgDimension :: Image -> Word -- dimension modification function , withDimension :: DisplayRegion -> Word -> DisplayRegion -- Oriented image concatenation , img_cat :: [Image] -> Image } instance Show (Box a b) where show b = concat [ "Box { spacing = ", show $ boxSpacing b , ", childSizePolicy = ", show $ boxChildSizePolicy b , ", orientation = ", show $ boxOrientation b , " }" ] -- |Create a horizontal box widget containing two widgets side by -- side. Space consumed by the box will depend on its contents, -- available space, and the box child size policy. hBox :: (Show a, Show b) => Widget a -> Widget b -> IO (Widget (Box a b)) hBox = box Horizontal 0 -- |Create a vertical box widget containing two widgets, one above the -- other. Space consumed by the box will depend on its contents, -- available space, and the box child size policy. vBox :: (Show a, Show b) => Widget a -> Widget b -> IO (Widget (Box a b)) vBox = box Vertical 0 -- |Create a vertical box widget using monadic widget constructors. (<-->) :: (Show a, Show b) => IO (Widget a) -> IO (Widget b) -> IO (Widget (Box a b)) (<-->) act1 act2 = do ch1 <- act1 ch2 <- act2 vBox ch1 ch2 -- |Create a horizontal box widget using monadic widget constructors. (<++>) :: (Show a, Show b) => IO (Widget a) -> IO (Widget b) -> IO (Widget (Box a b)) (<++>) act1 act2 = do ch1 <- act1 ch2 <- act2 hBox ch1 ch2 infixl 3 <--> infixl 3 <++> -- |The default box child size policy, which defers to the children to -- determine layout. defaultChildSizePolicy :: ChildSizePolicy defaultChildSizePolicy = PerChild BoxAuto BoxAuto box :: (Show a, Show b) => Orientation -> Int -> Widget a -> Widget b -> IO (Widget (Box a b)) box o spacing wa wb = do let initSt = Box { boxChildSizePolicy = defaultChildSizePolicy , boxOrientation = o , boxSpacing = spacing , boxFirst = wa , boxSecond = wb , firstGrows = (if o == Vertical then growVertical else growHorizontal) wa , secondGrows = (if o == Vertical then growVertical else growHorizontal) wb , regDimension = if o == Vertical then region_height else region_width , imgDimension = if o == Vertical then image_height else image_width , withDimension = if o == Vertical then withHeight else withWidth , img_cat = if o == Vertical then vert_cat else horiz_cat } wRef <- newWidget initSt $ \w -> w { growHorizontal_ = \b -> do case boxOrientation b of Vertical -> do h1 <- growHorizontal $ boxFirst b h2 <- growHorizontal $ boxSecond b return $ h1 || h2 Horizontal -> do case boxChildSizePolicy b of Percentage _ -> return True PerChild s1 s2 -> do h1 <- growHorizontal $ boxFirst b h2 <- growHorizontal $ boxSecond b return $ (h1 && s1 == BoxAuto) || (h2 && s2 == BoxAuto) , growVertical_ = \b -> do case boxOrientation b of Horizontal -> do h1 <- growVertical $ boxFirst b h2 <- growVertical $ boxSecond b return $ h1 || h2 Vertical -> do case boxChildSizePolicy b of Percentage _ -> return True PerChild s1 s2 -> do h1 <- growVertical $ boxFirst b h2 <- growVertical $ boxSecond b return $ (h1 && s1 == BoxAuto) || (h2 && s2 == BoxAuto) , keyEventHandler = \this key mods -> do b <- getState this handled <- handleKeyEvent (boxFirst b) key mods if handled then return True else handleKeyEvent (boxSecond b) key mods , render_ = \this s ctx -> do b <- getState this renderBox s ctx b , getCursorPosition_ = \this -> do b <- getState this ch1_pos <- getCursorPosition $ boxFirst b case ch1_pos of Just v -> return $ Just v Nothing -> getCursorPosition $ boxSecond b , setCurrentPosition_ = \this pos -> do b <- getState this ch1_size <- getCurrentSize $ boxFirst b setCurrentPosition (boxFirst b) pos case boxOrientation b of Horizontal -> setCurrentPosition (boxSecond b) $ pos `plusWidth` ((region_width ch1_size) + (toEnum $ boxSpacing b)) Vertical -> setCurrentPosition (boxSecond b) $ pos `plusHeight` ((region_height ch1_size) + (toEnum $ boxSpacing b)) } wRef `relayFocusEvents` wa wRef `relayFocusEvents` wb return wRef -- |Get a reference to the first (left or top) widget in a box. getFirstChild :: Widget (Box a b) -> IO (Widget a) getFirstChild = (boxFirst <~~) -- |Get a reference to the second (right or bottom) widget in a box. getSecondChild :: Widget (Box a b) -> IO (Widget b) getSecondChild = (boxSecond <~~) -- |Set the spacing in between a box's child widgets in rows or -- columns, depending on the box type. setBoxSpacing :: Widget (Box a b) -> Int -> IO () setBoxSpacing wRef spacing = updateWidgetState wRef $ \b -> b { boxSpacing = spacing } withBoxSpacing :: Int -> Widget (Box a b) -> IO (Widget (Box a b)) withBoxSpacing spacing wRef = do setBoxSpacing wRef spacing return wRef -- |Get the child size policy for a box. getBoxChildSizePolicy :: Widget (Box a b) -> IO ChildSizePolicy getBoxChildSizePolicy = (boxChildSizePolicy <~~) -- |Set the box child size policy. Throws 'BadPercentage' if the size -- policy uses an invalid percentage value, which must be between 0 -- and 100 inclusive. setBoxChildSizePolicy :: Widget (Box a b) -> ChildSizePolicy -> IO () setBoxChildSizePolicy b spol = do case spol of Percentage v -> when (v < 0 || v > 100) $ throw BadPercentage _ -> return () updateWidgetState b $ \s -> s { boxChildSizePolicy = spol } -- 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 :: (Show a, Show b) => DisplayRegion -> RenderContext -> Box a b -> IO Image renderBox s ctx this = do let actualSpace = regDimension this s - (toEnum (boxSpacing this)) (img1, img2) <- -- XXX fix for case where we don't have enough space to honor -- hard-coded sizes (either fixed or derived fixed) -- XXX also check for overflow case boxChildSizePolicy this of PerChild BoxAuto BoxAuto -> renderBoxAuto s ctx this Percentage v -> do let firstDim = round (fromRational (fromRational ((toRational v) / (100.0)) * (toRational actualSpace)) ::Rational) secondDim = fromEnum (actualSpace - firstDim) renderBoxFixed s ctx this (fromEnum firstDim) secondDim PerChild BoxAuto (BoxFixed v) -> do let remaining = fromEnum (actualSpace - toEnum v) renderBoxFixed s ctx this remaining v PerChild (BoxFixed v) BoxAuto -> do let remaining = fromEnum (actualSpace - toEnum v) renderBoxFixed s ctx this v remaining PerChild (BoxFixed v1) (BoxFixed v2) -> renderBoxFixed s ctx this v1 v2 let spAttr = getNormalAttr ctx spacing = boxSpacing this spacer = case spacing of 0 -> empty_image _ -> case boxOrientation this of Horizontal -> let h = max (image_height img1) (image_height img2) in char_fill spAttr ' ' (toEnum spacing) h Vertical -> let w = max (image_width img1) (image_width img2) in char_fill spAttr ' ' w (toEnum spacing) -- Use the larger of the two images to determine padding in the -- opposite dimension. E.g. if this is a vertical box, we want -- to pad the images such that they have the same width. common_opposite_dim = case boxOrientation this of Horizontal -> max (image_height img1) (image_height img2) Vertical -> max (image_width img1) (image_width img2) padded_img1 = case boxOrientation this of Horizontal -> img1 <-> (char_fill spAttr ' ' (image_width img1) (common_opposite_dim - image_height img1)) Vertical -> img1 <|> (char_fill spAttr ' ' (common_opposite_dim - image_width img1) (image_height img1)) padded_img2 = case boxOrientation this of Horizontal -> img2 <-> (char_fill spAttr ' ' (image_width img2) (common_opposite_dim - image_height img2)) Vertical -> img2 <|> (char_fill spAttr ' ' (common_opposite_dim - image_width img2) (image_height img2)) return $ (img_cat this) [padded_img1, spacer, padded_img2] renderBoxFixed :: (Show a, Show b) => DisplayRegion -> RenderContext -> Box a b -> Int -> Int -> IO (Image, Image) renderBoxFixed s ctx this firstDim secondDim -- If the box is too large to fit in the available space (since it -- has fixed dimensions and can't be scaled), return the empty -- image. | toEnum firstDim + toEnum secondDim > regDimension this s = return (empty_image, empty_image) | otherwise = do let withDim = withDimension this img1 <- render (boxFirst this) (s `withDim` (toEnum firstDim)) ctx img2 <- render (boxSecond this) (s `withDim` (toEnum secondDim)) ctx -- pad the images so they fill the space appropriately. let fill img amt = case boxOrientation this of Vertical -> char_fill (getNormalAttr ctx) ' ' (image_width img) amt Horizontal -> char_fill (getNormalAttr ctx) ' ' amt (image_height img) firstDimW = toEnum firstDim secondDimW = toEnum secondDim img1_size = (imgDimension this) img1 img2_size = (imgDimension this) img2 img1_padded = if img1_size < firstDimW then (img_cat this) [img1, fill img1 (firstDimW - img1_size)] else img1 img2_padded = if img2_size < secondDimW then (img_cat this) [img2, fill img2 (secondDimW - img2_size)] else img2 return (img1_padded, img2_padded) renderBoxAuto :: (Show a, Show b) => DisplayRegion -> RenderContext -> Box a b -> IO (Image, Image) renderBoxAuto s ctx this = do let spacing = boxSpacing this first = boxFirst this second = boxSecond this withDim = withDimension this renderDimension = imgDimension this regDim = regDimension this actualSpace = s `withDim` (max (regDim s - toEnum spacing) 0) renderOrdered a b = do a_img <- render a actualSpace ctx let remaining = regDim actualSpace - renderDimension a_img s' = actualSpace `withDim` remaining b_img <- render b s' ctx return $ if renderDimension a_img >= regDim actualSpace then [a_img, empty_image] else [a_img, b_img] renderHalves = do let half = actualSpace `withDim` div (regDim actualSpace) 2 half' = if regDim actualSpace `mod` 2 == 0 then half else half `withDim` (regDim half + 1) first_img <- render first half ctx second_img <- render second half' ctx return [first_img, second_img] gf <- firstGrows this gs <- secondGrows this [img1, img2] <- case (gf, gs) of (True, True) -> renderHalves (False, _) -> renderOrdered first second (_, False) -> do images <- renderOrdered second first return $ reverse images return (img1, img2)