The Event Loop

So far, our app isn't really "doing" anything – in fact, it won't even respond if we try to close it with the "x" button. To handle input from the user, we must use the event loop.

Let's edit main to handle events instead of doing nothing with loop {}:

fn main() {
    let event_loop = EventLoop::new();
    let (context, gl_display, window, surface) = create_window(&event_loop);

    let renderer = unsafe { OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s).cast()) }
        .expect("Cannot create renderer");

    let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");

    render(&context, &surface, &window, &mut canvas);

    event_loop.run(|event, _target, control_flow| match event {
        Event::WindowEvent { window_id, event } => {
            println!("{:?}", event)
        }
        _ => {}
    })
}

event_loop.run will call the provided closure for each new event, until the program exits.

Event is the first parameter of the closure, and the one we are most interested in. It is an enum with a few branches for different types of events. In the example above, we only capture and print WindowEvents, and ignore the rest.

Each Event::WindowEvent contains:

  • A WindowId – but since we only have 1 window in this example, the ID will always match with our window's ID.
  • A WindowEvent enum, which contains information about the window event.

Note: Event::WindowEvent is a branch of the Event enum, which contains another enum, WindowEvent, of the same name! It can get confusing, but Rust namespaces distinguish between the two – one is glutin::event::Event::WindowEvent and the other is glutin::event::WindowEvent.

You can run the example to see the different types of WindowEvents being printed as you interact with the window! You might see something like:

CursorMoved { device_id: DeviceId(X(DeviceId(2))), position: PhysicalPosition { x: 931.1624145507813, y: 270.08074951171875 }, modifiers: (empty) }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 0, value: 969.1624302163254 }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 1, value: 372.08075569337234 }
CursorMoved { device_id: DeviceId(X(DeviceId(2))), position: PhysicalPosition { x: 981.8682861328125, y: 257.404296875 }, modifiers: (empty) }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 0, value: 1019.8683132424485 }
AxisMotion { device_id: DeviceId(X(DeviceId(2))), axis: 1, value: 359.4042849368416 }
CursorLeft { device_id: DeviceId(X(DeviceId(2))) }
KeyboardInput { device_id: DeviceId(X(DeviceId(3))), input: KeyboardInput { scancode: 113, state: Released, virtual_keycode: None, modifiers: (empty) }, is_synthetic: true }
ModifiersChanged((empty))
Focused(false)

Exiting on Close

Clicking on the window's close button also creates a WindowEvent. We'll capture this event to exit the application when the user requests it:

event_loop.run(move |event, _target, control_flow| match event {
    Event::WindowEvent { event, .. } => match event {
        WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
        _ => {}
    },
    _ => {}
})

Try running it! Thr program should exit when you click the close button.

The third parameter of the closure is of type ControlFlow. You can set it to ControlFlow::Exit to request the application to quit.

Tracking Mouse Movement

Using the CursorMoved event, we can track the mouse position. Let's create a variable for storing the latest position we know, and updating it whenever we get a CursorMoved event:

let mut mouse_position = PhysicalPosition::new(0., 0.);

event_loop.run(move |event, _target, control_flow| match event {
    Event::WindowEvent { window_id, event } => match event {
        WindowEvent::CursorMoved { position, .. } => {
            mouse_position = position;
        }
        // ...
    },
    _ => {}
})

Re-rendering

So far, our code only renders the window once – after that, there's no rendering, only event handling. But suppose we wanted the red square to follow the mouse. How can we re-render the square in the new position?

We'll need to re-render every time the mouse position changes. The correct way to do this is to request_redraw, so that the platform knows we want to draw some new stuff. Then, we'll receive a Event::RedrawRequested event, and that's when we can render a new frame:

let mut mouse_position = PhysicalPosition::new(0., 0.);
event_loop.run(move |event, _target, control_flow| match event {
    Event::WindowEvent { window_id, event } => match event {
        WindowEvent::CursorMoved { position, .. } => {
            mouse_position = position;
            window.request_redraw();
        }
        // ...
    },
    Event::RedrawRequested(_) => {
        render(&context, &surface, &window, &mut canvas, mouse_position);
    }
    _ => {}
})

Finally, we should update our render function to take the mouse position into account – we can just use those coordinates for the square position:

fn render<T: Renderer>(
    context: &PossiblyCurrentContext,
    surface: &Surface<WindowSurface>,
    window: &Window,
    canvas: &mut Canvas<T>,
    square_position: PhysicalPosition<f64>,
) {
    //...
    canvas.clear_rect(
        square_position.x as u32,
        square_position.y as u32,
        30,
        30,
        Color::rgbf(1., 0., 0.),
    );
    //...
}

Our code runs! There's just one small problem...

Screenshot of a window: a trail of blocky red paint on a black background, with the cursor at the end of it

What happened? Since there's no code to "erase" the old square, all the squares pile up on each other, creating a red mess. To fix this, we should clear the entire window before rendering anything new.

canvas.clear_rect(0, 0, size.width, size.height, Color::black());

Now, if you run the code, the red square will happily follow the cursor wherever it goes.

Screenshot of a window: red square at the cursor position on a black background

To recap, here's the code we've written:

use std::num::NonZeroU32;

use femtovg::renderer::OpenGl;
use femtovg::{Canvas, Color};
use glutin::surface::Surface;
use glutin::{context::PossiblyCurrentContext, display::Display};
use glutin_winit::DisplayBuilder;
use raw_window_handle::HasRawWindowHandle;
use winit::dpi::PhysicalPosition;
use winit::event::{Event, WindowEvent};
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use winit::{dpi::PhysicalSize, window::Window};

use glutin::{
    config::ConfigTemplateBuilder,
    context::ContextAttributesBuilder,
    display::GetGlDisplay,
    prelude::*,
    surface::{SurfaceAttributesBuilder, WindowSurface},
};

fn main() {
    let event_loop = EventLoop::new().unwrap();
    let (context, gl_display, window, surface) = create_window(&event_loop);

    let renderer = unsafe { OpenGl::new_from_function_cstr(|s| gl_display.get_proc_address(s).cast()) }
        .expect("Cannot create renderer");

    let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");
    canvas.set_size(1000, 600, window.scale_factor() as f32);

    let mut mouse_position = PhysicalPosition::new(0., 0.);

    event_loop
        .run(move |event, target| match event {
            Event::WindowEvent { event, .. } => match event {
                WindowEvent::CursorMoved { position, .. } => {
                    mouse_position = position;
                    window.request_redraw();
                }
                WindowEvent::CloseRequested => target.exit(),
                WindowEvent::RedrawRequested { .. } => {
                    render(&context, &surface, &window, &mut canvas, mouse_position);
                }
                _ => {}
            },
            _ => {}
        })
        .unwrap();
}

fn create_window(event_loop: &EventLoop<()>) -> (PossiblyCurrentContext, Display, Window, Surface<WindowSurface>) {
    let window_builder = WindowBuilder::new()
        .with_inner_size(PhysicalSize::new(1000., 600.))
        .with_title("Femtovg");

    let template = ConfigTemplateBuilder::new().with_alpha_size(8);

    let display_builder = DisplayBuilder::new().with_window_builder(Some(window_builder));

    let (window, gl_config) = display_builder
        .build(event_loop, template, |mut configs| configs.next().unwrap())
        .unwrap();

    let window = window.unwrap();

    let gl_display = gl_config.display();

    let context_attributes = ContextAttributesBuilder::new().build(Some(window.raw_window_handle()));

    let mut not_current_gl_context =
        Some(unsafe { gl_display.create_context(&gl_config, &context_attributes).unwrap() });

    let attrs = SurfaceAttributesBuilder::<WindowSurface>::new().build(
        window.raw_window_handle(),
        NonZeroU32::new(1000).unwrap(),
        NonZeroU32::new(600).unwrap(),
    );

    let surface = unsafe { gl_config.display().create_window_surface(&gl_config, &attrs).unwrap() };

    (
        not_current_gl_context.take().unwrap().make_current(&surface).unwrap(),
        gl_display,
        window,
        surface,
    )
}

fn render(
    context: &PossiblyCurrentContext,
    surface: &Surface<WindowSurface>,
    window: &Window,
    canvas: &mut Canvas<OpenGl>,
    square_position: PhysicalPosition<f64>,
) {
    // Make sure the canvas has the right size:
    let size = window.inner_size();
    canvas.set_size(size.width, size.height, window.scale_factor() as f32);

    canvas.clear_rect(0, 0, size.width, size.height, Color::black());

    // Make smol red rectangle
    canvas.clear_rect(
        square_position.x as u32,
        square_position.y as u32,
        30,
        30,
        Color::rgbf(1., 0., 0.),
    );

    // Tell renderer to execute all drawing commands
    canvas.flush();
    // Display what we've just rendered
    surface.swap_buffers(context).expect("Could not swap buffers");
}