diff --git a/CHANGELOG.md b/CHANGELOG.md index ff850b1043..6a8ad3247a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,8 @@ You can find its changes [documented below](#070---2021-01-01). - Move macOS only function to Mac extension trait ([#1863] by [@Maan2003]) - x11: Only query atoms once instead of per window ([#1865] by [@psychon]) - remove prefix from platform extension traits ([#1873] by [@Maan2003]) +- Remove `set_level` on windows ([#1919] by [@JAicewizard]) +- Add parent windows to non-main windows. (Coordinate space is now from their origin) ([#1919] by [@JAicewizard]) - `ListIter` implementations for `Arc>`, `(S, Arc>)`, `Arc>` and `(S, Arc>)` ([#1967] by [@xarvic]) ### Deprecated @@ -788,6 +790,7 @@ Last release without a changelog :( [#1885]: https://github.com/linebender/druid/pull/1885 [#1886]: https://github.com/linebender/druid/pull/1886 [#1907]: https://github.com/linebender/druid/pull/1907 +[#1919]: https://github.com/linebender/druid/pull/1919 [#1929]: https://github.com/linebender/druid/pull/1929 [#1947]: https://github.com/linebender/druid/pull/1947 [#1967]: https://github.com/linebender/druid/pull/1967 diff --git a/druid-shell/src/backend/gtk/window.rs b/druid-shell/src/backend/gtk/window.rs index 744a333755..3841d1f3db 100644 --- a/druid-shell/src/backend/gtk/window.rs +++ b/druid-shell/src/backend/gtk/window.rs @@ -103,13 +103,24 @@ macro_rules! clone { ); } -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub struct WindowHandle { pub(crate) state: Weak, // Ensure that we don't implement Send, because it isn't actually safe to send the WindowState. marker: std::marker::PhantomData<*const ()>, } +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + match (self.state.upgrade(), other.state.upgrade()) { + (None, None) => true, + (Some(s), Some(o)) => std::sync::Arc::ptr_eq(&s, &o), + (_, _) => false, + } + } +} +impl Eq for WindowHandle {} + #[cfg(feature = "raw-win-handle")] unsafe impl HasRawWindowHandle for WindowHandle { fn raw_window_handle(&self) -> RawWindowHandle { @@ -191,6 +202,17 @@ pub(crate) struct WindowState { request_animation: Cell, in_draw: Cell, + + parent: Option, +} + +impl std::fmt::Debug for WindowState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("WindowState{")?; + self.window.fmt(f)?; + f.write_str("}")?; + Ok(()) + } } #[derive(Clone, PartialEq)] @@ -293,7 +315,48 @@ impl WindowBuilder { window.add(&vbox); let drawing_area = gtk::DrawingArea::new(); - let win_state = Arc::new(WindowState { + // Set the parent widget and handle level specific code + let mut parent: Option = None; + if let Some(level) = &self.level { + let hint = match level { + WindowLevel::AppWindow => WindowTypeHint::Normal, + WindowLevel::Tooltip(_) => WindowTypeHint::Tooltip, + WindowLevel::DropDown(_) => WindowTypeHint::DropdownMenu, + WindowLevel::Modal(_) => WindowTypeHint::Dialog, + }; + + window.set_type_hint(hint); + + match &level { + WindowLevel::Tooltip(p) => { + parent = Some(p.clone()); + } + WindowLevel::DropDown(p) => { + parent = Some(p.clone()); + } + WindowLevel::Modal(p) => { + parent = Some(p.clone()); + window.set_urgency_hint(true); + window.set_modal(true); + } + _ => (), + }; + if let Some(parent) = &parent { + if let Some(parent_state) = parent.0.state.upgrade() { + window.set_transient_for(Some(&parent_state.window)); + } + } + + let override_redirect = match level { + WindowLevel::AppWindow => false, + WindowLevel::Tooltip(_) | WindowLevel::DropDown(_) | WindowLevel::Modal(_) => true, + }; + if let Some(window) = window.window() { + window.set_override_redirect(override_redirect); + } + } + + let state = WindowState { window, scale: Cell::new(scale), area: Cell::new(area), @@ -311,7 +374,10 @@ impl WindowBuilder { deferred_queue: RefCell::new(Vec::new()), request_animation: Cell::new(false), in_draw: Cell::new(false), - }); + parent, + }; + + let win_state = Arc::new(state); self.app .gtk_app() @@ -326,12 +392,10 @@ impl WindowBuilder { state: Arc::downgrade(&win_state), marker: std::marker::PhantomData, }; - if let Some(level) = self.level { - handle.set_level(level); - } if let Some(pos) = self.position { handle.set_position(pos); } + if let Some(state) = self.state { handle.set_window_state(state) } @@ -735,10 +799,6 @@ impl WindowBuilder { .expect("realize didn't create window") .set_event_compression(false); - if let Some(level) = self.level { - handle.set_override_redirect(level); - } - let size = self.size; win_state.with_handler(|h| { h.connect(&handle.clone().into()); @@ -905,7 +965,15 @@ impl WindowHandle { } } - pub fn set_position(&self, position: Point) { + pub fn set_position(&self, mut position: Point) { + // TODO: Make the window follow the parent. + if let Some(state) = self.state.upgrade() { + if let Some(parent_state) = &state.parent { + let pos = (*parent_state).get_position(); + position += (pos.x, pos.y) + } + }; + if let Some(state) = self.state.upgrade() { let px = position.to_px(state.scale.get()); state.window.move_(px.x as i32, px.y as i32) @@ -915,7 +983,12 @@ impl WindowHandle { pub fn get_position(&self) -> Point { if let Some(state) = self.state.upgrade() { let (x, y) = state.window.position(); - Point::new(x as f64, y as f64).to_dp(state.scale.get()) + let mut position = Point::new(x as f64, y as f64); + if let Some(parent_state) = &state.parent { + let pos = (*parent_state).get_position(); + position -= (pos.x, pos.y) + } + position.to_dp(state.scale.get()) } else { Point::new(0.0, 0.0) } @@ -945,38 +1018,6 @@ impl WindowHandle { } } - pub fn set_level(&self, level: WindowLevel) { - if let Some(state) = self.state.upgrade() { - let hint = match level { - WindowLevel::AppWindow => WindowTypeHint::Normal, - WindowLevel::Tooltip => WindowTypeHint::Tooltip, - WindowLevel::DropDown => WindowTypeHint::DropdownMenu, - WindowLevel::Modal => WindowTypeHint::Dialog, - }; - - state.window.set_type_hint(hint); - } - - self.set_override_redirect(level); - } - - /// The override-redirect flag tells the window manager not to mess with the window; it should - /// be set for things like tooltips, dropdowns, etc. - /// - /// Note that this is exposed on the GDK window, so we can't set it until the GTK window is - /// realized. - fn set_override_redirect(&self, level: WindowLevel) { - let override_redirect = match level { - WindowLevel::AppWindow => false, - WindowLevel::Tooltip | WindowLevel::DropDown | WindowLevel::Modal => true, - }; - if let Some(state) = self.state.upgrade() { - if let Some(window) = state.window.window() { - window.set_override_redirect(override_redirect); - } - } - } - pub fn set_size(&self, size: Size) { if let Some(state) = self.state.upgrade() { let px = size.to_px(state.scale.get()); diff --git a/druid-shell/src/backend/mac/window.rs b/druid-shell/src/backend/mac/window.rs index dd7abc3cbe..836f49543f 100644 --- a/druid-shell/src/backend/mac/window.rs +++ b/druid-shell/src/backend/mac/window.rs @@ -89,9 +89,9 @@ mod levels { use WindowLevel::*; match window_level { AppWindow => NSNormalWindowLevel, - Tooltip => NSFloatingWindowLevel, - DropDown => NSFloatingWindowLevel, - Modal => NSModalPanelWindowLevel, + Tooltip(_) => NSFloatingWindowLevel, + DropDown(_) => NSFloatingWindowLevel, + Modal(_) => NSModalPanelWindowLevel, } } } @@ -103,6 +103,24 @@ pub(crate) struct WindowHandle { nsview: WeakPtr, idle_queue: Weak>>, } +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + match (self.idle_queue.upgrade(), other.idle_queue.upgrade()) { + (None, None) => true, + (Some(s), Some(o)) => std::sync::Arc::ptr_eq(&s, &o), + (_, _) => false, + } + } +} +impl Eq for WindowHandle {} + +impl std::fmt::Debug for WindowHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("WindowHandle{\n")?; + f.write_str("}")?; + Ok(()) + } +} impl Default for WindowHandle { fn default() -> Self { @@ -159,6 +177,7 @@ struct ViewState { keyboard_state: KeyboardState, text: PietText, active_text_input: Option, + parent: Option, } #[derive(Clone, PartialEq)] @@ -290,7 +309,13 @@ impl WindowBuilder { } if let Some(level) = self.level { - handle.set_level(level) + match &level { + WindowLevel::Tooltip(parent) => (*view_state).parent = Some(parent.clone()), + WindowLevel::DropDown(parent) => (*view_state).parent = Some(parent.clone()), + WindowLevel::Modal(parent) => (*view_state).parent = Some(parent.clone()), + _ => {} + } + handle.set_level(level); } // set_window_state above could have invalidated the frame size @@ -533,6 +558,7 @@ fn make_view(handler: Box) -> (id, Weak>>) { keyboard_state, text: PietText::new_with_unique_state(), active_text_input: None, + parent: None, }; let state_ptr = Box::into_raw(Box::new(state)); (*view).set_ivar("viewState", state_ptr as *mut c_void); @@ -1198,7 +1224,21 @@ impl WindowHandle { pub fn show_titlebar(&self, _show_titlebar: bool) {} // Need to translate mac y coords, as they start from bottom left - pub fn set_position(&self, position: Point) { + pub fn set_position(&self, mut position: Point) { + // TODO: Maybe @cmyr can get this into a state where modal windows follow the parent? + // There is an API to do child windows, (https://developer.apple.com/documentation/appkit/nswindow/1419152-addchildwindow) + // but I have no good way of testing and making sure this works. + unsafe { + if let Some(view) = self.nsview.load().as_ref() { + let state: *mut c_void = *view.get_ivar("viewState"); + let state = &mut (*(state as *mut ViewState)); + if let Some(parent_state) = &state.parent { + let pos = (*parent_state).get_position(); + position += (pos.x, pos.y) + } + } + } + self.defer(DeferredOp::SetPosition(position)) } @@ -1210,10 +1250,19 @@ impl WindowHandle { let window: id = msg_send![*self.nsview.load(), window]; let current_frame: NSRect = msg_send![window, frame]; - Point::new( + let mut position = Point::new( current_frame.origin.x, screen_height - current_frame.origin.y - current_frame.size.height, - ) + ); + if let Some(view) = self.nsview.load().as_ref() { + let state: *mut c_void = *view.get_ivar("viewState"); + let state = &mut (*(state as *mut ViewState)); + if let Some(parent_state) = &state.parent { + let pos = (*parent_state).get_position(); + position -= (pos.x, pos.y) + } + } + position } } @@ -1245,7 +1294,7 @@ impl WindowHandle { } } - pub fn set_level(&self, level: WindowLevel) { + fn set_level(&self, level: WindowLevel) { unsafe { let level = levels::as_raw_window_level(level); let window: id = msg_send![*self.nsview.load(), window]; diff --git a/druid-shell/src/backend/web/window.rs b/druid-shell/src/backend/web/window.rs index 08ceecdd50..f37f388709 100644 --- a/druid-shell/src/backend/web/window.rs +++ b/druid-shell/src/backend/web/window.rs @@ -79,6 +79,16 @@ pub(crate) struct WindowBuilder { #[derive(Clone, Default)] pub struct WindowHandle(Weak); +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + match (self.0.upgrade(), other.0.upgrade()) { + (None, None) => true, + (Some(s), Some(o)) => std::rc::Rc::ptr_eq(&s, &o), + (_, _) => false, + } + } +} +impl Eq for WindowHandle {} #[cfg(feature = "raw-win-handle")] unsafe impl HasRawWindowHandle for WindowHandle { @@ -479,10 +489,6 @@ impl WindowHandle { warn!("WindowHandle::set_position unimplemented for web"); } - pub fn set_level(&self, _level: WindowLevel) { - warn!("WindowHandle::set_level is currently 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/windows/window.rs b/druid-shell/src/backend/windows/window.rs index 3d7e0ae00e..264da3a699 100644 --- a/druid-shell/src/backend/windows/window.rs +++ b/druid-shell/src/backend/windows/window.rs @@ -163,12 +163,23 @@ enum DeferredOp { ReleaseMouseCapture, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct WindowHandle { text: PietText, state: Weak, } +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + match (self.state.upgrade(), other.state.upgrade()) { + (None, None) => true, + (Some(s), Some(o)) => std::rc::Rc::ptr_eq(&s, &o), + (_, _) => false, + } + } +} +impl Eq for WindowHandle {} + #[cfg(feature = "raw-win-handle")] unsafe impl HasRawWindowHandle for WindowHandle { fn raw_window_handle(&self) -> RawWindowHandle { @@ -226,6 +237,17 @@ struct WindowState { // Is the window focusable ("activatable" in Win32 terminology)? // False for tooltips, to prevent stealing focus from owner window. is_focusable: bool, + + parent: Option, +} + +impl std::fmt::Debug for WindowState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.write_str("WindowState{\n")?; + f.write_str(format!("{:p}", self.hwnd.get()).as_str())?; + f.write_str("}")?; + Ok(()) + } } /// Generic handler trait for the winapi window procedure entry point. @@ -1311,7 +1333,7 @@ impl WindowBuilder { pub fn set_level(&mut self, level: WindowLevel) { match level { - WindowLevel::AppWindow | WindowLevel::Tooltip => self.level = Some(level), + WindowLevel::AppWindow | WindowLevel::Tooltip(_) => self.level = Some(level), _ => { warn!("WindowBuilder::set_level({:?}) is currently unimplemented for Windows backend.", level); } @@ -1360,19 +1382,21 @@ impl WindowBuilder { let mut dwStyle = WS_OVERLAPPEDWINDOW; let mut dwExStyle: DWORD = 0; let mut focusable = true; + let mut parent: Option = None; if let Some(level) = self.level { match level { WindowLevel::AppWindow => (), - WindowLevel::Tooltip => { + WindowLevel::Tooltip(p) => { dwStyle = WS_POPUP; dwExStyle = WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW; focusable = false; + parent = Some(p) } - WindowLevel::DropDown => { + WindowLevel::DropDown(_) => { dwStyle = WS_CHILD; dwExStyle = 0; } - WindowLevel::Modal => { + WindowLevel::Modal(_) => { dwStyle = WS_OVERLAPPED; dwExStyle = WS_EX_TOPMOST; } @@ -1395,6 +1419,8 @@ impl WindowBuilder { handle_titlebar: Cell::new(false), active_text_input: Cell::new(None), is_focusable: focusable, + + parent, }; let win = Rc::new(window); let handle = WindowHandle { @@ -1823,15 +1849,18 @@ impl WindowHandle { } // Sets the position of the window in virtual screen coordinates - pub fn set_position(&self, position: Point) { + pub fn set_position(&self, mut position: Point) { + //TODO: Make the window follow the parent, mostly for modal windows. + if let Some(state) = self.state.upgrade() { + if let Some(parent_state) = &state.parent { + let pos = (*parent_state).get_position(); + position += (pos.x, pos.y) + } + }; self.defer(DeferredOp::SetWindowState(window::WindowState::Restored)); self.defer(DeferredOp::SetPosition(position)); } - pub fn set_level(&self, _level: WindowLevel) { - warn!("Window level unimplemented for Windows!"); - } - // Gets the position of the window in virtual screen coordinates pub fn get_position(&self) -> Point { if let Some(w) = self.state.upgrade() { @@ -1849,7 +1878,14 @@ impl WindowHandle { Error::Hr(HRESULT_FROM_WIN32(GetLastError())) ); }; - return Point::new(rect.left as f64, rect.top as f64); + let mut position = Point::new(rect.left as f64, rect.top as f64); + if let Some(state) = self.state.upgrade() { + if let Some(parent_state) = &state.parent { + let pos = (*parent_state).get_position(); + position -= (pos.x, pos.y) + } + }; + return position; } } 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 f95e6ac493..7d53709424 100644 --- a/druid-shell/src/backend/x11/window.rs +++ b/druid-shell/src/backend/x11/window.rs @@ -429,9 +429,9 @@ impl WindowBuilder { { let window_type = match self.level { WindowLevel::AppWindow => atoms._NET_WM_WINDOW_TYPE_NORMAL, - WindowLevel::Tooltip => atoms._NET_WM_WINDOW_TYPE_TOOLTIP, - WindowLevel::Modal => atoms._NET_WM_WINDOW_TYPE_DIALOG, - WindowLevel::DropDown => atoms._NET_WM_WINDOW_TYPE_DROPDOWN_MENU, + WindowLevel::Tooltip(_) => atoms._NET_WM_WINDOW_TYPE_TOOLTIP, + WindowLevel::Modal(_) => atoms._NET_WM_WINDOW_TYPE_DIALOG, + WindowLevel::DropDown(_) => atoms._NET_WM_WINDOW_TYPE_DROPDOWN_MENU, }; let conn = self.app.connection(); @@ -444,7 +444,7 @@ impl WindowBuilder { )); if matches!( self.level, - WindowLevel::DropDown | WindowLevel::Modal | WindowLevel::Tooltip + WindowLevel::DropDown(_) | WindowLevel::Modal(_) | WindowLevel::Tooltip(_) ) { log_x11!(conn.change_window_attributes( id, @@ -1549,6 +1549,12 @@ pub(crate) struct WindowHandle { id: u32, window: Weak, } +impl PartialEq for WindowHandle { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} +impl Eq for WindowHandle {} impl WindowHandle { fn new(id: u32, window: Weak) -> WindowHandle { @@ -1609,10 +1615,6 @@ impl WindowHandle { Insets::ZERO } - pub fn set_level(&self, _level: WindowLevel) { - warn!("WindowHandle::set_level unimplemented for X11 backend."); - } - pub fn set_size(&self, size: Size) { if let Some(w) = self.window.upgrade() { w.set_size(size); diff --git a/druid-shell/src/window.rs b/druid-shell/src/window.rs index 74b6976ae3..73d88f5655 100644 --- a/druid-shell/src/window.rs +++ b/druid-shell/src/window.rs @@ -15,6 +15,7 @@ //! Platform independent window types. use std::any::Any; +use std::fmt; use std::time::Duration; use crate::application::Application; @@ -148,16 +149,27 @@ impl FileDialogToken { /// Levels in the window system - Z order for display purposes. /// Describes the purpose of a window and should be mapped appropriately to match platform /// conventions. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub enum WindowLevel { /// A top level app window. AppWindow, /// A window that should stay above app windows - like a tooltip - Tooltip, + Tooltip(WindowHandle), /// A user interface element such as a dropdown menu or combo box - DropDown, + DropDown(WindowHandle), /// A modal dialog - Modal, + Modal(WindowHandle), +} + +impl fmt::Debug for WindowLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WindowLevel::AppWindow => write!(f, "AppWindow"), + WindowLevel::Tooltip(_) => write!(f, "ToolTip"), + WindowLevel::DropDown(_) => write!(f, "DropDown"), + WindowLevel::Modal(_) => write!(f, "Modal"), + } + } } /// Contains the different states a Window can be in. @@ -169,8 +181,8 @@ pub enum WindowState { } /// A handle to a platform window object. -#[derive(Clone, Default)] -pub struct WindowHandle(backend::WindowHandle); +#[derive(Clone, Default, PartialEq)] +pub struct WindowHandle(pub(crate) backend::WindowHandle); impl WindowHandle { /// Make this window visible. @@ -268,15 +280,6 @@ impl WindowHandle { self.0.get_size() } - /// Sets the [`WindowLevel`](crate::WindowLevel), the z-order in the Window system / compositor - /// - /// We do not currently have a getter method, mostly because the system's levels aren't a - /// perfect one-to-one map to `druid_shell`'s levels. A getter method may be added in the - /// future. - pub fn set_level(&self, level: WindowLevel) { - self.0.set_level(level) - } - /// Bring this window to the front of the window stack and give it focus. pub fn bring_to_front_and_focus(&self) { self.0.bring_to_front_and_focus() diff --git a/druid/examples/sub_window.rs b/druid/examples/sub_window.rs index e7b1280f69..7ed1a8c6e3 100644 --- a/druid/examples/sub_window.rs +++ b/druid/examples/sub_window.rs @@ -145,11 +145,9 @@ impl> Controller for TooltipController { WindowConfig::default() .show_titlebar(false) .window_size_policy(WindowSizePolicy::Content) - .set_level(WindowLevel::Tooltip) + .set_level(WindowLevel::Tooltip(ctx.window().clone())) .set_position( - ctx.window().get_position() - + window_pos.to_vec2() - + cursor_size.to_vec2(), + (window_pos.to_vec2() + cursor_size.to_vec2()).to_point(), ), Label::<()>::new(self.tip.clone()), (), diff --git a/druid/src/app.rs b/druid/src/app.rs index b9f79596d0..4f8c9bcb49 100644 --- a/druid/src/app.rs +++ b/druid/src/app.rs @@ -23,6 +23,8 @@ use crate::win_handler::{AppHandler, AppState}; use crate::window::WindowId; use crate::{AppDelegate, Data, Env, LocalizedString, Menu, Widget}; +use tracing::warn; + use druid_shell::WindowState; /// A function that modifies the initial environment. @@ -403,7 +405,7 @@ impl WindowConfig { builder.set_transparent(transparent); } - if let Some(level) = self.level { + if let Some(level) = self.level.clone() { builder.set_level(level) } @@ -437,8 +439,8 @@ impl WindowConfig { win_handle.set_position(position); } - if let Some(level) = self.level { - win_handle.set_level(level) + if self.level.is_some() { + warn!("Applying a level can only be done on window builders"); } if let Some(state) = self.state {