From 4a889fbb97d8e3d9cc396daf37199d2fe990a62b Mon Sep 17 00:00:00 2001 From: Adrian Cochrane Date: Fri, 24 Nov 2023 16:19:49 +1300 Subject: [PATCH] Improve documentation & naming for flexbox. --- Graphics/Layout.hs | 2 +- Graphics/Layout/Flex.hs | 87 +++++++++++++++++++++++++++---------- Graphics/Layout/Flex/CSS.hs | 24 ++++++++-- test/Test.hs | 12 ++--- 4 files changed, 91 insertions(+), 34 deletions(-) diff --git a/Graphics/Layout.hs b/Graphics/Layout.hs index db84b22..6daf431 100644 --- a/Graphics/Layout.hs +++ b/Graphics/Layout.hs @@ -58,7 +58,7 @@ data LayoutItem m n x = | LayoutConst x (PaddedBox m n) [LayoutItem m n x] -- | Children of a `LayoutInline` or `LayoutInline'`. | LayoutSpan (FragmentTree (UserData m n x)) - | LayoutFlex x (FlexParent (LayoutItem m n x) m) + | LayoutFlex x (Flex (LayoutItem m n x) m) deriving (Show, Eq) -- | An empty box. nullLayout :: (PropertyParser x, Zero m, Zero n) => LayoutItem m n x diff --git a/Graphics/Layout/Flex.hs b/Graphics/Layout/Flex.hs index 9fbf406..1ccee4d 100644 --- a/Graphics/Layout/Flex.hs +++ b/Graphics/Layout/Flex.hs @@ -1,4 +1,11 @@ -module Graphics.Layout.Flex where +-- | Layout formula positioning children horizontally or vertically, with or without wrapping. +module Graphics.Layout.Flex( + Flex(..), FlexChild(..), Direction(..), FlexWrapping(..), Justification(..), Alignment(..), + flexMap, flexResolve, flexMaxBasis, flexSumBasis, flexWrap, flexRowSize, flexRowsSize, + justifyOffset, justifySpacing, flexJustify, alignOffset, + outerMinMain, outerMain, outerMaxMain, outerMinCross, outerCross, outerMaxCross, + innerMinMain, innerMain, innerMaxMain, innerMinCross, innerCross, innerMaxCross, + flexGetBox, flexSplit, flexPosition) where import Graphics.Layout.Box as B (Length(..), lowerLength, Size(..), PaddedBox(..), maxWidth, width, minWidth, maxHeight, height, minHeight, CastDouble(..), Zero(..)) @@ -6,17 +13,28 @@ import Data.List (intersperse) import GHC.Real (infinity) import Data.Maybe (fromMaybe) -data FlexParent a b = FlexParent { +-- | Parameters to flexbox layout +data Flex a b = Flex { + -- | On which axis to position children direction :: Direction, + -- | Whether to reverse each row reverseRows :: Bool, + -- | Whether to wrap children to multiple lines wrap :: FlexWrapping, + -- | How to justify children within lines justify :: Justification, - alignLines :: Maybe Justification, -- `Nothing` is "stretch" + -- | How to justify lines within the flexbox. Nothing is CSS "stretch". + alignLines :: Maybe Justification, + -- | Gap between children baseGap :: b, + -- | Gap between lines crossGap :: b, - children :: [[FlexChild a b]], -- 2D list to store lines once split. - pageWidth :: Double -- Pagination argument + -- | The children to layout, 2D list so as to store lines once split. + children :: [[FlexChild a b]], + -- | How wide to consider the page when paginating. + pageWidth :: Double } deriving (Eq, Show, Read) +-- | Properties for positioning a child of a flexbox data FlexChild a b = FlexChild { grow :: Double, shrink :: Double, @@ -25,15 +43,20 @@ data FlexChild a b = FlexChild { flexInner :: a } deriving (Eq, Show, Read) +-- | Which axis to position children along data Direction = Row | Column deriving (Eq, Show, Read) +-- | Whether to wrap or reverse the wrapped lines data FlexWrapping = NoWrap | Wrap | WrapReverse deriving (Eq, Show, Read) +-- | How to position lines within a flexbox, or children within a line data Justification = JStart | JEnd | JCenter | JSpaceBetween | JSpaceAround | JSpaceEvenly deriving (Eq, Show, Read) +-- | How to align children along the cross-axis (opposite axis from which they're laid out) data Alignment = AlStretch | AlStart | AlEnd | AlCenter | AlBaseline deriving (Eq, Show, Read) -flexMap :: (a -> b) -> FlexParent a c -> FlexParent b c -flexMap cb self = FlexParent { +-- | Apply an operation to every child +flexMap :: (a -> b) -> Flex a c -> Flex b c +flexMap cb self = Flex { direction = direction self, reverseRows = reverseRows self, wrap = wrap self, justify = justify self, alignLines = alignLines self, baseGap = baseGap self, crossGap = crossGap self, pageWidth = pageWidth self, @@ -43,9 +66,10 @@ flexMap cb self = FlexParent { flexInner = cb $ flexInner kid -- The important line! } | kid <- row] | row <- children self] } +-- | Resolve lengths in the flexbox to doubles. flexResolve :: CastDouble b => (a -> Direction -> Double) -> Double -> - FlexParent a b -> FlexParent a Double -flexResolve cb size self = FlexParent { + Flex a b -> Flex a Double +flexResolve cb size self = Flex { direction = direction self, reverseRows = reverseRows self, wrap = wrap self, justify = justify self, alignLines = alignLines self, baseGap = toDoubleWithin size $ baseGap self, @@ -58,14 +82,17 @@ flexResolve cb size self = FlexParent { } | kid <- row] | row <- children self] } -flexMaxBasis :: FlexParent a Double -> Double +-- | The minimum size of the flexbox along `direction`, i.e. maximum size of a child. +flexMaxBasis :: Flex a Double -> Double flexMaxBasis self = maximum [basis child | row <- children self, child <- row] -flexSumBasis :: FlexParent a Double -> Double +-- | The maximum width of each row of the flexbox. +flexSumBasis :: Flex a Double -> Double flexSumBasis self = maximum [Prelude.sum $ intersperse (baseGap self) $ map basis row | row <- children self] +-- | Wrap all lines to a given size reassigning overflow or underflow space. -- NOTE: shrink propery may yield negative sizes. Caller will want to enforce min-sizes. -flexWrap :: CastDouble b => FlexParent a b -> Double -> FlexParent a b +flexWrap :: CastDouble b => Flex a b -> Double -> Flex a b flexWrap self size | NoWrap <- wrap self = post self | Wrap <- wrap self = post self' @@ -91,11 +118,11 @@ flexWrap self size in (kid:kids', rest) splitRow [] _ = ([], []) - post :: CastDouble b => FlexParent a b -> FlexParent a b + post :: CastDouble b => Flex a b -> Flex a b post flex | reverseRows self = post' flex { children = map reverse $ children flex } | otherwise = post' flex - post' :: CastDouble b => FlexParent a b -> FlexParent a b + post' :: CastDouble b => Flex a b -> Flex a b post' flex = flex { children = map resizeRow $ children flex } resizeRow :: CastDouble b => [FlexChild a b] -> [FlexChild a b] resizeRow row @@ -113,18 +140,21 @@ flexWrap self size nanguard x | isNaN x = 0 | isInfinite x = 0 | otherwise = x - baseGap' :: CastDouble b => FlexParent a b -> Double + baseGap' :: CastDouble b => Flex a b -> Double baseGap' = toDouble . baseGap basis' :: CastDouble b => FlexChild a b -> Double basis' = toDouble . basis +-- | The cross (opposite from `direction` axis) size of a row. flexRowSize :: (a -> Double) -> [FlexChild a b] -> Double flexRowSize cb row = maximum $ map (cb . flexInner) row -flexRowsSize :: (a -> Double) -> FlexParent a Double -> Double -flexRowsSize cb FlexParent { crossGap = gap, children = kids } = +-- | The cross (opposite from `direction` axis) size of all rows. +flexRowsSize :: (a -> Double) -> Flex a Double -> Double +flexRowsSize cb Flex { crossGap = gap, children = kids } = sum $ intersperse gap $ flexRowSize cb `map` kids justifyOffset, justifySpacing :: Double -> [Double] -> Double -> Justification -> Double +-- | How far right to shift some elements to achieve desired justification. justifyOffset _ _ _ JStart = 0 justifyOffset outersize ks g JEnd = outersize - innersize g ks justifyOffset outersize ks g JCenter = half $ outersize - innersize g ks @@ -133,11 +163,13 @@ justifyOffset outersize ks g JSpaceAround = half $ (outersize - innersize g ks)/length' ks justifyOffset _ ks _ _ | length ks <= 1 = 0 -- No gaps to space, avoid numeric errors. justifyOffset size ks g JSpaceEvenly = (size - innersize g ks)/(length' ks + 1) +-- | How much space to add between elements. justifySpacing size ks g JSpaceBetween = (size - innersize g ks)/(length' ks - 1) justifySpacing size ks g JSpaceAround = (size - innersize g ks)/length' ks justifySpacing size ks g JSpaceEvenly = (size - innersize g ks)/(length' ks + 1) justifySpacing _ _ _ _ = 0 +-- | Position new positions for the given items according to the given justification. flexJustify :: (a -> Double) -> Double -> [a] -> Double -> Justification -> [(Double, a)] flexJustify cb size kids gap just = inner kids offs where @@ -147,6 +179,7 @@ flexJustify cb size kids gap just = inner kids offs inner (k:ks) start = (start, k):inner ks (start + cb k + gap) inner [] _ = [] +-- | How far right to shift some elements to achieve desired alignment. alignOffset :: Double -> Double -> Alignment -> Double alignOffset _ _ AlStretch = 0 -- Needs special handling elsewhere alignOffset _ _ AlStart = 0 @@ -154,8 +187,11 @@ alignOffset outer inner AlEnd = outer - inner alignOffset outer inner AlCenter = half $ outer - inner alignOffset outer inner AlBaseline = half $ outer - inner -- FIXME: Implement properly! +-- | Sum given sizes with a specified gap between them. innersize gap = sum . intersperse gap +-- | divide by 2, can be syntactically in certain circumstances. half = (/2) +-- | length of an list, as a double. length' :: [a] -> Double length' = toEnum . length @@ -195,8 +231,9 @@ sizeMain self Column = block self sizeCross self Row = block self sizeCross self Column = inline self +-- | Compute the size bounds of a flexbox. flexGetBox :: (Zero m, CastDouble m, Zero n, CastDouble n) => - (a -> PaddedBox Double Double) -> FlexParent a m -> PaddedBox m n + (a -> PaddedBox Double Double) -> Flex a m -> PaddedBox m n flexGetBox cb self = zero { B.min = flexMaxBasis self' `size` flexRowsSize (cb' innerMinCross) self', B.max = fromRational infinity `size` fromRational infinity, @@ -209,9 +246,10 @@ flexGetBox cb self = zero { cb' cb_ = flip cb_ (direction self) . cb self' = flexResolve (innerMain . cb) 0 self -flexSplit :: (a -> Size Double Double) -> Double -> Double -> FlexParent a Double -> - (FlexParent a Double, FlexParent a Double) -flexSplit cb h _ self@FlexParent { direction = Row, pageWidth = w } = +-- | Split a flexbox over multiple pages. +flexSplit :: (a -> Size Double Double) -> Double -> Double -> Flex a Double -> + (Flex a Double, Flex a Double) +flexSplit cb h _ self@Flex { direction = Row, pageWidth = w } = (self' { children = page0 }, self' { children = page1 }) where self' = flexWrap self w @@ -223,7 +261,7 @@ flexSplit cb h _ self@FlexParent { direction = Row, pageWidth = w } = start + crossGap self + flexRowSize (inline . cb) row in (row:rows', rest) splitRows _ [] = ([], []) -flexSplit cb h h' self@FlexParent { direction = Column, pageWidth = w } +flexSplit cb h h' self@Flex { direction = Column, pageWidth = w } | measure h = (flexWrap self h, self { children = [] }) -- If it fits on neither page... Row-direction is more versatile! | not $ measure h' = flexSplit cb h h' self { direction = Row } @@ -231,10 +269,11 @@ flexSplit cb h h' self@FlexParent { direction = Column, pageWidth = w } where measure space = (block . cb) `flexRowsSize` flexWrap self space <= w +-- | Compute final position of a flexbox's children. flexPosition :: ((Double, Double) -> a -> b) -> (a -> Size Double Double) -> (Double, Double) -> Size Double Double -> - FlexParent a Double -> FlexParent b Double -flexPosition cb cb' (x,y) size self@FlexParent { direction = dir } = self { + Flex a Double -> Flex b Double +flexPosition cb cb' (x,y) size self@Flex { direction = dir } = self { children = map rowPosition $ flexJustify rowsize (sizeCross size dir) (children self) (crossGap self) (justify self) } where diff --git a/Graphics/Layout/Flex/CSS.hs b/Graphics/Layout/Flex/CSS.hs index a0fdfa2..3ae5211 100644 --- a/Graphics/Layout/Flex/CSS.hs +++ b/Graphics/Layout/Flex/CSS.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} +-- | Parse FlexBox-related CSS properties module Graphics.Layout.Flex.CSS(CSSFlex(..), lowerFlex) where import Data.CSS.Syntax.Tokens (Token(..), NumericValue(..)) @@ -9,21 +10,37 @@ import Graphics.Layout.CSS.Length (parseLength, finalizeLength, n2f, Unitted, Fo import Graphics.Layout.Box (Length) import Data.Maybe (isJust) +-- | Parsed FlexBox-related CSS properties. data CSSFlex = CSSFlex { + -- | Parsed CSS flex-direction, axis component. directionCSS :: Direction, + -- | Parsed CSS flex-direction, reversed flag reverseRowsCSS :: Bool, + -- | Parsed CSS flex-wrap wrapCSS :: FlexWrapping, + -- | Parsed CSS justify-content justifyCSS :: Maybe Justification, + -- | Parsed CSS align-items alignItemsCSS :: Alignment, - alignLinesCSS :: Maybe Justification, -- `Nothing` is "stretch" + -- | Parsed CSS align-content, `Nothing` is "stretch" + alignLinesCSS :: Maybe Justification, + -- | Parsed CSS row-gap rowGapCSS :: Unitted, + -- | Parsed CSS column-gap columnGapCSS :: Unitted, + -- flex children + -- | Parsed CSS order orderCSS :: Integer, + -- | Parsed CSS flex-grow growCSS :: Double, + -- | Parsed CSS flex-shrink shrinkCSS :: Double, + -- | Parsed CSS flex-basis basisCSS :: Unitted, + -- | Parsed CSS align-self alignSelfCSS :: Alignment, + -- | Whether justification or alignment properties should be parsed as right-to-left or left-to-right. textRTL :: Bool -- Extra parameter from caller. } @@ -167,8 +184,9 @@ instance PropertyParser CSSFlex where shorthand self k v | Just _ <- longhand self self k v = [(k, v)] | otherwise = [] -lowerFlex :: CSSFlex -> Font' -> [CSSFlex] -> [a] -> [Font'] -> FlexParent a Length -lowerFlex self font kids kids' fonts' = FlexParent { +-- | Lower the Flexbox styling tree to the Layout tree. +lowerFlex :: CSSFlex -> Font' -> [CSSFlex] -> [a] -> [Font'] -> Flex a Length +lowerFlex self font kids kids' fonts' = Flex { direction = directionCSS self, reverseRows = reverseRowsCSS self, wrap = wrapCSS self, diff --git a/test/Test.hs b/test/Test.hs index ac9e226..2b0dc6e 100644 --- a/test/Test.hs +++ b/test/Test.hs @@ -420,7 +420,7 @@ spec = do Flex.alignment = AlStart, flexInner = () } - let baseFlex = FlexParent { + let baseFlex = Flex.Flex { direction = Row, reverseRows = False, wrap = NoWrap, @@ -431,7 +431,7 @@ spec = do Flex.children = [[(child 10) { grow = 1 }, (child 20) { shrink = 1 }, (child 30) { grow = 2 }, (child 40) { shrink = 2 }]], pageWidth = 0 - } :: FlexParent () Double + } :: Flex.Flex () Double -- These test results don't look right... flexWrap baseFlex 50 `shouldBe` baseFlex { Flex.children = [[(child 10) { grow = 1 }, @@ -715,7 +715,7 @@ spec = do Flex.alignment = align, flexInner = l } - let baseFlex = FlexParent { + let baseFlex = Flex.Flex { direction = Row, reverseRows = False, wrap = Wrap, @@ -728,7 +728,7 @@ spec = do (child 30 AlEnd) { grow = 2 }, (child 40 AlStretch) { shrink = 2 }]], pageWidth = 80 - } :: FlexParent Double Double + } :: Flex.Flex Double Double let self = flexWrap baseFlex 50 self `shouldBe` baseFlex { Flex.children = [[(child 10 AlStart) { grow = 1 }, @@ -783,7 +783,7 @@ spec = do grow = 0, shrink = 0, basis = l, Flex.alignment = align, flexInner = (pos, l) } - let flex childs = FlexParent { + let flex childs = Flex.Flex { direction = Row, reverseRows = False, wrap = Wrap, @@ -797,7 +797,7 @@ spec = do let baseFlex = flex [[(child 10 AlStart) { grow = 1 }, (child 20 AlCenter) { shrink = 1 }, (child 30 AlEnd) { grow = 2 }, - (child 40 AlStretch) { shrink = 2 }]] :: FlexParent Double Double + (child 40 AlStretch) { shrink = 2 }]] :: Flex.Flex Double Double flexPosition (,) (\a -> Size a a) (10, 10) (Size 50 50) baseFlex `shouldBe` flex [[ (child' 10 AlStart (10, 10)) { grow = 1 }, (child' 20 AlCenter (22, 20)) { shrink = 1 }, -- 2.30.2