packages feed

monomer-1.5.1.0: src/Monomer/Widgets/Containers/Grid.hs

{-|
Module      : Monomer.Widgets.Containers.Grid
Copyright   : (c) 2018 Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

Layout container which distributes space evenly along the main axis. For the
secondary axis children will receive as much space as available for the grid
widget itself.

In the same way as with hstack and vstack, 'hgrid' and 'vgrid' can be combined
to create more complex layouts.

The hgrid widget requests maxWidth * elements as its width, and the max height
as its height. The inverse happens for vgrid.

@
hgrid [
    label "Third 1",
    label "Third 2",
    label "Third 3"
  ]
@
-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE Strict #-}

module Monomer.Widgets.Containers.Grid (
  -- * Configuration
  GridCfg,
  -- * Constructors
  hgrid,
  hgrid_,
  vgrid,
  vgrid_
) where

import Control.Applicative ((<|>))
import Control.Lens ((&), (^.), (.~), (%~))
import Data.Default
import Data.List (foldl')
import Data.Maybe
import Data.Sequence (Seq(..), (|>))

import qualified Data.Sequence as Seq

import Monomer.Helper (applyFnList)
import Monomer.Widgets.Container

import qualified Monomer.Lens as L

{-|
Configuration options for grid:

- 'childSpacing': spacing between the child widgets.
- 'sizeReqUpdater': allows modifying the 'SizeReq' generated by the grid.
-}
data GridCfg = GridCfg {
  _grcChildSpacing :: Maybe Double,
  _grcSizeReqUpdater :: [SizeReqUpdater]
}

instance Default GridCfg where
  def = GridCfg {
    _grcChildSpacing = Nothing,
    _grcSizeReqUpdater = []
  }

instance Semigroup GridCfg where
  (<>) s1 s2 = GridCfg {
    _grcChildSpacing = _grcChildSpacing s2 <|> _grcChildSpacing s1,
    _grcSizeReqUpdater = _grcSizeReqUpdater s1 <> _grcSizeReqUpdater s2
  }

instance Monoid GridCfg where
  mempty = def

instance CmbChildSpacing GridCfg where
  childSpacing_ spacing = def {
    _grcChildSpacing = Just spacing
  }

instance CmbSizeReqUpdater GridCfg where
  sizeReqUpdater updater = def {
    _grcSizeReqUpdater = [updater]
  }

-- | Creates a grid of items with the same width.
hgrid
  :: Traversable t
  => t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created grid.
hgrid children = hgrid_ def children

-- | Creates a grid of items with the same width. Accepts config.
hgrid_
  :: Traversable t
  => [GridCfg]           -- ^ The config options.
  -> t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created grid.
hgrid_ configs children = newNode where
  config = mconcat configs
  newNode = defaultWidgetNode "hgrid" (makeFixedGrid True config)
    & L.children .~ foldl' (|>) Empty children

-- | Creates a grid of items with the same height.
vgrid
  :: Traversable t
  => t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created grid.
vgrid children = vgrid_ def children

-- | Creates a grid of items with the same height. Accepts config.
vgrid_
  :: Traversable t
  => [GridCfg]           -- ^ The config options.
  -> t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created grid.
vgrid_ configs children = newNode where
  config = mconcat configs
  newNode = defaultWidgetNode "vgrid" (makeFixedGrid False config)
    & L.children .~ foldl' (|>) Empty children

makeFixedGrid :: Bool -> GridCfg -> Widget s e
makeFixedGrid isHorizontal config = widget where
  widget = createContainer () def {
    containerLayoutDirection = getLayoutDirection isHorizontal,
    containerGetSizeReq = getSizeReq,
    containerResize = resize
  }

  isVertical = not isHorizontal
  childSpacing = fromMaybe 0 (_grcChildSpacing config)

  getSizeReq wenv node children = newSizeReq where
    sizeReqFns = _grcSizeReqUpdater config
    vchildren = Seq.filter (_wniVisible . _wnInfo) children
    newSizeReqW = getDimSizeReq isHorizontal (_wniSizeReqW . _wnInfo) vchildren
    newSizeReqH = getDimSizeReq isVertical (_wniSizeReqH . _wnInfo) vchildren
    newSizeReq = applyFnList sizeReqFns (newSizeReqW, newSizeReqH)

  getDimSizeReq mainAxis accessor vchildren
    | Seq.null vreqs = fixedSize 0
    | mainAxis = foldl1 sizeReqMergeSum (Seq.replicate nreqs maxSize) & L.fixed %~ (+ totalSpacing)
    | otherwise = maxSize
    where
      vreqs = accessor <$> vchildren
      nreqs = Seq.length vreqs
      ~maxSize = foldl1 sizeReqMergeMax vreqs
      totalSpacing = fromIntegral (nreqs - 1) * childSpacing

  resize wenv node viewport children = resized where
    style = currentStyle wenv node
    contentArea = fromMaybe def (removeOuterBounds style viewport)
    Rect l t w h = contentArea
    vchildren = Seq.filter (_wniVisible . _wnInfo) children

    totalSpacingW = fromIntegral (max 0 (cols - 1)) * childSpacing
    totalSpacingH = fromIntegral (max 0 (rows - 1)) * childSpacing

    cols = if isHorizontal then length vchildren else 1
    rows = if isHorizontal then 1 else length vchildren

    cw = if cols > 0 then (w - totalSpacingW) / fromIntegral cols else 0
    ch = if rows > 0 then (h - totalSpacingH) / fromIntegral rows else 0

    cx i
      | rows == 0 = 0
      | isHorizontal = l + fromIntegral i * cw + spacingOffset i
      | otherwise = l
    cy i
      | cols == 0 = 0
      | isVertical = t + fromIntegral i * ch + spacingOffset i
      | otherwise = t
    spacingOffset i =
      fromIntegral i * childSpacing

    foldHelper (currAreas, index) child = (newAreas, newIndex) where
      (newIndex, newViewport)
        | child ^. L.info . L.visible = (index + 1, calcViewport index)
        | otherwise = (index, def)
      newArea = newViewport
      newAreas = currAreas |> newArea
    calcViewport i = Rect (cx i) (cy i) cw ch

    assignedAreas = fst $ foldl' foldHelper (Seq.empty, 0) children
    resized = (resultNode node, assignedAreas)