{-# LANGUAGE TypeFamilies #-} -- | Contains the SDL engine implementation of Helm. module Helm.Engine.SDL ( -- * Types SDLEngine , SDLEngineConfig(..) -- * Startup , defaultConfig , startup , startupWith -- * Asset Loading , withImage ) where import Control.Monad (when) import qualified Data.Text as T import FRP.Elerea.Param import Linear.Affine (Point(P)) import Linear.Metric (distance) import Linear.V2 (V2(V2)) import qualified SDL import qualified SDL.Event as Event import qualified SDL.Init as Init import SDL.Input.Keyboard (Keysym(..)) import qualified SDL.Time as Time import qualified SDL.Video as Video import SDL.Video (WindowConfig(..)) import qualified SDL.Video.Renderer as Renderer import Helm.Engine (Engine(..)) import Helm.Engine.SDL.Asset (withImage) import Helm.Engine.SDL.Engine (SDLEngine(..), SDLEngineConfig(..)) import qualified Helm.Engine.SDL.Graphics2D as Graphics2D import Helm.Engine.SDL.Keyboard (mapKey) import Helm.Engine.SDL.Mouse (mapMouseButton) import Helm.Graphics (Graphics(..)) import Helm.Graphics2D (Collage) -- FIXME: Find a nice and easy way to have this instance with the SDLEngine type. -- Can't avoid the orphan instance without dependency hell right now. instance Engine SDLEngine where render engine (Graphics2D coll) = render2d engine coll cleanup SDLEngine { window, renderer, texture } = do Renderer.destroyTexture texture Video.destroyWindow window Video.destroyRenderer renderer Init.quit tick engine = do mayhaps <- Event.pumpEvents >> Event.pollEvent case mayhaps of -- Handle the quit event exclusively first to simplify our code Just Event.Event { eventPayload = Event.QuitEvent } -> return Nothing Just Event.Event { .. } -> sinkEvent engine eventPayload >>= tick Nothing -> return $ Just engine mouseMoveSignal = mouseMoveEventSignal mouseDownSignal = mouseDownEventSignal mouseUpSignal = mouseUpEventSignal mouseClickSignal = mouseClickEventSignal keyboardDownSignal = keyboardDownEventSignal keyboardUpSignal = keyboardUpEventSignal keyboardPressSignal = keyboardPressEventSignal windowResizeSignal = windowResizeEventSignal runningTime _ = fromIntegral <$> Time.ticks windowSize SDLEngine { window } = fmap (fmap fromIntegral) . SDL.get $ Video.windowSize window -- | The default configuration for the engine. You should change the values where necessary. defaultConfig :: SDLEngineConfig defaultConfig = SDLEngineConfig { windowDimensions = V2 800 600 , windowIsFullscreen = False , windowIsResizable = True , windowTitle = "Helm" } -- | Initialize a new engine with default configuration. The engine can then be run later using 'run'. startup :: IO SDLEngine startup = startupWith defaultConfig -- | Prepare a texture for streamed rendering based of a window size. prepTexture :: V2 Int -> Video.Renderer -> IO Renderer.Texture prepTexture dims renderer = Renderer.createTexture renderer mode access $ fromIntegral <$> dims where mode = Renderer.ARGB8888 access = Renderer.TextureAccessStreaming -- | Initialize a new engine with some configration, ready to be 'run'. startupWith :: SDLEngineConfig -> IO SDLEngine startupWith config@SDLEngineConfig { .. } = do Init.initializeAll window <- Video.createWindow (T.pack windowTitle) windowConfig renderer <- Video.createRenderer window (-1) rendererConfig texture <- prepTexture windowDimensions renderer mouseMoveEvent <- externalMulti mouseDownEvent <- externalMulti mouseUpEvent <- externalMulti mouseClickEvent <- externalMulti keyboardDownEvent <- externalMulti keyboardUpEvent <- externalMulti keyboardPressEvent <- externalMulti windowResizeEvent <- externalMulti Video.showWindow window return SDLEngine { window = window , renderer = renderer , texture = texture , engineConfig = config , lastMousePress = Nothing , mouseMoveEventSignal = fst mouseMoveEvent , mouseMoveEventSink = snd mouseMoveEvent , mouseDownEventSignal = fst mouseDownEvent , mouseDownEventSink = snd mouseDownEvent , mouseUpEventSignal = fst mouseUpEvent , mouseUpEventSink = snd mouseUpEvent , mouseClickEventSignal = fst mouseClickEvent , mouseClickEventSink = snd mouseClickEvent , keyboardDownEventSignal = fst keyboardDownEvent , keyboardDownEventSink = snd keyboardDownEvent , keyboardUpEventSignal = fst keyboardUpEvent , keyboardUpEventSink = snd keyboardUpEvent , keyboardPressEventSignal = fst keyboardPressEvent , keyboardPressEventSink = snd keyboardPressEvent , windowResizeEventSignal = fst windowResizeEvent , windowResizeEventSink = snd windowResizeEvent } where rendererConfig = Video.RendererConfig Video.AcceleratedVSyncRenderer False windowConfig = Video.defaultWindow { windowInitialSize = fromIntegral <$> windowDimensions , windowMode = if windowIsFullscreen then Video.Fullscreen else Video.Windowed , windowResizable = windowIsResizable } -- | Renders a 2D element to the engine screen. render2d :: SDLEngine -> Collage SDLEngine -> IO () render2d SDLEngine { window, renderer, texture } element = do dims <- SDL.get $ Video.windowSize window Graphics2D.render texture dims element Renderer.clear renderer Renderer.copy renderer texture Nothing Nothing Renderer.present renderer -- | Turns a point containing a vector into a regular vector. depoint :: Point f a -> f a depoint (P x) = x -- | Sink an SDL event into the Elerea sinks initialized at startup of the SDL engine. -- These sinks then provide the data for the Elerea signals, which will be in -- turn will provide the Helm subscriptions with events. sinkEvent :: SDLEngine -> Event.EventPayload -> IO SDLEngine sinkEvent engine (Event.WindowResizedEvent Event.WindowResizedEventData { .. }) = do windowResizeEventSink engine dims Renderer.destroyTexture texture resized <- prepTexture dims renderer return engine { texture = resized } where dims = fromIntegral <$> windowResizedEventSize SDLEngine { texture, renderer } = engine sinkEvent engine (Event.MouseMotionEvent Event.MouseMotionEventData { .. }) = do mouseMoveEventSink engine $ fromIntegral <$> depoint mouseMotionEventPos return engine sinkEvent engine (Event.KeyboardEvent Event.KeyboardEventData { .. }) = case keyboardEventKeyMotion of Event.Pressed -> do keyboardDownEventSink engine key if keyboardEventRepeat then keyboardPressEventSink engine key >> return engine else return engine Event.Released -> do keyboardUpEventSink engine key keyboardPressEventSink engine key return engine where Keysym { .. } = keyboardEventKeysym key = mapKey keysymKeycode sinkEvent engine (Event.MouseButtonEvent Event.MouseButtonEventData { .. }) = case mouseButtonEventMotion of Event.Pressed -> do ticks <- Time.ticks mouseDownEventSink engine tup return engine { lastMousePress = Just (ticks, dubPos) } Event.Released -> do mouseUpEventSink engine tup -- Weirdly enough, SDL provides a value that says how many clicks there -- were, but this value is always set to one even if it's just a regular -- mouse up event. Note that here we're defining a click as a mouse up -- event being in a very close proximity to a previous mouse down event. -- We manually calculate whether this was a click or not. case lastMousePress of Just (lastTicks, lastPos) -> do ticks <- Time.ticks -- Check that it's a expected amount of time for a click and that the mouse -- has basically stayed in place when (distance dubPos lastPos <= clickRadius && ticks - lastTicks < clickMs) (mouseClickEventSink engine tup) Nothing -> return () return engine where SDLEngine { lastMousePress } = engine clickMs = 500 -- How long between mouse down/up to recognise clicks clickRadius = 3 -- The pixel radius to be considered a click. pos = depoint mouseButtonEventPos dubPos = fromIntegral <$> pos tup = (mapMouseButton mouseButtonEventButton, fromIntegral <$> pos) sinkEvent engine _ = return engine