mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
fix(linux): map pen barrel buttons before egui-winit drops them
## Summary On Linux (Wayland and X11), tablet stylus side buttons arrive as winit MouseButton::Other (BTN_STYLUS and neighbours). egui-winit ignores those codes, so barrel presses never reached the canvas. Run the desktop app through a thin winit ApplicationHandler wrapper on Linux that remaps barrel buttons to right and middle mouse buttons before eframe handles the event. ## Tests cargo test -p vectorcraft linux_stylus cargo clippy -p vectorcraft --all-targets -- -D warnings Closes storytold/vectorcraft#3632 Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
This commit is contained in:
committed by
bflatastic
co-authored by
nrq6993-a11y
parent
2d85603bcc
commit
b37522bbb3
@@ -0,0 +1,118 @@
|
||||
//! Linux pen tablets: winit reports barrel buttons as [`winit::event::MouseButton::Other`]
|
||||
//! (for example `BTN_STYLUS`), and egui-winit drops those before they reach the UI (#3632).
|
||||
|
||||
use eframe::AppCreator;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use eframe::UserEvent;
|
||||
|
||||
/// `BTN_STYLUS` and neighbours (`linux/input-event-codes.h`): pen barrel buttons Wayland passes
|
||||
/// through the pointer protocol as non-standard mouse buttons.
|
||||
const BTN_STYLUS: u16 = 0x014b;
|
||||
const BTN_STYLUS2: u16 = 0x014c;
|
||||
const BTN_STYLUS3: u16 = 0x014d;
|
||||
|
||||
/// Like [`eframe::run_native`], but turns pen barrel buttons into mouse buttons egui understands.
|
||||
pub fn run_native(app_name: &str, mut native_options: eframe::NativeOptions, app_creator: AppCreator<'_>) -> eframe::Result {
|
||||
let mut builder = EventLoop::<UserEvent>::with_user_event();
|
||||
if let Some(hook) = native_options.event_loop_builder.take() {
|
||||
hook(&mut builder);
|
||||
}
|
||||
let event_loop = builder.build().map_err(eframe::Error::WinitEventLoop)?;
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
let inner = eframe::create_native(app_name, native_options, app_creator, &event_loop);
|
||||
let mut app = Forwarder { inner };
|
||||
event_loop.run_app(&mut app).map_err(eframe::Error::WinitEventLoop)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Forwarder<'a> {
|
||||
inner: eframe::EframeWinitApplication<'a>,
|
||||
}
|
||||
|
||||
impl ApplicationHandler<UserEvent> for Forwarder<'_> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.inner.resumed(event_loop);
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
|
||||
self.inner.window_event(event_loop, window_id, map_window_event(event));
|
||||
}
|
||||
|
||||
fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
|
||||
self.inner.new_events(event_loop, cause);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||
self.inner.user_event(event_loop, event);
|
||||
}
|
||||
|
||||
fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: DeviceId, event: DeviceEvent) {
|
||||
self.inner.device_event(event_loop, device_id, event);
|
||||
}
|
||||
|
||||
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.inner.about_to_wait(event_loop);
|
||||
}
|
||||
|
||||
fn suspended(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.inner.suspended(event_loop);
|
||||
}
|
||||
|
||||
fn exiting(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.inner.exiting(event_loop);
|
||||
}
|
||||
|
||||
fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
|
||||
self.inner.memory_warning(event_loop);
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a tablet button code winit labels `MouseButton::Other` to a standard mouse button.
|
||||
pub(crate) fn map_tablet_button(code: u16) -> Option<winit::event::MouseButton> {
|
||||
use winit::event::MouseButton;
|
||||
match code {
|
||||
BTN_STYLUS | BTN_STYLUS2 => Some(MouseButton::Right),
|
||||
BTN_STYLUS3 => Some(MouseButton::Middle),
|
||||
// XInput2 on X11 often numbers the first barrel button 10 or 11.
|
||||
10 | 11 => Some(MouseButton::Right),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_window_event(event: WindowEvent) -> WindowEvent {
|
||||
if let WindowEvent::MouseInput { device_id, state, button: winit::event::MouseButton::Other(code), .. } = event
|
||||
&& let Some(button) = map_tablet_button(code)
|
||||
{
|
||||
WindowEvent::MouseInput { device_id, state, button }
|
||||
} else {
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
|
||||
#[test]
|
||||
fn stylus_barrel_buttons_become_secondary_clicks() {
|
||||
assert_eq!(map_tablet_button(BTN_STYLUS), Some(MouseButton::Right));
|
||||
assert_eq!(map_tablet_button(BTN_STYLUS2), Some(MouseButton::Right));
|
||||
assert_eq!(map_tablet_button(BTN_STYLUS3), Some(MouseButton::Middle));
|
||||
assert_eq!(map_tablet_button(0x110), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_mouse_input_is_remapped_before_eframe() {
|
||||
let device_id = winit::event::DeviceId::dummy();
|
||||
let press = WindowEvent::MouseInput { device_id, state: ElementState::Pressed, button: MouseButton::Other(BTN_STYLUS) };
|
||||
let mapped = map_window_event(press);
|
||||
assert!(matches!(mapped, WindowEvent::MouseInput { button: MouseButton::Right, state: ElementState::Pressed, .. }));
|
||||
let release = map_window_event(WindowEvent::MouseInput { device_id, state: ElementState::Released, button: MouseButton::Other(BTN_STYLUS2) });
|
||||
assert!(matches!(release, WindowEvent::MouseInput { button: MouseButton::Right, state: ElementState::Released, .. }));
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ mod control_server;
|
||||
mod cursor_theme;
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod gpu;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux_stylus;
|
||||
mod logging;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_fonts;
|
||||
@@ -952,9 +954,18 @@ fn main() -> std::process::ExitCode {
|
||||
#[cfg(feature = "wgpu")]
|
||||
let created = startup.clone();
|
||||
// A panic that ends the window (egui-wgpu's own, #502) is caught here: never a crash.
|
||||
let run = |options: eframe::NativeOptions, app_creator: eframe::AppCreator<'_>| {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux_stylus::run_native("VectorCraft", options, app_creator)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
eframe::run_native("VectorCraft", options, app_creator)
|
||||
}
|
||||
};
|
||||
let outcome = vectorcraft_engine::guard::catch_panic(|| {
|
||||
eframe::run_native(
|
||||
"VectorCraft",
|
||||
run(
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let mut session = Session::new();
|
||||
|
||||
Reference in New Issue
Block a user