packages feed

sylvia-0.2.1: Sylvia/Renderer/Impl.hs

-- |
-- Module      : Sylvia.Renderer.Impl
-- Copyright   : GPLv3
--
-- Maintainer  : chrisyco@gmail.com
-- Portability : portable
--
-- This module provides three things:
--
-- 1. An interface, 'RenderImpl', that all rendering methods must
--    implement;
--
-- 2. A function, 'render', that uses the aforementioned interface to
--    draw a pretty picture;
--
-- 3. Another function, 'render'', that spews its internals all over the
--    place.

module Sylvia.Renderer.Impl
    (
    -- * An interface
      RenderImpl(..)

    -- * A function
    , render

    -- * Another function
    , render'
    , Result(..)
    , Rhyme
    , RhymeUnit(..)

    -- * Miscellany
    , stackHorizontally
    ) where

import Control.Applicative
import Data.Foldable ( foldMap )
import Data.List ( foldl' )
import Data.Monoid
import Data.Void ( Void, vacuous )

import Sylvia.Model
import Sylvia.Renderer.Pair

-- | An action that yields an image.
--
-- 'mempty' should yield an empty image and 'mappend' should stack two
-- images together.
class Monoid r => RenderImpl r where
    -- | Draw a dotted rectangle.
    drawDottedRectangle
        :: PInt -- ^ Corner position
        -> PInt -- ^ Size
        -> r

    -- | Draw a line segment from one point to another.
    drawLine :: PInt -> PInt -> r

    -- | Draw a line, but instead of drawing a diagonal line, draw a
    -- zigzag instead.
    drawZigzag :: PInt -> PInt -> r

    -- | Draw a simple circle segment, centered at a point.
    drawCircleSegment
        :: PInt   -- ^ Center point
        -> Double -- ^ Start angle, in radians
        -> Double -- ^ End angle, also in radians. Radians are cool.
        -> r

    -- | Translate the given image by a vector.
    relativeTo :: PInt -> r -> r

-- | Draw a full circle, centered at a point.
drawDot :: RenderImpl r => PInt -> r
drawDot center = drawCircleSegment center 0 (2 * pi)

-- | Draw a box, complete with a throat and ear.
drawBox
    :: RenderImpl r
    => PInt -- ^ Top-left corner point
    -> PInt -- ^ Size
    -> Int  -- ^ Y offset of ear and throat
    -> r
drawBox corner size throatY
    =  drawDottedRectangle corner size
    <> drawCircleSegment (corner |+| (    0 :| height + throatY)) (1 * rightAngle) (3 * rightAngle)
    <> drawCircleSegment (corner |+| (width :| height + throatY)) (3 * rightAngle) (1 * rightAngle)
  where
    width :| height = size
    rightAngle = pi / 2

type Rhyme = [RhymeUnit]

-- | Specifies a /rhyme line/: a straight line protruding from the left
-- edge of a bounding box, connecting a variable to the sub-expression
-- that uses it.
data RhymeUnit = RhymeUnit
    { ruIndex :: Integer
    , ruDest  :: Int
    }
  deriving (Show)

-- | The result of a rendering operation.
data Result r = Result
    { resultImage :: r
      -- ^ The rendered image.
    , resultSize  :: PInt
      -- ^ The size of the image's bounding box in grid units, when all
      -- round things are removed.
    , resultRhyme :: Rhyme
      -- ^ The expression's rhyme.
    , resultThroatY :: Int
      -- ^ The Y offset of the expression's ear and throat, measured
      -- from the /bottom/ of its bounding box.
    }
  deriving (Show)

-- | Render an expression, returning an image along with its size.
render :: RenderImpl r => Exp Void -> (r, PInt)
render e =
    let Result image size rhyme _ = render' $ vacuous e
    in case rhyme of
        [] -> (image, size)
        _  -> error $ "render: the impossible happened -- "
                        ++ "extra free variables: " ++ show rhyme

-- | Render an expression, with extra juicy options.
render' :: RenderImpl r => Exp Integer -> Result r
render' e = case e of
    Ref x   -> Result mempty (0 :| 0) [RhymeUnit x 0] 0
    Lam e'  -> renderLambda e'
    App a b -> Result image size rhyme bThroatY
      where
        image = mconcat $
            -- Draw the two sub-expressions
            [ aImage
            , bImage
            -- Extend the shorter sub-expression so it matches up with
            -- the bigger one
            , extendRhyme (-aWidth) (-bWidth) bRhyme
            -- Connect them with a vertical line
            , drawLine (0 :| aThroatY) (0 :| bThroatY)
            -- Application dot
            , drawDot (0 :| bThroatY)
            ]
        Result aImage (aWidth :| aHeight) aRhyme aThroatY
            = shiftY (-1 - bHeight) $ renderWithThroatLine False bWidth a
        Result bImage (bWidth :| bHeight) bRhyme bThroatY
            = renderWithThroatLine False 1 b
        size = (aWidth :| aHeight + bHeight + 1)
        rhyme = aRhyme ++ bRhyme

-- | Render an expression with a horizontal line sticking out of its
-- throat. Doesn't sound too comfortable, to be honest.
--
-- The 'resultSize' includes the length of this extra line.
renderWithThroatLine
    :: RenderImpl r
    => Bool -- ^ Whether the enclosing expression is a lambda.
    -> Int  -- ^ Length of the throat line. This should be positive.
    -> Exp Integer -> Result r
renderWithThroatLine outerIsLam lineLength e = Result image size rhyme throatY
  where
    Result image' size' rhyme throatY' = render' e
    -- Shift the main image to the left, then draw a line next to it
    image = relativeTo (-lineLength :| 0) image' <> throatLine
    throatLine = drawZigzag (-lineLength :| throatY') (0 :| throatY)
    throatY = if outerIsLam && containsLam e then throatY' - 1 else throatY'
    size = size' |+| (lineLength :| 0)

containsLam :: Exp a -> Bool
containsLam e = case e of
    Ref _ -> False
    Lam _ -> True
    App _ b -> containsLam b

-- | Render a lambda expression.
renderLambda :: RenderImpl r => Exp (Inc Integer) -> Result r
renderLambda e' = Result image size rhyme throatY
  where
    Result image' (innerWidth :| innerHeight) innerRhyme throatY
        = shiftY (-1) . renderWithThroatLine True 1 $ fmap shiftDown e'
    image = drawBox (negateP size) size throatY
            <> relativeTo (-width :| 0) rhymeImage
            <> image'
    (rhymeImage, rhyme) = renderRhyme throatY innerRhyme
    rhymeHeight = fromInteger . maximumOr 0 $ map ruIndex innerRhyme
    size@(width :| _) = (innerWidth + 1 :| (max innerHeight rhymeHeight) + 2)

-- | Like 'maximum', but returns a default value on an empty list rather
-- than throwing a hissy fit.
maximumOr :: Ord a => a -> [a] -> a
maximumOr def = foldl' max def

-- | Render an expression's rhyme.
renderRhyme
    :: RenderImpl r
    => Int        -- ^ Throat offset (see 'resultThroatY')
    -> Rhyme      -- ^ The inner expression's rhyme
    -> (r, Rhyme) -- ^ The resulting image, along with the outer rhyme
renderRhyme throatY innerRhyme = (foldMap renderOne innerRhyme, outerRhyme)
  where
    renderOne (RhymeUnit index dest) = drawLine (0 :| throatY - fromInteger index) (1 :| dest)
    outerRhyme =
        [ RhymeUnit (pred index) (throatY - fromInteger index)
        | RhymeUnit index _ <- innerRhyme
        , index > 0
        ]

-- | Shift an image vertically by a specified amount, changing the rhyme
-- and throat position to compensate.
shiftY :: RenderImpl r => Int -> Result r -> Result r
shiftY dy (Result image size rhyme throatY)
    = Result image' size rhyme' throatY'
  where
    image' = relativeTo (0 :| dy) image
    rhyme' = map shiftRhyme rhyme
    throatY' = throatY + dy

    shiftRhyme :: RhymeUnit -> RhymeUnit
    shiftRhyme (RhymeUnit index dest) = RhymeUnit index (dest + dy)

extendRhyme :: RenderImpl r => Int -> Int -> Rhyme -> r
extendRhyme srcX destX = foldMap $ drawLine
                                    <$> (srcX  :|) . ruDest
                                    <*> (destX :|) . ruDest

-- | Take a list of images and line them up in a row.
stackHorizontally :: RenderImpl r => [(r, PInt)] -> (r, PInt)
stackHorizontally = foldr step (mempty, 0 :| 0)
  where
    step (image', (w' :| h')) (image, (w :| h))
        = (relativeTo ((-w - 1) :| 0) image' <> image, (w + w' + 2) :| max h h')