Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 20

Window Event Handling

Recommended resources

Introduction

Hello again! In our last lesson, we successfully used GLAD to load modern OpenGL functions, which allowed us to clear our window to a specific color. We now have a basic, but static, graphics application. The while loop is running, but it doesn't respond to any of our actions except for closing the window via the title bar.

Today, we'll bring our application to life. The goal for this lesson is to configure and handle window events such as user input and resizing. This is a fundamental step in creating any interactive program, especially a game. We will explore two primary ways of handling events and implement solutions for two of the most common event types: resizing the window and pressing a key.

This topic also provides a perfect, practical application for a C concept we touched on previously: function pointers. You'll see how we can pass our own functions to the GLFW library to have them execute automatically when specific events occur.


The GLFW Event Model

How does our application know when we press a key or resize the window? The key is the glfwPollEvents() function, which we already have in our main loop.

Every time you move the mouse, press a key, or resize the window, the operating system sends an event notification. GLFW catches these events and places them in an internal queue. When you call glfwPollEvents(), GLFW processes all the events currently in that queue. For each event, it checks if you have registered a corresponding function to handle it.

There are two main strategies for reacting to these events:

  1. Callbacks (Asynchronous): You write a function and "register" it with GLFW, telling it, "When an event of type X happens, call this function for me." This is an asynchronous, event-driven approach. It's ideal for discrete actions like a single key press, a mouse click, or a window resize. The function you register is called a callback, and in C, we pass it to GLFW as a function pointer.

  2. Polling (Synchronous): Inside your main loop, you actively ask GLFW for the current state of an input device. For example, "Is the 'W' key being held down right now?" This is a synchronous approach that checks the state on every single frame. It's perfect for continuous actions like character movement.

A robust game engine uses a combination of both. Let's start with a callback for window resizing.


Handling Window Resizing with a Callback

If you run your current code and try to resize the window, you'll notice the colored area doesn't change size with the window. The operating system resizes the window frame, but our OpenGL rendering area, the viewport, remains fixed at its original size.

Resizing | Game Engine series

The video 'Resizing' from The Cherno's game engine series provides an excellent visual demonstration of this problem and explains why it happens.

Watch from the beginning to 03:37. Focus on understanding why the rendered image gets cut off and the role of glViewport in telling OpenGL about the new rendering dimensions. The video uses C++, but the concepts apply directly to our C project.

As the video explains, we need to explicitly tell OpenGL to adjust its viewport whenever the window size changes. The function for this is glViewport(x, y, width, height), which maps the normalized coordinates of OpenGL (-1 to 1) to the pixel coordinates of your window.

To do this automatically, we will create a callback function and register it with GLFW.

Hello Window - LearnOpenGL

The 'Hello Window' tutorial on LearnOpenGL.com provides a clear, practical example of implementing a resize callback.

Read the section titled 'Viewport'. It provides the exact C++ code for a framebuffer_size_callback function and shows how to register it with GLFW. The syntax is nearly identical to what we will use in C.

Framebuffer Size vs. Window Size

One crucial detail is the distinction between window size and framebuffer size. Window size is measured in "screen coordinates," which can be an abstract unit. Framebuffer size is measured in pixels, which is what OpenGL's glViewport function expects. On standard displays, these might be the same, but on high-DPI (or "Retina") displays, one screen coordinate might correspond to multiple pixels.

Therefore, we should always use the framebuffer size for OpenGL operations. GLFW provides a specific callback just for this.

Window guide - GLFW

The official GLFW documentation explains this distinction clearly. It's a short but important read.

Read the section titled 'Framebuffer size'. Pay close attention to the explanation of why screen coordinates and pixels may not be the same and why glfwSetFramebufferSizeCallback is the correct choice for updating the viewport.

Let's implement this.

  1. Define the callback function. It must have a specific signature: void func(GLFWwindow* window, int width, int height).

    // A callback function that gets called whenever the window is resized
    void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
        // Tell OpenGL the new size of the rendering area
        glViewport(0, 0, width, height);
    }
    
  2. Register the callback. After creating the window but before the main loop, we tell GLFW to use our function.

    // In main(), after glfwCreateWindow()
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    

When you run the application now, the colored background will correctly resize along with the window!


Handling User Input with Polling

Now, let's add some keyboard input. A common requirement is to close the application when the user presses the ESC key. This is a perfect use case for polling, as we can simply check the key's state in every frame of our game loop.

Game Loop Structure with OpenGL and GLFW Window Creation
A common game loop structure. Our `processInput` function will fit neatly into the 'input' phase of each frame.

We'll create a function called processInput to keep our input-handling code organized and call it once per frame from our main loop.

Hello Window - LearnOpenGL

The same LearnOpenGL article also demonstrates how to handle basic keyboard input using polling.

Read the section titled 'Input'. It shows how to use glfwGetKey to check if a specific key is pressed and glfwSetWindowShouldClose to signal the main loop to terminate.

Let's add this to our code.

  1. Define the input processing function.

    void processInput(GLFWwindow *window) {
        // Check if the ESC key was pressed.
        // glfwGetKey returns GLFW_PRESS if the key is currently down.
        if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
            // Tell GLFW that the window should close.
            glfwSetWindowShouldClose(window, 1); // 1 is true
        }
    }
    
  2. Call this function from the main loop. It should be called on every iteration to ensure we never miss an input.

    // In main(), inside the while loop
    while (!glfwWindowShouldClose(window)) {
        // 1. Process Input
        processInput(window);
    
        // 2. Rendering commands
        // ...
    
        // 3. Swap buffers and poll events
        glfwPollEvents();
        glfwSwapBuffers(window);
    }
    

Now, when you run the application, you can close it not only with the title bar's close button but also by pressing the ESC key.


Putting It All Together

Here is the complete main.c file incorporating both the resize callback and the input polling function.




#include <stdio.h>
#include <glad/glad.h>
#include <GLFW/glfw3.h>

// Callback function for window resizing
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}

// Function to process all input
void processInput(GLFWwindow *window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, 1);
    }
}

int main(void) {
    // Initialize GLFW
    if (!glfwInit()) {
        printf("Failed to initialize GLFW\n");
        return -1;
    }

    // Set window hints for OpenGL 3.3 Core Profile
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // Create the window
    GLFWwindow* window = glfwCreateWindow(800, 600, "Interactive Window", NULL, NULL);
    if (window == NULL) {
        printf("Failed to create GLFW window\n");
        glfwTerminate();
        return -1;
    }

    // Make the window's context current
    glfwMakeContextCurrent(window);

    // Register our resize callback function
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // Initialize GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        printf("Failed to initialize GLAD\n");
        glfwTerminate();
        return -1;
    }

    // The Render Loop
    while (!glfwWindowShouldClose(window)) {
        // Input
        processInput(window);

        // Rendering
        glClearColor(0.2f, 0.3f, 0.8f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Swap buffers and poll for events
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // Clean up
    glfwDestroyWindow(window);
    glfwTerminate();
    
    return 0;
}

A Broader View of Window Events

We've only scratched the surface. GLFW provides a rich set of callbacks for many other events, including:

  • glfwSetKeyCallback (for discrete key press/release/repeat events)
  • glfwSetCursorPosCallback (for mouse movement)
  • glfwSetMouseButtonCallback (for mouse clicks)
  • glfwSetScrollCallback (for mouse scroll wheel)
  • glfwSetWindowFocusCallback (when the window gains or loses focus)

You can find a complete list in the GLFW "Window guide", which we consulted earlier. We will implement more of these in later lessons as we build our input and camera systems. For now, what's important is that you understand the fundamental mechanism of handling events.


Conclusion

In this lesson, we transformed our static window into an interactive application. You learned how to respond to events using both callbacks and polling—two fundamental techniques in game programming.

Key Takeaways:

  • glfwPollEvents() processes a queue of events gathered from the operating system.
  • Callbacks are functions you register to be called automatically when a specific event occurs (e.g., window resize). This is an asynchronous model.
  • Polling is the act of checking the state of an input device every frame within the main loop (e.g., checking if a key is held down). This is a synchronous model.
  • To handle window resizing, you must register a framebuffer_size_callback to update the OpenGL glViewport.
  • You can handle simple keyboard input by using glfwGetKey within a processInput function called every frame.

Preview of the Next Lesson:
Our main loop now correctly handles input and resizing. The next learning outcome in our module is to "Implement a main loop that clears the screen and swaps buffers." While we're already doing this, the next lesson will formalize the structure of our game loop. We'll take a closer look at the distinct phases within the loop (input, update, render), discuss the concept of double buffering in more detail, and understand why this structure is the universal heartbeat of real-time graphics applications.