From b7e3bd224ffb9aca253d8f5cd4a58b48df3c8b6c Mon Sep 17 00:00:00 2001 From: Alex Knauth Date: Tue, 21 May 2024 09:34:33 -0400 Subject: [PATCH 1/9] windows: set_mouse_pass_through --- druid-shell/src/backend/windows/window.rs | 45 +++++++++++++++++++++++ druid-shell/src/window.rs | 5 +++ 2 files changed, 50 insertions(+) diff --git a/druid-shell/src/backend/windows/window.rs b/druid-shell/src/backend/windows/window.rs index 7cfb29ff0..dbf375c5e 100644 --- a/druid-shell/src/backend/windows/window.rs +++ b/druid-shell/src/backend/windows/window.rs @@ -89,6 +89,7 @@ pub(crate) struct WindowBuilder { position: Option, level: Option, always_on_top: bool, + mouse_pass_through: bool, state: window::WindowState, } @@ -156,6 +157,7 @@ enum DeferredOp { ReleaseMouseCapture, SetRegion(Option), SetAlwaysOnTop(bool), + SetMousePassThrough(bool), } #[derive(Clone, Debug)] @@ -232,6 +234,7 @@ struct WindowState { is_focusable: bool, window_level: WindowLevel, is_always_on_top: Cell, + is_mouse_pass_through: Cell, } impl std::fmt::Debug for WindowState { @@ -464,6 +467,38 @@ fn set_ex_style(hwnd: HWND, always_on_top: bool) { } } +fn set_mouse_pass_through(hwnd: HWND, mouse_pass_through: bool) { + unsafe { + let mut style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE) as u32; + if style == 0 { + warn!( + "failed to get window ex style: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + return; + } + + if !mouse_pass_through { + // Not removing WS_EX_LAYERED because it may still be needed if Opacity != 1. + style &= !WS_EX_TRANSPARENT; + } else if (style & (WS_EX_LAYERED | WS_EX_TRANSPARENT)) + != (WS_EX_LAYERED | WS_EX_TRANSPARENT) + { + // We have to add WS_EX_LAYERED, because WS_EX_TRANSPARENT won't work otherwise. + style |= WS_EX_LAYERED | WS_EX_TRANSPARENT; + } else { + // nothing to do + return; + } + if SetWindowLongPtrW(hwnd, GWL_EXSTYLE, style as _) == 0 { + warn!( + "failed to set the window ex style: {}", + Error::Hr(HRESULT_FROM_WIN32(GetLastError())) + ); + } + } +} + impl WndState { fn rebuild_render_target(&mut self, d2d: &D2DFactory, scale: Scale) -> Result<(), Error> { unsafe { @@ -647,6 +682,10 @@ impl MyWndProc { self.with_window_state(|s| s.is_always_on_top.set(always_on_top)); set_ex_style(hwnd, always_on_top); } + DeferredOp::SetMousePassThrough(mouse_pass_through) => { + self.with_window_state(|s| s.is_mouse_pass_through.set(mouse_pass_through)); + set_mouse_pass_through(hwnd, mouse_pass_through); + } DeferredOp::SetWindowState(val) => { let show = if self.handle.borrow().is_focusable() { match val { @@ -1490,6 +1529,7 @@ impl WindowBuilder { position: None, level: None, always_on_top: false, + mouse_pass_through: false, state: window::WindowState::Restored, } } @@ -1640,6 +1680,7 @@ impl WindowBuilder { is_focusable: focusable, window_level, is_always_on_top: Cell::new(self.always_on_top), + is_mouse_pass_through: Cell::new(self.mouse_pass_through), }; let win = Rc::new(window); let handle = WindowHandle { @@ -2252,6 +2293,10 @@ impl WindowHandle { self.defer(DeferredOp::SetAlwaysOnTop(always_on_top)); } + pub fn set_mouse_pass_through(&self, mouse_pass_through: bool) { + self.defer(DeferredOp::SetMousePassThrough(mouse_pass_through)); + } + pub fn resizable(&self, resizable: bool) { self.defer(DeferredOp::SetResizable(resizable)); } diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index 4aabac13d..842e7c68b 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -225,6 +225,11 @@ impl WindowHandle { self.0.set_always_on_top(always_on_top); } + /// Sets whether the mouse passes through the window to whatever is behind. + pub fn set_mouse_pass_through(&self, mouse_pass_through: bool) { + self.0.set_mouse_pass_through(mouse_pass_through); + } + /// Sets where in the window the user can interact with the program. /// /// This enables irregularly shaped windows. For example, you can make it simply From 26fad5a725f10957328db7c932f63069c450b8a7 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Tue, 21 May 2024 09:56:59 -0400 Subject: [PATCH 2/9] mac: set_mouse_pass_through --- druid-shell/src/backend/mac/window.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/druid-shell/src/backend/mac/window.rs b/druid-shell/src/backend/mac/window.rs index d8a280a5c..c8f9399b5 100644 --- a/druid-shell/src/backend/mac/window.rs +++ b/druid-shell/src/backend/mac/window.rs @@ -1335,6 +1335,13 @@ impl WindowHandle { } } + pub fn set_mouse_pass_through(&self, mouse_pass_through: bool) { + unsafe { + let window: id = msg_send![*self.nsview.load(), window]; + window.setIgnoresMouseEvents_(mouse_pass_through as BOOL); + } + } + fn set_level(&self, level: WindowLevel) { unsafe { let level = levels::as_raw_window_level(level); From d5e4cadb2475adb1510c8dffc98644f993af6dcf Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Tue, 21 May 2024 10:07:06 -0400 Subject: [PATCH 3/9] set_mouse_pass_through stubs for other backends --- druid-shell/src/backend/gtk/window.rs | 4 ++++ druid-shell/src/backend/wayland/window.rs | 4 ++++ druid-shell/src/backend/web/window.rs | 4 ++++ druid-shell/src/backend/x11/window.rs | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/druid-shell/src/backend/gtk/window.rs b/druid-shell/src/backend/gtk/window.rs index d75a97a68..f413fbb8e 100644 --- a/druid-shell/src/backend/gtk/window.rs +++ b/druid-shell/src/backend/gtk/window.rs @@ -1122,6 +1122,10 @@ impl WindowHandle { } } + pub fn set_mouse_pass_through(&self, _mouse_pass_thorugh: bool) { + warn!("set_mouse_pass_through unimplemented"); + } + pub fn handle_titlebar(&self, val: bool) { if let Some(state) = self.state.upgrade() { state.handle_titlebar.set(val); diff --git a/druid-shell/src/backend/wayland/window.rs b/druid-shell/src/backend/wayland/window.rs index 52b0e8cee..ff77140c1 100644 --- a/druid-shell/src/backend/wayland/window.rs +++ b/druid-shell/src/backend/wayland/window.rs @@ -109,6 +109,10 @@ impl WindowHandle { tracing::warn!("set_always_on_top is unimplemented on wayland"); } + pub fn set_mouse_pass_through(&self, _mouse_pass_thorugh: bool) { + tracing::warn!("set_mouse_pass_through unimplemented"); + } + pub fn set_input_region(&self, region: Option) { self.inner.surface.set_input_region(region); } diff --git a/druid-shell/src/backend/web/window.rs b/druid-shell/src/backend/web/window.rs index 945733fcb..a3ac1e48c 100644 --- a/druid-shell/src/backend/web/window.rs +++ b/druid-shell/src/backend/web/window.rs @@ -510,6 +510,10 @@ impl WindowHandle { warn!("WindowHandle::set_always_on_top unimplemented for web"); } + pub fn set_mouse_pass_through(&self, _mouse_pass_thorugh: bool) { + warn!("WindowHandle::set_mouse_pass_through unimplemented for web"); + } + pub fn get_position(&self) -> Point { warn!("WindowHandle::get_position unimplemented for web."); Point::new(0.0, 0.0) diff --git a/druid-shell/src/backend/x11/window.rs b/druid-shell/src/backend/x11/window.rs index 2d7a0c5eb..c6e8ee00a 100644 --- a/druid-shell/src/backend/x11/window.rs +++ b/druid-shell/src/backend/x11/window.rs @@ -1675,6 +1675,10 @@ impl WindowHandle { } } + pub fn set_mouse_pass_through(&self, _mouse_pass_thorugh: bool) { + warn!("set_mouse_pass_through unimplemented"); + } + pub fn set_input_region(&self, region: Option) { if let Some(w) = self.window.upgrade() { w.set_input_region(region); From e4055ecc4e9f2c3c2f80c78a63ba41eb12686704 Mon Sep 17 00:00:00 2001 From: Alex Knauth Date: Tue, 21 May 2024 11:24:48 -0400 Subject: [PATCH 4/9] windows: is_foreground_window --- druid-shell/src/backend/windows/window.rs | 8 ++++++++ druid-shell/src/window.rs | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/druid-shell/src/backend/windows/window.rs b/druid-shell/src/backend/windows/window.rs index dbf375c5e..58deb0b4c 100644 --- a/druid-shell/src/backend/windows/window.rs +++ b/druid-shell/src/backend/windows/window.rs @@ -2285,6 +2285,14 @@ impl WindowHandle { Size::new(0.0, 0.0) } + pub fn is_foreground_window(&self) -> bool { + let Some(w) = self.state.upgrade() else { + return true; + }; + let hwnd = w.hwnd.get(); + unsafe { GetForegroundWindow() == hwnd } + } + pub fn set_input_region(&self, area: Option) { self.defer(DeferredOp::SetRegion(area)); } diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index 842e7c68b..d2957cc04 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -249,6 +249,12 @@ impl WindowHandle { self.0.set_input_region(region) } + /// Returns true if the window is the foreground window or this is unknown. + /// Returns false if a different window is known to be the foreground window. + pub fn is_foreground_window(&self) -> bool { + self.0.is_foreground_window() + } + /// Returns the position of the top left corner of the window. /// /// The position is returned in [display points], measured relative to the parent window if From d174e435c1b753280943b90b321aaf5b7f20a949 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Tue, 21 May 2024 11:31:13 -0400 Subject: [PATCH 5/9] is_foreground_window stubs for other backends --- druid-shell/src/backend/gtk/window.rs | 4 ++++ druid-shell/src/backend/mac/window.rs | 4 ++++ druid-shell/src/backend/wayland/window.rs | 4 ++++ druid-shell/src/backend/web/window.rs | 4 ++++ druid-shell/src/backend/x11/window.rs | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/druid-shell/src/backend/gtk/window.rs b/druid-shell/src/backend/gtk/window.rs index f413fbb8e..688610583 100644 --- a/druid-shell/src/backend/gtk/window.rs +++ b/druid-shell/src/backend/gtk/window.rs @@ -1056,6 +1056,10 @@ impl WindowHandle { } } + pub fn is_foreground_window(&self) -> bool { + true + } + pub fn set_window_state(&mut self, size_state: window::WindowState) { use window::WindowState::{Maximized, Minimized, Restored}; let cur_size_state = self.get_window_state(); diff --git a/druid-shell/src/backend/mac/window.rs b/druid-shell/src/backend/mac/window.rs index c8f9399b5..2707cfd91 100644 --- a/druid-shell/src/backend/mac/window.rs +++ b/druid-shell/src/backend/mac/window.rs @@ -1362,6 +1362,10 @@ impl WindowHandle { } } + pub fn is_foreground_window(&self) -> bool { + true + } + pub fn get_window_state(&self) -> WindowState { unsafe { let window: id = msg_send![*self.nsview.load(), window]; diff --git a/druid-shell/src/backend/wayland/window.rs b/druid-shell/src/backend/wayland/window.rs index ff77140c1..c9b741413 100644 --- a/druid-shell/src/backend/wayland/window.rs +++ b/druid-shell/src/backend/wayland/window.rs @@ -134,6 +134,10 @@ impl WindowHandle { self.inner.surface.get_size() } + pub fn is_foreground_window(&self) -> bool { + true + } + pub fn set_window_state(&mut self, _current_state: window::WindowState) { tracing::warn!("set_window_state is unimplemented on wayland"); } diff --git a/druid-shell/src/backend/web/window.rs b/druid-shell/src/backend/web/window.rs index a3ac1e48c..80f74fb77 100644 --- a/druid-shell/src/backend/web/window.rs +++ b/druid-shell/src/backend/web/window.rs @@ -528,6 +528,10 @@ impl WindowHandle { Size::new(0.0, 0.0) } + pub fn is_foreground_window(&self) -> bool { + true + } + pub fn content_insets(&self) -> Insets { warn!("WindowHandle::content_insets unimplemented for web."); Insets::ZERO diff --git a/druid-shell/src/backend/x11/window.rs b/druid-shell/src/backend/x11/window.rs index c6e8ee00a..d2cf0c7a0 100644 --- a/druid-shell/src/backend/x11/window.rs +++ b/druid-shell/src/backend/x11/window.rs @@ -1718,6 +1718,10 @@ impl WindowHandle { } } + pub fn is_foreground_window(&self) -> bool { + true + } + pub fn set_window_state(&self, _state: window::WindowState) { warn!("WindowHandle::set_window_state is currently unimplemented for X11 backend."); } From 87d12b92185b7525eb107101ff8c43fd97299599 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Tue, 21 May 2024 11:42:15 -0400 Subject: [PATCH 6/9] mac: is_foreground_window --- druid-shell/src/backend/mac/window.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/druid-shell/src/backend/mac/window.rs b/druid-shell/src/backend/mac/window.rs index 2707cfd91..86d6772ed 100644 --- a/druid-shell/src/backend/mac/window.rs +++ b/druid-shell/src/backend/mac/window.rs @@ -1363,7 +1363,11 @@ impl WindowHandle { } pub fn is_foreground_window(&self) -> bool { - true + unsafe { + let application: id = msg_send![class![NSRunningApplication], currentApplication]; + let is_active: BOOL = msg_send![application, isActive]; + is_active != NO + } } pub fn get_window_state(&self) -> WindowState { From 40aed37ddfd5077a0db389a845ed5489895d3269 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Tue, 21 May 2024 13:49:49 -0400 Subject: [PATCH 7/9] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0fb248e2..6e51d5a07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ You can find its changes [documented below](#083---2023-02-28). ### Added - Type name is now included in panic error messages in `WidgetPod`. ([#2380] by [@matthewgapp]) +- `set_mouse_pass_through` sets whether the mouse passes through the window to whatever is behind. ([#2402] by [@AlexKnauth]) +- `is_foreground_window` returns true if the window is the foreground window or this is unknown, and returns false if a different window is known to be the foreground window. ([#2402] by [@AlexKnauth]) ### Changed @@ -790,6 +792,7 @@ Last release without a changelog :( [@AtomicGamer9523]: https://github.com/AtomicGamer9523 [@Insprill]: https://github.com/Insprill [@matthewgapp]: https://github.com/matthewgapp +[@AlexKnauth]: https://github.com/AlexKnauth [#599]: https://github.com/linebender/druid/pull/599 [#611]: https://github.com/linebender/druid/pull/611 @@ -1237,6 +1240,7 @@ Last release without a changelog :( [#2375]: https://github.com/linebender/druid/pull/2375 [#2378]: https://github.com/linebender/druid/pull/2378 [#2380]: https://github.com/linebender/druid/pull/2380 +[#2402]: https://github.com/linebender/druid/pull/2402 [Unreleased]: https://github.com/linebender/druid/compare/v0.8.3...master [0.8.3]: https://github.com/linebender/druid/compare/v0.8.2...v0.8.3 From e3045fd4c90bf331ec19d9716fb065d7ef54f058 Mon Sep 17 00:00:00 2001 From: Alex Knauth Date: Tue, 21 May 2024 08:45:48 -0400 Subject: [PATCH 8/9] example input_region: Toggle Mouse Pass Through --- druid/examples/input_region.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/druid/examples/input_region.rs b/druid/examples/input_region.rs index c056a3916..192c8eea8 100644 --- a/druid/examples/input_region.rs +++ b/druid/examples/input_region.rs @@ -26,6 +26,8 @@ struct AppState { limit_input_region: bool, show_titlebar: bool, always_on_top: bool, + mouse_pass_through_while_not_in_focus: bool, + mouse_pass_through: bool, } struct InputRegionExampleWidget { @@ -38,7 +40,7 @@ impl InputRegionExampleWidget { let info_label = Label::new(INFO_TEXT) .with_line_break_mode(LineBreaking::WordWrap) .padding(20.0) - .background(Color::rgba(0.2, 0.2, 0.2, 1.0)); + .background(Color::rgba(0.2, 0.2, 0.2, 0.5)); let toggle_input_region = Button::new("Toggle Input Region") .on_click(|ctx, data: &mut bool, _: &Env| { *data = !*data; @@ -61,10 +63,17 @@ impl InputRegionExampleWidget { ctx.window().set_always_on_top(*data); }) .lens(AppState::always_on_top); + let toggle_mouse_pass_through_while_not_in_focus = Button::new("Toggle Mouse Pass Through") + .on_click(|_, data: &mut bool, _: &Env| { + *data = !*data; + tracing::debug!("Setting mouse pass through while not in focus to: {}", *data); + }) + .lens(AppState::mouse_pass_through_while_not_in_focus); let controls_flex = Flex::row() .with_child(toggle_input_region) .with_child(toggle_titlebar) - .with_child(toggle_always_on_top); + .with_child(toggle_always_on_top) + .with_child(toggle_mouse_pass_through_while_not_in_focus); Self { info_label: WidgetPod::new(info_label), controls: WidgetPod::new(controls_flex), @@ -82,6 +91,12 @@ impl Widget for InputRegionExampleWidget { ) { self.info_label.event(ctx, event, data, env); self.controls.event(ctx, event, data, env); + let mouse_pass_through = data.mouse_pass_through_while_not_in_focus && !ctx.window().is_foreground_window(); + if mouse_pass_through != data.mouse_pass_through { + data.mouse_pass_through = mouse_pass_through; + tracing::debug!("Setting mouse pass through to: {}", mouse_pass_through); + ctx.window().set_mouse_pass_through(mouse_pass_through); + } } fn lifecycle( @@ -196,6 +211,8 @@ fn main() { limit_input_region: true, always_on_top: false, show_titlebar: false, + mouse_pass_through_while_not_in_focus: false, + mouse_pass_through: false, }; AppLauncher::with_window(main_window) From 71d33d98695d16877d18e4d2288db4dc3e397651 Mon Sep 17 00:00:00 2001 From: AlexKnauth Date: Thu, 23 May 2024 00:04:08 -0400 Subject: [PATCH 9/9] cargo fmt --- druid/examples/input_region.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/druid/examples/input_region.rs b/druid/examples/input_region.rs index 192c8eea8..e0f1d00de 100644 --- a/druid/examples/input_region.rs +++ b/druid/examples/input_region.rs @@ -66,7 +66,10 @@ impl InputRegionExampleWidget { let toggle_mouse_pass_through_while_not_in_focus = Button::new("Toggle Mouse Pass Through") .on_click(|_, data: &mut bool, _: &Env| { *data = !*data; - tracing::debug!("Setting mouse pass through while not in focus to: {}", *data); + tracing::debug!( + "Setting mouse pass through while not in focus to: {}", + *data + ); }) .lens(AppState::mouse_pass_through_while_not_in_focus); let controls_flex = Flex::row() @@ -91,7 +94,8 @@ impl Widget for InputRegionExampleWidget { ) { self.info_label.event(ctx, event, data, env); self.controls.event(ctx, event, data, env); - let mouse_pass_through = data.mouse_pass_through_while_not_in_focus && !ctx.window().is_foreground_window(); + let mouse_pass_through = + data.mouse_pass_through_while_not_in_focus && !ctx.window().is_foreground_window(); if mouse_pass_through != data.mouse_pass_through { data.mouse_pass_through = mouse_pass_through; tracing::debug!("Setting mouse pass through to: {}", mouse_pass_through);