Author: Eiko

Time: 2026-01-23 10:55:05 - 2026-01-23 10:55:05 (UTC)

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}

{- |
This executable is a small interactive tour of /h-raylib/:

  * How to open a window and drive the renderer ('beginDrawing'/'endDrawing').
  * How to draw 2D primitives (fills + “strokes” via *Lines* APIs).
  * How to work with colors and alpha blending.
  * How to load and draw textures (including a generated texture).
  * How to render into an offscreen 'RenderTexture' and then draw it to the screen.

Controls are shown in the overlay (toggle with @H@).
-}
module Main (main) where

import Control.Monad (forM_, unless, void, when)
import Data.Fixed (mod')

import qualified Raylib.Core as RL
import qualified Raylib.Core.Shapes as Shapes
import qualified Raylib.Core.Text as Text
import qualified Raylib.Core.Textures as Tex
import qualified Raylib.Util.Colors as C
import Raylib.Util (WindowResources, managed)
import Raylib.Types
  ( BlendMode (..)
  , Color (..)
  , ConfigFlag (..)
  , KeyboardKey (..)
  , MouseButton (..)
  , Rectangle (..)
  , RenderTexture
  , Texture
  , TextureFilter (..)
  , TextureWrap (..)
  , Vector2
  , pattern Vector2
  , renderTexture'texture
  , texture'height
  , texture'width
  )

main :: IO ()
main = do
  RL.setConfigFlags [Msaa4xHint, WindowResizable]
  window <- RL.initWindow screenW screenH "tryraylib: h-raylib primitives demo"
  RL.setTargetFPS 60

  assets <- initAssets window
  loop assets initialState

  shutdown window

-- | Initial window size.
screenW, screenH :: Int
screenW = 1100
screenH = 720

-- | All asset-like things we want to keep alive across frames.
data Assets = Assets
  { assetsTexture :: Texture
  , assetsRenderTexture :: RenderTexture
  }

-- | Initialize GPU resources after the window (OpenGL context) exists.
initAssets :: WindowResources -> IO Assets
initAssets window = do
  image <- Tex.genImageChecked 256 256 32 32 C.rayWhite C.lightGray
  texture <- managed window (Tex.loadTextureFromImage image)
  rt <- managed window (Tex.loadRenderTexture 512 512)

  -- Configure texture defaults once.
  void $ Tex.setTextureWrap texture TextureWrapRepeat
  void $ Tex.setTextureFilter texture TextureFilterPoint

  pure Assets
    { assetsTexture = texture
    , assetsRenderTexture = rt
    }

-- | Manual resource cleanup (the high-level unload APIs need 'WindowResources').
shutdown :: WindowResources -> IO ()
shutdown window =
  RL.closeWindow (Just window)

data Page
  = PageShapes
  | PageColors
  | PageTextures
  deriving (Eq, Enum, Bounded, Show)

-- | The bits of application state that are /not/ owned by raylib.
data DemoState = DemoState
  { stPage :: Page
  , stShowHelp :: Bool
  , stLineThickness :: Float
  , stBlendModeIx :: Int
  , stTextureFilter :: TextureFilter
  , stRotationDeg :: Float
  }

initialState :: DemoState
initialState =
  DemoState
    { stPage = PageShapes
    , stShowHelp = True
    , stLineThickness = 6
    , stBlendModeIx = 0
    , stTextureFilter = TextureFilterPoint
    , stRotationDeg = 0
    }

-- | Input snapshot for a single frame.
data Controls = Controls
  { ctlDt :: Float
  , ctlMouse :: Vector2
  , ctlWheel :: Float
  , ctlMouseDown :: Bool
  , ctlPrevPage :: Bool
  , ctlNextPage :: Bool
  , ctlPage1 :: Bool
  , ctlPage2 :: Bool
  , ctlPage3 :: Bool
  , ctlToggleHelp :: Bool
  , ctlCycleBlend :: Bool
  , ctlToggleFilter :: Bool
  }

readControls :: IO Controls
readControls = do
  dt <- RL.getFrameTime
  mouse <- RL.getMousePosition
  wheel <- RL.getMouseWheelMove
  mouseDown <- RL.isMouseButtonDown MouseButtonLeft

  prevPage <- RL.isKeyPressed KeyLeft
  nextPage <- RL.isKeyPressed KeyRight
  p1 <- RL.isKeyPressed KeyOne
  p2 <- RL.isKeyPressed KeyTwo
  p3 <- RL.isKeyPressed KeyThree
  toggleHelp <- RL.isKeyPressed KeyH
  cycleBlend <- RL.isKeyPressed KeyB
  toggleFilter <- RL.isKeyPressed KeyF

  pure Controls
    { ctlDt = dt
    , ctlMouse = mouse
    , ctlWheel = wheel
    , ctlMouseDown = mouseDown
    , ctlPrevPage = prevPage
    , ctlNextPage = nextPage
    , ctlPage1 = p1
    , ctlPage2 = p2
    , ctlPage3 = p3
    , ctlToggleHelp = toggleHelp
    , ctlCycleBlend = cycleBlend
    , ctlToggleFilter = toggleFilter
    }

-- | Pure state update logic.
stepDemo :: Controls -> DemoState -> DemoState
stepDemo ctl st =
  st
    { stPage = newPage
    , stShowHelp = if ctlToggleHelp ctl then not (stShowHelp st) else stShowHelp st
    , stLineThickness = newThickness
    , stBlendModeIx = newBlendIx
    , stTextureFilter = newFilter
    , stRotationDeg = newRot
    }
 where
  newPage
    | ctlPage1 ctl = PageShapes
    | ctlPage2 ctl = PageColors
    | ctlPage3 ctl = PageTextures
    | ctlNextPage ctl = nextEnum (stPage st)
    | ctlPrevPage ctl = prevEnum (stPage st)
    | otherwise = stPage st

  newThickness =
    clamp 1 30 (stLineThickness st + (ctlWheel ctl * 2))

  newBlendIx
    | ctlCycleBlend ctl = (stBlendModeIx st + 1) `mod` length blendModes
    | otherwise = stBlendModeIx st

  newFilter
    | ctlToggleFilter ctl = toggleTextureFilter (stTextureFilter st)
    | otherwise = stTextureFilter st

  newRot = (stRotationDeg st + ctlDt ctl * 90) `mod'` 360

-- | Main loop driven by 'windowShouldClose'.
loop :: Assets -> DemoState -> IO ()
loop assets = go
 where
  go st = do
    shouldClose <- RL.windowShouldClose
    unless shouldClose $ do
      ctl <- readControls
      let st' = stepDemo ctl st
      drawFrame assets ctl st'
      go st'

drawFrame :: Assets -> Controls -> DemoState -> IO ()
drawFrame assets ctl st = do
  -- Offscreen pass (also a good place for “post-processing” style work).
  drawToRenderTexture assets st

  RL.beginDrawing
  RL.clearBackground C.rayWhite

  drawHeader st
  drawPage assets ctl st
  when (stShowHelp st) (drawHelpOverlay st)

  RL.endDrawing

drawHeader :: DemoState -> IO ()
drawHeader st = do
  fps <- RL.getFPS
  let title = pageTitle (stPage st) <> "  |  FPS: " <> show fps
  Shapes.drawRectangle 0 0 screenW 36 C.darkGray
  Text.drawText title 12 10 18 C.rayWhite

pageTitle :: Page -> String
pageTitle = \case
  PageShapes -> "1: Shapes (fills + strokes)"
  PageColors -> "2: Colors + blending + scissor"
  PageTextures -> "3: Textures + render textures"

drawHelpOverlay :: DemoState -> IO ()
drawHelpOverlay st = do
  let x = 14
  let y = 48
  let w = 520
  let h = 170

  Shapes.drawRectangle x y w h (Tex.fade C.black 0.55)
  Shapes.drawRectangleLines x y w h C.rayWhite

  let helpLines =
        [ "Controls:"
        , "  1/2/3: switch pages   |   Left/Right: cycle pages"
        , "  Mouse wheel: line thickness (" <> show (round (stLineThickness st) :: Int) <> ")"
        , "  B: cycle blend mode (" <> blendModeName (currentBlendMode st) <> ")"
        , "  F: toggle texture filter (" <> textureFilterName (stTextureFilter st) <> ")"
        , "  H: toggle this help overlay"
        , "  ESC: quit"
        ]

  drawTextLines (x + 12) (y + 10) 18 22 C.rayWhite helpLines

drawTextLines :: Int -> Int -> Int -> Int -> Color -> [String] -> IO ()
drawTextLines x0 y0 fontSize lineHeight color ls =
  forM_ (zip [0 :: Int ..] ls) $ \(i, s) ->
    Text.drawText s x0 (y0 + i * lineHeight) fontSize color

drawPage :: Assets -> Controls -> DemoState -> IO ()
drawPage assets ctl st =
  case stPage st of
    PageShapes -> drawShapesPage ctl st
    PageColors -> drawColorsPage ctl st
    PageTextures -> drawTexturesPage assets ctl st

drawShapesPage :: Controls -> DemoState -> IO ()
drawShapesPage ctl st = do
  drawGrid 40 C.lightGray

  let Vector2 mx my = ctlMouse ctl
  let thickness = stLineThickness st

  -- Thick line “stroke” that follows the mouse.
  Shapes.drawLineEx (Vector2 80 120) (Vector2 mx my) thickness C.red
  Shapes.drawCircleV (Vector2 mx my) 8 C.blue
  Shapes.drawCircleLinesV (Vector2 mx my) 20 C.darkBlue

  -- Rectangle: fill + outline.
  let rect = Rectangle 80 160 240 140
  Shapes.drawRectangleRec rect (Tex.fade C.skyBlue 0.5)
  Shapes.drawRectangleLinesEx rect thickness C.blue

  -- Rounded rectangle stroke thickness has its own API.
  let rrect = Rectangle 360 160 260 140
  Shapes.drawRectangleRounded rrect 0.25 12 (Tex.fade C.lime 0.55)
  Shapes.drawRectangleRoundedLinesEx rrect 0.25 12 thickness C.darkGreen

  -- Circles / gradients / rings.
  Shapes.drawCircleGradient 220 420 70 C.yellow C.orange
  Shapes.drawCircleLines 220 420 70 C.darkBrown
  Shapes.drawRing (Vector2 520 420) 40 70 20 320 32 (Tex.fade C.purple 0.6)
  Shapes.drawRingLines (Vector2 520 420) 40 70 20 320 32 C.darkPurple

  -- Regular polygons.
  let polyCenter = Vector2 820 260
  Shapes.drawPoly polyCenter 6 70 (stRotationDeg st) (Tex.fade C.pink 0.6)
  Shapes.drawPolyLinesEx polyCenter 6 70 (stRotationDeg st) thickness C.maroon

  -- Triangle fan + line strip from a pure point generator.
  let star = starPoints (Vector2 820 470) 90 40 5 (stRotationDeg st)
  Shapes.drawTriangleFan star (Tex.fade C.gold 0.65)
  case star of
    [] -> pure ()
    p0 : _ -> Shapes.drawLineStrip (star <> [p0]) C.brown

  Text.drawText "Try moving the mouse; use the wheel to change line thickness." 80 610 18 C.darkGray

drawColorsPage :: Controls -> DemoState -> IO ()
drawColorsPage _ctl st = do
  drawGrid 32 (Tex.fade C.lightGray 0.6)

  let mode = currentBlendMode st
  let blendLabel = "Blend mode (press B): " <> blendModeName mode
  Text.drawText blendLabel 60 70 20 C.darkGray

  RL.beginBlendMode mode

  -- Layered alpha shapes (fill) + outlines (stroke).
  Shapes.drawCircle 220 260 110 (Tex.fade C.red 0.35)
  Shapes.drawCircle 320 260 110 (Tex.fade C.blue 0.35)
  Shapes.drawCircleLines 220 260 110 C.darkGray
  Shapes.drawCircleLines 320 260 110 C.darkGray

  Shapes.drawRectangle 160 380 320 170 (Tex.fade C.green 0.25)
  Shapes.drawRectangleGradientEx
    (Rectangle 160 380 320 170)
    (Tex.fade C.yellow 0.3)
    (Tex.fade C.blue 0.3)
    (Tex.fade C.red 0.3)
    (Tex.fade C.green 0.3)
  Shapes.drawRectangleLinesEx (Rectangle 160 380 320 170) (stLineThickness st) C.darkGray

  RL.endBlendMode

  -- Scissor mode clips rendering.
  Text.drawText "Scissor mode (clipping):" 560 70 20 C.darkGray
  let scX = 560
  let scY = 110
  let scW = 480
  let scH = 260
  Shapes.drawRectangleLines scX scY scW scH C.darkGray

  RL.beginScissorMode scX scY scW scH
  Shapes.drawRectangleGradientH (scX - 80) (scY + 20) (scW + 160) (scH - 40) C.skyBlue C.purple
  Shapes.drawCircleGradient (scX + 250) (scY + 140) 160 C.yellow C.orange
  RL.endScissorMode

  -- Small palette swatches.
  Text.drawText "Palette swatches:" 560 410 20 C.darkGray
  drawSwatches 560 440 34 palette

drawTexturesPage :: Assets -> Controls -> DemoState -> IO ()
drawTexturesPage assets ctl st = do
  -- Apply texture filter based on state (toggle with F).
  void $ Tex.setTextureFilter (assetsTexture assets) (stTextureFilter st)

  drawGrid 40 (Tex.fade C.lightGray 0.55)

  Text.drawText "Generated texture (genImageChecked):" 60 70 20 C.darkGray

  let tex = assetsTexture assets
  Tex.drawTexture tex 60 110 C.white
  Shapes.drawRectangleLines 60 110 256 256 C.darkGray

  -- Draw a region (“sprite sheet style”).
  Text.drawText "drawTextureRec (sub-rectangle):" 360 70 20 C.darkGray
  let src = Rectangle 0 0 128 128
  Tex.drawTextureRec tex src (Vector2 360 110) C.white
  Shapes.drawRectangleLines 360 110 128 128 C.darkGray

  -- Draw with scaling + rotation around an origin.
  Text.drawText "drawTexturePro (scale + rotate + tint):" 60 410 20 C.darkGray

  let Vector2 mx my = ctlMouse ctl
  let dest = Rectangle (mx - 90) (my - 90) 180 180
  let origin = Vector2 90 90
  Tex.drawTexturePro tex (Rectangle 0 0 256 256) dest origin (stRotationDeg st) (Tex.fade C.white 0.85)
  Shapes.drawRectangleLinesEx dest (stLineThickness st) C.darkGray

  Text.drawText "Move mouse to position the sprite." 60 650 18 C.darkGray
  Text.drawText "Left mouse held doesn't do anything yet; it's here to show input polling." 60 670 18 C.darkGray

  drawRenderTexturePreview assets st

drawToRenderTexture :: Assets -> DemoState -> IO ()
drawToRenderTexture assets st = do
  let rt = assetsRenderTexture assets

  RL.beginTextureMode rt
  RL.clearBackground C.blank

  Shapes.drawRectangle 0 0 512 512 (Tex.fade C.black 0.25)
  drawGridIntoRT 32 (Tex.fade C.rayWhite 0.15)

  Shapes.drawCircleGradient 256 256 170 C.blue C.pink
  Shapes.drawRingLines (Vector2 256 256) 120 190 0 360 60 C.rayWhite
  Shapes.drawPolyLinesEx (Vector2 256 256) 6 110 (stRotationDeg st) 10 C.yellow
  Text.drawText "RenderTexture" 168 20 30 C.rayWhite

  RL.endTextureMode

drawRenderTexturePreview :: Assets -> DemoState -> IO ()
drawRenderTexturePreview assets st = do
  Text.drawText "Offscreen render texture:" 720 70 20 C.darkGray

  let rt = assetsRenderTexture assets
  let rtTex = renderTexture'texture rt

  let w = fromIntegral (texture'width rtTex)
  let h = fromIntegral (texture'height rtTex)

  -- Note: render textures are “upside down” when sampled; negative height flips the source.
  let src = Rectangle 0 0 w (-h)
  let dest = Rectangle 720 110 340 340
  let origin = Vector2 0 0

  Tex.drawTexturePro rtTex src dest origin 0 C.white
  Shapes.drawRectangleLinesEx dest (stLineThickness st) C.darkGray

  let label = "This was drawn via beginTextureMode/endTextureMode."
  Text.drawText label 720 462 18 C.darkGray

-- | A simple grid in screen space.
drawGrid :: Int -> Color -> IO ()
drawGrid cellSize color = do
  forM_ [0, cellSize .. screenW] $ \x ->
    Shapes.drawLine x 36 x screenH color
  forM_ [36, 36 + cellSize .. screenH] $ \y ->
    Shapes.drawLine 0 y screenW y color

drawGridIntoRT :: Int -> Color -> IO ()
drawGridIntoRT cellSize color = do
  forM_ [0, cellSize .. 512] $ \x ->
    Shapes.drawLine x 0 x 512 color
  forM_ [0, cellSize .. 512] $ \y ->
    Shapes.drawLine 0 y 512 y color

-- | A small hand-picked palette: (label, color).
palette :: [(String, Color)]
palette =
  [ ("rayWhite", C.rayWhite)
  , ("lightGray", C.lightGray)
  , ("gray", C.gray)
  , ("darkGray", C.darkGray)
  , ("red", C.red)
  , ("orange", C.orange)
  , ("yellow", C.yellow)
  , ("green", C.green)
  , ("skyBlue", C.skyBlue)
  , ("blue", C.blue)
  , ("purple", C.purple)
  , ("magenta", C.magenta)
  ]

drawSwatches :: Int -> Int -> Int -> [(String, Color)] -> IO ()
drawSwatches x0 y0 swatchSize xs = do
  forM_ (zip [0 :: Int ..] xs) $ \(i, (name, col)) -> do
    let x = x0 + (i `mod` 6) * (swatchSize + 10)
    let y = y0 + (i `div` 6) * (swatchSize + 34)
    Shapes.drawRectangle x y swatchSize swatchSize col
    Shapes.drawRectangleLines x y swatchSize swatchSize C.darkGray
    Text.drawText name x (y + swatchSize + 6) 16 C.darkGray

blendModes :: [BlendMode]
blendModes =
  [ BlendAlpha
  , BlendAdditive
  , BlendMultiplied
  , BlendSubtractColors
  , BlendAlphaPremultiply
  ]

currentBlendMode :: DemoState -> BlendMode
currentBlendMode st =
  blendModes !! (stBlendModeIx st `mod` length blendModes)

blendModeName :: BlendMode -> String
blendModeName = \case
  BlendAlpha -> "alpha"
  BlendAdditive -> "additive"
  BlendMultiplied -> "multiplied"
  BlendAddColors -> "add colors"
  BlendSubtractColors -> "subtract colors"
  BlendAlphaPremultiply -> "alpha premultiply"
  BlendCustom -> "custom"
  BlendCustomSeparate -> "custom separate"

textureFilterName :: TextureFilter -> String
textureFilterName = \case
  TextureFilterPoint -> "point (pixelated)"
  TextureFilterBilinear -> "bilinear (smooth)"
  TextureFilterTrilinear -> "trilinear"
  TextureFilterAnisotropic4x -> "anisotropic 4x"
  TextureFilterAnisotropic8x -> "anisotropic 8x"
  TextureFilterAnisotropic16x -> "anisotropic 16x"

toggleTextureFilter :: TextureFilter -> TextureFilter
toggleTextureFilter = \case
  TextureFilterPoint -> TextureFilterBilinear
  _ -> TextureFilterPoint

nextEnum :: (Eq a, Enum a, Bounded a) => a -> a
nextEnum x
  | x == maxBound = minBound
  | otherwise = succ x

prevEnum :: (Eq a, Enum a, Bounded a) => a -> a
prevEnum x
  | x == minBound = maxBound
  | otherwise = pred x

clamp :: Ord a => a -> a -> a -> a
clamp lo hi =
  max lo . min hi

-- | Generate star points in screen space.
--
-- This is intentionally pure; the rendering functions can consume the resulting points.
starPoints :: Vector2 -> Float -> Float -> Int -> Float -> [Vector2]
starPoints (Vector2 cx cy) outer inner spikes rotDeg =
  [ Vector2 (cx + r * cos a) (cy + r * sin a)
  | i <- [0 .. spikes * 2 - 1]
  , let r = if even i then outer else inner
  , let a = degToRad (rotDeg + fromIntegral i * (360 / fromIntegral (spikes * 2)) - 90)
  ]

degToRad :: Float -> Float
degToRad deg =
  deg * pi / 180