packages feed

geomancy-layout-0.1.1: test/Examples.hs

{-# 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 "πŸ’…"
          ]
    ]