Skip to content

Commit

Permalink
Add X11 opt-in function for device events
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chrisduerr committed Jun 7, 2022
1 parent c7f7181 commit f10a984
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
39 changes: 39 additions & 0 deletions src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,28 @@ impl<T> EventLoopWindowTarget<T> {
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
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`.
Expand Down Expand Up @@ -330,3 +352,20 @@ impl<T> fmt::Display for EventLoopClosed<T> {
}

impl<T: fmt::Debug> error::Error for EventLoopClosed<T> {}

/// 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
}
}
16 changes: 14 additions & 2 deletions src/platform_impl/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -732,7 +734,7 @@ impl<T: 'static> EventLoop<T> {
}

pub fn window_target(&self) -> &crate::event_loop::EventLoopWindowTarget<T> {
x11_or_wayland!(match self; EventLoop(evl) => evl.window_target())
x11_or_wayland!(match self; EventLoop(evlp) => evlp.window_target())
}
}

Expand Down Expand Up @@ -793,6 +795,16 @@ impl<T> EventLoopWindowTarget<T> {
}
}
}

#[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<T, F>(
Expand Down
7 changes: 6 additions & 1 deletion src/platform_impl/linux/x11/event_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl<T: 'static> EventProcessor<T> {
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));
}
}
}
Expand Down Expand Up @@ -907,6 +907,8 @@ impl<T: 'static> EventProcessor<T> {
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);

Expand Down Expand Up @@ -956,6 +958,7 @@ impl<T: 'static> EventProcessor<T> {
if !self.window_exists(xev.event) {
return;
}

wt.ime
.borrow_mut()
.unfocus(xev.event)
Expand All @@ -964,6 +967,8 @@ impl<T: 'static> EventProcessor<T> {
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,
Expand Down
77 changes: 48 additions & 29 deletions src/platform_impl/linux/x11/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -105,6 +107,7 @@ pub struct EventLoopWindowTarget<T> {
ime: RefCell<Ime>,
windows: RefCell<HashMap<WindowId, Weak<UnownedWindow>>>,
redraw_sender: WakeSender<WindowId>,
device_event_filter: DeviceEventFilter,
_marker: ::std::marker::PhantomData<T>,
}

Expand Down Expand Up @@ -229,21 +232,27 @@ impl<T: 'static> EventLoop<T> {
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,
});

Expand Down Expand Up @@ -524,6 +533,29 @@ impl<T> EventLoopWindowTarget<T> {
pub fn x_connection(&self) -> &Arc<XConnection> {
&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<T: 'static> EventLoopProxy<T> {
Expand Down Expand Up @@ -698,24 +730,11 @@ enum ScrollOrientation {
}

impl Device {
fn new<T: 'static>(el: &EventProcessor<T>, 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 };
Expand Down

0 comments on commit f10a984

Please sign in to comment.