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