packages feed

monomer-1.0.0.3: test/unit/Monomer/TestUtil.hs

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

Helper functions for testing Monomer widgets.
-}
{-# LANGUAGE FlexibleContexts #-}

module Monomer.TestUtil where

import Control.Concurrent (newMVar)
import Control.Concurrent.STM.TChan (newTChanIO)
import Control.Lens ((&), (^.), (.~), (.=))
import Control.Monad.State
import Data.Default
import Data.Maybe
import Data.Text (Text)
import Data.Sequence (Seq)
import System.IO.Unsafe

import qualified Data.ByteString as BS
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import qualified Data.Sequence as Seq

import Monomer.Core
import Monomer.Event
import Monomer.Graphics
import Monomer.Main.Handlers
import Monomer.Main.Types
import Monomer.Main.Util

import qualified Monomer.Lens as L

data InitWidget
  = WInit
  | WNoInit
  deriving (Eq, Show)

testW :: Double
testW = 640

testH :: Double
testH = 480

testWindowSize :: Size
testWindowSize = Size testW testH

testWindowRect :: Rect
testWindowRect = Rect 0 0 testW testH

mockTextMetrics :: Font -> FontSize -> TextMetrics
mockTextMetrics font fontSize = TextMetrics {
  _txmAsc = 15,
  _txmDesc = 5,
  _txmLineH = 20,
  _txmLowerX = 10
}

mockTextSize :: Maybe Double -> Font -> FontSize -> FontSpace -> Text -> Size
mockTextSize mw font (FontSize fs) spaceH text = Size width height where
  w = fromMaybe fs mw + unFontSpace spaceH
  width = fromIntegral (T.length text) * w
  height = 20

mockGlyphsPos
  :: Maybe Double -> Font -> FontSize -> FontSpace -> Text -> Seq GlyphPos
mockGlyphsPos mw font (FontSize fs) spaceH text = glyphs where
  w = fromMaybe fs mw + unFontSpace spaceH
  chars = Seq.fromList $ T.unpack text
  mkGlyph idx chr = GlyphPos {
    _glpGlyph = chr,
    _glpXMin = fromIntegral idx * w,
    _glpXMax = (fromIntegral idx + 1) * w,
    _glpYMin = 0,
    _glpYMax = 10,
    _glpW = w,
    _glpH = 10
  }
  glyphs = Seq.mapWithIndex mkGlyph chars

mockRenderText :: Point -> Font -> FontSize -> FontSpace -> Text -> IO ()
mockRenderText point font size spaceH text = return ()

mockRenderer :: Renderer
mockRenderer = Renderer {
  beginFrame = \w h -> return (),
  endFrame = return (),
  -- Path
  beginPath = return (),
  closePath = return (),
  -- Context management
  saveContext = return (),
  restoreContext = return (),
  -- Overlays
  createOverlay = \overlay -> return (),
  renderOverlays = return (),
  -- Raw tasks
  createRawTask = \task -> return (),
  renderRawTasks = return (),
  -- Raw overlays
  createRawOverlay = \overlay -> return (),
  renderRawOverlays = return (),
  -- Scissor operations
  intersectScissor = \rect -> return (),
  -- Translation
  setTranslation = \point -> return (),
  -- Scale
  setScale = \point -> return (),
  -- Rotation
  setRotation = \angle -> return (),
  -- Global Alpha
  setGlobalAlpha = \alpha -> return (),
  -- Path Winding
  setPathWinding = \winding -> return (),
  -- Strokes
  stroke = return (),
  setStrokeColor = \color -> return (),
  setStrokeWidth = \width -> return (),
  setStrokeLinearGradient = \p1 p2 c1 c2 -> return (),
  setStrokeRadialGradient = \p1 a1 a2 c1 c2 -> return (),
  setStrokeImagePattern = \n1 p1 s1 w1 h1 -> return (),
  -- Fill
  fill = return (),
  setFillColor = \color -> return (),
  setFillLinearGradient = \p1 p2 c1 c2 -> return (),
  setFillRadialGradient = \p1 a1 a2 c1 c2 -> return (),
  setFillImagePattern = \n1 p1 s1 w1 h1 -> return (),
  -- Drawing
  moveTo = \point -> return (),
  renderLine = \p1 p2 -> return (),
  renderLineTo = \point -> return (),
  renderRect = \rect -> return (),
  renderRoundedRect = \rect r1 r2 r3 r4 -> return (),
  renderArc = \center radius angleStart angleEnd winding -> return (),
  renderQuadTo = \p1 p2 -> return (),
  renderEllipse = \rect -> return (),
  -- Text
  renderText = mockRenderText,

  -- Image
  getImage = const . return $ Just $ ImageDef "test" def BS.empty [],
  addImage = \name size imgData flags -> return (),
  updateImage = \name size imgData -> return (),
  deleteImage = \name -> return ()
}

mockFontManager :: FontManager
mockFontManager = FontManager {
  computeTextMetrics = mockTextMetrics,
  computeTextSize = mockTextSize (Just 10),
  computeGlyphsPos = mockGlyphsPos (Just 10)
}

mockWenv :: s -> WidgetEnv s e
mockWenv model = WidgetEnv {
  _weOs = "Mac OS X",
  _weFontManager = mockFontManager,
  _weFindByPath = const Nothing,
  _weMainButton = BtnLeft,
  _weContextButton = BtnRight,
  _weTheme = def,
  _weWindowSize = testWindowSize,
  _weWidgetShared = unsafePerformIO (newMVar M.empty),
  _weWidgetKeyMap = M.empty,
  _weHoveredPath = Nothing,
  _weFocusedPath = emptyPath,
  _weOverlayPath = Nothing,
  _weDragStatus = Nothing,
  _weMainBtnPress = Nothing,
  _weCursor = Nothing,
  _weModel = model,
  _weInputStatus = def,
  _weTimestamp = 0,
  _weThemeChanged = False,
  _weInTopLayer = const True,
  _weLayoutDirection = LayoutNone,
  _weViewport = Rect 0 0 testW testH,
  _weOffset = def
}

mockWenvEvtUnit :: s -> WidgetEnv s ()
mockWenvEvtUnit model = mockWenv model

nodeInit :: (Eq s) => WidgetEnv s e -> WidgetNode s e -> WidgetNode s e
nodeInit wenv node = nodeHandleEventRoot wenv [] node

nodeMerge :: WidgetEnv s e -> WidgetNode s e -> WidgetNode s e -> WidgetNode s e
nodeMerge wenv node oldNode = resNode where
  newNode = node
    & L.info . L.path .~ oldNode ^. L.info . L.path
  WidgetResult resNode _ = widgetMerge (newNode^. L.widget) wenv newNode oldNode

nodeGetSizeReq :: WidgetEnv s e -> WidgetNode s e -> (SizeReq, SizeReq)
nodeGetSizeReq wenv node = (sizeReqW,  sizeReqH) where
  WidgetResult node2 _ = widgetInit (node ^. L.widget) wenv node
  sizeReqW = node2 ^. L.info . L.sizeReqW
  sizeReqH = node2 ^. L.info . L.sizeReqH

nodeResize :: WidgetEnv s e -> WidgetNode s e -> Rect -> WidgetNode s e
nodeResize wenv node viewport = result ^. L.node where
  resizeCheck = const True
  widget = node ^. L.widget
  result = widgetResize widget wenv node viewport resizeCheck

nodeHandleEventCtx
  :: (Eq s)
  => WidgetEnv s e
  -> [SystemEvent]
  -> WidgetNode s e
  -> MonomerCtx s e
nodeHandleEventCtx wenv evts node = ctx where
  ctx = snd $ nodeHandleEvents wenv WInit evts node

nodeHandleEventModel
  :: (Eq s)
  => WidgetEnv s e
  -> [SystemEvent]
  -> WidgetNode s e
  -> s
nodeHandleEventModel wenv evts node = _weModel wenv2 where
  (wenv2, _, _) = fst $ nodeHandleEvents wenv WInit evts node

nodeHandleEventRoot
  :: (Eq s)
  => WidgetEnv s e
  -> [SystemEvent]
  -> WidgetNode s e
  -> WidgetNode s e
nodeHandleEventRoot wenv evts node = newRoot where
  (_, newRoot, _) = fst $ nodeHandleEvents wenv WInit evts node

nodeHandleEventReqs
  :: (Eq s)
  => WidgetEnv s e
  -> [SystemEvent]
  -> WidgetNode s e
  -> Seq (WidgetRequest s e)
nodeHandleEventReqs wenv evts node = reqs where
  (_, _, reqs) = fst $ nodeHandleEvents wenv WInit evts node

nodeHandleEventEvts
  :: (Eq s)
  => WidgetEnv s e
  -> [SystemEvent]
  -> WidgetNode s e
  -> Seq e
nodeHandleEventEvts wenv evts node = eventsFromReqs reqs where
  (_, _, reqs) = fst $ nodeHandleEvents wenv WInit evts node

nodeHandleEvents
  :: (Eq s)
  => WidgetEnv s e
  -> InitWidget
  -> [SystemEvent]
  -> WidgetNode s e
  -> (HandlerStep s e, MonomerCtx s e)
nodeHandleEvents wenv init evts node = result where
  steps
    | init == WInit = tail $ nodeHandleEvents_ wenv init [evts] node
    | otherwise = nodeHandleEvents_ wenv init [evts] node
  result = foldl1 stepper steps
  stepper step1 step2 = result where
    ((_, _, reqs1), _) = step1
    ((wenv2, root2, reqs2), ctx2) = step2
    result = ((wenv2, root2, reqs1 <> reqs2), ctx2)

nodeHandleEvents_
  :: (Eq s)
  => WidgetEnv s e
  -> InitWidget
  -> [[SystemEvent]]
  -> WidgetNode s e
  -> [(HandlerStep s e, MonomerCtx s e)]
nodeHandleEvents_ wenv init evtsG node = unsafePerformIO $ do
  -- Do NOT test code involving SDL Window functions
  channel <- liftIO newTChanIO

  fmap fst $ flip runStateT (monomerCtx channel) $ do
    L.inputStatus .= wenv ^. L.inputStatus
    handleResourcesInit

    stepA <- if init == WInit
      then do
        handleWidgetInit wenv pathReadyRoot
      else
        return (wenv, pathReadyRoot, Seq.empty)

    ctxA <- get
    let (wenvA, nodeA, reqsA) = stepA
    let resizeCheck = const True
    let resizeRes = widgetResize (nodeA ^. L.widget) wenv nodeA vp resizeCheck
    step <- handleWidgetResult wenvA True resizeRes
    ctx <- get
    let (wenvr, rootr, reqsr) = step

    (_, _, steps) <- foldM runStep (wenvr, rootr, [(step, ctx)]) evtsG

    return $ if init == WInit
      then (stepA, ctxA) : reverse steps
      else reverse steps
  where
    winSize = _weWindowSize wenv
    vp = Rect 0 0 (_sW winSize) (_sH winSize)
    dpr = 1
    epr = 1
    model = _weModel wenv
    monomerCtx channel = initMonomerCtx undefined channel winSize dpr epr model
    pathReadyRoot = node
      & L.info . L.path .~ rootPath
      & L.info . L.widgetId .~ WidgetId (wenv ^. L.timestamp) rootPath
    runStep (wenv, root, accum) evts = do
      step <- handleSystemEvents wenv root evts
      ctx <- get
      let (wenv2, root2, reqs2) = step

      return (wenv2, root2, (step, ctx) : accum)

nodeHandleResult
  :: (Eq s)
  => WidgetEnv s e
  -> WidgetResult s e
  -> (HandlerStep s e, MonomerCtx s e)
nodeHandleResult wenv result = unsafePerformIO $ do
  channel <- liftIO newTChanIO

  let winSize = _weWindowSize wenv
  let dpr = 1
  let epr = 1
  let model = _weModel wenv
  -- Do NOT test code involving SDL Window functions
  let monomerCtx = initMonomerCtx undefined channel winSize dpr epr model

  flip runStateT monomerCtx $ do
    L.inputStatus .= wenv ^. L.inputStatus
    handleResourcesInit

    handleWidgetResult wenv True result

roundRectUnits :: Rect -> Rect
roundRectUnits (Rect x y w h) = Rect nx ny nw nh where
  nx = fromIntegral (round x)
  ny = fromIntegral (round y)
  nw = fromIntegral (round w)
  nh = fromIntegral (round h)