Hello Window
You should first read the equivalent Learn OpenGL article, so that you know vaguely what we're doing and you get the appropriate background. I'll link to and assume that you've read the Learn OpenGL version of each article as I write the Haskell versions, mostly because I'm not an OpenGL expert and I don't want to accidentally say something wrong while paraphrasing things.
Imports
The GLEW stuff that the Learn OpenGL site talks about is a bit of C code to dynamically load all the OpenGL functions according to the local drivers. The gl package will automatically do that for us, so we don't need it. One thing to note is that if you import Graphics.GL directly then it will import the newest version of OpenGL stuff (4.5), and we want to stick with only 3.3 things, so we will import a specific submodule as well as the types module. Note that importing Graphics.GL.Core33 will also automatically import Graphics.GL.Core32 as well (which is where most of the functions we'll use reside).
We'll also define some values to use later near the top of the file so we can fiddle with them if we want.
-- base
import Control.Monad (when)
-- GLFW-b, qualified for clarity
import qualified Graphics.UI.GLFW as GLFW
-- gl, all types and funcs here will already start with "gl"
import Graphics.GL.Core33
import Graphics.GL.Types
winWidth = 800
winHeight = 600
winTitle = "Hello Window"
KeyCallback
Next we define the callback for what to do when any key is pressed. Right now we'll just print the key no matter what, and then also set the "should close" value of the window to True if the user pressed the Escape key. The GLFW library calls any of our set callback functions as necessary when we use GLFW.pollEvents, and they will always be called using the main thread.
-- type KeyCallback = Window -> Key -> Int -> KeyState -> ModifierKeys -> IO ()
callback :: GLFW.KeyCallback
callback window key scanCode keyState modKeys = do
print key
when (key == GLFW.Key'Escape && keyState == GLFW.KeyState'Pressed)
(GLFW.setWindowShouldClose window True)
Main
During main, we initialize GLFW, give it our hints, try to get a window with a callback set, and then render a black screen.
main :: IO ()
main = do
GLFW.init
GLFW.windowHint (GLFW.WindowHint'ContextVersionMajor 3)
GLFW.windowHint (GLFW.WindowHint'ContextVersionMinor 3)
GLFW.windowHint (GLFW.WindowHint'OpenGLProfile GLFW.OpenGLProfile'Core)
GLFW.windowHint (GLFW.WindowHint'Resizable False)
maybeWindow <- GLFW.createWindow winWidth winHeight winTitle Nothing Nothing
case maybeWindow of
Nothing -> putStrLn "Failed to create a GLFW window!"
Just window -> do
-- enable keys
GLFW.setKeyCallback window (Just callback)
-- calibrate the viewport
GLFW.makeContextCurrent (Just window)
(x,y) <- GLFW.getFramebufferSize window
glViewport 0 0 (fromIntegral x) (fromIntegral y)
-- enter our main loop
let loop = do
shouldContinue <- not <$> GLFW.windowShouldClose window
when shouldContinue $ do
-- event poll
GLFW.pollEvents
-- drawing
glClearColor 0.2 0.3 0.3 1.0
glClear GL_COLOR_BUFFER_BIT
-- swap buffers and go again
GLFW.swapBuffers window
loop
loop
GLFW.terminate
However, this is a little error prone. We have to match init calls with terminate calls, and technically we should be checking the result of the init call to see if we should continue. If there's an exception or something then our program will crash without having called terminate, which is probably just slightly annoying, but it's bad form. Thankfully, we can write a little helper to make sure that our inits and terminates match up no matter what, by way of the bracket function:
-- | Ensures that we only run GLFW code while it's initialized, and also that we
-- always terminate it when we're done.
bracketGLFW :: IO () -> IO ()
bracketGLFW act = bracket GLFW.init (const GLFW.terminate) $ \initWorked ->
when initWorked act
GLFW Dangers
Before you try to build too much with GLFW you should probably browse the official introduction page. Importantly it explains several limitations that the library has in the name of cross-platform compatibility.
The Haskell GLFW-b package actually eases some of the danger for us, but the current version on hackage doesn't document this fact. To find that out you have to check their git repo and read the latest documentation updates directly (such as at the top the GLFW.hs file). Even then, not all the dangerous bits can be fixed by a wrapper library.
What This Means For Us
- You can't create or destroy Windows, Contexts, or Cursors from any thread except the main thread.
- You have to follow the "current context" threading restrictions as well.
- pollEvents and waitEvents actually just schedule the callbacks to be run after they're done, so your callbacks can be reentrant if they need to be.
- As explained in the Input Guide, it's possible on some platforms for event callbacks to be fired (in our case, scheduled) directly by the OS, even if we don't call pollEvents or waitEvents at all. Your code should be written accordingly.
Note also that there are still platform differences and possible bugs and such. The GLFW-b package has some good tests though, so you can download their github project and run the tests to see how things are working on your own system. That way if something goes wrong you can at least know if it's just not supported on your system or not.
On my own system (Windows 7), I usually get 42/44 tests passing, but "getWindowFocused" and "window size" seem to fail. Also, if I click the mouse during the test it can screw up the clipboard test as well. These are the sorts of imprecise details that you just have to cope with when doing User Interface stuff.