geomancy-layout 0.1 → 0.1.1
raw patch · 9 files changed
+1136/−15 lines, 9 filesdep +geomancy-layoutdep +lucid-svgdep +showerPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: geomancy-layout, lucid-svg, shower, tasty, tasty-hunit
API changes (from Hackage documentation)
- Geomancy.Layout.Alignment: type Alignment = Vec2
+ Geomancy.Layout.Alignment: Alignment :: Vec2 -> Alignment
+ Geomancy.Layout.Alignment: instance GHC.Classes.Eq Geomancy.Layout.Alignment.Alignment
+ Geomancy.Layout.Alignment: instance GHC.Classes.Ord Geomancy.Layout.Alignment.Alignment
+ Geomancy.Layout.Alignment: instance GHC.Num.Num Geomancy.Layout.Alignment.Alignment
+ Geomancy.Layout.Alignment: instance GHC.Show.Show Geomancy.Layout.Alignment.Alignment
+ Geomancy.Layout.Alignment: newtype Alignment
+ Geomancy.Layout.Box: height :: Box -> Float
+ Geomancy.Layout.Box: width :: Box -> Float
+ Geomancy.Layout.View: ($dmplaceView) :: (LayoutView stuff, ReportedCache stuff ~ (Box -> Placed stuff)) => Box -> ReportedCache stuff -> Placed stuff
+ Geomancy.Layout.View: ($dmproposeView) :: (LayoutView stuff, ReportedCache stuff ~ stuff) => Vec2 -> stuff -> (Vec2, ReportedCache stuff)
+ Geomancy.Layout.View: ($dmviewFlexibility) :: LayoutView stuff => stuff -> ViewSize
+ Geomancy.Layout.View: AspectRatio :: ann -> Vec2 -> Alignment -> View ann stuff -> View ann stuff
+ Geomancy.Layout.View: FlexibleFrame :: ann -> ViewSize -> Alignment -> View ann stuff -> View ann stuff
+ Geomancy.Layout.View: Frame :: ann -> Vec2 -> Alignment -> View ann stuff -> View ann stuff
+ Geomancy.Layout.View: HStack :: ann -> [View ann stuff] -> View ann stuff
+ Geomancy.Layout.View: Offset :: ann -> Vec2 -> View ann stuff -> View ann stuff
+ Geomancy.Layout.View: Overlay :: ann -> View ann stuff -> View ann stuff -> View ann stuff
+ Geomancy.Layout.View: Padding :: ann -> TRBL -> View ann stuff -> View ann stuff
+ Geomancy.Layout.View: Spacer :: ann -> View ann stuff
+ Geomancy.Layout.View: VStack :: ann -> [View ann stuff] -> View ann stuff
+ Geomancy.Layout.View: View :: ann -> stuff -> View ann stuff
+ Geomancy.Layout.View: ViewFun :: ViewSize -> (Vec2 -> (Vec2, Box -> stuff)) -> ViewFun stuff
+ Geomancy.Layout.View: ViewSize :: Maybe Float -> Maybe Float -> Maybe Float -> Maybe Float -> ViewSize
+ Geomancy.Layout.View: ZStack :: ann -> [View ann stuff] -> View ann stuff
+ Geomancy.Layout.View: [align] :: View ann stuff -> Alignment
+ Geomancy.Layout.View: [ann] :: View ann stuff -> ann
+ Geomancy.Layout.View: [aspect] :: View ann stuff -> Vec2
+ Geomancy.Layout.View: [children] :: View ann stuff -> [View ann stuff]
+ Geomancy.Layout.View: [flex] :: View ann stuff -> ViewSize
+ Geomancy.Layout.View: [inner] :: View ann stuff -> View ann stuff
+ Geomancy.Layout.View: [maxHeight] :: ViewSize -> Maybe Float
+ Geomancy.Layout.View: [maxWidth] :: ViewSize -> Maybe Float
+ Geomancy.Layout.View: [minHeight] :: ViewSize -> Maybe Float
+ Geomancy.Layout.View: [minWidth] :: ViewSize -> Maybe Float
+ Geomancy.Layout.View: [primary] :: View ann stuff -> View ann stuff
+ Geomancy.Layout.View: [secondary] :: View ann stuff -> View ann stuff
+ Geomancy.Layout.View: [size] :: View ann stuff -> Vec2
+ Geomancy.Layout.View: [stuff] :: View ann stuff -> stuff
+ Geomancy.Layout.View: [trbl] :: View ann stuff -> TRBL
+ Geomancy.Layout.View: [xy] :: View ann stuff -> Vec2
+ Geomancy.Layout.View: aspect_ :: Vec2 -> View () stuff -> View () stuff
+ Geomancy.Layout.View: boxFun :: (Box -> stuff) -> View () (ViewFun stuff)
+ Geomancy.Layout.View: class LayoutView stuff where {
+ Geomancy.Layout.View: combineH :: Vec2 -> Vec2 -> Vec2
+ Geomancy.Layout.View: combineV :: Vec2 -> Vec2 -> Vec2
+ Geomancy.Layout.View: data View ann stuff
+ Geomancy.Layout.View: data ViewFun stuff
+ Geomancy.Layout.View: data ViewSize
+ Geomancy.Layout.View: fixed_ :: Vec2 -> ViewSize
+ Geomancy.Layout.View: flexible_ :: ViewSize -> Alignment -> View () stuff -> View () stuff
+ Geomancy.Layout.View: foldWith :: (LayoutView stuff, Monoid a) => (Placed stuff -> a) -> Box -> View () stuff -> a
+ Geomancy.Layout.View: frame_ :: Vec2 -> View () stuff -> View () stuff
+ Geomancy.Layout.View: heightFlexibility :: ViewSize -> Float
+ Geomancy.Layout.View: hstack_ :: [View () stuff] -> View () stuff
+ Geomancy.Layout.View: inf :: Float
+ Geomancy.Layout.View: infinite_ :: ViewSize
+ Geomancy.Layout.View: instance (GHC.Classes.Eq stuff, GHC.Classes.Eq ann) => GHC.Classes.Eq (Geomancy.Layout.View.View ann stuff)
+ Geomancy.Layout.View: instance (GHC.Show.Show stuff, GHC.Show.Show ann) => GHC.Show.Show (Geomancy.Layout.View.View ann stuff)
+ Geomancy.Layout.View: instance (Geomancy.Layout.View.LayoutView l, Geomancy.Layout.View.LayoutView r) => Geomancy.Layout.View.LayoutView (Data.Either.Either l r)
+ Geomancy.Layout.View: instance Data.Bifunctor.Bifunctor Geomancy.Layout.View.View
+ Geomancy.Layout.View: instance Data.Foldable.Foldable (Geomancy.Layout.View.View ann)
+ Geomancy.Layout.View: instance Data.Traversable.Traversable (Geomancy.Layout.View.View ann)
+ Geomancy.Layout.View: instance GHC.Base.Functor (Geomancy.Layout.View.View ann)
+ Geomancy.Layout.View: instance GHC.Base.Functor Geomancy.Layout.View.ViewFun
+ Geomancy.Layout.View: instance GHC.Classes.Eq Geomancy.Layout.View.ViewSize
+ Geomancy.Layout.View: instance GHC.Show.Show Geomancy.Layout.View.ViewSize
+ Geomancy.Layout.View: instance Geomancy.Layout.View.LayoutView (Data.Functor.Identity.Identity stuff)
+ Geomancy.Layout.View: instance Geomancy.Layout.View.LayoutView (Geomancy.Layout.View.ViewFun stuff)
+ Geomancy.Layout.View: layout :: LayoutView stuff => Box -> View () stuff -> View Box (Placed stuff)
+ Geomancy.Layout.View: max2 :: Vec2 -> Vec2 -> Vec2
+ Geomancy.Layout.View: maybeBoth :: t1 -> (t2 -> t3 -> t1) -> Maybe t2 -> Maybe t3 -> t1
+ Geomancy.Layout.View: measure :: LayoutView stuff => View () stuff -> View ViewSize stuff
+ Geomancy.Layout.View: offset_ :: Vec2 -> View () stuff -> View () stuff
+ Geomancy.Layout.View: overlay_ :: View () stuff -> View () stuff -> View () stuff
+ Geomancy.Layout.View: padding_ :: Vec4 -> View () stuff -> View () stuff
+ Geomancy.Layout.View: parentHeight :: ViewSize -> ViewSize
+ Geomancy.Layout.View: parentWidth :: ViewSize -> ViewSize
+ Geomancy.Layout.View: parent_ :: ViewSize
+ Geomancy.Layout.View: place :: LayoutView stuff => Box -> View ViewSize stuff -> View Box (Placed stuff)
+ Geomancy.Layout.View: placeView :: LayoutView stuff => Box -> ReportedCache stuff -> Placed stuff
+ Geomancy.Layout.View: proposeView :: LayoutView stuff => Vec2 -> stuff -> (Vec2, ReportedCache stuff)
+ Geomancy.Layout.View: range_ :: Vec2 -> Vec2 -> ViewSize
+ Geomancy.Layout.View: spacer_ :: View () stuff
+ Geomancy.Layout.View: suspend :: LayoutView stuff => Vec2 -> View ViewSize stuff -> (Vec2, Box -> View Box (Placed stuff))
+ Geomancy.Layout.View: type Placed stuff = stuff;
+ Geomancy.Layout.View: type ReportedCache stuff = stuff;
+ Geomancy.Layout.View: viewFlexibility :: LayoutView stuff => stuff -> ViewSize
+ Geomancy.Layout.View: viewFun :: ViewSize -> (Vec2 -> (Vec2, Box -> stuff)) -> View () (ViewFun stuff)
+ Geomancy.Layout.View: view_ :: stuff -> View () stuff
+ Geomancy.Layout.View: vsClamp :: Vec2 -> ViewSize -> Vec2 -> Vec2
+ Geomancy.Layout.View: vstack_ :: [View () stuff] -> View () stuff
+ Geomancy.Layout.View: widthFlexibility :: ViewSize -> Float
+ Geomancy.Layout.View: zstack_ :: [View () stuff] -> View () stuff
+ Geomancy.Layout.View: }
Files
- CHANGELOG.md +4/−0
- README.md +1/−0
- geomancy-layout.cabal +35/−3
- src/Geomancy/Layout.hs +2/−2
- src/Geomancy/Layout/Alignment.hs +12/−10
- src/Geomancy/Layout/Box.hs +8/−0
- src/Geomancy/Layout/View.hs +635/−0
- test/Examples.hs +419/−0
- test/Spec.hs +20/−0
CHANGELOG.md view
@@ -1,3 +1,7 @@+## 0.1.1++- Added `Geomancy.Layout.View` - an experimental SwiftUI-like propose-report-place protocol.+ ## 0.1 Initial import.
README.md view
@@ -5,5 +5,6 @@ * `Geomancy.Layout` - box cutting and stacking. * `Geomancy.Layout.Box` - size/position and top/right/bottom/left boxes and manipulation. * `Geomancy.Layout.Alignment` - distribute size leftovers proportionally.+* `Geomancy.Layout.View` - SwiftUI-like containers. [geomancy]: https://hackage.haskell.org/package/geomancy
geomancy-layout.cabal view
@@ -1,17 +1,17 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.2.+-- This file has been generated from package.yaml by hpack version 0.37.0. -- -- see: https://github.com/sol/hpack name: geomancy-layout-version: 0.1+version: 0.1.1 synopsis: Geometry and matrix manipulation description: Layout primitives and algorithms for geomancy data. category: Graphics author: IC Rainbow maintainer: aenor.realm@gmail.com-copyright: 2023 IC Rainbow+copyright: 2025 IC Rainbow license: BSD3 license-file: LICENSE build-type: Simple@@ -28,6 +28,7 @@ Geomancy.Layout Geomancy.Layout.Alignment Geomancy.Layout.Box+ Geomancy.Layout.View other-modules: Paths_geomancy_layout hs-source-dirs:@@ -42,9 +43,40 @@ OverloadedRecordDot PatternSynonyms RecordWildCards+ TypeFamilies ghc-options: -Wall build-depends: base >=4.7 && <5 , geomancy >=0.2.6 && <1 , gl-block >=1.0 && <2+ default-language: GHC2021++test-suite geomancy-layout-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ Examples+ Paths_geomancy_layout+ hs-source-dirs:+ test+ default-extensions:+ BlockArguments+ DeriveAnyClass+ DerivingStrategies+ DerivingVia+ ImportQualifiedPost+ LambdaCase+ OverloadedRecordDot+ PatternSynonyms+ RecordWildCards+ TypeFamilies+ ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -with-rtsopts=-qn1 -with-rtsopts=-A128m+ build-depends:+ base >=4.7 && <5+ , geomancy >=0.2.6 && <1+ , geomancy-layout+ , lucid-svg+ , shower+ , tasty+ , tasty-hunit default-language: GHC2021
src/Geomancy/Layout.hs view
@@ -2,7 +2,7 @@ import Geomancy -import Geomancy.Layout.Alignment (Alignment, Origin)+import Geomancy.Layout.Alignment (Alignment(..), Origin) import Geomancy.Layout.Alignment qualified as Alignment import Geomancy.Layout.Box (Box(..), TRBL(..)) import Geomancy.Layout.Box qualified as Box@@ -194,7 +194,7 @@ {-# INLINEABLE placeSize #-} placeSize :: Alignment -> Vec2 -> Box -> Box-placeSize align size parent =+placeSize (Alignment align) size parent = withVec2 align \ah av -> withVec2 size \w h -> withVec2 parent.size \pw ph ->
src/Geomancy/Layout/Alignment.hs view
@@ -3,34 +3,36 @@ import Geomancy -- | @left/center/right@ & @top/middle/bottom@-type Alignment = Vec2+newtype Alignment = Alignment Vec2+ deriving (Eq, Ord, Show)+ deriving newtype (Num) leftTop :: Alignment-leftTop = vec2 Begin Begin+leftTop = Alignment $ vec2 Begin Begin leftMiddle :: Alignment-leftMiddle = vec2 Begin Middle+leftMiddle = Alignment $ vec2 Begin Middle leftBottom :: Alignment-leftBottom = vec2 Begin End+leftBottom = Alignment $ vec2 Begin End centerTop :: Alignment-centerTop = vec2 Middle Begin+centerTop = Alignment $ vec2 Middle Begin center :: Alignment-center = vec2 Middle Middle+center = Alignment $ vec2 Middle Middle centerBottom :: Alignment-centerBottom = vec2 Middle End+centerBottom = Alignment $ vec2 Middle End rightTop :: Alignment-rightTop = vec2 End Begin+rightTop = Alignment $ vec2 End Begin rightMiddle :: Alignment-rightMiddle = vec2 End Middle+rightMiddle = Alignment $ vec2 End Middle rightBottom :: Alignment-rightBottom = vec2 End End+rightBottom = Alignment $ vec2 End End type Origin = Float
src/Geomancy/Layout/Box.hs view
@@ -29,6 +29,12 @@ deriving anyclass Block.Block deriving Foreign.Storable via (Block.Packed Box) +width :: Box -> Float+width Box{size} = withVec2 size \w _ -> w++height :: Box -> Float+height Box{size} = withVec2 size \_ h -> h+ -- | Place a 'Box' with given dimensions at @(0,0)@. {-# INLINE box_ #-} box_ :: Vec2 -> Box@@ -37,6 +43,8 @@ instance Semigroup Box where {-# INLINE (<>) #-} (<>) = union++-- XXX: Box is not a Monoid because mempty = box_ 0 would add (0,0) to every box. -- | Check if one of the dimensions is negative. {-# INLINE degenerate #-}
+ src/Geomancy/Layout/View.hs view
@@ -0,0 +1,635 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DefaultSignatures #-}++{- | SwiftUI-like view tree++The layout keeps the propose-report-place protocol, but chops some parts away.++* Views are abstracted and the 'View' constructor punches a hole in a tree (a hollow?) for a UI toolkit to fill in later.+* There's no "tree builder", so containers and modifiers are constructed directly. Although it is easy to recover modifier with the "Data.Function.(&)" operator.+* There's no "ideal size", "fixed size" etc. and the views are fully flexible. The UI tookit wrappers can instead self-wrap their Views in a 'Frame'/'FlexibleFrame' as needed.+-}++module Geomancy.Layout.View where++import Data.Bifunctor (Bifunctor(..))+import Data.Foldable (toList)+import Data.Foldable1 (foldMap1')+import Data.Functor.Identity (Identity)+import Data.List (mapAccumL, foldl', sortOn)+import Data.List.NonEmpty (nonEmpty)+import Geomancy ((^*))+import Geomancy.Gl.Funs (glClamp)+import Geomancy.Layout qualified as Layout+import Geomancy.Layout.Alignment (Alignment)+import Geomancy.Layout.Alignment qualified as Alignment+import Geomancy.Layout.Box (Box(..))+import Geomancy.Layout.Box qualified as Box+import Geomancy.Vec2 (Vec2, vec2, withVec2, pattern WithVec2)+import Geomancy.Vec4 (Vec4, withVec4)+import GHC.Arr (array)++-- | Run all the layout steps to annotate the nodes with their final placement boxes.+layout :: LayoutView stuff => Box -> View () stuff -> View Box (Placed stuff)+layout initialBox = place initialBox . measure++-- | Do all of the layout steps and fold placed views+--+-- Mostly an example. The better way is to cache intermediate steps.+--+-- (Actually a pun on "fold views".)+foldWith :: (LayoutView stuff, Monoid a) => (Placed stuff -> a) -> Box -> View () stuff -> a+foldWith f initialBox = foldMap f . place initialBox . measure++{- | An interface to plug UI toolkits into the layout.++Alternatively, convert them to `ViewFun`, which has the instance.++The "minimal: none" is a lie. Most likely you will need to adjust either 'ReportedCache ' or 'Placed' types and implement the other function accordingly.++The most gnarly code would be for "rendered text" widgets since they would be dynamically sized and have to be processed/cached on each step.+-}+class LayoutView stuff where+ -- | Measurement pre-pass+ viewFlexibility :: stuff -> ViewSize++ -- XXX: argument order is optimized for \case++ -- | Propose step: return the actual size and store intermediate data.+ proposeView :: Vec2 -> stuff -> (Vec2, ReportedCache stuff)+ type ReportedCache stuff+ type ReportedCache stuff = stuff++ -- | Placement step: get the reported size augmented with a final position, produce the result.+ placeView :: Box -> ReportedCache stuff -> Placed stuff+ type Placed stuff+ type Placed stuff = stuff++ -- | Default implementation is "the view *will* adapt to any size"+ default viewFlexibility :: stuff -> ViewSize+ viewFlexibility = const infinite_++ -- | Default implementation for "accept everything" reporting+ default proposeView :: (ReportedCache stuff ~ stuff) => Vec2 -> stuff -> (Vec2, ReportedCache stuff)+ proposeView = (,)++ -- | Default implementation for "box function" placement+ default placeView :: (ReportedCache stuff ~ (Box -> Placed stuff)) => Box -> ReportedCache stuff -> Placed stuff+ placeView placed f = f placed++-- | "Variant-lite" instance to mix widgets of different type+--+-- XXX: the foldable/traversable would skip the Lefts.+-- Use something like `foldWith` to pre-process and normalize to a common "backend" type+instance (LayoutView l, LayoutView r) => LayoutView (Either l r) where+ type ReportedCache (Either l r) = Either (ReportedCache l) (ReportedCache r)+ type Placed (Either l r) = Either (Placed l) (Placed r)+ viewFlexibility = either viewFlexibility viewFlexibility+ proposeView proposed = either (fmap Left <$> proposeView proposed) (fmap Right <$> proposeView proposed)+ placeView placed = \case+ Left x -> Left $ placeView @l placed x+ Right x -> Right $ placeView @r placed x++{- | Do-nothing pass-through instance++You'll have to peek at the @View{ann}@ of the placement tree.+-}+instance LayoutView (Identity stuff) where+ viewFlexibility = const infinite_+ proposeView = (,)+ placeView _placed = id++{- | A model type for the protocol++The fields correspond to the 3 steps:++* Measurement into ViewSize - static.+* Proposing and reporting - depends on parent size / caches size-related data.+* Placement - depends on placement ('Box' = reported size + position), can use cached data.+-}+data ViewFun stuff = ViewFun ViewSize (Vec2 -> (Vec2, Box -> stuff))+ deriving (Functor)++-- | A wrapper to construct 'ViewFun'-based views+viewFun :: ViewSize -> (Vec2 -> (Vec2, Box -> stuff)) -> View () (ViewFun stuff)+viewFun flex steps = View{ann=(), stuff=ViewFun flex steps}++-- | A wrapper to construct 'ViewFun'-based views by assuming infinite flex and no intrinsic size+boxFun :: (Box -> stuff) -> View () (ViewFun stuff)+boxFun f = viewFun infinite_ (,f)++instance LayoutView (ViewFun stuff) where+ type ReportedCache (ViewFun stuff) = Box -> stuff -- ^ Propose to resolve the outer+ type Placed (ViewFun stuff) = stuff+ viewFlexibility (ViewFun vs _) = vs+ proposeView size (ViewFun _ f) = f size+ placeView placed f = f placed++-- * View tree++{- | Layout skeleton++It is Functor-Foldable-Traversable on 'View' so you can use things like 'Control.Monad.void' to suppress payload and 'show' the intermediate structure.+-}+data View ann stuff+ = View+ { ann :: ann+ , stuff :: stuff+ } -- ^ UI-specific payload+ | Spacer+ { ann :: ann+ }++ -- "containers"+ | HStack+ { ann :: ann+ -- TODO: placement direction+ -- TODO: spacing to inject+ , children :: [View ann stuff]+ } -- ^ Place children side-by-side (LTR)+ | VStack+ { ann :: ann+ -- TODO: placement direction+ -- TODO: spacing to inject+ , children :: [View ann stuff]+ } -- ^ Place children top-to-bottom+ | ZStack+ { ann :: ann+ , children :: [View ann stuff]+ } -- ^ Place children atop of each other+ | Overlay+ { ann :: ann+ , secondary :: View ann stuff+ , primary :: View ann stuff+ } -- ^ Propose the size of the primary view to the secondary view++ -- "modifiers"+ | Frame+ { ann :: ann+ , size :: Vec2+ , align :: Alignment+ , inner :: View ann stuff+ } -- ^ Propose/report size unconditionally+ | FlexibleFrame+ { ann :: ann+ , flex :: ViewSize -- ^ Acceptable size ranges+ , align :: Alignment+ , inner :: View ann stuff+ } -- ^ Adjust measurement flex and clamp proposed/reported size+ | AspectRatio+ { ann :: ann+ , aspect :: Vec2 -- ^ Like 'Ratio Float', for example @vec2 16 9@ or @1@ (= @vec2 1 1@, i.e. square)+ , align :: Alignment+ -- TODO: fit/crop+ , inner :: View ann stuff+ } -- ^ Frame-like wrapper that fits+ | Padding+ { ann :: ann+ , trbl :: Box.TRBL+ , inner :: View ann stuff+ } -- ^ Subtracts a CSS-tyle padding (top/right/bottom/left) from parent+ | Offset+ { ann :: ann+ , xy :: Vec2+ , inner :: View ann stuff+ } -- ^ Adds offset when placing a view+ deriving (Eq, Show, Functor, Foldable, Traversable)++instance Bifunctor View where+ first f = \case+ View{..} -> View{ann = f ann, stuff}+ Spacer{..} -> Spacer{ann = f ann}+ HStack{ann, children} -> HStack{ann = f ann, children = map (first f) children}+ VStack{ann, children} -> VStack{ann = f ann, children = map (first f) children}+ ZStack{ann, children} -> ZStack{ann = f ann, children = map (first f) children}+ Overlay{ann, primary, secondary} -> Overlay{ann = f ann, primary = first f primary, secondary = first f secondary}+ Frame{..} -> Frame{ann = f ann, inner = first f inner, ..}+ FlexibleFrame{..} -> FlexibleFrame{ann = f ann, inner = first f inner, ..}+ Padding{..} -> Padding{ann = f ann, inner = first f inner, ..}+ Offset{..} -> Offset{ann = f ann, inner = first f inner, ..}+ AspectRatio{..} -> AspectRatio{ann = f ann, inner = first f inner, ..}+ second = fmap++-- ** Some shortcuts++view_ :: stuff -> View () stuff+view_ stuff = View{ann=(), ..}++spacer_ :: View () stuff+spacer_ = Spacer{ann=()}++-- *** Containers++hstack_ :: [View () stuff] -> View () stuff+hstack_ children = HStack{ann=(), ..}++vstack_ :: [View () stuff] -> View () stuff+vstack_ children = VStack{ann=(), ..}++zstack_ :: [View () stuff] -> View () stuff+zstack_ children = ZStack{ann=(), ..}++overlay_ :: View () stuff -> View () stuff -> View () stuff+overlay_ secondary primary = Overlay{ann=(), ..}++-- *** Modifiers++frame_ :: Vec2 -> View () stuff -> View () stuff+frame_ size inner = Frame{ann=(), align = Alignment.center, ..}++flexible_ :: ViewSize -> Alignment -> View () stuff -> View () stuff+flexible_ flex align inner = FlexibleFrame{ann=(), ..}++aspect_ :: Vec2 -> View () stuff -> View () stuff+aspect_ ratio inner = AspectRatio{ann=(), aspect=ratio, align=Alignment.center, ..}++padding_ :: Vec4 -> View () stuff -> View () stuff+padding_ trbl inner = Padding{ann=(), trbl=Box.TRBL trbl, ..}++offset_ :: Vec2 -> View () stuff -> View () stuff+offset_ xy inner = Offset{ann=(), ..}++-- * Flexibility helper++-- | Represents size constraints and flexibility for a layout node.+data ViewSize = ViewSize+ { minWidth, maxWidth+ , minHeight, maxHeight :: Maybe Float+ -- XXX: put ideal size here?+ } deriving (Eq, Show)++-- | Creates a ViewSize for a fixed-size view.+fixed_ :: Vec2 -> ViewSize+fixed_ size =+ withVec2 size \w h ->+ ViewSize+ { minWidth = Just w+ , maxWidth = Just w+ , minHeight = Just h+ , maxHeight = Just h+ }++-- | Creates a ViewSize for a fully flexible view.+infinite_ :: ViewSize+infinite_ = ViewSize+ { minWidth = Just 0+ , maxWidth = Just inf+ , minHeight = Just 0+ , maxHeight = Just inf+ }++range_ :: Vec2 -> Vec2 -> ViewSize+range_ a b =+ withVec2 a \aw ah ->+ withVec2 b \bw bh ->+ ViewSize+ { minWidth = Just $ min aw bw+ , maxWidth = Just $ max aw bw+ , minHeight = Just $ min ah bh+ , maxHeight = Just $ max ah bh+ }++-- | Creates a ViewSize that reports parent size+parent_ :: ViewSize+parent_ = ViewSize+ { minWidth = Nothing+ , maxWidth = Nothing+ , minHeight = Nothing+ , maxHeight = Nothing+ }++parentWidth :: ViewSize -> ViewSize+parentWidth a = a {minWidth = Nothing, maxWidth = Nothing}++parentHeight :: ViewSize -> ViewSize+parentHeight a = a {minHeight = Nothing, maxHeight = Nothing}++-- * Layout steps++-- | Measurement pre-pass+measure :: LayoutView stuff => View () stuff -> View ViewSize stuff+measure = \case+ View {stuff} -> View{ann=viewFlexibility stuff, stuff}+ Spacer{} -> Spacer{ann=infinite_}+ Frame{..} -> Frame{ann=fixed_ size, inner=measure inner, ..}+ FlexibleFrame{..} -> FlexibleFrame{ann=flex', inner=inner', ..}+ where+ inner' = measure inner+ innerFlex = inner'.ann+ flex' = ViewSize -- intersection of flexibility ranges+ { minWidth = max <$> flex.minWidth <*> innerFlex.minWidth+ , maxWidth = min <$> flex.maxWidth <*> innerFlex.maxWidth+ , minHeight = max <$> flex.minHeight <*> innerFlex.minHeight+ , maxHeight = min <$> flex.maxHeight <*> innerFlex.maxHeight+ }+ Padding{..} -> Padding{ann=expanded, inner=inner', ..}+ where+ inner' = measure inner+ innerFlex = inner'.ann+ expanded = withVec4 trbl4 \t r b l -> innerFlex+ { minWidth = (+ (l + r)) <$> innerFlex.minWidth+ , minHeight = (+ (t + b)) <$> innerFlex.minHeight+ }+ Box.TRBL trbl4 = trbl+ Offset{..} -> Offset{ann=inner'.ann, inner=inner', ..}+ where+ inner' = measure inner+ AspectRatio{..} -> AspectRatio{ann=ann', inner=inner', ..}+ where+ ann' = inner'.ann -- XXX: pass-through?+ inner' = measure inner+ HStack{children} -> HStack{ann=stackViewSize, children=measured}+ where+ measured = map measure children+ stackViewSize = foldl' (\vs -> stackViewSize' vs . ann) (fixed_ 0) measured+ stackViewSize' a b = ViewSize+ { minWidth = (+) <$> a.minWidth <*> b.minWidth -- combined horizontal flex+ , maxWidth = (+) <$> a.maxWidth <*> b.maxWidth+ , minHeight = max <$> a.minHeight <*> b.minHeight -- intersection of vertical flex+ , maxHeight = min <$> a.maxHeight <*> b.maxHeight+ }+ VStack{children} -> VStack{ann=stackViewSize, children=measured}+ where+ measured = map measure children+ stackViewSize = foldl' (\vs -> stackViewSize' vs . ann) (fixed_ 0) measured+ stackViewSize' a b = ViewSize+ { minWidth = max <$> a.minWidth <*> b.minWidth -- intersection of horizontal flex+ , maxWidth = min <$> a.maxWidth <*> b.maxWidth+ , minHeight = (+) <$> a.minHeight <*> b.minHeight -- combined vertical flex+ , maxHeight = (+) <$> a.maxHeight <*> b.maxHeight+ }+ ZStack{children} -> ZStack{ann=stackViewSize, children=measured}+ where+ measured = map measure children+ stackViewSize = foldl' (\vs -> stackViewSize' vs . ann) (fixed_ 0) measured+ stackViewSize' a b = ViewSize+ { minWidth = max <$> a.minWidth <*> b.minWidth -- intersection of horizontal flex+ , maxWidth = min <$> a.maxWidth <*> b.maxWidth+ , minHeight = max <$> a.minHeight <*> b.minHeight -- intersection of vertical flex+ , maxHeight = min <$> a.maxHeight <*> b.maxHeight+ }+ Overlay{primary, secondary} -> Overlay{ann=primary'.ann, primary=primary', secondary=secondary'}+ where+ primary' = measure primary -- pass-through wrt primary measurements+ secondary' = measure secondary -- ignore secondary flex++-- | Run the propose-report-place protocol on a flex-measured view tree+place :: LayoutView stuff => Box -> View ViewSize stuff -> View Box (Placed stuff)+place parent element = snd (suspend parent.size element) parent++-- | Propose-Report-Place protocol using continuations+--+-- Inner children can't be placed until the whole propose-report dance finishes.+suspend+ :: forall stuff+ . LayoutView stuff+ => Vec2+ -> View ViewSize stuff+ -> (Vec2, Box -> View Box (Placed stuff))+suspend proposed = \case+ Spacer{} ->+ ( proposed -- report the proposed+ , \placed -> Spacer{ann=placed} -- keep the placed+ )+ View{stuff} ->+ ( reported -- report dynamic size+ , \placed ->+ View+ { ann = placed+ , stuff = placeView @stuff placed cache -- instantiate the placed view+ }+ )+ where+ (reported, cache) = proposeView proposed stuff+ Frame{..} ->+ ( size -- frame reports its size right away+ , \placedFrame ->+ Frame+ { ann = placedFrame -- keep the placed for itself+ , inner =+ let+ -- now that the frame has its own box, it can properly place its contents+ innerBox = Layout.placeSize align reported placedFrame+ in+ -- and resolve the continuation+ suspended innerBox+ , ..+ }+ )+ where+ (reported, suspended) = suspend size inner -- frame proposes its size+ FlexibleFrame{..} ->+ ( vsClamp proposed flex reported -- reports clamped to self, while defaulting to parent+ , \placed ->+ let+ innerBox = Layout.placeSize align reported placed+ in+ FlexibleFrame+ { ann = placed+ , inner = suspended innerBox+ , ..+ }+ )+ where+ -- proposes clamped parent+ (reported, suspended) = suspend (vsClamp proposed flex proposed) inner+ Padding{..} ->+ ( reported + pads+ , \placed ->+ let+ innerBox = Box.addPadding trbl placed+ in+ Padding+ { ann = placed+ , inner = suspended innerBox+ , ..+ }+ )+ where+ (reported, suspended) = suspend (proposed - pads) inner+ pads = withVec4 trbl4 \t r b l -> vec2 (l + r) (t + b)+ Box.TRBL trbl4 = trbl+ Offset{..} ->+ ( reported+ , \placed -> Offset+ { ann = placed+ , inner = suspended $ Box.move xy placed+ , ..+ }+ )+ where+ (reported, suspended) = suspend proposed inner+ AspectRatio{..} ->+ ( reported+ , \placed ->+ let+ innerBox = Layout.placeSize align reported placed+ in+ AspectRatio+ { ann = placed+ , inner = suspended innerBox+ , ..+ }+ )+ where+ -- 1. query direct size, ignore suspended+ probed = fst $ suspend (aspect ^* scale proposed) inner+ -- 2. re-query with the fit size+ (reported, suspended) = suspend (aspect ^* scale probed) inner+ scale p =+ withVec2 aspect \aw ah ->+ withVec2 p \pw ph ->+ if pw / ph > aw / ah then+ ph / ah+ else+ pw / aw+ HStack{children} ->+ -- Like the Frame, but operating on multiple elements+ ( foldl' combineH 0 $ map fst children' -- stack reports the combined size of its children reports+ , \placedStack ->+ let+ placedChildren :: [View Box (Placed stuff)]+ placedChildren = snd $ mapAccumL placeLeftToRight placedStack children'+ where+ placeLeftToRight :: Box -> (Vec2, Box -> View Box (Placed stuff)) -> (Box, View Box (Placed stuff))+ placeLeftToRight remaining (reportedSize, suspended) =+ ( remaining'+ , suspended placed+ )+ where+ (slot, remaining') = withVec2 reportedSize \w _h -> Layout.cutLeft w remaining+ placed = Layout.placeSize Alignment.center reportedSize slot+ in+ HStack+ { ann = maybe placedStack (foldMap1' ann) $ nonEmpty placedChildren+ , children = placedChildren+ }+ )+ where+ -- | Record index, sort by flexibility, process, restore order+ children' =+ toList . array (0, numChildren - 1) $+ snd $ mapAccumL f (fromIntegral numChildren, proposed) $+ sortOn (widthFlexibility . ann . snd) $ zip [0 :: Int ..] children+ numChildren = length children++ f (childrenRemaining, WithVec2 remW remH) (ix, child) =+ ( (childrenRemaining - 1.0, vec2 (remW - reportedWidth) remH)+ , ( ix+ , child'+ ) -- XXX: can use combineH here+ )+ where+ WithVec2 reportedWidth _ = fst child'+ child' = suspend (vec2 proposeWidth remH) child+ proposeWidth = remW / childrenRemaining+ VStack{children} ->+ -- Like the HStack, but advancing vertically+ ( foldl' combineV 0 $ map fst children'+ , \placedStack ->+ let+ placedChildren :: [View Box (Placed stuff)]+ placedChildren = snd $ mapAccumL placeTopToBottom placedStack children'+ where+ placeTopToBottom :: Box -> (Vec2, Box -> View Box (Placed stuff)) -> (Box, View Box (Placed stuff))+ placeTopToBottom remaining (reportedSize, suspended) =+ ( remaining'+ , suspended placed+ )+ where+ (slot, remaining') = withVec2 reportedSize \_w h -> Layout.cutTop h remaining+ placed = Layout.placeSize Alignment.center reportedSize slot+ in+ VStack+ { ann = maybe placedStack (foldMap1' ann) $ nonEmpty placedChildren+ , children = placedChildren+ }+ )+ where+ -- | Record index, sort by flexibility, process, restore order+ children' =+ toList . array (0, numChildren - 1) $+ snd $ mapAccumL f (fromIntegral numChildren, proposed) $+ sortOn (heightFlexibility . ann . snd) $ zip [0 :: Int ..] children+ numChildren = length children++ f (childrenRemaining, WithVec2 remW remH) (ix, child) =+ ( (childrenRemaining - 1.0, vec2 remW (remH - reportedHeight))+ , ( ix+ , child'+ ) -- XXX: can use combineH here+ )+ where+ WithVec2 _ reportedHeight = fst child'+ child' = suspend (vec2 remW proposeHeight) child+ proposeHeight = remH / childrenRemaining+ ZStack{children} ->+ ( foldl' max2 0 reporteds+ , \placedStack ->+ let+ placedChildren = map ($ placedStack) suspendeds+ in+ ZStack+ { ann = maybe placedStack (foldMap1' ann) $ nonEmpty placedChildren+ , children = placedChildren+ }+ )+ where+ (reporteds, suspendeds) = unzip $ map (suspend proposed) children+ Overlay{primary, secondary} ->+ ( reportedPri+ , \placed ->+ let+ placedPrimary = suspendedPri placed+ in+ Overlay+ { ann = placed+ , primary = placedPrimary+ , secondary = suspendedSec placedPrimary.ann -- inspect primary box+ }+ )+ where+ (reportedPri, suspendedPri) = suspend proposed primary+ (_reportedSec, suspendedSec) = suspend reportedPri secondary -- inspect primary size++-- * Internals++inf :: Float+inf = 1 / 0++widthFlexibility :: ViewSize -> Float+widthFlexibility ViewSize{..} = maybeBoth (-1) (-) maxWidth minWidth++heightFlexibility :: ViewSize -> Float+heightFlexibility ViewSize{..} = maybeBoth (-1) (-) maxHeight minHeight++vsClamp :: Vec2 -> ViewSize -> Vec2 -> Vec2+vsClamp parent ViewSize{..} v =+ withVec2 parent \pw ph ->+ withVec2 v \w h ->+ vec2+ (maybeBoth pw (glClamp w) minWidth maxWidth)+ (maybeBoth ph (glClamp h) minHeight maxHeight)++{-# INLINE maybeBoth #-}+maybeBoth :: t1 -> (t2 -> t3 -> t1) -> Maybe t2 -> Maybe t3 -> t1+maybeBoth _ j (Just a) (Just b) = j a b+maybeBoth n _ _ _ = n++combineH :: Vec2 -> Vec2 -> Vec2+combineH a b =+ withVec2 a \aw ah ->+ withVec2 b \bw bh ->+ vec2 (aw + bw) (max ah bh)++combineV :: Vec2 -> Vec2 -> Vec2+combineV a b =+ withVec2 a \aw ah ->+ withVec2 b \bw bh ->+ vec2 (max aw bw) (ah + bh)++max2 :: Vec2 -> Vec2 -> Vec2+max2 a b =+ withVec2 a \aw ah ->+ withVec2 b \bw bh ->+ vec2 (max aw bw) (max ah bh)
+ test/Examples.hs view
@@ -0,0 +1,419 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE OverloadedStrings #-}++module Examples where++import Lucid.Svg++import Control.Monad (unless)+import Data.Foldable (for_)+import Data.Function ((&))+import Data.String (IsString(..))+import Geomancy.Layout.Alignment qualified as Alignment+import Geomancy.Layout.Box (Box(..))+import Geomancy.Layout.Box qualified as Box+import Geomancy.Layout.View (LayoutView(..), View(..), ViewSize(..), layout)+import Geomancy.Layout.View qualified as ViewSize+import Geomancy.Vec2 (Vec2, vec2, pattern WithVec2)+import Lucid.Svg qualified as SVG++toSVGfile :: forall stuff . (LayoutView stuff, Show (Placed stuff), SVGStuff (Placed stuff)) => FilePath -> View () stuff -> IO ()+toSVGfile path =+ SVG.renderToFile path . toSVG @stuff . layout exampleScreenBox++class SVGStuff stuff where+ svgStuff :: stuff -> String++instance SVGStuff String where+ svgStuff = id++instance SVGStuff Dynamic where+ svgStuff = \case+ Label s -> s+ Icon s -> s++toSVG :: (LayoutView stuff, SVGStuff (Placed stuff)) => View Box (Placed stuff) -> Svg ()+toSVG = svg . go+ where+ go = \case+ View{ann, stuff} -> g_ [class_ "view"] $ contents ann "red" (svgStuff stuff)+ Spacer{ann} -> g_ [class_ "spacer"] $ contents ann "#acf" ""+ HStack{ann, children} -> g_ [class_ "hstack"] $ contents ann "#afa" "" <> for_ children go+ VStack{ann, children} -> g_ [class_ "vstack"] $ contents ann "#aaf" "" <> for_ children go+ ZStack{ann, children} -> g_ [class_ "zstack"] $ contents ann "#faa" "" <> for_ children go+ Overlay{primary, secondary} -> g_ [class_ "overlay"] $ go primary <> go secondary+ Frame{ann, inner} -> g_ [class_ "frame"] $ contents ann "#faf" "" <> go inner+ FlexibleFrame{ann, inner} -> g_ [class_ "flex"] $ contents ann "#ffa" "" <> go inner+ Padding{inner} -> g_ [class_ "padding"] $ go inner -- dashed rect?+ Offset{inner} -> g_ [class_ "offset"] $ go inner -- dashed rect?+ AspectRatio{ann, inner} -> g_ [class_ "aspect"] $ contents ann "#aff" "" <> go inner++ svg :: Svg () -> Svg ()+ svg content = do+ doctype_+ with+ (svg11_ $ rect_ [width_ "100%", height_ "100%", fill_ "#ddd"] <> content+ )+ [ version_ "1.1"+ , width_ "800"+ , height_ "600"+ ]++ -- contents :: SVG.Svg ()++ contents box color ss = do+ let (WithVec2 x y, _br) = Box.toCorners box+ -- let WithVec2 x y = position - size * 0.5+ let WithVec2 w h = box.size+ rect_+ [ x_ (fromString $ show x)+ , y_ (fromString $ show y)+ , width_ (fromString $ show w)+ , height_ (fromString $ show h)+ , fill_ color+ , fill_opacity_ "0.5"+ , stroke_ "black"+ , stroke_width_ "1"+ , rx_ "12"+ ]+ unless (null ss) $ text_+ [ x_ (fromString $ show $ x + w / 2)+ , y_ (fromString $ show $ y + (h + 10) / 2)+ , font_size_ "16"+ , text_anchor_ "middle"+ , text_ "middle"+ , fill_ "black"+ ]+ (fromString ss)+ -- (fromString . show $ (position, size))++newtype Static = Static String+ deriving newtype (Eq, Ord, Show, IsString)++instance LayoutView Static where+ type ReportedCache Static = String+ type Placed Static = String+ viewFlexibility _ = ViewSize.infinite_{minWidth=Just 20, minHeight=Just 20}+ proposeView proposed (Static s) = (proposed, s)+ placeView _box = id++data Dynamic+ = Label String -- ^ Trims content to fit+ | Icon String -- ^ Fixed size/content+ deriving (Eq, Ord, Show)++instance LayoutView Dynamic where+ type ReportedCache Dynamic = (Int, Dynamic)+ viewFlexibility = \case+ Label s ->+ -- The way SwiftUI does it (allegedly)+ ViewSize.range_+ (fst . proposeView 0 $ Label s)+ (fst . proposeView (1/0) $ Label s)+ Icon{} -> ViewSize.fixed_ 32+ proposeView (WithVec2 w _h) e = case e of+ Label s -> (vec2 maxWidth 32, (len, e'))+ where+ charWidth = 10+ maxChars = floor (w / charWidth)+ trimmed =+ if maxChars > 0 && maxChars < length s then+ take maxChars s+ else+ s+ len = length trimmed+ maxWidth = fromIntegral len * charWidth+ e' = Label trimmed+ Icon _ -> (32, (2, e))+ placeView _parent (_len, e) = e++-- | Set up screen box with the top-left corner at 0,0 and origin at 400,300+exampleScreenBox :: Box+exampleScreenBox = Box+ { position = exampleScreenSize / 2 -- use 0,0 origin to match SVG+ , size = exampleScreenSize+ }++exampleScreenSize :: Vec2+exampleScreenSize = vec2 800 600++{- | 2 flexible views split parent in equal halves++@+HStack+ {ann =+ Box+ {position = Vec2 400.0 300.0,+ size = Vec2 800.0 600.0},+ children =+ [View+ {ann =+ Box+ {position = Vec2 200.0 300.0,+ size = Vec2 400.0 600.0},+ stuff = "400x600"},+ View+ {ann =+ Box+ {position = Vec2 600.0 300.0,+ size = Vec2 400.0 600.0},+ stuff = "400x600"}]}+@+-}+exampleHalves :: View () Static+exampleHalves =+ HStack ()+ [ View () "400x600"+ , View () "400x600"+ ]++{- | Mixing fixed and flexible views++@+HStack+ {ann =+ Box+ {position = Vec2 400.0 300.0,+ size = Vec2 800.0 600.0},+ children =+ [Frame+ {ann =+ Box+ {position = Vec2 50.0 300.0, size = Vec2 100.0 50.0},+ size = Vec2 100.0 50.0,+ align = Alignment (Vec2 0.5 0.5),+ inner =+ View+ {ann =+ Box+ {position = Vec2 50.0 300.0, size = Vec2 100.0 50.0},+ stuff = "Fixed 100x50"}},+ Spacer+ {ann =+ Box+ {position = Vec2 255.0 300.0,+ size = Vec2 310.0 600.0}},+ View+ {ann =+ Box+ {position = Vec2 565.0 300.0,+ size = Vec2 310.0 600.0},+ stuff = "Flexible 310x600"},+ Frame+ {ann =+ Box+ {position = Vec2 760.0 40.0, size = Vec2 80.0 80.0},+ size = Vec2 80.0 80.0,+ align = Alignment (Vec2 0.0 0.0),+ inner =+ View+ {ann =+ Box+ {position = Vec2 760.0 40.0, size = Vec2 80.0 80.0},+ stuff = "Fixed 80x80"}}]}+@+-}+exampleTree :: View () Static+exampleTree =+ HStack ()+ [ Frame () (vec2 100 50) Alignment.centerBottom $ -- Fixed size+ View () "Fixed 100x50" -- Inherited+ , Spacer () -- Flexible (Infinite)+ , View () "Flexible 310x600" -- Flexible (Infinite)+ , Frame () (vec2 80 80) Alignment.leftTop $ -- Fixed size+ View () "Fixed 80x80" -- Inherited+ ]++{- | Smoke test for frames asserting the view sizes++@+Frame+ {ann =+ Box+ {position = Vec2 400.0 300.0,+ size = Vec2 800.0 600.0},+ size = Vec2 800.0 600.0,+ align = Alignment (Vec2 0.5 0.5),+ inner =+ HStack+ {ann =+ Box+ {position = Vec2 400.0 300.0,+ size = Vec2 800.0 600.0},+ children =+ [Frame+ {ann =+ Box+ {position = Vec2 200.0 300.0,+ size = Vec2 400.0 600.0},+ size = Vec2 400.0 600.0,+ align = Alignment (Vec2 0.5 0.5),+ inner =+ View+ {ann =+ Box+ {position = Vec2 200.0 300.0,+ size = Vec2 400.0 600.0},+ stuff = "400x600"}},+ Frame+ {ann =+ Box+ {position = Vec2 600.0 300.0,+ size = Vec2 400.0 600.0},+ size = Vec2 400.0 600.0,+ align = Alignment (Vec2 0.5 0.5),+ inner =+ View+ {ann =+ Box+ {position = Vec2 600.0 300.0,+ size = Vec2 400.0 600.0},+ stuff = "400x600"}}]}}+@+-}+exampleFramed :: View () Static+exampleFramed =+ Frame () exampleScreenBox.size Alignment.center $ -- XXX: no-op+ HStack ()+ [ Frame () (vec2 400 600) Alignment.center $ View () "400x600"+ , Frame () (vec2 400 600) Alignment.center $ View () "400x600"+ ]++{- | Smoke test for nesting HStacks++@+HStack+ {ann =+ Box+ {position = Vec2 400.0 300.0,+ size = Vec2 800.0 600.0},+ children =+ [HStack+ {ann =+ Box+ {position = Vec2 400.0 300.0,+ size = Vec2 800.0 600.0},+ children =+ [HStack+ {ann =+ Box+ {position = Vec2 200.0 300.0,+ size = Vec2 400.0 600.0},+ children =+ [View+ {ann =+ Box+ {position = Vec2 200.0 300.0,+ size = Vec2 400.0 600.0},+ stuff = "400x600"}]},+ HStack+ {ann =+ Box+ {position = Vec2 600.0 300.0,+ size = Vec2 400.0 600.0},+ children =+ [View+ {ann =+ Box+ {position = Vec2 600.0 300.0,+ size = Vec2 400.0 600.0},+ stuff = "400x600"}]}]}]}+@+-}+exampleNested :: View () Static+exampleNested =+ HStack () . pure $ -- XXX: no-op+ HStack ()+ [ HStack () [View () "400x600"] -- XXX: no-op wrapped halves+ , HStack () [View () "400x600"]+ ]++{- | The stack conents is larger than the screen size.++@+HStack+ {ann =+ Box+ {position = Vec2 550.0 300.0,+ size = Vec2 1100.0 600.0},+ children =+ [Frame+ {ann =+ Box+ {position = Vec2 250.0 300.0,+ size = Vec2 500.0 600.0},+ size = Vec2 500.0 600.0,+ align = Alignment (Vec2 0.5 0.5),+ inner =+ View+ {ann =+ Box+ {position = Vec2 250.0 300.0,+ size = Vec2 500.0 600.0},+ stuff = "500x600"}},+ Frame+ {ann =+ Box+ {position = Vec2 800.0 300.0,+ size = Vec2 600.0 600.0},+ size = Vec2 600.0 600.0,+ align = Alignment (Vec2 0.5 0.5),+ inner =+ View+ {ann =+ Box+ {position = Vec2 800.0 300.0,+ size = Vec2 600.0 600.0},+ stuff = "600x600"}}]}+@+-}+exampleOverflow :: View () Static+exampleOverflow =+ HStack ()+ [ Frame () (vec2 500 600) Alignment.center $ View () "500x600"+ , Frame () (vec2 600 600) Alignment.center $ View () "600x600"+ ]++{- | The views report their flex and real size.++Icons should be flush with their labels.+The pair-hstack should hug its elements.+The spacer between them should push the pairs away from the center.+-}+exampleDynamic :: View () Dynamic+exampleDynamic =+ Padding () (Box.TRBL 32) $+ Offset () (vec2 0 32) $+ AspectRatio () 1 Alignment.center $+ HStack ()+ [ HStack ()+ [ View () $ Label "very large"+ , View () $ Icon "💅"+ ]+ , Spacer ()+ , FlexibleFrame () (ViewSize.range_ 60 100 & ViewSize.parentHeight) Alignment.centerTop $+ HStack ()+ [ View () $ Label "very large"+ , View () $ Icon "💅"+ ]+ ]++exampleDynamicVert :: View () Dynamic+exampleDynamicVert =+ Padding () (Box.TRBL 32) $+ ZStack () . (FlexibleFrame () ViewSize.parent_ Alignment.rightBottom (View () $ Icon "✋") :) . pure $ -- this is getting out of hand...+ Offset () (vec2 0 32) $+ AspectRatio () 1 Alignment.center $+ VStack ()+ [ HStack ()+ [ View () $ Label "very large"+ , View () $ Icon "💅"+ ]+ , Spacer ()+ & Overlay () (Frame () 128 Alignment.rightTop . Offset () (vec2 16 (-16)) $ View () $ Icon "🌈")+ , FlexibleFrame () (ViewSize.range_ 60 100 & ViewSize.parentWidth) Alignment.leftMiddle $+ HStack ()+ [ View () $ Label "very large"+ , View () $ Icon "💅"+ ]+ ]
+ test/Spec.hs view
@@ -0,0 +1,20 @@+import Prelude++import Test.Tasty+import Test.Tasty.HUnit++import Examples++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests = testGroup "Examples"+ [ testCase "Tree" $ toSVGfile "examples/Tree.svg" exampleTree+ , testCase "Halves" $ toSVGfile "examples/Halves.svg" exampleHalves+ , testCase "Overflow" $ toSVGfile "examples/Overflow.svg" exampleOverflow+ , testCase "Framed" $ toSVGfile "examples/Framed.svg" exampleFramed+ , testCase "Nested" $ toSVGfile "examples/Nested.svg" exampleNested+ , testCase "Dynamic" $ toSVGfile "examples/Dynamic.svg" exampleDynamic+ , testCase "DynamicVert" $ toSVGfile "examples/DynamicVert.svg" exampleDynamicVert+ ]