{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} module Graphics.NanoVG.Window ( Window (..) , simpleWindow , MiddleWare , run ) where import Control.Concurrent import Control.Exception.Safe import Control.Monad import Control.Monad.Loops import Foreign.C.Types import System.IO (hPutStrLn, stderr) import qualified Graphics.Rendering.OpenGL as GL import qualified Graphics.UI.GLFW as GLFW import qualified NanoVG as NVG import qualified Graphics.NanoVG.FrameSize as FS foreign import ccall unsafe "initGlew" glewInit :: IO CInt -- | Window keep state and repeatedly calls render/afterRender. -- -- Invocation of 'winRender' action happens inside nanovg frame, so you can use -- the provided context to render whatever you want. The buffers are cleared before -- each frame. -- -- There is no interface to update state directly so mutable containers should be used, if so desired. data Window st = Window { winInit :: !(NVG.Context -> IO st) , winRender :: !(st -> NVG.Context -> IO ()) , winAfterRender :: !(st -> NVG.Context -> IO ()) } -- | Create new window which does not need own persistent state. simpleWindow :: (NVG.Context -> IO ()) -> Window () simpleWindow render = Window { winInit = const $ pure () , winRender = const render , winAfterRender = \_ _ -> pure () } -- | Middleware adds some piece of functionality to existing window. type MiddleWare st0 st = Window st0 -> Window st -- | Run given rendering instructions ('Window' structure) in new GLFW window of given size and title. -- -- __NOTE__: It is currently impossible to run multiple windows simultaneously in same application. run :: Int -- ^ Initial window width. -> Int -- ^ Initial window height. -> String -- ^ Window title. -> Window st -- ^ Rendering instructions to be executed in the context of the new window. -> IO () run initWidth initHeight title Window {..} = withGLFW $ createWindow initWidth initHeight title >>= go where go win = do GLFW.makeContextCurrent $ Just win void glewInit -- Leave it up to vblank_mode / __GL_SYNC_TO_VBLANK -- GLFW.swapInterval 0 ctx <- NVG.createGL3 [NVG.Antialias, NVG.StencilStrokes, NVG.Debug] frameSize <- FS.init win st <- winInit ctx whileM_ (not <$> GLFW.windowShouldClose win) $ do throwError (width, height) <- FS.size frameSize GL.clear [GL.ColorBuffer, GL.StencilBuffer] runFrame ctx width height $ winRender st ctx GLFW.swapBuffers win GL.flush GLFW.pollEvents winAfterRender st ctx runFrame :: NVG.Context -> Int -> Int -> IO a -> IO a runFrame c w h act = NVG.beginFrame c (fromIntegral w) (fromIntegral h) 1 *> act <* NVG.endFrame c throwError :: IO () throwError = do errs <- GL.errors case errs of err:_ -> throwString $ "OpenGL error: " <> show err _ -> pure () createWindow :: Int -> Int -> String -> IO GLFW.Window createWindow w h title = do GLFW.windowHint $ GLFW.WindowHint'ContextVersionMajor 3 GLFW.windowHint $ GLFW.WindowHint'ContextVersionMinor 3 GLFW.windowHint $ GLFW.WindowHint'OpenGLProfile GLFW.OpenGLProfile'Core GLFW.windowHint $ GLFW.WindowHint'OpenGLDebugContext True Just win <- GLFW.createWindow w h title Nothing Nothing pure win -- | Do NOT try to nest calls! -- TODO: use global flag (via unsafePerformIO) to seemlessly handle nesting. withGLFW :: IO a -> IO a withGLFW act = runInBoundThread $ do GLFW.setErrorCallback $ Just $ \err msg -> hPutStrLn stderr $ "GLFW error: " <> show err <> "\n" <> msg bracket_ (assertTrueM GLFW.init) GLFW.terminate act where assertTrueM predM = do True <- predM pure ()