Skip to content

Commit

Permalink
Add cursor hittest window functionality (rust-windowing#2232)
Browse files Browse the repository at this point in the history
Co-authored-by: z4122 <412213484@qq.com>
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
  • Loading branch information
3 people committed Apr 12, 2022
1 parent 142d55f commit bf366cb
Show file tree
Hide file tree
Showing 14 changed files with 120 additions and 6 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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`.
Expand Down
2 changes: 2 additions & 0 deletions FEATURES.md
Expand Up @@ -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.
Expand Down Expand Up @@ -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 |✔️ ||✔️ |✔️ |✔️ |✔️ ||
Expand Down
6 changes: 6 additions & 0 deletions src/platform_impl/android/mod.rs
Expand Up @@ -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() {
Expand Down
4 changes: 4 additions & 0 deletions src/platform_impl/ios/window.rs
Expand Up @@ -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")
}
Expand Down
5 changes: 5 additions & 0 deletions src/platform_impl/linux/mod.rs
Expand Up @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions src/platform_impl/linux/wayland/window/mod.rs
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions 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;
Expand Down Expand Up @@ -75,6 +76,9 @@ pub enum WindowRequest {
/// `None` unsets the attention request.
Attention(Option<UserAttentionType>),

/// Passthrough mouse input to underlying windows.
PassthroughMouseInput(bool),

/// Redraw was requested.
Redraw,

Expand Down Expand Up @@ -167,6 +171,9 @@ pub struct WindowHandle {

/// Indicator whether user attention is requested.
attention_requested: Cell<bool>,

/// Compositor
compositor: Attached<WlCompositor>,
}

impl WindowHandle {
Expand All @@ -177,6 +184,9 @@ impl WindowHandle {
pending_window_requests: Arc<Mutex<Vec<WindowRequest>>>,
) -> Self {
let xdg_activation = env.get_global::<XdgActivationV1>();
// Unwrap is safe, since we can't create window without compositor anyway and won't be
// here.
let compositor = env.get_global::<WlCompositor>().unwrap();

Self {
window,
Expand All @@ -189,6 +199,7 @@ impl WindowHandle {
text_inputs: Vec::new(),
xdg_activation,
attention_requested: Cell::new(false),
compositor,
}
}

Expand Down Expand Up @@ -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(&region.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 {
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 5 additions & 0 deletions src/platform_impl/linux/x11/window.rs
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion src/platform_impl/macos/util/async.rs
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
9 changes: 9 additions & 0 deletions src/platform_impl/macos/window.rs
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/platform_impl/web/window.rs
Expand Up @@ -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'
Expand Down
13 changes: 13 additions & 0 deletions src/platform_impl/windows/window.rs
Expand Up @@ -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())
Expand Down
13 changes: 9 additions & 4 deletions src/platform_impl/windows/window_state.rs
Expand Up @@ -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,
},
};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions src/window.rs
Expand Up @@ -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.
Expand Down

0 comments on commit bf366cb

Please sign in to comment.