packages feed

monomer-1.4.0.0: src/Monomer/Graphics/NanoVGRenderer.hs

{-|
Module      : Monomer.Graphics.NanoVGRenderer
Copyright   : (c) 2018 Francisco Vallarino
License     : BSD-3-Clause (see the LICENSE file)
Maintainer  : fjvallarino@gmail.com
Stability   : experimental
Portability : non-portable

Renderer based on the nanovg library.
-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TupleSections #-}

module Monomer.Graphics.NanoVGRenderer (makeRenderer) where

import Control.Lens ((&), (^.), (.~))
import Control.Monad (foldM, forM_, unless, when)
import Data.Default
import Data.Functor ((<&>))
import Data.IORef
import Data.List (foldl')
import Data.Maybe
import Data.Sequence (Seq(..), (<|), (|>))
import Data.Set (Set(..))
import Data.Text (Text)
import Data.Text.Foreign (withCStringLen)
import Foreign.C.Types (CFloat)
import Foreign.Ptr
import System.IO.Unsafe

import qualified Data.ByteString as BS
import qualified Data.Map as M
import qualified Data.Sequence as Seq
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified NanoVG as VG
import qualified NanoVG.Internal.Image as VGI

import Monomer.Common
import Monomer.Graphics.Types

import qualified Monomer.Common.Lens as L
import qualified Monomer.Graphics.Lens as L

type ImagesMap = M.Map Text Image

data ImageAction
  = ImageAdd
  | ImageUpdate
  | ImageDelete
  deriving (Eq, Show)

data Image = Image {
  _imImageDef :: ImageDef,
  _imNvImage :: VG.Image,
  _imCount :: Int
}

data ImageReq = ImageReq {
  _irName :: Text,
  _irSize :: Size,
  _irImgData :: Maybe BS.ByteString,
  _irAction :: ImageAction,
  _irFlags :: [ImageFlag]
}

data Env = Env {
  overlays :: Seq (IO ()),
  tasksRaw :: Seq (IO ()),
  overlaysRaw :: Seq (IO ()),
  validFonts :: Set Text,
  imagesMap :: ImagesMap
}

data CSize
  = CSize CFloat CFloat
  deriving (Eq, Show)

data CPoint
  = CPoint CFloat CFloat
  deriving (Eq, Show)

data CRect
  = CRect CFloat CFloat CFloat CFloat
  deriving (Eq, Show)

-- | Creates a nanovg based renderer.
makeRenderer
  :: [FontDef]    -- ^ The font definitions.
  -> Double       -- ^ The device pixel rate.
  -> IO Renderer  -- ^ The created renderer.
makeRenderer fonts dpr = do
  c <- VG.createGL3 (Set.fromList [VG.Antialias, VG.StencilStrokes])

  validFonts <- foldM (loadFont c) Set.empty fonts

  when (null validFonts) $
    putStrLn "Could not find any valid fonts. Text will fail to be displayed."

  envRef <- newIORef $ Env {
    overlays = Seq.empty,
    tasksRaw = Seq.empty,
    overlaysRaw = Seq.empty,
    validFonts = validFonts,
    imagesMap = M.empty
  }

  return $ newRenderer c dpr envRef

newRenderer :: VG.Context -> Double -> IORef Env -> Renderer
newRenderer c rdpr envRef = Renderer {..} where
  {-
  rdpr is used to let nanovg know the real device pixel rate.
  dpr is set to 1 to disable all NanoVGRenderer internal calculations.
  -}
  dpr = 1

  beginFrame w h = do
    VG.beginFrame c cw ch cdpr
    where
      cw = realToFrac (w / dpr)
      ch = realToFrac (h / dpr)
      cdpr = realToFrac rdpr

  endFrame =
    VG.endFrame c

  beginPath =
    VG.beginPath c

  closePath =
    VG.closePath c

  -- Context management
  saveContext =
    VG.save c

  restoreContext =
    VG.restore c

  -- Overlays
  createOverlay overlay =
    modifyIORef envRef $ \env -> env {
      overlays = overlays env |> overlay
    }

  renderOverlays = do
    env <- readIORef envRef
    sequence_ $ overlays env
    writeIORef envRef env {
      overlays = Seq.empty
    }

  -- Raw tasks
  createRawTask task =
    modifyIORef envRef $ \env -> env {
      tasksRaw = tasksRaw env |> task
    }

  renderRawTasks = do
    env <- readIORef envRef
    sequence_ $ tasksRaw env
    writeIORef envRef env {
      tasksRaw = Seq.empty
    }

  -- Raw overlays
  createRawOverlay overlay =
    modifyIORef envRef $ \env -> env {
      overlaysRaw = overlaysRaw env |> overlay
    }

  renderRawOverlays = do
    env <- readIORef envRef
    sequence_ $ overlaysRaw env
    writeIORef envRef env {
      overlaysRaw = Seq.empty
    }

  -- Scissor
  intersectScissor rect = do
    VG.intersectScissor c cx cy cw ch
    where
      CRect cx cy cw ch = rectToCRect rect dpr

  -- Translation
  setTranslation offset = do
    VG.translate c tx ty
    where
      CPoint tx ty = pointToCPoint offset dpr

  -- Scale
  setScale point = do
    VG.scale c sx sy
    where
      sx = realToFrac (point ^. L.x)
      sy = realToFrac (point ^. L.y)

  -- Rotation
  setRotation angle = do
    VG.rotate c cangle
    where
      cangle = VG.degToRad $ realToFrac angle

  -- Alpha
  setGlobalAlpha alpha = do
    VG.globalAlpha c calpha
    where
      calpha = max 0 . min 1 $ realToFrac alpha

  -- Winding
  setPathWinding winding = do
    VG.pathWinding c cwinding
    where
      cwinding = if winding == CW then 0 else 1

  -- Strokes
  stroke =
    VG.stroke c

  setStrokeWidth width =
    VG.strokeWidth c (realToFrac $ width * dpr)

  setStrokeColor color =
    VG.strokeColor c (colorToPaint color)

  setStrokeLinearGradient p1 p2 color1 color2 = do
    gradient <- makeLinearGradient c dpr p1 p2 color1 color2
    VG.strokePaint c gradient

  setStrokeRadialGradient p1 rad1 rad2 color1 color2 = do
    gradient <- makeRadialGradient c dpr p1 rad1 rad2 color1 color2
    VG.strokePaint c gradient

  setStrokeImagePattern name topLeft size angle alpha = do
    env <- readIORef envRef

    forM_ (M.lookup name (imagesMap env)) $ \image -> do
      imgPattern <- makeImagePattern c dpr image topLeft size angle alpha
      VG.strokePaint c imgPattern

  -- Fill
  fill =
    VG.fill c

  setFillColor color =
    VG.fillColor c (colorToPaint color)

  setFillLinearGradient p1 p2 color1 color2 = do
    gradient <- makeLinearGradient c dpr p1 p2 color1 color2
    VG.fillPaint c gradient

  setFillRadialGradient p1 rad1 rad2 color1 color2 = do
    gradient <- makeRadialGradient c dpr p1 rad1 rad2 color1 color2
    VG.fillPaint c gradient

  setFillImagePattern name topLeft size angle alpha = do
    env <- readIORef envRef

    forM_ (M.lookup name (imagesMap env)) $ \image -> do
      imgPattern <- makeImagePattern c dpr image topLeft size angle alpha
      VG.fillPaint c imgPattern

  -- Drawing
  moveTo !point =
    VG.moveTo c x y
    where
      CPoint x y = pointToCPoint point dpr

  renderLine p1 p2 = do
    VG.moveTo c x1 y1
    VG.lineTo c x2 y2
    where
      CPoint x1 y1 = pointToCPoint p1 dpr
      CPoint x2 y2 = pointToCPoint p2 dpr

  renderLineTo !point = do
    VG.lineJoin c VG.Bevel
    VG.lineTo c x y
    where
      CPoint x y = pointToCPoint point dpr

  renderRect !rect =
    VG.rect c x y w h
    where
      CRect x y w h = rectToCRect rect dpr

  renderRoundedRect !rect tl tr br bl =
    VG.roundedRectVarying c x y w h ctl ctr cbr cbl
    where
      CRect x y w h = rectToCRect rect dpr
      ctl = realToFrac tl
      ctr = realToFrac tr
      cbr = realToFrac br
      cbl = realToFrac bl

  renderArc !point rad angleStart angleEnd winding =
    VG.arc c x y radius start end wind
    where
      CPoint x y = pointToCPoint point dpr
      radius = realToFrac $ rad * dpr
      start = VG.degToRad $ realToFrac angleStart
      end = VG.degToRad $ realToFrac angleEnd
      wind = convertWinding winding

  renderQuadTo p1 p2 =
    VG.quadTo c x1 y1 x2 y2
    where
      CPoint x1 y1 = pointToCPoint p1 dpr
      CPoint x2 y2 = pointToCPoint p2 dpr

  renderEllipse !rect =
    VG.ellipse c cx cy rx ry
    where
      CRect x y w h = rectToCRect rect dpr
      cx = x + rx
      cy = y + ry
      rx = w / 2
      ry = h / 2

  -- Text
  renderText !point font fontSize fontSpaceH message = do
    setFont c envRef dpr font fontSize fontSpaceH

    when (message /= "") $
      VG.text c tx ty message
    where
      CPoint tx ty = pointToCPoint point dpr

  getImage name = do
    env <- readIORef envRef
    let image = M.lookup name (imagesMap env)
    return $ fmap _imImageDef image

  addImage name size imgData flags = do
    processImgReq c envRef req
    where
      req = ImageReq name size (Just imgData) ImageAdd flags

  updateImage name size imgData = do
    processImgReq c envRef req
    where
      req = ImageReq name size (Just imgData) ImageUpdate []

  deleteImage name = do
    processImgReq c envRef req
    where
    req = ImageReq name def Nothing ImageDelete []

loadFont :: VG.Context -> Set Text -> FontDef -> IO (Set Text)
loadFont c fonts (FontDef name path) = do
  res <- VG.createFont c name (VG.FileName path)
  case res of
    Just{} -> return $ Set.insert name fonts
    _ -> putStrLn ("Failed to load font: " ++ T.unpack name) >> return fonts

setFont
  :: VG.Context
  -> IORef Env
  -> Double
  -> Font
  -> FontSize
  -> FontSpace
  -> IO ()
setFont c envRef dpr (Font name) (FontSize size) (FontSpace spaceH) = do
  env <- readIORef envRef
  handleSetFont (validFonts env)
  where
    handleSetFont validFonts
      | Set.member name validFonts = do
          VG.fontFace c name
          VG.fontSize c $ realToFrac $ size * dpr
          VG.textLetterSpacing c $ realToFrac $ spaceH * dpr
      | otherwise = return ()

makeLinearGradient
  :: VG.Context -> Double -> Point -> Point -> Color -> Color -> IO VG.Paint
makeLinearGradient c dpr p1 p2 color1 color2 = do
  VG.linearGradient c x1 y1 x2 y2 col1 col2
  where
    CPoint x1 y1 = pointToCPoint p1 dpr
    CPoint x2 y2 = pointToCPoint p2 dpr
    col1 = colorToPaint color1
    col2 = colorToPaint color2

makeRadialGradient
  :: VG.Context -> Double -> Point -> Double -> Double -> Color -> Color -> IO VG.Paint
makeRadialGradient c dpr center rad1 rad2 color1 color2 = do
  VG.radialGradient c cx cy crad1 crad2 col1 col2
  where
    CPoint cx cy = pointToCPoint center dpr
    crad1 = realToFrac rad1
    crad2 = realToFrac rad2
    col1 = colorToPaint color1
    col2 = colorToPaint color2

makeImagePattern
  :: VG.Context -> Double -> Image -> Point -> Size -> Double -> Double -> IO VG.Paint
makeImagePattern c dpr image topLeft size angle alpha = do
  VG.imagePattern c x y w h cangle nvImg calpha
  where
    CPoint x y = pointToCPoint topLeft dpr
    CSize w h = sizeToCSize size dpr
    cangle = realToFrac angle
    calpha = realToFrac alpha
    nvImg = _imNvImage image

processImgReq :: VG.Context -> IORef Env -> ImageReq -> IO ()
processImgReq c envRef imageReq = do
  env <- readIORef envRef
  newImgMap <- handlePendingImage c (imagesMap env) imageReq

  writeIORef envRef $ env {
    imagesMap = newImgMap
  }

handlePendingImage :: VG.Context -> ImagesMap -> ImageReq -> IO ImagesMap
handlePendingImage c imagesMap imageReq
  | action == ImageAdd && imageExists =
      return $ imgIncreaseCount name imagesMap
  | action `elem` [ImageAdd, ImageUpdate] && not imageExists = do
      -- Attempt to create image. If it fails, remove existing images and retry.
      -- Ideally only LRU should be removed.
      tmpImg <- createImage
      (newImgMap, nvImg) <- if isNothing tmpImg
        then clearImagesMap c imagesMap >> createImage <&> (M.empty, )
        else return (imagesMap, tmpImg)
      return $ maybe newImgMap (imgInsertNew name imgDef newImgMap) nvImg
  | action == ImageUpdate && imageExists && sizeMatches = do
      VG.updateImage c (_imNvImage image) imgData
      return imagesMap
  | action == ImageDelete && imageExists = do
      when (_imCount image == 1) $
        VG.deleteImage c (_imNvImage image)
      return $ imgDelete name imagesMap
  | otherwise =
      return imagesMap
  where
    name = _irName imageReq
    action = _irAction imageReq
    size = _irSize imageReq

    cw = round (size ^. L.w)
    ch = round (size ^. L.h)

    imgData = fromJust $ _irImgData imageReq
    imgFlags = _irFlags imageReq
    flags = Set.fromList (toVGImgFlag <$> imgFlags)

    imgDef = ImageDef name size imgData imgFlags
    createImage = VG.createImageRGBA c cw ch flags imgData

    mimage = M.lookup name imagesMap
    imageExists = isJust mimage
    image = fromJust mimage
    sizeMatches = size == _imImageDef image ^. L.size

toVGImgFlag :: ImageFlag -> VGI.ImageFlags
toVGImgFlag ImageNearest = VGI.ImageNearest
toVGImgFlag ImageRepeatX = VGI.ImageRepeatx
toVGImgFlag ImageRepeatY = VGI.ImageRepeaty

imgIncreaseCount :: Text -> ImagesMap -> ImagesMap
imgIncreaseCount name imagesMap = newImageMap where
  incCount img = img { _imCount = _imCount img + 1 }
  newImageMap = M.adjust incCount name imagesMap

imgInsertNew :: Text -> ImageDef -> ImagesMap -> VG.Image -> ImagesMap
imgInsertNew name imageDef imagesMap nvImg = newImagesMap where
  image = Image imageDef nvImg 0
  newImagesMap = M.insert name image imagesMap

imgDelete :: Text -> ImagesMap -> ImagesMap
imgDelete name imagesMap = newImageMap where
  deleteInstance img
    | _imCount img > 1 = Just $ img { _imCount = _imCount img - 1 }
    | otherwise = Nothing
  newImageMap = M.update deleteInstance name imagesMap

clearImagesMap :: VG.Context -> ImagesMap -> IO ()
clearImagesMap c imagesMap = do
  putStrLn "Clearing images map"
  forM_ (M.elems imagesMap) $ \image ->
    VG.deleteImage c (_imNvImage image)

colorToPaint :: Color -> VG.Color
colorToPaint (Color r g b a)
  | a >= 1.0  = VG.rgb red green blue
  | otherwise = VG.rgba red green blue alpha
  where
    red = fromIntegral r
    green = fromIntegral g
    blue = fromIntegral b
    alpha = round $ a * 255

convertWinding :: Winding -> VG.Winding
convertWinding CW = VG.CW
convertWinding CCW = VG.CCW

sizeToCSize :: Size -> Double -> CSize
sizeToCSize (Size w h) dpr = CSize cw ch where
  cw = realToFrac $ w * dpr
  ch = realToFrac $ h * dpr

pointToCPoint :: Point -> Double -> CPoint
pointToCPoint (Point x y) dpr = CPoint cx cy where
  cx = realToFrac $ x * dpr
  cy = realToFrac $ y * dpr

rectToCRect :: Rect -> Double -> CRect
rectToCRect (Rect x y w h) dpr = CRect cx cy cw ch where
  cx = realToFrac $ x * dpr
  cy = realToFrac $ y * dpr
  ch = realToFrac $ h * dpr
  cw = realToFrac $ w * dpr