yoga-0.0.0.7: lib/Yoga.hs
{-|
Module : Yoga
Description : Bindings to Facebook's Yoga layout engine
Copyright : (c) Pavel Krajcevski, 2017
License : MIT
Maintainer : Krajcevski@gmail.com
Stability : experimental
Portability : POSIX
This module holds a high-level interface to the bindings associated with this
library that are maintained in "Bindings.Yoga". Application developers will
likely want to use this module to interface with the library, but are available
to use the C-level bindings if more control is desired.
These bindings are not affiliated with Facebook in any way, and have been
developed separately for the sole purpose of interfacing with their open source
library.
Full documentation can be found at <http://facebook.github.io/yoga>
-}
{-# LANGUAGE TupleSections #-}
module Yoga (
-- ** Main datatype
Layout,
-- ** Children layouts
-- | These layouts describe the way that children are ordered and spaced
-- within their parent.
Children, startToEnd, endToStart, centered, spaceBetween, spaceAround,
wrapped,
-- ** Containers
hbox, vbox,
hboxLeftToRight, hboxRightToLeft,
vboxTopToBottom, vboxBottomToTop,
-- ** Leaf nodes
Size(..),
shrinkable, growable, exact, withDimensions,
-- ** Attributes
Edge(..), Gutter(..),
stretched, setMargin, setPadding, setBorder, setGap,
-- ** Rendering
LayoutInfo(..), RenderFn, render, foldRender,
) where
--------------------------------------------------------------------------------
import Bindings.Yoga
import Bindings.Yoga.Enums
import Control.Applicative
import Control.Monad hiding (mapM, forM_)
import Control.Monad.IO.Class (MonadIO(..))
import Data.Foldable
import Data.Traversable
import Data.Monoid
import Foreign.C.Types (CFloat, CUInt)
import Foreign.ForeignPtr
import GHC.Ptr (Ptr)
import Numeric.IEEE
-- Last to avoid compiler warnings due to the Foldable/Traversable Proposal
import Prelude hiding (foldl, foldr, mapM)
--------------------------------------------------------------------------------
data LayoutTree a
= Root { _payload :: a,
_children :: [LayoutTree a],
_rootPtr :: ForeignPtr C'YGNode }
| Container { _payload :: a,
_children :: [LayoutTree a] }
| Leaf { _payload :: a }
deriving (Show, Eq, Ord)
-- | The main datatype in the high level bindings is a 'Layout'. Layouts are
-- used to store a tree of nodes that represent the different components of a
-- layout. Layouts can be composed by adding one as a sub-tree to another. The
-- parent-child relationship dictates different parameters such as width, height
-- and position. This type is opaque to the user in order to facilitate updates
-- to the library. For more control, use the C-level bindings in Yoga.Bindings.
newtype Layout a = Layout { generateLayout :: IO (LayoutTree a) }
withNativePtr :: LayoutTree a -> (Ptr C'YGNode -> IO b) -> IO b
withNativePtr (Root _ _ ptr) f = withForeignPtr ptr f
withNativePtr _ _ = error "Internal: Only root nodes have pointers"
-- | Children are a list of layouts annotated with a style for how they should
-- be laid out in their container.
data Children a
= StartToEnd [Layout a]
| EndToStart [Layout a]
| Centered [Layout a]
| SpaceBetween [Layout a]
| SpaceAround [Layout a]
| Wrap (Children a)
instance Functor LayoutTree where
fmap f (Root x cs ptr) = Root (f x) (fmap (fmap f) cs) ptr
fmap f (Container x cs) = Container (f x) (fmap (fmap f) cs)
fmap f (Leaf x) = Leaf (f x)
instance Foldable LayoutTree where
foldMap f (Root x cs _) = f x `mappend` foldMap (foldMap f) cs
foldMap f (Container x cs) = f x `mappend` foldMap (foldMap f) cs
foldMap f (Leaf x) = f x
foldl f z (Root x cs _) = foldl (foldl f) (f z x) cs
foldl f z (Container x cs) = foldl (foldl f) (f z x) cs
foldl f z (Leaf x) = f z x
foldr f z (Root x cs _) = f x $ foldr (flip $ foldr f) z cs
foldr f z (Container x cs) = f x $ foldr (flip $ foldr f) z cs
foldr f z (Leaf x) = f x z
instance Traversable LayoutTree where
traverse f (Root x cs ptr) =
Root <$> f x <*> traverse (traverse f) cs <*> pure ptr
traverse f (Container x cs) =
Container <$> f x <*> traverse (traverse f) cs
traverse f (Leaf x) = Leaf <$> f x
sequenceA (Root x cs ptr) =
Root <$> x <*> traverse sequenceA cs <*> pure ptr
sequenceA (Container x cs) =
Container <$> x <*> traverse sequenceA cs
sequenceA (Leaf x) = Leaf <$> x
mkNode :: a -> Layout a
mkNode x = Layout $ do
ptr <- c'YGNodeNew
Root x [] <$> newForeignPtr p'YGNodeFree ptr
-- | Collects a list of layouts and orients them from start to end depending
-- on the orientation of the parent (LTR vs RTL).
startToEnd :: [Layout a] -> Children a
startToEnd = StartToEnd
-- | Collects a list of layouts and orients them from end to start depending
-- on the orientation of the parent (LTR vs RTL).
endToStart :: [Layout a] -> Children a
endToStart = EndToStart
-- | Collects a list of children and centers them within the parent.
centered :: [Layout a] -> Children a
centered = Centered
-- | Collects a list of children that span the parent such that the space
-- between each child is equal
spaceBetween :: [Layout a] -> Children a
spaceBetween = SpaceBetween
-- | Collects a list of children that span the parent such that the space to the
-- left and right of each child is equal.
spaceAround :: [Layout a] -> Children a
spaceAround = SpaceAround
-- | Permits children to wrap to a new line if they exceed the bounds of their
-- parent
wrapped :: Children a -> Children a
wrapped (Wrap xs) = Wrap xs
wrapped childs = childs
justifiedContainer :: CUInt -> [Layout a] -> a -> Layout a
justifiedContainer just cs x = Layout $ do
ptr <- c'YGNodeNew
c'YGNodeStyleSetJustifyContent ptr just
c'YGNodeStyleSetFlexWrap ptr c'YGWrapNoWrap
cs' <- forM cs $ \node -> do
nodeTree <- generateLayout node
case nodeTree of
Root p children fptr -> withForeignPtr fptr $ \oldptr -> do
newptr <- c'YGNodeClone oldptr
c'YGNodeInsertChild ptr newptr 0
return $ if null children
then Leaf p
else Container p children
_ -> error "Internal: expected root node"
Root x cs' <$> newForeignPtr p'YGNodeFreeRecursive ptr
assembleChildren :: Children a -> a -> Layout a
assembleChildren (StartToEnd cs) x = justifiedContainer c'YGJustifyFlexStart cs x
assembleChildren (EndToStart cs) x = justifiedContainer c'YGJustifyFlexEnd cs x
assembleChildren (Centered cs) x = justifiedContainer c'YGJustifyCenter cs x
assembleChildren (SpaceBetween cs) x =
justifiedContainer c'YGJustifySpaceBetween cs x
assembleChildren (SpaceAround cs) x =
justifiedContainer c'YGJustifySpaceAround cs x
assembleChildren (Wrap cs) x = wrapContainer $ assembleChildren cs x
where
wrapContainer :: Layout a -> Layout a
wrapContainer lyt = Layout $ do
lytTree <- generateLayout lyt
withNativePtr lytTree $ \ptr ->
c'YGNodeStyleSetFlexWrap ptr c'YGWrapWrap
return lytTree
setContainerDirection :: CUInt -> CUInt -> LayoutTree a -> IO ()
setContainerDirection dir flexDir lyt =
withNativePtr lyt $ \ptr -> do
c'YGNodeStyleSetDirection ptr dir
c'YGNodeStyleSetFlexDirection ptr flexDir
-- | Generates a layout from a group of children and a payload such that the
-- children are laid out horizontally. The orientation (RTL vs LTR) is
-- inherited from the parent
hbox :: Children a -> a -> Layout a
hbox cs x = Layout $ do
node <- generateLayout $ assembleChildren cs x
setContainerDirection c'YGDirectionInherit c'YGFlexDirectionRow node
return node
-- | Generates a layout from a group of children and a payload such that the
-- children are laid out vertically. The orientation (top to bottom vs bottom to
-- top) is inherited from the parent.
vbox :: Children a -> a -> Layout a
vbox cs x = Layout $ do
node <- generateLayout $ assembleChildren cs x
setContainerDirection c'YGDirectionInherit c'YGFlexDirectionColumn node
return node
-- | Generates a layout from a group of children and a payload such that the
-- children are laid out horizontally from left to right.
hboxLeftToRight :: Children a -> a -> Layout a
hboxLeftToRight cs x = Layout $ do
node <- generateLayout $ assembleChildren cs x
setContainerDirection c'YGDirectionLTR c'YGFlexDirectionRow node
return node
-- | Generates a layout from a group of children and a payload such that the
-- children are laid out horizontally from right to left.
hboxRightToLeft :: Children a -> a -> Layout a
hboxRightToLeft cs x = Layout $ do
node <- generateLayout $ assembleChildren cs x
setContainerDirection c'YGDirectionRTL c'YGFlexDirectionRow node
return node
-- | Generates a layout from a group of children and a payload such that the
-- children are laid out vertically from top to bottom.
vboxTopToBottom :: Children a -> a -> Layout a
vboxTopToBottom cs x = Layout $ do
node <- generateLayout $ assembleChildren cs x
setContainerDirection c'YGDirectionLTR c'YGFlexDirectionColumn node
return node
-- | Generates a layout from a group of children and a payload such that the
-- children are laid out vertically from bottom to top.
vboxBottomToTop :: Children a -> a -> Layout a
vboxBottomToTop cs x = Layout $ do
node <- generateLayout $ assembleChildren cs x
setContainerDirection c'YGDirectionRTL c'YGFlexDirectionColumn node
return node
-- | A 'Size' is used to set properties about given layouts. In general, the
-- width and height of a node along with its position are laid out by Yoga's
-- internal layout engine. However, the user may decide to set limits on how
-- much internal nodes can shrink or grow. This datatype controls those
-- properties
data Size
= Exact Float
| Min Float
| Max Float
| Range Float Float
deriving (Read, Show, Eq, Ord)
setWidth :: Size -> LayoutTree a -> IO ()
setWidth (Exact w) lyt =
withNativePtr lyt $ \ptr -> c'YGNodeStyleSetWidth ptr $ realToFrac w
setWidth (Min w) lyt =
withNativePtr lyt $ \ptr -> c'YGNodeStyleSetMinWidth ptr $ realToFrac w
setWidth (Max w) lyt =
withNativePtr lyt $ \ptr -> c'YGNodeStyleSetMaxWidth ptr $ realToFrac w
setWidth (Range minWidth maxWidth) lyt =
withNativePtr lyt $ \ptr -> do
c'YGNodeStyleSetMinWidth ptr $ realToFrac minWidth
c'YGNodeStyleSetMaxWidth ptr $ realToFrac maxWidth
setHeight :: Size -> LayoutTree a -> IO ()
setHeight (Exact h) lyt =
withNativePtr lyt $ \ptr -> c'YGNodeStyleSetHeight ptr $ realToFrac h
setHeight (Min h) lyt =
withNativePtr lyt $ \ptr -> c'YGNodeStyleSetMinHeight ptr $ realToFrac h
setHeight (Max h) lyt =
withNativePtr lyt $ \ptr -> c'YGNodeStyleSetMaxHeight ptr $ realToFrac h
setHeight (Range minHeight maxHeight) lyt =
withNativePtr lyt $ \ptr -> do
c'YGNodeStyleSetMinHeight ptr $ realToFrac minHeight
c'YGNodeStyleSetMaxHeight ptr $ realToFrac maxHeight
-- | Specifies layout may shrink up to the given size. The weight parameter
-- is used to determine how much this layout will shrink in relation to any
-- siblings.
shrinkable :: Float -> Size -> Size -> Layout a -> Layout a
shrinkable weight width height lyt = Layout $ do
n <- generateLayout lyt
setWidth width n
setHeight height n
withNativePtr n $ \ptr -> c'YGNodeStyleSetFlexShrink ptr $ realToFrac weight
return n
-- | Specifies layout may grow up to the given size. The weight parameter
-- is used to determine how much this layout will grow in relation to any
-- siblings.
growable :: Float -> Size -> Size -> Layout a -> Layout a
growable weight width height lyt = Layout $ do
n <- generateLayout lyt
setWidth width n
setHeight height n
withNativePtr n $ \ptr -> c'YGNodeStyleSetFlexGrow ptr $ realToFrac weight
return n
-- | Creates a layout with the exact width and height for the given payload.
exact :: Float -> Float -> a -> Layout a
exact width height x = Layout $ do
n <- generateLayout $ mkNode x
setWidth (Exact width) n
setHeight (Exact height) n
return n
-- | Specifies the exact dimensions expected for a layout. Can be used for
-- containers and such when there is not necessarily any rendering involved.
withDimensions :: Float -> Float -> Layout b -> Layout b
withDimensions width height lyt = Layout $ do
n <- generateLayout lyt
setWidth (Exact width) n
setHeight (Exact height) n
return n
-- | Allows a container to stretch to fit its parent
stretched :: Layout a -> Layout a
stretched lyt = Layout $ do
node <- generateLayout lyt
withNativePtr node $ \ptr -> c'YGNodeStyleSetAlignSelf ptr c'YGAlignStretch
return node
-- | Edges are used to describe the direction from which we want to alter an
-- attribute of a node.
data Edge
= Edge'Left
| Edge'Top
| Edge'Right
| Edge'Bottom
| Edge'Start
| Edge'End
| Edge'Horizontal
| Edge'Vertical
| Edge'All
deriving (Eq, Ord, Bounded, Enum, Read, Show)
edgeToCEdge :: Edge -> CUInt
edgeToCEdge Edge'Left = c'YGEdgeLeft
edgeToCEdge Edge'Top = c'YGEdgeTop
edgeToCEdge Edge'Right = c'YGEdgeRight
edgeToCEdge Edge'Bottom = c'YGEdgeBottom
edgeToCEdge Edge'Start = c'YGEdgeStart
edgeToCEdge Edge'End = c'YGEdgeEnd
edgeToCEdge Edge'Horizontal = c'YGEdgeHorizontal
edgeToCEdge Edge'Vertical = c'YGEdgeVertical
edgeToCEdge Edge'All = c'YGEdgeAll
-- | Overrides the margin for a layout with the given margin.
setMargin :: Edge -> Float -> Layout a -> Layout a
setMargin = setMargin' . edgeToCEdge
where
setMargin' edge px lyt = Layout $ do
node <- generateLayout lyt
withNativePtr node $ \ptr ->
c'YGNodeStyleSetMargin ptr edge $ realToFrac px
return node
-- | Overrides the padding for a layout with the given padding.
setPadding :: Edge -> Float -> Layout a -> Layout a
setPadding = setPadding' . edgeToCEdge
where
setPadding' edge px lyt = Layout $ do
node <- generateLayout lyt
withNativePtr node $ \ptr ->
c'YGNodeStyleSetPadding ptr edge $ realToFrac px
return node
-- | Overrides the border for a layout with the given border.
setBorder :: Edge -> Float -> Layout a -> Layout a
setBorder = setBorder' . edgeToCEdge
where
setBorder' edge px lyt = Layout $ do
node <- generateLayout lyt
withNativePtr node $ \ptr ->
c'YGNodeStyleSetBorder ptr edge $ realToFrac px
return node
-- | Gutters are used to denote the size of a gap between elements.
data Gutter
= Gutter'Column
| Gutter'Row
| Gutter'All
deriving (Eq, Ord, Bounded, Enum, Read, Show)
-- | Overrides the gaps for a layout with the given gap size.
setGap :: Gutter -> Float -> Layout a -> Layout a
setGap = setGap' . gapToCGap
where
gapToCGap Gutter'Column = c'YGGutterColumn
gapToCGap Gutter'Row = c'YGGutterRow
gapToCGap Gutter'All = c'YGGutterAll
setGap' gutter px lyt = Layout $ do
node <- generateLayout lyt
withNativePtr node $ \ptr ->
c'YGNodeStyleSetGap ptr gutter $ realToFrac px
return node
--------------------------------------------------------------------------------
-- Rendering
-- | Stores the calculated layout information for a given node. During
-- rendering, the rendering function will take the payload and layout info to
-- facilitate the renderer to do whatever it needs to with the given layout
-- calculations.
data LayoutInfo = LayoutInfo {
nodeTop :: Float, -- ^ The y-coordinate of this node
nodeLeft :: Float, -- ^ The x-coordinate of this node
nodeWidth :: Float, -- ^ The width of this node
nodeHeight :: Float -- ^ The height of this node
} deriving (Eq, Show)
emptyInfo :: LayoutInfo
emptyInfo = LayoutInfo 0 0 0 0
layoutWithParent :: LayoutInfo -> LayoutInfo -> LayoutInfo
layoutWithParent parent child =
let (x, y) = (nodeLeft parent, nodeTop parent)
(x', y') = (nodeLeft child, nodeTop child)
in child { nodeLeft = x + x', nodeTop = y + y' }
-- | A 'RenderFn' takes a top-left position and a width and height of a node
-- with the given payload. The function is expected to perform some monadic
-- action in the 'Monad' m, and return a new payload of type b. This function is
-- called on each node in order during a call to render.
type RenderFn m a b = LayoutInfo -> a -> m b
calculateLayout :: Ptr C'YGNode -> IO ()
calculateLayout ptr =
let n = (nan :: CFloat)
in c'YGNodeStyleGetDirection ptr >>= c'YGNodeCalculateLayout ptr n n
layoutInfo :: Ptr C'YGNode -> IO LayoutInfo
layoutInfo ptr = do
left <- realToFrac <$> c'YGNodeLayoutGetLeft ptr
top <- realToFrac <$> c'YGNodeLayoutGetTop ptr
width <- realToFrac <$> c'YGNodeLayoutGetWidth ptr
height <- realToFrac <$> c'YGNodeLayoutGetHeight ptr
return $ LayoutInfo top left width height
renderNodeWithChildren :: (MonadIO m, Monoid b) =>
LayoutInfo -> a -> [LayoutTree a] -> Ptr C'YGNode
-> RenderFn m a (b, c)
-> m (b, c, [LayoutTree c])
renderNodeWithChildren parentInfo x children ptr f = do
info <- liftIO $ layoutInfo ptr
let thisInfo = layoutWithParent parentInfo info
(m, y) <- f thisInfo x
cs <- forM (zip children [0..]) $ \(child, childIdx) -> do
childPtr <- liftIO $ c'YGNodeGetChild ptr childIdx
foldRenderTree thisInfo child childPtr f
return (mappend m . foldr (mappend . fst) mempty $ cs, y, map snd cs)
foldRenderTree :: (MonadIO m, Monoid b) =>
LayoutInfo -> LayoutTree a -> Ptr C'YGNode -> RenderFn m a (b, c) ->
m (b, LayoutTree c)
foldRenderTree parentInfo (Root x children fptr) ptr f = do
(result, y, cs) <- renderNodeWithChildren parentInfo x children ptr f
return (result, Root y cs fptr)
foldRenderTree parentInfo (Container x children) ptr f = do
(result, y, cs) <- renderNodeWithChildren parentInfo x children ptr f
return (result, Container y cs)
foldRenderTree parentInfo (Leaf x) ptr f = do
(result, y, cs) <- renderNodeWithChildren parentInfo x [] ptr f
return $ cs `seq` (result, Leaf y)
-- | Renders a layout with the user-supplied function. For each return value
-- of type '(b, c)', we append the first result to the output of the previous
-- node. The second result is stored as the new payload for the given node.
-- Hence, the resulting monadic action produces a 'mappend'-ed set of 'b's and
-- a new layout with payloads of type 'c'.
foldRender :: (MonadIO m, Monoid b) =>
Layout a -> RenderFn m a (b, c) -> m (b, Layout c)
foldRender lyt f = do
node <- liftIO $ generateLayout lyt
case node of
Root _ _ fptr -> do
rootPtr <- liftIO $ withForeignPtr fptr $ \ptr -> do
calculateLayout ptr
return ptr
(bs, tree) <- foldRenderTree emptyInfo node rootPtr f
return (bs, Layout $ return tree)
_ -> error "Internal: Rendering must be done from the root node"
-- | Renders a layout with the user-supplied function. The renderer traverses
-- the tree from root node to children and transforms each payload using the
-- user-supplied function.
render :: MonadIO m => Layout a -> RenderFn m a b -> m (Layout b)
render lyt f =
let f' lytInfo x = (() ,) <$> f lytInfo x
in snd <$> foldRender lyt f'