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 diff --git a/druid-shell/src/backend/gtk/window.rs b/druid-shell/src/backend/gtk/window.rs index d75a97a68..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(); @@ -1122,6 +1126,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/mac/window.rs b/druid-shell/src/backend/mac/window.rs index d8a280a5c..86d6772ed 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); @@ -1355,6 +1362,14 @@ impl WindowHandle { } } + pub fn is_foreground_window(&self) -> bool { + 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 { 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 52b0e8cee..c9b741413 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); } @@ -130,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 945733fcb..80f74fb77 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) @@ -524,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/windows/window.rs b/druid-shell/src/backend/windows/window.rs index 7cfb29ff0..58deb0b4c 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 { @@ -2244,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)); } @@ -2252,6 +2301,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/backend/x11/window.rs b/druid-shell/src/backend/x11/window.rs index 2d7a0c5eb..d2cf0c7a0 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); @@ -1714,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."); } diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index 4aabac13d..d2957cc04 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 @@ -244,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 diff --git a/druid/examples/input_region.rs b/druid/examples/input_region.rs index c056a3916..e0f1d00de 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,20 @@ 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 +94,13 @@ 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 +215,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)