{-# LANGUAGE RecordWildCards, OverloadedStrings, DeriveGeneric #-}
-- | Sizes grid cells & positions elements to them.
module Graphics.Layout.Grid(Grid(..), Track(..), GridItem(..), GridItem'(..), Alignment(..),
buildTrack, buildGrid, setCellBox, enumerate, gridItemBox, cellSize,
trackMin, trackNat, gridEstWidth, sizeTrackMins, sizeTrackNats, sizeTrackMaxs,
trackPosition, gridPosition, trackLayout, gridLayout) where
import Data.Either (fromRight)
import Data.Text (Text)
import Data.List (intersperse)
import Graphics.Layout.Box as B
import Debug.Trace (trace)
import GHC.Generics (Generic)
import Control.DeepSeq (NFData)
-- | An element which positions it's children within a grid.
type Grid m n = Size (Track m) (Track n)
-- | The sizes to which children are alonged on a single axis.
data Track x = Track {
-- | The desired size of each cell.
-- If Left specifies ratio of excess space to use.
cells :: [Either x Double],
-- | The minimum amount of space each cell should take.
trackMins :: [Double],
-- | The ideal amount of space each cell should take.
trackNats :: [Double],
-- | How much space to add between cells.
gap :: x
} deriving (Show, Read, Eq, Ord)
-- | Which cells a child should be aligned to.
type GridItem = Size GridItem' GridItem'
-- | How a grid child should be aligned per-axis.
data GridItem' = GridItem {
-- | On which cell should this child start.
cellStart :: Int,
-- | Before which cell should this child end.
cellEnd :: Int,
-- | How to redistribute excess space.
alignment :: Alignment,
-- | The minimum amount of space to allocate to this child.
minSize :: Double,
-- | The maximum aount of space to allocate to this child.
natSize :: Double
} deriving (Read, Show, Ord, Eq, Generic)
instance NFData GridItem'
-- | How to redistribute excess space.
data Alignment = Start | Mid | End deriving (Read, Show, Enum, Ord, Eq, Generic)
instance NFData Alignment
-- | Constructs a track with default (to-be-computed) values & given cell sizes.
buildTrack :: CastDouble x => [Either x Double] -> Track x
buildTrack cells = Track cells [] [] $ fromDouble 0
-- | Constructs a grid with default (to-be-computed) values & given cell sizes.
buildGrid :: (CastDouble m, CastDouble n) =>
[Either m Double] -> [Either n Double] -> Grid m n
buildGrid rows cols = Size (buildTrack cols) (buildTrack rows)
-- | Verify that the track is properly formed & can be validly processed.
verifyTrack :: Track x -> [GridItem'] -> Bool
verifyTrack track cells' = and [
cellStart cell < length (cells track) && cellStart cell >= 0 &&
cellEnd cell < length (cells track) && cellEnd cell > cellStart cell
| cell <- cells']
-- | Verify that the grid is properly formed & can be validly processed.
verifyGrid :: Grid m n -> [GridItem] -> Bool
verifyGrid grid cells =
verifyTrack (inline grid) (map inline cells) && verifyTrack (block grid) (map block cells)
-- | Compute the minimum size for the track given cell sizes.
-- Refers to computed min sizes if cached.
trackMin :: (n -> Double) -> Track n -> Double
trackMin cb self@Track { trackMins = [] } =
sum $ intersperse (cb $ gap self) [cb x | Left x <- cells self]
trackMin cb self = sum $ intersperse (cb $ gap self) $ trackMins self
-- | Compute the natural size for the track given cell sizes.
-- Refers to compute natural sizes if cached.
trackNat :: (n -> Double) -> Track n -> Double
trackNat cb self@Track { trackNats = [] } =
sum $ intersperse (cb $ gap self) [cb x | Left x <- cells self]
trackNat cb self = sum $ intersperse (cb $ gap self) $ trackNats self
-- | Selects all children entirely on the specified cell.
cellsForIndex :: [GridItem'] -> Int -> [GridItem']
cellsForIndex cells ix =
[cell | cell <- cells, cellStart cell == ix, cellStart cell == pred (cellEnd cell)]
-- | Sets minimum & natural sizes from the given padded box.
setCellBox :: (CastDouble m, CastDouble n) => GridItem -> PaddedBox m n -> GridItem
setCellBox (Size x y) box = Size x {
minSize = B.minWidth $ mapX' toDouble box,
natSize = B.width $ mapX' toDouble box
} y {
minSize = B.minHeight $ mapY' toDouble box,
natSize = B.height $ mapY' toDouble box
}
-- | Estimate grid width to inform proper width calculation.
gridEstWidth :: Grid y Length -> [GridItem] -> Double
gridEstWidth (Size cols _) childs = trackNat toDouble cols {
trackMins = sizeTrackMins 0 cols $ map inline childs,
trackNats = sizeTrackNats 0 cols $ map inline childs
}
-- | Calculate minimum sizes for all cells in the track.
-- Sized to fit given children.
sizeTrackMins :: Double -> Track Length -> [GridItem'] -> [Double]
sizeTrackMins parent track childs = map inner $ enumerate $ cells track
where
inner (_, Left (Pixels x)) = x
inner (_, Left (Percent x)) = x * parent
inner arg@(ix, Left Preferred) =
maximum $ (0:) $ map natSize $ cellsForIndex childs ix
inner (ix, _) =
maximum $ (0:) $ map minSize $ cellsForIndex childs ix
-- | Compute natural sizes for all cells in the track.
-- Sized to fit given children.
sizeTrackNats :: Double -> Track Length -> [GridItem'] -> [Double]
sizeTrackNats parent track childs = map inner $ enumerate $ cells track
where
inner (_, Left (Pixels x)) = x
inner (_, Left (Percent x)) = x * parent
inner arg@(ix, Left Min) =
maximum $ (0:) $ map minSize $ cellsForIndex childs ix
inner (ix, _) =
maximum $ (0:) $ map natSize $ cellsForIndex childs ix
-- | Compute maximum sizes for all cells in the track, sized to the parent element.
sizeTrackMaxs :: Double -> Track Length -> [Double]
sizeTrackMaxs parent track = map (inner fr) $ zip subsizes $ cells track
where
subsizes = zip (trackMins track) (trackNats track)
fr = Prelude.max 0 fr'
fr' = (parent - estimate)/(countFRs $ cells track)
estimate = sum $ intersperse (lowerLength parent $ gap track) $
map (inner 0) $ zip subsizes $ cells track
inner _ (_, Left (Pixels x)) = x
inner _ (_, Left (Percent x)) = x*parent
inner _ ((_, nat), Left Preferred) = nat
inner _ ((min, _), Left Min) = min
inner fr ((_, nat), Left Auto) = Prelude.min nat fr
inner fr (_, Right x) = x*fr
-- | Compute the position of all children within the grid.
trackPosition :: Track Double -> [GridItem'] -> [Double]
trackPosition self childs = map gridCellPosition childs
where
gridCellPosition child = track (cellStart child) + align whitespace (alignment child)
where
whitespace = track (cellEnd child) - track (cellStart child) - natSize child
track = flip track' $ cells self
track' ix (size:sizes) = fromRight 0 size + track' (pred ix) sizes
track' 0 _ = 0
track' ix [] = trace "WARNING! Malformed input table!" 0
align _ Start = 0
align excess Mid = excess/2
align excess End = excess
-- | Compute the maximum size along an axis of a child, for it to be sized to.
cellSize :: CastDouble x => Track x -> GridItem' -> Double
cellSize self child = track (cellEnd child) - track (cellStart child)
where
track = flip track' $ cells self
track' ix (size:sizes) =
(toDouble $ fromRight (fromDouble 0) size) + track' (pred ix) sizes
track' 0 _ = 0
track' ix [] = trace "WARNING! Malformed input table!" 0
-- | Compute the maximum size as a PaddedBox of a child, for it to be sized to.
gridItemBox :: (CastDouble x, CastDouble y) => Grid y x -> GridItem -> PaddedBox Double Double
gridItemBox (Size cols rows) cell =
size2box (cellSize cols (inline cell) `Size` cellSize rows (block cell))
where
size2box size = zero { B.min = size, B.max = size, B.size = size }
-- | Compute the position of all children in a grid.
gridPosition :: Grid Double Double -> [GridItem] -> [(Double, Double)]
gridPosition (Size cols rows) childs =
trackPosition rows (map inline childs) `zip` trackPosition cols (map block childs)
-- | Compute the track sizes & child positions along a single axis.
trackLayout :: Double -> Double -> Track Length -> [GridItem'] ->
(Track Double, [(Double, GridItem')])
trackLayout parent width self childs = (self', zip positions childs)
where
positions = trackPosition self' childs
self' = self {
cells = map Left sizes,
trackMins = mins, trackNats = nats,
gap = lowerLength width $ gap self
}
sizes = sizeTrackMaxs parent self { trackMins = mins, trackNats = nats }
mins = sizeTrackMins parent self childs
nats = sizeTrackNats parent self childs
-- | Compute the track sizes & child positions along both axes.
gridLayout :: Size Double Double -> Grid Length Length -> [GridItem] ->
(Grid Double Double, [((Double, Double), GridItem)])
gridLayout parent (Size cols rows) childs = (self', zip positions childs)
where
positions = gridPosition self' childs
self' = Size cols' { gap = lowerLength width $ gap cols } rows'
(rows', _) = trackLayout (block parent) width rows $ map block childs
width = trackNat id cols'
(cols', _) = trackLayout (inline parent) 0 cols $ map inline childs
-- | Utility for associate an index with each item in a list.
enumerate = zip [0..]
-- | Utility for summing the divisor used to compute the fr unit.
countFRs (Left Auto:rest) = succ $ countFRs rest
countFRs (Right x:rest) = x + countFRs rest
countFRs (_:rest) = countFRs rest
countFRs [] = 0