diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d3757419..e34f0fbdc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,11 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- On macOS, Windows, and Wayland, add `set_cursor_hittest` to let the window ignore mouse events. - On Windows, added `WindowExtWindows::set_skip_taskbar` and `WindowBuilderExtWindows::with_skip_taskbar`. - On Windows, added `EventLoopBuilderExtWindows::with_msg_hook`. - On Windows, remove internally unique DC per window. -- macOS: Remove the need to call `set_ime_position` after moving the window. +- On macOS, remove the need to call `set_ime_position` after moving the window. - Added `Window::is_visible`. - Added `Window::is_resizable`. - Added `Window::is_decorated`. diff --git a/FEATURES.md b/FEATURES.md index 484dacbc19..15aacb6b6b 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -102,6 +102,7 @@ If your PR makes notable changes to Winit's features, please update this section - **Mouse set location**: Forcibly changing the location of the pointer. - **Cursor grab**: Locking the cursor so it cannot exit the client area of a window. - **Cursor icon**: Changing the cursor icon, or hiding the cursor. +- **Cursor hittest**: Handle or ignore mouse events for a window. - **Touch events**: Single-touch events. - **Touch pressure**: Touch events contain information about the amount of force being applied. - **Multitouch**: Multi-touch events, including cancellation of a gesture. @@ -199,6 +200,7 @@ Legend: |Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|**N/A**| |Cursor grab |✔️ |▢[#165] |▢[#242] |✔️ |**N/A**|**N/A**|✔️ | |Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | +|Cursor hittest |✔️ |✔️ |❌ |✔️ |**N/A**|**N/A**|❌ | |Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | |Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |❌ | |Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ | diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 79894ea034..9457894e81 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -774,6 +774,12 @@ impl Window { )) } + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), error::ExternalError> { + Err(error::ExternalError::NotSupported( + error::NotSupportedError::new(), + )) + } + pub fn raw_window_handle(&self) -> RawWindowHandle { let mut handle = AndroidNdkHandle::empty(); if let Some(native_window) = ndk_glue::native_window().as_ref() { diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index f007bcaab1..81c225ef1b 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -196,6 +196,10 @@ impl Inner { Err(ExternalError::NotSupported(NotSupportedError::new())) } + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + pub fn set_minimized(&self, _minimized: bool) { warn!("`Window::set_minimized` is ignored on iOS") } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 16f66e34b4..ece8d34e60 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -396,6 +396,11 @@ impl Window { x11_or_wayland!(match self; Window(window) => window.drag_window()) } + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + x11_or_wayland!(match self; Window(w) => w.set_cursor_hittest(hittest)) + } + #[inline] pub fn scale_factor(&self) -> f64 { x11_or_wayland!(match self; Window(w) => w.scale_factor() as f64) diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index cda1fc6e22..73f2ec67db 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -482,6 +482,13 @@ impl Window { Ok(()) } + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + self.send_request(WindowRequest::PassthroughMouseInput(!hittest)); + + Ok(()) + } + #[inline] pub fn set_ime_position(&self, position: Position) { let scale_factor = self.scale_factor() as f64; diff --git a/src/platform_impl/linux/wayland/window/shim.rs b/src/platform_impl/linux/wayland/window/shim.rs index 7c76a2fa63..60247b317e 100644 --- a/src/platform_impl/linux/wayland/window/shim.rs +++ b/src/platform_impl/linux/wayland/window/shim.rs @@ -1,6 +1,7 @@ use std::cell::Cell; use std::sync::{Arc, Mutex}; +use sctk::reexports::client::protocol::wl_compositor::WlCompositor; use sctk::reexports::client::protocol::wl_output::WlOutput; use sctk::reexports::client::Attached; use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activation_token_v1; @@ -75,6 +76,9 @@ pub enum WindowRequest { /// `None` unsets the attention request. Attention(Option), + /// Passthrough mouse input to underlying windows. + PassthroughMouseInput(bool), + /// Redraw was requested. Redraw, @@ -167,6 +171,9 @@ pub struct WindowHandle { /// Indicator whether user attention is requested. attention_requested: Cell, + + /// Compositor + compositor: Attached, } impl WindowHandle { @@ -177,6 +184,9 @@ impl WindowHandle { pending_window_requests: Arc>>, ) -> Self { let xdg_activation = env.get_global::(); + // Unwrap is safe, since we can't create window without compositor anyway and won't be + // here. + let compositor = env.get_global::().unwrap(); Self { window, @@ -189,6 +199,7 @@ impl WindowHandle { text_inputs: Vec::new(), xdg_activation, attention_requested: Cell::new(false), + compositor, } } @@ -304,6 +315,20 @@ impl WindowHandle { } } + pub fn passthrough_mouse_input(&self, passthrough_mouse_input: bool) { + if passthrough_mouse_input { + let region = self.compositor.create_region(); + region.add(0, 0, 0, 0); + self.window + .surface() + .set_input_region(Some(®ion.detach())); + region.destroy(); + } else { + // Using `None` results in the entire window being clickable. + self.window.surface().set_input_region(None); + } + } + pub fn set_cursor_visible(&self, visible: bool) { self.cursor_visible.replace(visible); let cursor_icon = match visible { @@ -425,6 +450,12 @@ pub fn handle_window_requests(winit_state: &mut WinitState) { let window_update = window_updates.get_mut(window_id).unwrap(); window_update.refresh_frame = true; } + WindowRequest::PassthroughMouseInput(passthrough) => { + window_handle.passthrough_mouse_input(passthrough); + + let window_update = window_updates.get_mut(window_id).unwrap(); + window_update.refresh_frame = true; + } WindowRequest::Attention(request_type) => { window_handle.set_user_attention(request_type); } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index ddeb0d4582..4af0a76b16 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1358,6 +1358,11 @@ impl UnownedWindow { self.set_cursor_position_physical(x, y) } + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + pub fn drag_window(&self) -> Result<(), ExternalError> { let pointer = self .xconn diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs index 0bd4b7d82a..afd62c2e9b 100644 --- a/src/platform_impl/macos/util/async.rs +++ b/src/platform_impl/macos/util/async.rs @@ -10,7 +10,7 @@ use cocoa::{ }; use dispatch::Queue; use objc::rc::autoreleasepool; -use objc::runtime::{BOOL, NO}; +use objc::runtime::{BOOL, NO, YES}; use crate::{ dpi::LogicalSize, @@ -93,6 +93,14 @@ pub unsafe fn set_level_async(ns_window: id, level: ffi::NSWindowLevel) { }); } +// `setIgnoresMouseEvents_:` isn't thread-safe, and fails silently. +pub unsafe fn set_ignore_mouse_events(ns_window: id, ignore: bool) { + let ns_window = MainThreadSafe(ns_window); + Queue::main().exec_async(move || { + ns_window.setIgnoresMouseEvents_(if ignore { YES } else { NO }); + }); +} + // `toggleFullScreen` is thread-safe, but our additional logic to account for // window styles isn't. pub unsafe fn toggle_full_screen_async( diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 147ab97132..a4b827c702 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -678,6 +678,15 @@ impl UnownedWindow { Ok(()) } + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + unsafe { + util::set_ignore_mouse_events(*self.ns_window, !hittest); + } + + Ok(()) + } + pub(crate) fn is_zoomed(&self) -> bool { // because `isZoomed` doesn't work if the window's borderless, // we make it resizable temporalily. diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index a4efdcb30e..886514cab3 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -239,6 +239,11 @@ impl Window { Err(ExternalError::NotSupported(NotSupportedError::new())) } + #[inline] + pub fn set_cursor_hittest(&self, _hittest: bool) -> Result<(), ExternalError> { + Err(ExternalError::NotSupported(NotSupportedError::new())) + } + #[inline] pub fn set_minimized(&self, _minimized: bool) { // Intentionally a no-op, as canvases cannot be 'minimized' diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index a8d502aac9..a5febfe846 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -361,6 +361,19 @@ impl Window { Ok(()) } + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + let window = self.window.clone(); + let window_state = Arc::clone(&self.window_state); + self.thread_executor.execute_in_thread(move || { + WindowState::set_window_flags(window_state.lock(), window.0, |f| { + f.set(WindowFlags::IGNORE_CURSOR_EVENT, !hittest) + }); + }); + + Ok(()) + } + #[inline] pub fn id(&self) -> WindowId { WindowId(self.hwnd()) diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index d545e1efe6..9122649b9d 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -15,10 +15,10 @@ use windows_sys::Win32::{ HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, WS_BORDER, WS_CAPTION, WS_CHILD, - WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LEFT, - WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, - WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPED, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, - WS_SYSMENU, WS_VISIBLE, + WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, + WS_EX_LEFT, WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, + WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPED, + WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE, }, }; @@ -93,6 +93,8 @@ bitflags! { const MINIMIZED = 1 << 12; + const IGNORE_CURSOR_EVENT = 1 << 14; + const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; const NO_DECORATIONS_AND_MASK = !WindowFlags::RESIZABLE.bits; const INVISIBLE_AND_MASK = !WindowFlags::MAXIMIZED.bits; @@ -228,6 +230,9 @@ impl WindowFlags { if self.contains(WindowFlags::MAXIMIZED) { style |= WS_MAXIMIZE; } + if self.contains(WindowFlags::IGNORE_CURSOR_EVENT) { + style_ex |= WS_EX_TRANSPARENT | WS_EX_LAYERED; + } style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU; style_ex |= WS_EX_ACCEPTFILES; diff --git a/src/window.rs b/src/window.rs index 9bb9a360f7..f03033256b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -949,6 +949,19 @@ impl Window { pub fn drag_window(&self) -> Result<(), ExternalError> { self.window.drag_window() } + + /// Modifies whether the window catches cursor events. + /// + /// If `true`, the window will catch the cursor events. If `false`, events are passed through + /// the window such that any other window behind it receives them. By default hittest is enabled. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / X11:** Always returns an [`ExternalError::NotSupported`]. + #[inline] + pub fn set_cursor_hittest(&self, hittest: bool) -> Result<(), ExternalError> { + self.window.set_cursor_hittest(hittest) + } } /// Monitor info functions.