packages feed

monomer-1.6.0.0: src/Monomer/Widgets/Containers/Stack.hs

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

Container that stacks its children along a main axis.

An hstack widget will assign horizontal space to its children according to their
size requests. The inverse happens with vstack and vertical space, which assigns
vertical space as requested and all the horizontal space available.

For example, a label will get enough space to be displayed completely, and it
will also get all the vertical space the hstack has. This means that if the
hstack is the top level widget in the window, the label will also be as tall as
the window.

Both can be combined to create complex layouts. Considering the situation of a
top-level hstack which created a large vertical label, we could wrap the hstack
with a vstack to only use as much horizontal and vertical space as needed. Both
stack widgets will request space from their parent, along their corresponding
axis, based on the requests of their children.

The layout algorithm considers the different type of size requirements and
assigns space according to the logic defined in 'SizeReq'. If the requested
fixed space is larger that the viewport of the stack, the content will overflow.

@
vstack_ [childSpacing] [
    label "Selected image",
    image "assets/large-image.jpg"
      \`styleBasic\` [maxHeight 400],
    button \"Complete\" CompleteAction
  ]
@
-}
{-# LANGUAGE Strict #-}

module Monomer.Widgets.Containers.Stack (
  -- * Configuration
  StackCfg,
  -- * Constructors
  hstack,
  hstack_,
  vstack,
  vstack_,
  -- * Helpers
  assignStackAreas
) 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 stack:

- 'childSpacing': spacing between the child widgets.
- 'ignoreEmptyArea': when the widgets do not use all the available space,
  ignoring the unassigned space allows for mouse events to pass through. This is
  useful in zstack layers.
- 'sizeReqUpdater': allows modifying the 'SizeReq' generated by the stack.
-}
data StackCfg = StackCfg {
  _stcChildSpacing :: Maybe Double,
  _stcIgnoreEmptyArea :: Maybe Bool,
  _stcSizeReqUpdater :: [SizeReqUpdater]
}

instance Default StackCfg where
  def = StackCfg {
    _stcChildSpacing = Nothing,
    _stcIgnoreEmptyArea = Nothing,
    _stcSizeReqUpdater = []
  }

instance Semigroup StackCfg where
  (<>) s1 s2 = StackCfg {
    _stcChildSpacing = _stcChildSpacing s2 <|> _stcChildSpacing s1,
    _stcIgnoreEmptyArea = _stcIgnoreEmptyArea s2 <|> _stcIgnoreEmptyArea s1,
    _stcSizeReqUpdater = _stcSizeReqUpdater s1 <> _stcSizeReqUpdater s2
  }

instance Monoid StackCfg where
  mempty = def

instance CmbChildSpacing StackCfg where
  childSpacing_ spacing = def {
    _stcChildSpacing = Just spacing
  }

instance CmbIgnoreEmptyArea StackCfg where
  ignoreEmptyArea_ ignore = def {
    _stcIgnoreEmptyArea = Just ignore
  }

instance CmbSizeReqUpdater StackCfg where
  sizeReqUpdater updater = def {
    _stcSizeReqUpdater = [updater]
  }

-- | Creates a horizontal stack.
hstack
  :: (Traversable t)
  => t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created stack.
hstack children = hstack_ def children

-- | Creates a horizontal stack. Accepts config.
hstack_
  :: (Traversable t)
  => [StackCfg]          -- ^ The config options.
  -> t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created stack.
hstack_ configs children = newNode where
  config = mconcat configs
  newNode = defaultWidgetNode "hstack" (makeStack True config)
    & L.children .~ foldl' (|>) Empty children

-- | Creates a vertical stack.
vstack
  :: (Traversable t)
  => t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created stack.
vstack children = vstack_ def children

-- | Creates a vertical stack. Accepts config.
vstack_
  :: (Traversable t)
  => [StackCfg]          -- ^ The config options.
  -> t (WidgetNode s e)  -- ^ The list of items.
  -> WidgetNode s e      -- ^ The created stack.
vstack_ configs children = newNode where
  config = mconcat configs
  newNode = defaultWidgetNode "vstack" (makeStack False config)
    & L.children .~ foldl' (|>) Empty children

makeStack :: Bool -> StackCfg -> Widget s e
makeStack isHorizontal config = widget where
  widget = createContainer () def {
    containerIgnoreEmptyArea = ignoreEmptyArea,
    containerLayoutDirection = getLayoutDirection isHorizontal,
    containerUseCustomSize = True,
    containerGetSizeReq = getSizeReq,
    containerResize = resize
  }

  isVertical = not isHorizontal
  ignoreEmptyArea = fromMaybe False (_stcIgnoreEmptyArea config)
  childSpacing = fromMaybe 0 (_stcChildSpacing config)

  getSizeReq wenv node children = newSizeReq where
    sizeReqFns = _stcSizeReqUpdater 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 vreqs & L.fixed %~ (+ totalSpacing)
    | otherwise = foldl1 sizeReqMergeMax vreqs
    where
      vreqs = accessor <$> vchildren
      totalSpacing = fromIntegral (Seq.length vchildren - 1) * childSpacing

  resize wenv node viewport children = resized where
    style = currentStyle wenv node
    contentArea = fromMaybe def (removeOuterBounds style viewport)
    (newVps, newDim) = assignStackAreas isHorizontal contentArea childSpacing children
    newCa
      | isHorizontal = contentArea & L.w .~ newDim
      | otherwise = contentArea & L.h .~ newDim
    newNode = node
      & L.info . L.viewport .~ fromMaybe newCa (addOuterBounds style newCa)
    resized = (resultNode newNode, newVps)

{-|
Assigns space from rect to each of the provided widgets based on their size
requirements.
-}
assignStackAreas
  :: Bool                 -- ^ True if horizontal, False for vertical.
  -> Rect                 -- ^ The available space to assign.
  -> Double               -- ^ The spacing between adjacent children.
  -> Seq (WidgetNode s e) -- ^ The widgets that will be assigned space.
  -> (Seq Rect, Double)   -- ^ The assigned areas and used space in main axis.
assignStackAreas isHorizontal contentArea childSpacing children = result where
  Rect x y w h = contentArea
  mainSize = if isHorizontal then w else h
  mainStart = if isHorizontal then x else y
  rectSelector
    | isHorizontal = _rW
    | otherwise = _rH
  vchildren = Seq.filter (_wniVisible . _wnInfo) children
  reqs = fmap (mainReqSelector isHorizontal) vchildren

  sumSizes accum req = newStep where
    (cFixed, cFlex, cFlexFac, cExtraFac) = accum
    newFixed = cFixed + sizeReqFixed req
    newFlex = cFlex + sizeReqFlex req
    newFlexFac = cFlexFac + sizeReqFlex req * sizeReqFactor req
    newExtraFac = cExtraFac + sizeReqExtra req * sizeReqFactor req
    newStep = (newFixed, newFlex, newFlexFac, newExtraFac)

  visibleSpacings = max 0 (Seq.length vchildren - 1)
  totalSpacing = fromIntegral visibleSpacings * childSpacing

  (fixed, flex, flexFac, extraFac) = foldl' sumSizes def reqs
  flexAvail = min flex (mainSize - fixed - totalSpacing)
  extraAvail = max 0 (mainSize - fixed - totalSpacing - flexAvail)

  -- flexCoeff can only be negative
  flexCoeff
    | flexAvail < flex && flexFac > 0 = (flexAvail - flex) / flexFac
    | otherwise = 0
  extraCoeff
    | extraAvail > 0 && extraFac > 0 = extraAvail / extraFac
    | otherwise = 0

  foldHelper (accum, offset, vIndex) child = (newAccum, newOffset, newVIndex) where
    newRect = resizeChild isHorizontal contentArea flexCoeff extraCoeff offset child
    newAccum = accum |> newRect
    newOffset = offset + rectSelector newRect + spacing
    newVIndex = vIndex + if visible then 1 else 0
    spacing = if vIndex < Seq.length vchildren - 1 && visible then childSpacing else 0
    visible = _wniVisible (_wnInfo child)

  (areas, usedDim, _) = foldl' foldHelper (Seq.empty, mainStart, 0) children
  result = (areas, usedDim - mainStart)

resizeChild :: Bool -> Rect -> Factor -> Factor -> Double -> WidgetNode s e -> Rect
resizeChild horizontal contentArea flexCoeff extraCoeff offset child = result where
  Rect l t w h = contentArea
  emptyRect = Rect l t 0 0
  -- Either flex or extra is active (flex is negative or extra is >= 0)
  SizeReq fixed flex extra factor = mainReqSelector horizontal child

  tempMainSize = fixed
    + (1 + flexCoeff * factor) * flex
    + extraCoeff * factor * extra
  mainSize = max 0 tempMainSize

  hRect = Rect offset t mainSize h
  vRect = Rect l offset w mainSize
  result
    | not $ (_wniVisible . _wnInfo) child = emptyRect
    | horizontal = hRect
    | otherwise = vRect

mainReqSelector :: Bool -> WidgetNode s e -> SizeReq
mainReqSelector isHorizontal
  | isHorizontal = _wniSizeReqW . _wnInfo
  | otherwise = _wniSizeReqH . _wnInfo