Search code examples
rusteventsdispatcher

Simple event dispatcher in Rust


I'm trying to develop a simple application to create simple objects (like a cube) with OpenGL.

So far I've created a wrapper to OpenGL using the "gl" module to initialize vbos, vaos, programs, shaders etc...

Initially I handled the events I was interested on in the main function, using the "glutin" module. The code looked something like this:

main.rs

fn main() {
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new().with_title("Rust OpenGL");

    let gl_context = ContextBuilder::new()
        .with_gl(GlRequest::Specific(Api::OpenGl, (3, 3)))
        .build_windowed(window, &event_loop)
        .expect("Cannot create windowed context");

    let gl_context = unsafe {
        gl_context
            .make_current()
            .expect("Failed to make context current")
    };

    gl::load_with(|ptr| gl_context.get_proc_address(ptr) as *const _);

    let v1 = vec![
        Vertex((-0.5, -0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
        Vertex((0.5, -0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
        Vertex((0.5, 0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
        Vertex((-0.5, 0.5, 0.5).into(), (1.0, 0.0, 0.0).into()),
    ];
    let front_face = Square::new(&v1);

    let v2 = vec![
        Vertex((-0.5, -0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
        Vertex((0.5, -0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
        Vertex((0.5, 0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
        Vertex((-0.5, 0.5, -0.5).into(), (0.0, 1.0, 0.0).into()),
    ];
    let back_face = Square::new(&v2);

    let cube = Cube::from(&[front_face, back_face]);

    let mut rotation_angle: Rad<f32> = Deg(0.0).into();

    event_loop.run(move |event, target, control_flow| {
        *control_flow = ControlFlow::Wait;

        match event {
            Event::LoopDestroyed => (),
            Event::WindowEvent { event, .. } => match event {
                WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
                WindowEvent::Resized(physical_size) => gl_context.resize(physical_size),
                WindowEvent::KeyboardInput { input, .. } => {
                    if let Some(VirtualKeyCode::A) = input.virtual_keycode {
                        match input.state {
                            ElementState::Pressed => {
                                rotation_angle -= Deg(0.5).into();
                                gl_context.window().request_redraw();
                            },
                            _ => ()
                        }
                    }
                    if let Some(VirtualKeyCode::D) = input.virtual_keycode {
                        match input.state {
                            ElementState::Pressed => {
                                rotation_angle += Deg(0.5).into();
                                gl_context.window().request_redraw();
                            },
                            _ => ()
                        }
                    }
                }
                _ => (),
            },
            Event::RedrawRequested(_) => {
                println!("{:?}", rotation_angle);

                let rotation_matrix_y = Matrix4::from_axis_angle(Vector3::unit_y(), rotation_angle);
                let rotation_matrix_x = Matrix4::from_axis_angle(Vector3::unit_x(), Deg(30.0));
                let transformation_matrix = rotation_matrix_y * rotation_matrix_x;

                let transform_location = cube.gl_wrapper().program.get_uniform_location("Transform").unwrap();

                unsafe {
                    gl::UniformMatrix4fv(transform_location as GLint, 1, gl::FALSE, transformation_matrix.as_ptr());

                    gl::ClearColor(0.0,  0.0,  0.0,  1.0);
                    gl::Clear(gl::COLOR_BUFFER_BIT);

                    Renderer::draw(&cube);
                }
                gl_context.swap_buffers().unwrap();
            }
            _ => (),
        }
    });
}

In this demo, the Renderer struct was responsible for drawing simple shapes, and by pressing A and D keys I could rotate the cube respectively to the left and to the right; I then modified the code a little, but I'd like to focus on the event handling part.

As you might see, the code is a lot, especially to be all in the same function, the main function. I so thought it would be good to refactor this mess a little, and I came up with this:

main.rs

fn main() {
    let event_loop = EventLoop::new();
    let mut app = Application::new(&event_loop);

    event_loop.run(move |event, _, control_flow| {
        // Commenting these out as i'm not sure it works
        // app.set_control_flow_reference(control_flow);
        // app.set_control_flow(ControlFlow::Poll);
        
        *control_flow = ControlFlow::Poll;

        let mut generic_dispatcher = GenericDispatcher::new(&mut app);
        generic_dispatcher.handle(event);
    });
}

application.rs

#[derive(Debug)]
pub struct Application {
    control_flow: ControlFlow,
    gl_context: ContextWrapper<PossiblyCurrent, Window>
}

impl Application {
    pub fn new(event_loop: &EventLoop<()>) -> Self {
        let window_builder = WindowBuilder::new().with_title("Rust OpenGL");
        let context = ContextBuilder::new()
            .with_gl(GlRequest::Specific(Api::OpenGl, (3, 3)))
            .build_windowed(window_builder, event_loop)
            .expect("Cannot create windowed context");

        let context = unsafe {
            context
                .make_current()
                .expect("Failed to make context current")
        };

        gl::load_with(|ptr| context.get_proc_address(ptr) as *const _);
        unsafe {
            info::log_error("loading gl");
        }

        Self {
            control_flow: ControlFlow::default(),
            gl_context: context,
        }
    }

    pub fn set_control_flow_reference(&mut self, new_control_flow_reference: &mut ControlFlow) {
        let mut control_flow_reference = &mut self.control_flow;
        control_flow_reference = new_control_flow_reference;
    }

    pub fn set_control_flow(&mut self, new_control_flow: ControlFlow) {
        let control_flow_reference = &mut self.control_flow;
        *control_flow_reference = new_control_flow;
    }

    pub fn gl_context(&self) -> &ContextWrapper<PossiblyCurrent, Window> {
        &self.gl_context
    }
}

handler.rs

pub trait Handler<E> {
    fn handle(&mut self, item: E);
}

pub trait Dispatcher<'a> {
    fn new(application: &'a mut Application) -> Self;
    fn application(&'a self) -> &'a Application;
}

generic.rs

pub struct GenericDispatcher<'a> {
    application: &'a mut Application,
    angle_x: Deg<f32>,
    angle_y: Deg<f32>,
    cube: Cube
}

// snip the impl for the new associated function

impl<'a> Handler<Event<'_, ()>> for GenericDispatcher<'a> {
    fn handle(&mut self, item: Event<'_, ()>) {
        match item {
            Event::NewEvents(_) => {}
            Event::WindowEvent { window_id, event } => {
                let mut window_dispatcher = WindowDispatcher::new(self.application);
                window_dispatcher.handle(event);
            }
            Event::DeviceEvent { device_id, event } => {
                let mut device_dispatcher = DeviceDispatcher::new(self.application);
                device_dispatcher.handle(event);
            }
            Event::UserEvent(_) => {}
            Event::Suspended => {}
            Event::Resumed => {}
            Event::MainEventsCleared => {
                self.application.gl_context().window().request_redraw();
            }
            Event::RedrawRequested(_) => {
                println!("angle_y: {:?}", self.angle_y);
                println!("angle_x: {:?}", self.angle_x);

                let rotation_matrix_y = Matrix4::from_axis_angle(Vector3::unit_y(), self.angle_y);
                let rotation_matrix_x = Matrix4::from_axis_angle(Vector3::unit_x(), self.angle_x);
                let model_matrix = rotation_matrix_y * rotation_matrix_x;

                let model_location = self.cube.gl_wrapper().program.get_uniform_location("Model").unwrap();

                unsafe {
                    gl::UniformMatrix4fv(model_location as GLint, 1, gl::FALSE, model_matrix.as_ptr());
                    gl::Enable(gl::DEPTH_TEST);
                    gl::DepthFunc(gl::LESS);
                    gl::ClearColor(0.0,  0.0,  0.0,  1.0);
                    gl::Clear(gl::COLOR_BUFFER_BIT);
                    gl::Clear(gl::DEPTH_BUFFER_BIT);

                    Renderer::draw(&self.cube);
                }
                self.application.gl_context().swap_buffers().unwrap();
            }
            Event::RedrawEventsCleared => {}
            Event::LoopDestroyed => {}
        }
    }
}

window.rs

pub struct WindowDispatcher<'a> {
    application: &'a mut Application
}

impl<'a> Handler<WindowEvent<'_>> for WindowDispatcher<'a> {
    fn handle(&mut self, item: WindowEvent<'_>) {
        let mut keyboard_dispatcher = KeyboardDispatcher::new(self.application);
        match item {
            WindowEvent::Resized(physical_size) => {
                self.application.gl_context().resize(physical_size);
            }
            WindowEvent::Moved(_) => {}
            WindowEvent::CloseRequested => {
                self.application.set_control_flow(ControlFlow::Exit);
            }
            WindowEvent::Destroyed => {}
            WindowEvent::DroppedFile(_) => {}
            WindowEvent::HoveredFile(_) => {}
            WindowEvent::HoveredFileCancelled => {}
            WindowEvent::ReceivedCharacter(_) => {}
            WindowEvent::Focused(_) => {}
            WindowEvent::KeyboardInput { device_id, input, is_synthetic } => {
                keyboard_dispatcher.handle(input);
            }
            WindowEvent::ModifiersChanged(_) => {}
            WindowEvent::Ime(_) => {}
            WindowEvent::CursorMoved { device_id, position, modifiers } => {}
            WindowEvent::CursorEntered { device_id } => {}
            WindowEvent::CursorLeft { device_id } => {}
            WindowEvent::MouseWheel { device_id, delta, phase, modifiers } => {}
            WindowEvent::MouseInput { device_id, state, button, modifiers } => {}
            WindowEvent::TouchpadPressure { device_id, pressure, stage } => {}
            WindowEvent::AxisMotion { device_id, axis, value } => {}
            WindowEvent::Touch(_) => {}
            WindowEvent::ScaleFactorChanged { scale_factor, new_inner_size } => {}
            WindowEvent::ThemeChanged(_) => {}
            WindowEvent::Occluded(_) => {}
        }
    }
}

keyboard.rs

pub struct KeyboardDispatcher<'a> {
    application: &'a mut Application
}

impl<'a> Handler<KeyboardInput> for KeyboardDispatcher<'a> {
    fn handle(&mut self, item: KeyboardInput) {
        match item {
            KeyboardInput { scancode, state, virtual_keycode, modifiers } => {
                match state {
                    ElementState::Pressed => {
                        println!("{:?} pressed!", virtual_keycode.unwrap());
                    }
                    ElementState::Released => {
                        println!("{:?} released!", virtual_keycode.unwrap());
                    }
                }
            }
        }
    }
}

When executing the program I only get a white window, nothing gets drawn and no event seems to be catched. I can't even move the window around nor close it, to stop the program I have to force the execution to stop.

I've then tried to comment out the creation of the WindowDispatcher and DeviceDispatcher and the call Renderer::draw() inside the handle method of the GenericDispatcher to see if that was the problem:

impl<'a> Handler<Event<'_, ()>> for GenericDispatcher<'a> {
    fn handle(&mut self, item: Event<'_, ()>) {
            // snip
            Event::WindowEvent { window_id, event } => {
                // let mut window_dispatcher = WindowDispatcher::new(self.application);
                // window_dispatcher.handle(event);
            }
            Event::DeviceEvent { device_id, event } => {
                // let mut device_dispatcher = DeviceDispatcher::new(self.application);
                // device_dispatcher.handle(event);
            }
            // snip
            Event::RedrawRequested(_) => {
                // snip
                unsafe {
                    gl::UniformMatrix4fv(model_location as GLint, 1, gl::FALSE, model_matrix.as_ptr());
                    gl::Enable(gl::DEPTH_TEST);
                    gl::DepthFunc(gl::LESS);
                    gl::ClearColor(0.0,  0.0,  0.0,  1.0);
                    gl::Clear(gl::COLOR_BUFFER_BIT);
                    gl::Clear(gl::DEPTH_BUFFER_BIT);

                    // Renderer::draw(&self.cube);
                }
                // snip
        }
    }
}

But i'm still facing the same issue. Sometimes, without changing the code at all, I get a black window and I get printed the angles, as expected, but then everything freezes once again and I have to stop the execution by force.

How can I fix this?


Solution

  • For anyone wondering, I've managed to solve my problem.

    Here's the 5 key steps:

    1. Fix dependencies

    Turns out you need more recent version of the gl and glutin crates and you also need the winit, glutin-winit and raw-window-handle crates.

    2. Create custom events

    Use a enum for every event your application might need.

    pub enum CustomEvent {
        Log(String),
        // others
    }
    

    3. Create a dispatcher

    Use a struct to store useful data (like the proxy used to send data to the application):

    use winit::event_loop::EventLoopProxy;
    
    pub struct Dispatcher {
        // Make sure to pass as generic the enum created in point 2.
        proxy: EventLoopProxy<CustomEvent>,
        // others
    }
    

    and to implement methods to handle events:

        pub fn handle_key_pressed(&mut self, physical_key: PhysicalKey, logical_key: Key) {
    
            // Match physical and logical keys
            match logical_key {
                Key::Named(named) => match named {
                    // Implement logic
    
                    // Send event to the application
                    self.proxy.
                        send_event(CustomEvent::Log("hello".to_string())
                        .unwrap();
                }
        }
    
        pub fn handle_mouse_pressed(&self) {
            println!("Mouse pressed");
        }
    
        // Implement other methods
    
    

    4. Implement the ApplicationHandler trait for your application

    Define an Application (or whatever you want to call it) struct and implement this trait from winit::application:

    use winit::application::ApplicationHandler;
    
    pub struct Application {
       // your fields
    }
    

    In particular, pay attention to the user_event function:

    
    // Once again use as generic your CustomEvent enum
    impl ApplicationHandler<CustomEvent> for Application {
        // Implement every method
    
    
        fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: CustomEvent) {    
            // Match events received from the Dispatcher event proxy
            match event {
                CustomEvent::Log(str) => {
                    println!("{str}");
                }
                // Implement your logic
            }
        }
    
    }
    

    5. Initialize and run your Application

    // Initialize your app
    let app = Application::new();
    
    // Create the event loop (and for the 3rd time, make sure to use the your CustomEvent enum)
    let event_loop = EventLoop::<CustomEvent>::with_user_event().build().unwrap();
    
    // Create the event loop proxy
    let proxy = event_loop.create_proxy();
    
    // Create your dispatcher, which will need the proxy
    let dispatcher = Dispatcher::new(proxy);
    
    // Feel free to organize your code as you like (e.g. store the dispatcher inside the Application itself)
    
    // Run the application
    event_loop.run_app(app).unwrap();
    

    And that's it!

    Thanks for the help of everyone.

    Please know that this implementation might not be the best one or follow idiomatic Rust, it's just how I've done it.