packages feed

mischief-sdl-0.1.0.0: src/Mischief/SDL/Window.hs

module Mischief.SDL.Window where

import Control.Monad.IO.Class
import Data.Bits ((.|.))
import Data.Default
import Data.Foldable (for_)
import Foreign.C
import Foreign.C.ConstPtr (ConstPtr (..))
import Mischief.ECS
import Mischief.ECS.App.Schedules (First)
import Mischief.ECS.Messages qualified as Messages
import Mischief.SDL.Events
import SDL3.Sys qualified as SDL3
import SDL3.Sys.Video

-- | A component that will create a new window when added to an entity, and destroy it when removed.
--
-- Windows can be configured by inserting a 'WindowSize', 'WindowTitle', 'WindowFlags' alongside this component.
data Window = Window

instance Component Window where
  onAdd = [hook handleNewWindow]
  onRemove = [hook handleWindowRemove]
  required = require @(WindowSize, WindowTitle, WindowFlags)

-- | The size of a window in pixels.
data WindowSize = WindowSize {width :: Int, height :: Int} deriving (Component)

instance Default WindowSize where
  def = WindowSize 600 400

-- | The displayed title (or name) of a window.
newtype WindowTitle = WindowTitle String deriving anyclass (Component)

instance Default WindowTitle where
  def = WindowTitle "Mischief Window"

handleNewWindow :: HookContext -> System ()
handleNewWindow (HookContext entity) = do
  Just (WindowSize w h, WindowTitle title, flags) <- single [q|entity. WindowSize, WindowTitle, WindowFlags|]
  window <- liftIO $ withCString title $ \title -> SDL3.createWindow (ConstPtr title) (fromIntegral w) (fromIntegral h) (flagsToBit flags)
  insert (SDLWindow window) entity

handleWindowRemove :: HookContext -> System ()
handleWindowRemove (HookContext entity) = do
  Just (SDLWindow p) <- single [q|entity. SDLWindow|]
  liftIO $ SDL3.destroyWindow p

-- | These correspond to the [SDL3 Window Flags](https://wiki.libsdl.org/SDL3/SDL_WindowFlags).
--
-- The most important ones are 'WindowFullscreen' and 'WindowResizable'.
data WindowFlag
  = WindowFullscreen
  | WindowHidden
  | WindowBorderless
  | WindowResizable
  | WindowMinimized
  | WindowMaximized
  | WindowModal
  | WindowHighPixelDensity
  | WindowMouseRelativeMode
  | WindowAlwaysOnTop
  | WindowUtility
  | WindowTooltip
  | WindowPopupMenu
  | WindowTransparent
  | WindowNotFocuable
  | WindowMetal

flagToBit :: WindowFlag -> SDL_WindowFlags
flagToBit WindowFullscreen = SDL_WINDOW_FULLSCREEN
flagToBit WindowHidden = SDL_WINDOW_HIDDEN
flagToBit WindowBorderless = SDL_WINDOW_BORDERLESS
flagToBit WindowResizable = SDL_WINDOW_RESIZABLE
flagToBit WindowMinimized = SDL_WINDOW_MINIMIZED
flagToBit WindowMaximized = SDL_WINDOW_MAXIMIZED
flagToBit WindowModal = SDL_WINDOW_MODAL
flagToBit WindowHighPixelDensity = SDL_WINDOW_HIGH_PIXEL_DENSITY
flagToBit WindowMouseRelativeMode = SDL_WINDOW_MOUSE_RELATIVE_MODE
flagToBit WindowAlwaysOnTop = SDL_WINDOW_ALWAYS_ON_TOP
flagToBit WindowUtility = SDL_WINDOW_UTILITY
flagToBit WindowTooltip = SDL_WINDOW_TOOLTIP
flagToBit WindowPopupMenu = SDL_WINDOW_POPUP_MENU
flagToBit WindowTransparent = SDL_WINDOW_TRANSPARENT
flagToBit WindowNotFocuable = SDL_WINDOW_NOT_FOCUSABLE
flagToBit WindowMetal = SDL_WINDOW_METAL

-- | A list of 'WindowFlag's specifying various configurations for the window. The only one set by default is 'WindowResizable'.
newtype WindowFlags = WindowFlags [WindowFlag] deriving anyclass (Component)

instance Default WindowFlags where
  def = WindowFlags [WindowResizable]

flagsToBit :: WindowFlags -> SDL_WindowFlags
flagsToBit (WindowFlags a) = foldr (\a b -> b .|. flagToBit a) 0 a

-- | Plugin adding windowing functionality.
data WindowPlugin

instance Plugin WindowPlugin where
  init = do
    systems handleWindowEvents
      & after handleEvents
      & schedule @First

handleWindowEvents :: System ()
handleWindowEvents = do
  messages <- Messages.read @(SDLMessage SDL3.SDL_WindowEvent)
  for_ messages handleWindowEvent

handleWindowEvent :: SDLMessage SDL3.SDL_WindowEvent -> System ()
handleWindowEvent (SDLMessage SDL3.SDL_EVENT_WINDOW_RESIZED w) = do
  let width :: Int = fromIntegral w.data1
  let height :: Int = fromIntegral w.data2

  window <- liftIO $ SDL3.getWindowFromID w.windowID

  [q|(Entity, SDLWindow) / With SDLWindow|]
    & qfilter (\(_, SDLWindow p) -> p == window)
    & qinsert (const $ WindowSize width height)
    & query_
handleWindowEvent _ = pure ()