@@ 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
@@ 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,
@@ 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 },