{-# LANGUAGE OverloadedStrings, RecordWildCards #-}
-- | Generic layout logic, handling a hierarchy of varying formulas.
-- Unless callers have more specific needs they probably wish to use this abstraction.
-- Attempts to follow the CSS specs.
-- See `boxLayout` for a main entrypoint,
-- & `Graphics.Layout.CSS` to receive CSS input.
module Graphics.Layout(LayoutItem(..), UserData,
layoutGetBox, layoutGetChilds, layoutGetInner,
boxMinWidth, boxMaxWidth, boxNatWidth, boxWidth,
boxNatHeight, boxMinHeight, boxMaxHeight, boxHeight,
boxSplit, boxPaginate, boxPosition, boxLayout,
glyphs, codepoints, fragmentFont, {-, glyphsPerFont-}) where
import Data.Text.ParagraphLayout.Rich (Paragraph(..), ParagraphOptions(..),
ParagraphLayout(..), layoutRich)
import Data.Text.ParagraphLayout (paginate, PageContinuity(..), PageOptions(..))
import Stylist (PropertyParser(temp))
import Control.Parallel.Strategies
import Control.DeepSeq (NFData(..))
import Graphics.Layout.Box as B
import Graphics.Layout.Grid as G
import Graphics.Layout.Flow as F
import Graphics.Layout.Inline as I
import Graphics.Layout.CSS.Font (Font'(..))
import Data.Maybe (fromMaybe)
-- To gather glyphs for atlases.
import qualified Data.IntSet as IS
import qualified Data.Map.Strict as M
import qualified Data.Text.Glyphize as Hb
import Graphics.Text.Font.Choose (Pattern)
-- | Additional data routed through Balkon.
type UserData m n x = ((Font', Int), PaddedBox m n, x)
-- | A tree of different layout algorithms.
-- More to come...
data LayoutItem m n x =
-- | A block element. With margins, borders, & padding.
LayoutFlow x (PaddedBox m n) [LayoutItem m n x]
-- | A grid or table element.
| LayoutGrid x (Grid m n) [GridItem] [LayoutItem m n x]
-- | Some richtext. (Balkón holds children)
| LayoutInline x (Paragraph (UserData m n x)) PageOptions
-- | Results laying out richtext, has fixed width.
-- Generated from `LayoutInline` for the sake of pagination.
| LayoutInline' x (ParagraphLayout (UserData m n x)) PageOptions
-- | A branch with constant bounding box.
-- Generated from `LayoutInline` when attaching position info.
| LayoutConst x (PaddedBox m n) [LayoutItem m n x]
-- | Children of a `LayoutInline` or `LayoutInline'`.
| LayoutSpan (FragmentTree (UserData m n x))
-- | An empty box.
nullLayout :: (PropertyParser x, Zero m, Zero n) => LayoutItem m n x
nullLayout = LayoutFlow temp zero []
instance (Zero m, CastDouble m, NFData m, Zero n, CastDouble n, NFData n) =>
NFData (LayoutItem m n x) where
rnf = rnf . layoutGetBox -- Avoid auxiliary properties that don't cleanly `rnf`
--- | Retrieve the surrounding box for a layout item.
layoutGetBox :: (Zero m, Zero n, CastDouble m, CastDouble n) =>
LayoutItem m n x -> PaddedBox m n
layoutGetBox (LayoutFlow _ ret _) = ret
layoutGetBox (LayoutGrid _ self _ _) = zero {
B.min = Size (fromDouble $ trackMin toDouble $ inline self)
(fromDouble $ trackMin toDouble $ block self),
B.size = Size (fromDouble $ trackNat toDouble $ inline self)
(fromDouble $ trackNat toDouble $ block self),
B.max = Size (fromDouble $ trackNat toDouble $ inline self)
(fromDouble $ trackNat toDouble $ block self)
}
layoutGetBox (LayoutInline _ self _) = zero {
B.min = inlineMin self, B.size = inlineSize self, B.max = inlineSize self
}
layoutGetBox (LayoutInline' _ self _) = zero {
B.min = layoutSize self, B.size = layoutSize self, B.max = layoutSize self
}
layoutGetBox (LayoutSpan self) = treeBox self
layoutGetBox (LayoutConst _ ret _) = ret
-- | Retrieve the subtree under a node.
layoutGetChilds (LayoutFlow _ _ ret) = ret
layoutGetChilds (LayoutGrid _ _ _ ret) = ret
layoutGetChilds (LayoutSpan _) = []
layoutGetChilds (LayoutInline _ self _) = map LayoutSpan $ inlineChildren self
layoutGetChilds (LayoutInline' _ self _) = map LayoutSpan $ layoutChildren self
layoutGetChilds (LayoutConst _ _ childs) = childs
-- | Retrieve the caller-specified data attached to a layout node.
layoutGetInner (LayoutFlow ret _ _) = ret
layoutGetInner (LayoutGrid ret _ _ _) = ret
layoutGetInner (LayoutInline ret _ _) = ret
layoutGetInner (LayoutInline' ret _ _) = ret
layoutGetInner (LayoutConst ret _ _) = ret
layoutGetInner (LayoutSpan x) = treeInner x
fragmentFont x = let (ret, _, _) = treeInner' x in ret
-- | map-ready wrapper around `setCellBox` sourcing from a child node.
setCellBox' (child, cell) = setCellBox cell $ layoutGetBox child
-- | Update a (sub)tree to compute & cache minimum legible sizes.
boxMinWidth :: (Zero y, CastDouble y, NFData y) =>
Maybe Double -> LayoutItem y Length x -> LayoutItem y Length x
boxMinWidth parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
self' = self { B.min = mapSizeX (B.mapAuto min') (B.min self) }
min' = flowMinWidth parent' self childs''
childs'' = map (mapX' $ lowerLength selfWidth) $ map layoutGetBox childs'
childs' = parMap' (boxMinWidth $ Just selfWidth) childs
selfWidth = width $ mapX' (lowerLength parent') self
parent' = fromMaybe 0 parent
boxMinWidth parent (LayoutGrid val self cells0 childs) = LayoutGrid val self' cells' childs'
where
self' = Size (inline self) { trackMins = cells } (block self)
cells = sizeTrackMins parent' (inline self) $ map inline cells'
cells' = map setCellBox' $ zip childs' cells0 -- Flatten subgrids
childs'' = map (mapX' $ lowerLength selfWidth) $ map layoutGetBox childs'
childs' = parMap' (boxMinWidth $ Just selfWidth) childs
selfWidth = trackNat (lowerLength parent') $ inline self
parent' = fromMaybe (gridEstWidth self cells0) parent
zeroBox :: PaddedBox Double Double
zeroBox = zero
boxMinWidth _ self@(LayoutInline _ _ _) = self
boxMinWidth _ self@(LayoutInline' _ _ _) = self
boxMinWidth _ (LayoutConst val self' childs) =
LayoutConst val self' $ map (boxMinWidth Nothing) childs
boxMinWidth _ self@(LayoutSpan _) = self
-- | Update a (sub)tree to compute & cache ideal width.
boxNatWidth :: (Zero y, CastDouble y, NFData y) =>
Maybe Double -> LayoutItem y Length x -> LayoutItem y Length x
boxNatWidth parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
self' = self { B.nat = Size size' $ block $ B.nat self }
size' = flowNatWidth parent' self childs''
childs'' = map (mapX' $ lowerLength selfWidth) $ map layoutGetBox childs'
childs' = parMap' (boxNatWidth $ Just selfWidth) childs
selfWidth = width $ mapX' (lowerLength parent') self
parent' = fromMaybe 0 parent
boxNatWidth parent (LayoutGrid val self cells0 childs) = LayoutGrid val self' cells' childs'
where
self' = Size (inline self) { trackNats = cells } (block self)
cells = sizeTrackNats parent' (inline $ self) $ map inline cells'
cells' = map setCellBox' $ zip childs' cells0 -- Flatten subgrids
childs'' = map (mapX' $ lowerLength selfWidth) $ map layoutGetBox childs'
childs' = parMap' (boxNatWidth $ Just selfWidth) childs
selfWidth = trackNat (lowerLength parent') $ inline self
parent' = fromMaybe (gridEstWidth self cells0) parent
zeroBox :: PaddedBox Double Double
zeroBox = zero
boxNatWidth _ self@(LayoutInline _ _ _) = self
boxNatWidth _ self@(LayoutInline' _ _ _) = self
boxNatWidth _ (LayoutConst val self' childs) =
LayoutConst val self' $ map (boxNatWidth Nothing) childs
boxNatWidth _ self@(LayoutSpan _) = self
-- | Update a (sub)tree to compute & cache maximum legible width.
boxMaxWidth :: (CastDouble y, Zero y, NFData y) =>
PaddedBox a Double -> LayoutItem y Length x -> LayoutItem y Length x
boxMaxWidth parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
childs' = parMap' (boxMaxWidth self'') childs
self'' = mapX' (lowerLength $ inline $ B.size parent) self'
self' = self { B.max = Size (Pixels max') (block $ B.max self) }
max' = flowMaxWidth parent self
boxMaxWidth parent (LayoutGrid val self cells childs) = LayoutGrid val self cells childs'
where -- Propagate parent track as default.
childs' = parMap' inner $ zip cells childs
inner (Size cellx celly, child) =
boxMaxWidth (cellSize (inline self) cellx `size2box` cellSize (block self) celly) child
size2box x y = zeroBox { B.min = Size x y, B.max = Size x y, B.size = Size x y }
boxMaxWidth parent self@(LayoutInline _ _ _) = self
boxMaxWidth parent self@(LayoutInline' _ _ _) = self
boxMaxWidth _ (LayoutConst val self' childs) = LayoutConst val self' $
map (boxMaxWidth $ mapY' toDouble $ mapX' toDouble self') childs
boxMaxWidth parent self@(LayoutSpan _) = self
-- | Update a (sub)tree to compute & cache final width.
boxWidth :: (Zero y, CastDouble y, NFData y) =>
PaddedBox b Double -> LayoutItem y Length x -> LayoutItem y Double x
boxWidth parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
childs' = parMap' (boxWidth self') childs
self' = (mapX' (lowerLength $ inline $ size parent) self) {
size = Size size' $ block $ B.max self
}
size' = flowWidth parent self
boxWidth parent (LayoutGrid val self cells childs) = LayoutGrid val self' cells' childs'
where -- Propagate parent track as default
(cells', childs') = unzip $ parMap' recurse $ zip cells childs
recurse (cell, child) = (cell', child')
where
cell' = setCellBox cell $ layoutGetBox child'
child' = boxWidth (gridItemBox self cell) child
self' = flip Size (block self) Track {
cells = map Left widths,
trackMins = trackMins $ inline self, trackNats = trackNats $ inline self,
gap = lowerLength outerwidth $ gap $ inline self
}
outerwidth = inline $ size parent
widths = sizeTrackMaxs (inline $ size parent) $ inline self
boxWidth parent (LayoutInline val (Paragraph a b c d) paging) =
LayoutInline val (paragraphMap (mapX' $ lowerLength width) $
Paragraph a b c d { paragraphMaxWidth = round width }) paging
where width = B.inline $ B.size parent
boxWidth p (LayoutInline' a b c) =
LayoutInline' a (layoutMap (mapX' $ lowerLength $ B.inline $ B.size p) b) c
boxWidth p (LayoutConst val self childs) = LayoutConst val (mapX' cb self) $
map (boxWidth $ mapY' toDouble $ mapX' cb self) childs
where cb = lowerLength $ width p
boxWidth parent (LayoutSpan self') =
LayoutSpan $ treeMap (mapX' $ lowerLength $ width parent) self'
-- | Update a (sub)tree to compute & cache ideal legible height.
boxNatHeight :: Double -> LayoutItem Length Double x -> LayoutItem Length Double x
boxNatHeight parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
self' = self { size = mapSizeY (mapAuto size') (size self) }
size' = flowNatHeight parent self childs''
childs'' = map (mapY' (lowerLength parent)) $ map layoutGetBox childs'
childs' = parMap' (boxNatHeight $ inline $ size self) childs
boxNatHeight parent (LayoutGrid val self cells childs) = LayoutGrid val self' cells childs'
where
self' = Size (inline self) (block self) { trackNats = heights }
heights = sizeTrackNats parent (block self) $ map block cells'
cells' = map setCellBox' $ zip childs' cells -- Flatten subgrids
childs' = parMap' (boxNatHeight width) childs
width = trackNat id $ inline self
boxNatHeight parent self@(LayoutInline _ _ _) = self
boxNatHeight parent self@(LayoutInline' _ _ _) = self
boxNatHeight p (LayoutConst val self' childs) = LayoutConst val self' $
map (boxNatHeight $ width $ mapY' (lowerLength p) self') childs
boxNatHeight parent self@(LayoutSpan _) = self
-- | Update a (sub)tree to compute & cache minimum legible height.
boxMinHeight :: Double -> LayoutItem Length Double x -> LayoutItem Length Double x
boxMinHeight parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
childs' = parMap' (boxMinHeight $ inline $ size self) childs
self' = self { B.min = Size (inline $ B.min self) (Pixels min') }
min' = flowMinHeight parent self
boxMinHeight parent (LayoutGrid val self cells childs) = LayoutGrid val self' cells' childs'
where
(cells', childs') = unzip $ parMap' recurse $ zip cells childs
recurse (cell, child) = (cell', child') -- Propagate track into subgrids.
where
cell' = setCellBox cell (layoutGetBox child')
child' = boxMinHeight width child
self' = Size (inline self) (block self) { trackMins = heights }
heights = sizeTrackMins width (block self) $ map block cells
width = trackNat id $ inline self
boxMinHeight parent self@(LayoutInline _ _ _) = self
boxMinHeight _ self@(LayoutInline' _ _ _) = self
boxMinHeight p (LayoutConst val self' childs) = LayoutConst val self' $
map (boxMinHeight $ width $ mapY' (lowerLength p) self') childs
boxMinHeight parent self@(LayoutSpan _) = self
-- | Update a subtree to compute & cache maximum legible height.
boxMaxHeight :: PaddedBox Double Double -> LayoutItem Length Double x ->
LayoutItem Length Double x
boxMaxHeight parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
childs' = parMap' (boxMaxHeight $ mapY' (lowerLength width) self') childs
self' = self { B.max = Size (inline $ B.max self) (Pixels max') }
max' = flowMaxHeight (inline $ size parent) self
width = inline $ size self
boxMaxHeight parent (LayoutGrid val self cells childs) = LayoutGrid val self cells' childs'
where
(cells', childs') = unzip $ parMap' recurse $ zip cells childs
recurse (cell, child) = (cell', child') -- Propagate track into subgrids
where
cell' = setCellBox cell (layoutGetBox child')
child' = boxMaxHeight (gridItemBox self cell) child
heights = sizeTrackMaxs (inline $ size parent) (block self)
width = inline $ size parent
boxMaxHeight _ (LayoutInline val self' paging) = LayoutInline val self' paging
boxMaxHeight _ (LayoutInline' val self' paging) = LayoutInline' val self' paging
boxMaxHeight p (LayoutConst val self' childs) = LayoutConst val self' $
map (boxMaxHeight $ mapY' (lowerLength $ width p) self') childs
boxMaxHeight parent (LayoutSpan self') = LayoutSpan self'
-- | Update a (sub)tree to compute & cache final height.
boxHeight :: PaddedBox Double Double -> LayoutItem Length Double x -> LayoutItem Double Double x
boxHeight parent (LayoutFlow val self childs) = LayoutFlow val self' childs'
where
childs' = parMap' (boxHeight self') childs
self' = (mapY' (lowerLength $ inline $ size parent) self) {
size = Size (inline $ size self) size'
}
size' = flowHeight parent self
width = inline $ size self
boxHeight parent (LayoutGrid val self cells0 childs) = LayoutGrid val self' cells' childs'
where
(cells', childs') = unzip $ parMap' recurse $ zip cells0 childs
recurse (cell, child) = (cell', child') -- Propagate track into subgrids.
where
cell' = setCellBox cell (layoutGetBox child')
child' = boxHeight (layoutGetBox $ LayoutGrid val self' [] []) child
self' = Size (inline self) Track {
gap = lowerLength width $ gap $ block self,
cells = map lowerSize $ cells $ block self,
trackMins = trackMins $ block self, trackNats = trackNats $ block self
}
heights = sizeTrackMaxs (inline $ size parent) $ block self
lowerSize (Left x) = Left $ lowerLength width x
lowerSize (Right x) = Right x
width = inline $ size parent
boxHeight p (LayoutInline val self' paging) =
LayoutInline val (paragraphMap (mapY' $ lowerLength $ width p) self') paging
boxHeight p (LayoutInline' val self' paging) =
LayoutInline' val (layoutMap (mapY' $ lowerLength $ width p) self') paging
boxHeight p (LayoutConst val self childs) =
let self' = mapY' (lowerLength $ width p) self
in LayoutConst val self' $ map (boxHeight self') childs
boxHeight p (LayoutSpan self') =
LayoutSpan $ treeMap (mapY' $ lowerLength $ width p) self'
-- | Split a (sub)tree to fit within max-height.
-- May take full page height into account.
boxSplit :: PropertyParser x => Double -> Double -> LayoutItem Double Double x ->
(LayoutItem Double Double x, Maybe (LayoutItem Double Double x))
boxSplit maxheight _ node | height (layoutGetBox node) <= maxheight = (node, Nothing)
boxSplit maxheight pageheight (LayoutFlow val self childs)
| (next:_) <- childs1, ((y,_):_) <- childs0',
(tail,Just nextpage) <- boxSplit (maxheight - y) pageheight next =
(LayoutFlow val self {
size = (size self) { B.block = y }
} (childs0 ++ [tail]),
Just $ LayoutFlow val self {
size = (size self) { B.block = B.block (size self) - y }
} (nextpage:childs1))
| otherwise =
(LayoutFlow val self { size = (size self) { B.block = maxheight } } childs0,
Just $ LayoutFlow val self childs1) -- TODO recompute height
where
childs0 = map snd childs0'
childs1 = map snd childs1'
(childs0', childs1') = break overflowed $ inner 0 childs
overflowed (y, _) = y >= maxheight
inner start (child:childs) = (start', child):inner start' childs -- TODO margin collapse?
where start' = start + height (layoutGetBox child)
inner _ [] = []
boxSplit _ _ self@(LayoutConst _ _ _) = (self, Nothing) -- Doesn't split.
boxSplit _ _ self@(LayoutGrid _ _ _ _) = (self, Nothing) -- TODO
boxSplit maxheight pageheight (LayoutInline a self b) =
boxSplit maxheight pageheight $ LayoutInline' a (layoutRich self) b
boxSplit maxheight pageheight (LayoutInline' a self paging) =
case paginate paging {
pageCurrentHeight = toEnum $ fromEnum maxheight,
pageNextHeight = toEnum $ fromEnum pageheight
} self of
(Continue, self', next) -> (wrap self', wrap <$> next)
(Break, _, _) -> (nullLayout, Just $ wrap self)
where
wrap self' = LayoutInline' a self' paging
boxSplit _ _ self@(LayoutSpan _) = (self, Nothing) -- Can't split!
-- | Generate a list of pages from a node, splitting subtrees where necessary.
boxPaginate pageheight node
| (page, Just overflow) <- boxSplit pageheight pageheight node =
page:boxPaginate pageheight overflow
| otherwise = [node]
-- | Compute position of all nodes in the (sub)tree relative to a base coordinate.
boxPosition :: (PropertyParser x, Eq x) => (Double, Double) ->
LayoutItem Double Double x -> LayoutItem Double Double ((Double, Double), x)
boxPosition pos@(x, y) (LayoutFlow val self childs) = LayoutFlow (pos, val) self childs'
where
childs' = parMap' recurse $ zip pos' childs
recurse ((Size x' y'), child) = boxPosition (x + x', y + y') child
pos' = positionFlow $ map layoutGetBox childs
boxPosition pos@(x, y) (LayoutGrid val self cells childs) = LayoutGrid (pos, val) self cells childs'
where
childs' = parMap' recurse $ zip pos' childs
recurse ((x', y'), child) = boxPosition (x + x', y + y') child
pos' = gridPosition self cells
boxPosition pos@(x, y) (LayoutInline val self paging) =
boxPosition pos $ LayoutInline' val (layoutRich self) paging
boxPosition pos@(x, y) self@(LayoutInline' val _ _) =
boxPosition pos $ LayoutConst val (layoutGetBox self) $ layoutGetChilds self
boxPosition pos (LayoutConst val self childs) =
LayoutConst (pos, val) self $ parMap' (boxPosition pos) childs
boxPosition pos (LayoutSpan self) = LayoutSpan $ positionTree pos self
-- | Compute sizes & position information for all nodes in the (sub)tree.
boxLayout :: (PropertyParser x, Eq x) => PaddedBox Double Double ->
LayoutItem Length Length x -> Bool ->
[LayoutItem Double Double ((Double, Double), x)]
boxLayout parent self paginate = self9
where
self0 = boxMinWidth Nothing self
self1 = boxNatWidth Nothing self0
self2 = boxMaxWidth parent self1
self3 = boxWidth parent self2
self4 = boxNatHeight (inline $ size parent) self3
self5 = boxMinHeight (inline $ size parent) self4
self6 = boxMaxHeight parent self5
self7 = boxHeight parent self6
self8 | paginate = boxPaginate (block $ size parent) self7
| otherwise = [self7]
self9 = map (boxPosition (0, 0)) self8
-- | Compute a mapping from a layout tree indicating which glyphs for which fonts
-- are required.
-- Useful for assembling glyph atlases.
glyphsPerFont :: (CastDouble x, CastDouble y, Eq x, Eq y, Eq z) =>
LayoutItem x y z -> M.Map (Pattern, Double) IS.IntSet
glyphsPerFont (LayoutSpan self) | (_:_) <- glyphs =
(pattern font, fontSize font) `M.singleton` IS.fromList glyphs
| otherwise = M.empty
where
glyphs = map fromEnum $ codepoints self
(font, _) = fragmentFont self
glyphsPerFont node = M.unionsWith IS.union $ map glyphsPerFont $ layoutGetChilds node
parMap' :: NFData b => (a -> b) -> [a] -> [b]
parMap' = parMap rdeepseq