module Graphics.Layout.Flex where import Graphics.Layout.Box as B (Length(..), lowerLength, Size(..), PaddedBox(..), maxWidth, width, minWidth, maxHeight, height, minHeight, CastDouble(..), Zero(..)) import Data.List (intersperse) import GHC.Real (infinity) import Data.Maybe (fromMaybe) data FlexParent a b = FlexParent { direction :: Direction, reverseRows :: Bool, wrap :: FlexWrapping, justify :: Justification, alignLines :: Maybe Justification, -- `Nothing` is "stretch" baseGap :: b, crossGap :: b, children :: [[FlexChild a b]], -- 2D list to store lines once split. pageWidth :: Double -- Pagination argument } deriving (Eq, Show, Read) data FlexChild a b = FlexChild { grow :: Double, shrink :: Double, basis :: b, alignment :: Alignment, flexInner :: a } deriving (Eq, Show, Read) data Direction = Row | Column deriving (Eq, Show, Read) data FlexWrapping = NoWrap | Wrap | WrapReverse deriving (Eq, Show, Read) data Justification = JStart | JEnd | JCenter | JSpaceBetween | JSpaceAround | JSpaceEvenly deriving (Eq, Show, Read) data Alignment = AlStretch | AlStart | AlEnd | AlCenter | AlBaseline deriving (Eq, Show, Read) flexMap :: (a -> b) -> FlexParent a c -> FlexParent b c flexMap cb self = FlexParent { direction = direction self, reverseRows = reverseRows self, wrap = wrap self, justify = justify self, alignLines = alignLines self, baseGap = baseGap self, crossGap = crossGap self, pageWidth = pageWidth self, children = [[FlexChild { grow = grow kid, shrink = shrink kid, basis = basis kid, alignment = alignment kid, flexInner = cb $ flexInner kid -- The important line! } | kid <- row] | row <- children self] } flexResolve :: CastDouble b => (a -> Direction -> Double) -> Double -> FlexParent a b -> FlexParent a Double flexResolve cb size self = FlexParent { direction = direction self, reverseRows = reverseRows self, wrap = wrap self, justify = justify self, alignLines = alignLines self, baseGap = toDoubleWithin size $ baseGap self, crossGap = toDoubleWithin size $ crossGap self, pageWidth = pageWidth self, children = [[FlexChild { grow = grow kid, shrink = shrink kid, basis = toDoubleWithinAuto (flexInner kid `cb` direction self) size $ basis kid, alignment = alignment kid, flexInner = flexInner kid } | kid <- row] | row <- children self] } flexMaxBasis :: FlexParent a Double -> Double flexMaxBasis self = maximum [basis child | row <- children self, child <- row] flexSumBasis :: FlexParent a Double -> Double flexSumBasis self = maximum [Prelude.sum $ intersperse (baseGap self) $ map basis row | row <- children self] -- 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 self size | NoWrap <- wrap self = post self | Wrap <- wrap self = post self' | WrapReverse <- wrap self = post self' { children=reverse $ children self' } where self' = self { children = concatMap wrapRow $ children self } wrapRow :: CastDouble b => [FlexChild a b] -> [[FlexChild a b]] wrapRow [] = [] wrapRow kids@(kid:_) = let (row, rest) = splitRow' kids $ basis' kid in row:wrapRow rest splitRow, splitRow' :: CastDouble b => [FlexChild a b] -> Double -> ([FlexChild a b], [FlexChild a b]) -- This wrapper function ensures we don't end up with empty rows, or infinite loops. splitRow' (kid:kids) end = let (kids', rest) = splitRow kids (end + baseGap' self + basis' kid) in (kid:kids', rest) splitRow' [] _ = ([], []) splitRow (kid:kids) end | end > size = ([], kid:kids) | otherwise = let (kids', rest) = splitRow kids (end + baseGap' self + basis' kid) in (kid:kids', rest) splitRow [] _ = ([], []) post :: CastDouble b => FlexParent a b -> FlexParent 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' flex = flex { children = map resizeRow $ children flex } resizeRow :: CastDouble b => [FlexChild a b] -> [FlexChild a b] resizeRow row | rowSize > size = [kid { basis = fromDouble $ basis' kid - shrink kid * nanguard sfr } | kid <- row] | rowSize < size = [kid { basis = fromDouble $ basis' kid + grow kid * nanguard gfr } | kid <- row] | otherwise = row where rowSize = Prelude.sum $ intersperse (baseGap' self) $ map basis' row sfr = (rowSize - size)/(Prelude.sum $ map shrink row) gfr = (size - rowSize)/(Prelude.sum $ map grow row) nanguard x | isNaN x = 0 | isInfinite x = 0 | otherwise = x baseGap' :: CastDouble b => FlexParent a b -> Double baseGap' = toDouble . baseGap basis' :: CastDouble b => FlexChild a b -> Double basis' = toDouble . basis 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 } = sum $ intersperse gap $ flexRowSize cb `map` kids justifyOffset, justifySpacing :: Double -> [Double] -> Double -> Justification -> Double justifyOffset _ _ _ JStart = 0 justifyOffset outersize ks g JEnd = outersize - innersize g ks justifyOffset outersize ks g JCenter = half $ outersize - innersize g ks justifyOffset _ _ _ JSpaceBetween = 0 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) 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 flexJustify :: (a -> Double) -> Double -> [a] -> Double -> Justification -> [(Double, a)] flexJustify cb size kids gap just = inner kids offs where offs = justifyOffset size kids' gap just spacing = justifySpacing size kids' gap just kids' = map cb kids inner (k:ks) start = (start, k):inner ks (start + cb k + gap) inner [] _ = [] alignOffset :: Double -> Double -> Alignment -> Double alignOffset _ _ AlStretch = 0 -- Needs special handling elsewhere alignOffset _ _ AlStart = 0 alignOffset outer inner AlEnd = outer - inner alignOffset outer inner AlCenter = half $ outer - inner alignOffset outer inner AlBaseline = half $ outer - inner -- FIXME: Implement properly! innersize gap = sum . intersperse gap half = (/2) length' :: [a] -> Double length' = toEnum . length ------ --- Mapping Box Model axes <-> Flex Box axes ------ outerMinMain, outerMain, outerMaxMain :: Num m => PaddedBox m m -> Direction -> m outerMinMain box Row = minWidth box outerMinMain box Column = minHeight box outerMain box Row = width box outerMain box Column = height box outerMaxMain box Row = maxWidth box outerMaxMain box Column = maxHeight box outerMinCross, outerCross, outerMaxCross :: Num m => PaddedBox m m -> Direction -> m outerMinCross box Row = minHeight box outerMinCross box Column = minWidth box outerCross box Row = height box outerCross box Column = width box outerMaxCross box Row = maxHeight box outerMaxCross box Column = maxWidth box innerMinMain, innerMain, innerMaxMain :: Num m => PaddedBox m m -> Direction -> m innerMinMain box = sizeMain $ B.min box innerMain box = sizeMain $ B.size box innerMaxMain box = sizeMain $ B.max box innerMinCross, innerCross, innerMaxCross :: Num m => PaddedBox m m -> Direction -> m innerMinCross box = sizeCross $ B.min box innerCross box = sizeCross $ B.size box innerMaxCross box = sizeCross $ B.max box sizeMain, sizeCross :: Num m => Size m m -> Direction -> m sizeMain self Row = inline self sizeMain self Column = block self sizeCross self Row = block self sizeCross self Column = inline self flexGetBox :: (Zero m, CastDouble m, Zero n, CastDouble n) => (a -> PaddedBox Double Double) -> FlexParent 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, B.nat = flexSumBasis self' `size` flexRowsSize (cb' innerCross) self', B.size = flexSumBasis self' `size` flexRowsSize (cb' innerCross) self' } where size main cross | Row <- direction self = fromDouble main `Size` fromDouble cross | otherwise = fromDouble cross `Size` fromDouble main 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 } = (self' { children = page0 }, self' { children = page1 }) where self' = flexWrap self w (page0, page1) = splitRows (-crossGap self) $ children self splitRows start (row:rows) | start >= h = ([], row:rows) | otherwise = let (rows', rest) = flip splitRows rows $ start + crossGap self + flexRowSize (inline . cb) row in (row:rows', rest) splitRows _ [] = ([], []) flexSplit cb h h' self@FlexParent { 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 } | otherwise = (self { children = [] }, flexWrap self h') where measure space = (block . cb) `flexRowsSize` flexWrap self space <= w 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 { children = map rowPosition $ flexJustify rowsize (sizeCross size dir) (children self) (crossGap self) (justify self) } where rowsize = flexRowSize $ flip sizeCross dir . cb' -- TODO: Handle stretch properly rowPosition (rpos, row) = let rsize = flexRowSize (flip sizeCross dir . cb') row in map (alignChild rsize rpos) $ flexJustify basis rsize row (baseGap self) (fromMaybe JSpaceAround $ alignLines self) alignChild rsize rpos (kpos, kid@FlexChild { flexInner = kid', alignment = align' }) = kid { flexInner = flip cb kid' $ sz kpos $ rpos + alignOffset rsize (flip sizeCross dir $ cb' kid') align' } sz m c | Row <- direction self = (x + m, y + c) | otherwise = (x + c, y + m)