rustwindowuser-inputoverlaywinit

How can I make a click-through overlay in Rust that still captures input?


I already have code that creates a transparent window and draws a square on it using winit and pixels, but I can't make it click-through, that is, let the user interact with what is behind the overlay window, while still letting the app capture input. Here's a minimal example of my code:

use pixels::{wgpu::Color, Pixels, SurfaceTexture};
use winit::{
    event::{DeviceEvent, ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent},
    event_loop::EventLoop,
    platform::windows::{WindowExtWindows, HWND},
    window::{WindowBuilder, WindowLevel},
};

fn main() {
    let event_loop = EventLoop::new();
    let window = WindowBuilder::new()
        .with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)))
        .with_transparent(true)
        .build(&event_loop)
        .unwrap();

    window.set_window_level(WindowLevel::AlwaysOnTop);
    window.set_cursor_hittest(false).unwrap();

    let window_size = window.inner_size();
    let surface = SurfaceTexture::new(window_size.width, window_size.height, &window);

    let mut pixels = Pixels::new(window_size.width, window_size.height, surface).unwrap();
    pixels.set_clear_color(Color::TRANSPARENT);

    event_loop.run(move |event, _, control_flow| {
        control_flow.set_wait();

        match event {
            Event::WindowEvent {
                event: window_event,
                ..
            } => match window_event {
                WindowEvent::KeyboardInput {
                    input:
                        KeyboardInput {
                            virtual_keycode: Some(VirtualKeyCode::Space),
                            state: ElementState::Pressed,
                            ..
                        },
                    ..
                } => {
                    println!("Input from window event");
                }

                WindowEvent::CloseRequested => control_flow.set_exit(),

                _ => (),
            },

            Event::DeviceEvent {
                event:
                    DeviceEvent::Key(KeyboardInput {
                        virtual_keycode: Some(VirtualKeyCode::Space),
                        state: ElementState::Pressed,
                        ..
                    }),
                ..
            } => {
                println!("Input from device event");
            }

            Event::RedrawRequested(_) => {
                pixels.render().unwrap();
            }

            _ => (),
        }
    });
}

I thought that Event::DeviceEvent would work because it seemed like it wasn't restricted to a specific window, but it is. In every scenario I've tried, both or none of the println!()s were called. Do I need another crate for that?


Solution

  • The crate device_query can solve the problem. It doesn't even need a window to function, as the input is queried on demand by calling DeviceState::get_keys() and DeviceState::mouse() on a DeviceState instance for keyboard and mouse respectively.

    use device_query::{DeviceState, DeviceQuery};
    
    // Cheaply creates an empty DeviceState
    let device_state = DeviceState::new();
    
    // Those methods query the input. They are individualy lazily queried.
    let keys = device_state.get_keys();
    let mouse = device_state.get_mouse();
    
    let is_alt_pressed = keys.contains(&Keycode::LAlt);
    let is_m1_pressed = mouse.button_pressed[1]; // It starts at [1] for M1. [0] has no meaning.
    

    The above code can capture input without window focus and even without a window at all. In the code provided in the question, it should be put inside the MainEventsCleared event. It is also required to replace control_flow.set_wait() in the first line inside the event_loop.run() closure with control_flow.set_poll(), so that MainEventsCleared will always run, even without new events.