monomer-1.0.0.2: src/Monomer/Main/Core.hs
{-|
Module : Monomer.Main.Core
Copyright : (c) 2018 Francisco Vallarino
License : BSD-3-Clause (see the LICENSE file)
Maintainer : fjvallarino@gmail.com
Stability : experimental
Portability : non-portable
Core glue for running an application.
-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards #-}
module Monomer.Main.Core (
AppEventResponse(..),
AppEventHandler(..),
AppUIBuilder(..),
startApp
) where
import Control.Concurrent (MVar, forkIO, forkOS, newMVar, threadDelay)
import Control.Concurrent.STM.TChan (TChan, newTChanIO, readTChan, writeTChan)
import Control.Lens ((&), (^.), (.=), (.~), use)
import Control.Monad (unless, void, when)
import Control.Monad.Catch
import Control.Monad.Extra
import Control.Monad.State
import Control.Monad.STM (atomically)
import Data.Default
import Data.Maybe
import Data.Map (Map)
import Data.List (foldl')
import Data.Text (Text)
import qualified Data.Map as Map
import qualified Graphics.Rendering.OpenGL as GL
import qualified SDL
import qualified Data.Sequence as Seq
import Monomer.Core
import Monomer.Core.Combinators
import Monomer.Event
import Monomer.Main.Handlers
import Monomer.Main.Platform
import Monomer.Main.Types
import Monomer.Main.Util
import Monomer.Main.WidgetTask
import Monomer.Graphics
import Monomer.Widgets.Composite
import qualified Monomer.Lens as L
{-|
Type of response an App event handler can return, with __s__ being the model and
__e__ the user's event type.
-}
type AppEventResponse s e = EventResponse s e s ()
-- | Type of an App event handler.
type AppEventHandler s e
= WidgetEnv s e -- ^ The widget environment.
-> WidgetNode s e -- ^ The root node of the application.
-> s -- ^ The application's model.
-> e -- ^ The event to handle.
-> [AppEventResponse s e] -- ^ The list of requested actions.
-- | Type of the function responsible of creating the App UI.
type AppUIBuilder s e = UIBuilder s e
data MainLoopArgs sp e ep = MainLoopArgs {
_mlOS :: Text,
_mlRenderer :: Maybe Renderer,
_mlTheme :: Theme,
_mlAppStartTs :: Int,
_mlMaxFps :: Int,
_mlLatestRenderTs :: Int,
_mlFrameStartTs :: Int,
_mlFrameAccumTs :: Int,
_mlFrameCount :: Int,
_mlExitEvents :: [e],
_mlWidgetRoot :: WidgetNode sp ep,
_mlWidgetShared :: MVar (Map Text WidgetShared),
_mlChannel :: TChan (RenderMsg sp ep)
}
data RenderState s e = RenderState {
_rstDpr :: Double,
_rstWidgetEnv :: WidgetEnv s e,
_rstRootNode :: WidgetNode s e
}
{-|
Runs an application, creating the UI with the provided function and initial
model, handling future events with the event handler.
Control will not be returned until the UI exits. This needs to be ran in the
main thread if using macOS.
-}
startApp
:: (Eq s, WidgetModel s, WidgetEvent e)
=> s -- ^ The initial model.
-> AppEventHandler s e -- ^ The event handler.
-> AppUIBuilder s e -- ^ The UI builder.
-> [AppConfig e] -- ^ The application config.
-> IO () -- ^ The application action.
startApp model eventHandler uiBuilder configs = do
(window, dpr, epr, glCtx) <- initSDLWindow config
vpSize <- getViewportSize window dpr
channel <- newTChanIO
let monomerCtx = initMonomerCtx window channel vpSize dpr epr model
runStateT (runAppLoop window glCtx channel appWidget config) monomerCtx
detroySDLWindow window
where
config = mconcat configs
compCfgs
= (onInit <$> _apcInitEvent config)
++ (onDispose <$> _apcDisposeEvent config)
++ (onResize <$> _apcResizeEvent config)
appWidget = composite_ "app" id uiBuilder eventHandler compCfgs
runAppLoop
:: (MonomerM sp ep m, Eq sp, WidgetEvent e, WidgetEvent ep)
=> SDL.Window
-> SDL.GLContext
-> TChan (RenderMsg sp ep)
-> WidgetNode sp ep
-> AppConfig e
-> m ()
runAppLoop window glCtx channel widgetRoot config = do
dpr <- use L.dpr
winSize <- use L.windowSize
let useRenderThread = fromMaybe True (_apcUseRenderThread config)
let maxFps = fromMaybe 60 (_apcMaxFps config)
let fonts = _apcFonts config
let theme = fromMaybe def (_apcTheme config)
let exitEvents = _apcExitEvent config
let mainBtn = fromMaybe BtnLeft (_apcMainButton config)
let contextBtn = fromMaybe BtnRight (_apcContextButton config)
startTs <- fmap fromIntegral SDL.ticks
model <- use L.mainModel
os <- getPlatform
widgetSharedMVar <- liftIO $ newMVar Map.empty
renderer <- if useRenderThread
then return Nothing
else liftIO $ Just <$> makeRenderer fonts dpr
fontManager <- liftIO $ makeFontManager fonts dpr
let wenv = WidgetEnv {
_weOs = os,
_weFontManager = fontManager,
_weFindByPath = const Nothing,
_weMainButton = mainBtn,
_weContextButton = contextBtn,
_weTheme = theme,
_weWindowSize = winSize,
_weWidgetShared = widgetSharedMVar,
_weWidgetKeyMap = Map.empty,
_weCursor = Nothing,
_weHoveredPath = Nothing,
_weFocusedPath = emptyPath,
_weOverlayPath = Nothing,
_weDragStatus = Nothing,
_weMainBtnPress = Nothing,
_weModel = model,
_weInputStatus = def,
_weTimestamp = startTs,
_weThemeChanged = False,
_weInTopLayer = const True,
_weLayoutDirection = LayoutNone,
_weViewport = Rect 0 0 (winSize ^. L.w) (winSize ^. L.h),
_weOffset = def
}
let pathReadyRoot = widgetRoot
& L.info . L.path .~ rootPath
& L.info . L.widgetId .~ WidgetId (wenv ^. L.timestamp) rootPath
handleResourcesInit
(newWenv, newRoot, _) <- handleWidgetInit wenv pathReadyRoot
let loopArgs = MainLoopArgs {
_mlOS = os,
_mlRenderer = renderer,
_mlTheme = theme,
_mlMaxFps = maxFps,
_mlAppStartTs = 0,
_mlLatestRenderTs = 0,
_mlFrameStartTs = startTs,
_mlFrameAccumTs = 0,
_mlFrameCount = 0,
_mlExitEvents = exitEvents,
_mlWidgetRoot = newRoot,
_mlWidgetShared = widgetSharedMVar,
_mlChannel = channel
}
L.mainModel .= _weModel newWenv
when useRenderThread $ do
liftIO $ watchWindowResize channel
liftIO . void . forkOS $
startRenderThread channel window glCtx fonts dpr newWenv newRoot
mainLoop window fontManager config loopArgs
mainLoop
:: (MonomerM sp ep m, WidgetEvent e)
=> SDL.Window
-> FontManager
-> AppConfig e
-> MainLoopArgs sp e ep
-> m ()
mainLoop window fontManager config loopArgs = do
let MainLoopArgs{..} = loopArgs
startTicks <- fmap fromIntegral SDL.ticks
events <- SDL.pollEvents
windowSize <- use L.windowSize
dpr <- use L.dpr
epr <- use L.epr
currentModel <- use L.mainModel
cursorIcon <- getCurrentCursorIcon
hovered <- getHoveredPath
focused <- getFocusedPath
overlay <- getOverlayPath
dragged <- getDraggedMsgInfo
mainPress <- use L.mainBtnPress
inputStatus <- use L.inputStatus
mousePos <- getCurrentMousePos epr
currWinSize <- getViewportSize window dpr
let Size rw rh = windowSize
let ts = startTicks - _mlFrameStartTs
let eventsPayload = fmap SDL.eventPayload events
let quit = SDL.QuitEvent `elem` eventsPayload
let windowResized = currWinSize /= windowSize && isWindowResized eventsPayload
let windowExposed = isWindowExposed eventsPayload
let mouseEntered = isMouseEntered eventsPayload
let baseSystemEvents = convertEvents dpr epr mousePos eventsPayload
-- when newSecond $
-- liftIO . putStrLn $ "Frames: " ++ show _mlFrameCount
when quit $
L.exitApplication .= True
when windowExposed $
L.mainBtnPress .= Nothing
let newSecond = _mlFrameAccumTs > 1000
let mainBtn = fromMaybe BtnLeft (_apcMainButton config)
let contextBtn = fromMaybe BtnRight (_apcContextButton config)
let wenv = WidgetEnv {
_weOs = _mlOS,
_weFontManager = fontManager,
_weFindByPath = findWidgetByPath wenv _mlWidgetRoot,
_weMainButton = mainBtn,
_weContextButton = contextBtn,
_weTheme = _mlTheme,
_weWindowSize = windowSize,
_weWidgetShared = _mlWidgetShared,
_weWidgetKeyMap = Map.empty,
_weCursor = cursorIcon,
_weHoveredPath = hovered,
_weFocusedPath = focused,
_weOverlayPath = overlay,
_weDragStatus = dragged,
_weMainBtnPress = mainPress,
_weModel = currentModel,
_weInputStatus = inputStatus,
_weTimestamp = startTicks,
_weThemeChanged = False,
_weInTopLayer = const True,
_weLayoutDirection = LayoutNone,
_weViewport = Rect 0 0 rw rh,
_weOffset = def
}
-- Exit handler
let baseWidgetId = _mlWidgetRoot ^. L.info . L.widgetId
let exitMsgs = SendMessage baseWidgetId <$> _mlExitEvents
let baseReqs
| quit = Seq.fromList exitMsgs
| otherwise = Seq.Empty
let baseStep = (wenv, _mlWidgetRoot, Seq.empty)
(rqWenv, rqRoot, _) <- handleRequests baseReqs baseStep
(wtWenv, wtRoot, _) <- handleWidgetTasks rqWenv rqRoot
(seWenv, seRoot, _) <- handleSystemEvents wtWenv wtRoot baseSystemEvents
(newWenv, newRoot, _) <- if windowResized
then do
L.windowSize .= currWinSize
handleResizeWidgets (seWenv, seRoot, Seq.empty)
else return (seWenv, seRoot, Seq.empty)
endTicks <- fmap fromIntegral SDL.ticks
-- Rendering
renderCurrentReq <- checkRenderCurrent startTicks _mlLatestRenderTs
let useRenderThread = fromMaybe True (_apcUseRenderThread config)
let renderEvent = any isActionEvent eventsPayload
let winRedrawEvt = windowResized || windowExposed
let renderNeeded = winRedrawEvt || renderEvent || renderCurrentReq
when (renderNeeded && useRenderThread) $
liftIO . atomically $ writeTChan _mlChannel (MsgRender newWenv newRoot)
when (renderNeeded && not useRenderThread) $ do
let renderer = fromJust _mlRenderer
let bgColor = newWenv ^. L.theme . L.clearColor
liftIO $ renderWidgets window dpr renderer bgColor newWenv newRoot
L.renderRequested .= windowResized
let fps = realToFrac _mlMaxFps
let frameLength = round (1000000 / fps)
let remainingMs = endTicks - startTicks
let tempDelay = abs (frameLength - remainingMs * 1000)
let nextFrameDelay = min frameLength tempDelay
let latestRenderTs = if renderNeeded then startTicks else _mlLatestRenderTs
let newLoopArgs = loopArgs {
_mlAppStartTs = _mlAppStartTs + ts,
_mlLatestRenderTs = latestRenderTs,
_mlFrameStartTs = startTicks,
_mlFrameAccumTs = if newSecond then 0 else _mlFrameAccumTs + ts,
_mlFrameCount = if newSecond then 0 else _mlFrameCount + 1,
_mlWidgetRoot = newRoot
}
liftIO $ threadDelay nextFrameDelay
shouldQuit <- use L.exitApplication
when shouldQuit $
void $ handleWidgetDispose newWenv newRoot
unless shouldQuit (mainLoop window fontManager config newLoopArgs)
startRenderThread
:: (Eq s, WidgetEvent e)
=> TChan (RenderMsg s e)
-> SDL.Window
-> SDL.GLContext
-> [FontDef]
-> Double
-> WidgetEnv s e
-> WidgetNode s e
-> IO ()
startRenderThread channel window glCtx fonts dpr wenv root = do
SDL.glMakeCurrent window glCtx
renderer <- liftIO $ makeRenderer fonts dpr
fontMgr <- liftIO $ makeFontManager fonts dpr
waitRenderMsg channel window renderer fontMgr state
where
state = RenderState dpr wenv root
waitRenderMsg
:: (Eq s, WidgetEvent e)
=> TChan (RenderMsg s e)
-> SDL.Window
-> Renderer
-> FontManager
-> RenderState s e
-> IO ()
waitRenderMsg channel window renderer fontMgr state = do
msg <- liftIO . atomically $ readTChan channel
newState <- handleRenderMsg window renderer fontMgr state msg
waitRenderMsg channel window renderer fontMgr newState
handleRenderMsg
:: (Eq s, WidgetEvent e)
=> SDL.Window
-> Renderer
-> FontManager
-> RenderState s e
-> RenderMsg s e
-> IO (RenderState s e)
handleRenderMsg window renderer fontMgr state (MsgRender tmpWenv newRoot) = do
let RenderState dpr _ _ = state
let newWenv = tmpWenv
& L.fontManager .~ fontMgr
let color = newWenv ^. L.theme . L.clearColor
renderWidgets window dpr renderer color newWenv newRoot
return (RenderState dpr newWenv newRoot)
handleRenderMsg window renderer fontMgr state (MsgResize _) = do
newSize <- getViewportSize window (_rstDpr state)
let RenderState dpr wenv root = state
let viewport = Rect 0 0 (newSize ^. L.w) (newSize ^. L.h)
let newWenv = wenv
& L.fontManager .~ fontMgr
& L.windowSize .~ newSize
& L.viewport .~ viewport
let color = newWenv ^. L.theme . L.clearColor
let resizeCheck = const False
let result = widgetResize (root ^. L.widget) newWenv root viewport resizeCheck
let newRoot = result ^. L.node
renderWidgets window dpr renderer color newWenv newRoot
return state
handleRenderMsg window renderer fontMgr state (MsgRemoveImage name) = do
deleteImage renderer name
return state
renderWidgets
:: SDL.Window
-> Double
-> Renderer
-> Color
-> WidgetEnv s e
-> WidgetNode s e
-> IO ()
renderWidgets !window dpr renderer clearColor wenv widgetRoot = do
Size dwW dwH <- getDrawableSize window
Size vpW vpH <- getViewportSize window dpr
let position = GL.Position 0 0
let size = GL.Size (round dwW) (round dwH)
liftIO $ GL.viewport GL.$= (position, size)
liftIO $ GL.clearColor GL.$= clearColor4
liftIO $ GL.clear [GL.ColorBuffer]
liftIO $ beginFrame renderer vpW vpH
liftIO $ widgetRender (widgetRoot ^. L.widget) wenv widgetRoot renderer
liftIO $ endFrame renderer
liftIO $ renderRawTasks renderer
liftIO $ beginFrame renderer vpW vpH
liftIO $ renderOverlays renderer
liftIO $ endFrame renderer
liftIO $ renderRawOverlays renderer
SDL.glSwapWindow window
where
r = fromIntegral (clearColor ^. L.r) / 255
g = fromIntegral (clearColor ^. L.g) / 255
b = fromIntegral (clearColor ^. L.b) / 255
a = clearColor ^. L.a
clearColor4 = GL.Color4 r g b (realToFrac a)
watchWindowResize :: TChan (RenderMsg s e) -> IO ()
watchWindowResize channel = do
void . SDL.addEventWatch $ \ev -> do
case SDL.eventPayload ev of
SDL.WindowSizeChangedEvent sizeChangeData -> do
let SDL.V2 nw nh = SDL.windowSizeChangedEventSize sizeChangeData
let newSize = Size (fromIntegral nw) (fromIntegral nh)
atomically $ writeTChan channel (MsgResize newSize)
_ -> return ()
checkRenderCurrent :: (MonomerM s e m) => Int -> Int -> m Bool
checkRenderCurrent currTs renderTs = do
renderCurrent <- use L.renderRequested
schedule <- use L.renderSchedule
L.renderSchedule .= Map.filter (renderScheduleActive currTs) schedule
return (renderCurrent || renderNext schedule)
where
requiresRender = renderScheduleReq currTs renderTs
renderNext schedule = any requiresRender schedule
renderScheduleReq :: Int -> Int -> RenderSchedule -> Bool
renderScheduleReq currTs renderTs schedule = required where
RenderSchedule _ start ms _ = schedule
stepCount = floor (fromIntegral (currTs - start) / fromIntegral ms)
stepTs = start + ms * stepCount
required = renderTs < stepTs
renderScheduleActive :: Int -> RenderSchedule -> Bool
renderScheduleActive currTs schedule = scheduleActive where
RenderSchedule _ start ms count = schedule
stepCount = floor (fromIntegral (currTs - start) / fromIntegral ms)
scheduleActive = maybe True (> stepCount) count
isWindowResized :: [SDL.EventPayload] -> Bool
isWindowResized eventsPayload = not status where
status = null [ e | e@SDL.WindowResizedEvent {} <- eventsPayload ]
isWindowExposed :: [SDL.EventPayload] -> Bool
isWindowExposed eventsPayload = not status where
status = null [ e | e@SDL.WindowExposedEvent {} <- eventsPayload ]
isMouseEntered :: [SDL.EventPayload] -> Bool
isMouseEntered eventsPayload = not status where
status = null [ e | e@SDL.WindowGainedMouseFocusEvent {} <- eventsPayload ]