From f10a984ba3035e3b539d4b5b93937279bb5cc862 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 7 Jun 2022 23:17:45 +0200 Subject: [PATCH] Add X11 opt-in function for device events Previously on X11, by default all global events were broadcasted to every winit application. This unnecessarily drains battery due to excessive CPU usage when moving the mouse. To resolve this, device events are now ignored by default and users must manually opt into it using `EventLoopWindowTarget::set_filter_device_events`. Fixes (#1634) on Linux. --- CHANGELOG.md | 1 + src/event_loop.rs | 39 ++++++++++ src/platform_impl/linux/mod.rs | 16 +++- .../linux/x11/event_processor.rs | 7 +- src/platform_impl/linux/x11/mod.rs | 77 ++++++++++++------- 5 files changed, 108 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cfed90221..84e6b0ba7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ And please only add new entries to the top of this list, right below the `# Unre - Added `Window::set_ime_allowed` supported on desktop platforms. - **Breaking:** IME input on desktop platforms won't be received unless it's explicitly allowed via `Window::set_ime_allowed` and new `WindowEvent::Ime` events are handled. - On macOS, `WindowEvent::Resized` is now emitted in `frameDidChange` instead of `windowDidResize`. +- **Breaking:** On X11, device events are now ignored for unfocused windows by default, use `EventLoopWindowTarget::set_device_event_filter` to set the filter level. # 0.26.1 (2022-01-05) diff --git a/src/event_loop.rs b/src/event_loop.rs index d6a15cfa9a..58181b3f80 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -286,6 +286,28 @@ impl EventLoopWindowTarget { pub fn primary_monitor(&self) -> Option { self.p.primary_monitor() } + + /// Change [`DeviceEvent`] filter mode. + /// + /// Since the [`DeviceEvent`] capture can lead to high CPU usage for unfocused windows, winit + /// will ignore them by default for unfocused windows on Linux/BSD. This method allows changing + /// this filter at runtime to explicitly capture them again. + /// + /// ## Platform-specific + /// + /// - **Wayland / Windows / macOS / iOS / Android / Web**: Unsupported. + /// + /// [`DeviceEvent`]: crate::event::DeviceEvent + pub fn set_device_event_filter(&mut self, _filter: DeviceEventFilter) { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + self.p.set_device_event_filter(_filter); + } } /// Used to send custom events to `EventLoop`. @@ -330,3 +352,20 @@ impl fmt::Display for EventLoopClosed { } impl error::Error for EventLoopClosed {} + +/// Fiter controlling the propagation of device events. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub enum DeviceEventFilter { + /// Always filter out device events. + Always, + /// Filter out device events while the window is not focused. + Unfocused, + /// Report all device events regardless of window focus. + Never, +} + +impl Default for DeviceEventFilter { + fn default() -> Self { + Self::Unfocused + } +} diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 6552a8227d..39511dd79e 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -30,7 +30,9 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::Event, - event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + event_loop::{ + ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW, + }, icon::Icon, monitor::{MonitorHandle as RootMonitorHandle, VideoMode as RootVideoMode}, window::{CursorIcon, Fullscreen, UserAttentionType, WindowAttributes}, @@ -732,7 +734,7 @@ impl EventLoop { } pub fn window_target(&self) -> &crate::event_loop::EventLoopWindowTarget { - x11_or_wayland!(match self; EventLoop(evl) => evl.window_target()) + x11_or_wayland!(match self; EventLoop(evlp) => evlp.window_target()) } } @@ -793,6 +795,16 @@ impl EventLoopWindowTarget { } } } + + #[inline] + pub fn set_device_event_filter(&mut self, _filter: DeviceEventFilter) { + match *self { + #[cfg(feature = "wayland")] + EventLoopWindowTarget::Wayland(_) => (), + #[cfg(feature = "x11")] + EventLoopWindowTarget::X(ref mut evlp) => evlp.set_device_event_filter(_filter), + } + } } fn sticky_exit_callback( diff --git a/src/platform_impl/linux/x11/event_processor.rs b/src/platform_impl/linux/x11/event_processor.rs index 0db198bbab..8ec326526c 100644 --- a/src/platform_impl/linux/x11/event_processor.rs +++ b/src/platform_impl/linux/x11/event_processor.rs @@ -49,7 +49,7 @@ impl EventProcessor { let mut devices = self.devices.borrow_mut(); if let Some(info) = DeviceInfo::get(&wt.xconn, device) { for info in info.iter() { - devices.insert(DeviceId(info.deviceid), Device::new(self, info)); + devices.insert(DeviceId(info.deviceid), Device::new(info)); } } } @@ -907,6 +907,8 @@ impl EventProcessor { if self.active_window != Some(xev.event) { self.active_window = Some(xev.event); + wt.update_device_event_filter(true); + let window_id = mkwid(xev.event); let position = PhysicalPosition::new(xev.event_x, xev.event_y); @@ -956,6 +958,7 @@ impl EventProcessor { if !self.window_exists(xev.event) { return; } + wt.ime .borrow_mut() .unfocus(xev.event) @@ -964,6 +967,8 @@ impl EventProcessor { if self.active_window.take() == Some(xev.event) { let window_id = mkwid(xev.event); + wt.update_device_event_filter(false); + // Issue key release events for all pressed keys Self::handle_pressed_keys( wt, diff --git a/src/platform_impl/linux/x11/mod.rs b/src/platform_impl/linux/x11/mod.rs index 491e37d6d9..a38b9030ec 100644 --- a/src/platform_impl/linux/x11/mod.rs +++ b/src/platform_impl/linux/x11/mod.rs @@ -50,7 +50,9 @@ use self::{ use crate::{ error::OsError as RootOsError, event::{Event, StartCause}, - event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, + event_loop::{ + ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW, + }, platform_impl::{platform::sticky_exit_callback, PlatformSpecificWindowBuilderAttributes}, window::WindowAttributes, }; @@ -105,6 +107,7 @@ pub struct EventLoopWindowTarget { ime: RefCell, windows: RefCell>>, redraw_sender: WakeSender, + device_event_filter: DeviceEventFilter, _marker: ::std::marker::PhantomData, } @@ -229,21 +232,27 @@ impl EventLoop { let (user_sender, user_channel) = std::sync::mpsc::channel(); let (redraw_sender, redraw_channel) = std::sync::mpsc::channel(); + let window_target = EventLoopWindowTarget { + ime, + root, + windows: Default::default(), + _marker: ::std::marker::PhantomData, + ime_sender, + xconn, + wm_delete_window, + net_wm_ping, + redraw_sender: WakeSender { + sender: redraw_sender, // not used again so no clone + waker: waker.clone(), + }, + device_event_filter: Default::default(), + }; + + // Set initial device event filter. + window_target.update_device_event_filter(true); + let target = Rc::new(RootELW { - p: super::EventLoopWindowTarget::X(EventLoopWindowTarget { - ime, - root, - windows: Default::default(), - _marker: ::std::marker::PhantomData, - ime_sender, - xconn, - wm_delete_window, - net_wm_ping, - redraw_sender: WakeSender { - sender: redraw_sender, // not used again so no clone - waker: waker.clone(), - }, - }), + p: super::EventLoopWindowTarget::X(window_target), _marker: ::std::marker::PhantomData, }); @@ -524,6 +533,29 @@ impl EventLoopWindowTarget { pub fn x_connection(&self) -> &Arc { &self.xconn } + + pub fn set_device_event_filter(&mut self, filter: DeviceEventFilter) { + self.device_event_filter = filter; + } + + /// Update the device event filter based on window focus. + pub fn update_device_event_filter(&self, focus: bool) { + let filter_events = self.device_event_filter == DeviceEventFilter::Never + || (self.device_event_filter == DeviceEventFilter::Unfocused && !focus); + + let mut mask = 0; + if !filter_events { + mask = ffi::XI_RawMotionMask + | ffi::XI_RawButtonPressMask + | ffi::XI_RawButtonReleaseMask + | ffi::XI_RawKeyPressMask + | ffi::XI_RawKeyReleaseMask; + } + + self.xconn + .select_xinput_events(self.root, ffi::XIAllDevices, mask) + .queue(); + } } impl EventLoopProxy { @@ -698,24 +730,11 @@ enum ScrollOrientation { } impl Device { - fn new(el: &EventProcessor, info: &ffi::XIDeviceInfo) -> Self { + fn new(info: &ffi::XIDeviceInfo) -> Self { let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() }; let mut scroll_axes = Vec::new(); - let wt = get_xtarget(&el.target); - if Device::physical_device(info) { - // Register for global raw events - let mask = ffi::XI_RawMotionMask - | ffi::XI_RawButtonPressMask - | ffi::XI_RawButtonReleaseMask - | ffi::XI_RawKeyPressMask - | ffi::XI_RawKeyReleaseMask; - // The request buffer is flushed when we poll for events - wt.xconn - .select_xinput_events(wt.root, info.deviceid, mask) - .queue(); - // Identify scroll axes for class_ptr in Device::classes(info) { let class = unsafe { &**class_ptr };