diff --git a/winit-android/src/event_loop.rs b/winit-android/src/event_loop.rs index 43552f0b0e..da9f0fe516 100644 --- a/winit-android/src/event_loop.rs +++ b/winit-android/src/event_loop.rs @@ -813,6 +813,7 @@ impl Window { match window_attrs.window_type() { window::WindowType::Window => (), window::WindowType::Popup => return Err(CreateWindowError::PopupNotSupported), + window::WindowType::Dialog => return Err(CreateWindowError::DialogNotSupported), _ => panic!("Unknown WindowType"), } diff --git a/winit-appkit/Cargo.toml b/winit-appkit/Cargo.toml index 0f6a1bd977..468488d065 100644 --- a/winit-appkit/Cargo.toml +++ b/winit-appkit/Cargo.toml @@ -28,6 +28,7 @@ dispatch2 = { workspace = true, features = ["std", "objc2"] } objc2.workspace = true objc2-app-kit = { workspace = true, features = [ "std", + "block2", "objc2-core-foundation", "NSAppearance", "NSApplication", diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index c9969bfa80..7f5432851d 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -85,6 +85,7 @@ impl Drop for Window { self.set_fullscreen(None); } + self.maybe_wait_on_main(|delegate| delegate.end_modal_sheet()); self.window.get_on_main(|window| autoreleasepool(|_| window.close())) } } diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index bb73006d55..3cb46896c1 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -135,7 +135,13 @@ pub(crate) struct State { /// attributes were set on a [`WindowType::Window`]. Gates the parent-relative coordinate /// frame used when applying anchor/gravity/positioner-offset changes. anchored: bool, + /// Whether this window is placed by the positioner system (and re-placed when its parent + /// moves). Unlike `anchored`, this is not the case for a [`WindowType::Dialog`], which only + /// uses the parent's coordinate frame. + auto_placed: bool, positioner: RefCell, + /// For a modal [`WindowType::Dialog`]: the parent window it is presented on as a sheet. + modal_parent: Option>, } define_class!( @@ -943,12 +949,16 @@ impl WindowDelegate { .unwrap_or_default(); let window_type = attrs.window_type(); - let is_popup = match window_type { - WindowType::Window => false, - WindowType::Popup => true, - _ => panic!("Unknown WindowType"), - }; - let anchored = is_popup || attrs.positioner.is_some(); + let is_popup = matches!(window_type, WindowType::Popup); + let is_dialog = matches!(window_type, WindowType::Dialog); + // A modal dialog is presented as a sheet on its parent, which AppKit places itself. + let is_modal = is_dialog && attrs.modal; + let auto_placed = is_popup || attrs.positioner.is_some(); + let anchored = auto_placed || (is_dialog && !is_modal); + if is_dialog { + // A dialog can't be minimized independently of its parent. + attrs.enabled_buttons.remove(WindowButtons::MINIMIZE); + } if is_popup { // A popup is an undecorated, non-activating panel with no titlebar buttons. Model it // as such so it flows through the existing borderless + panel paths in `new_window` @@ -965,6 +975,7 @@ impl WindowDelegate { let (window, view) = new_window(app_state, &attrs, &macos_attrs, anchored, mtm) .ok_or_else(|| os_error!("couldn't create `NSWindow`"))?; + let mut parent_window = None; match attrs.parent_window() { Some(rwh_06::RawWindowHandle::AppKit(handle)) => { // SAFETY: Caller ensures the pointer is valid or NULL @@ -975,17 +986,25 @@ impl WindowDelegate { .window() .ok_or_else(|| os_error!("parent view should be installed in a window"))?; - // SAFETY: We know that there are no parent -> child -> parent cycles since the only - // place in `winit` where we allow making a window a child window is - // right here, just after it's been created. - unsafe { parent.addChildWindow_ordered(&window, NSWindowOrderingMode::Above) }; + // A modal dialog is presented as a sheet instead, once it is shown. + if !is_modal { + // SAFETY: We know that there are no parent -> child -> parent cycles since the + // only place in `winit` where we allow making a window a child window is + // right here, just after it's been created. + unsafe { parent.addChildWindow_ordered(&window, NSWindowOrderingMode::Above) }; + } + parent_window = Some(parent); }, Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"), None if is_popup => { return Err(InvalidInput::new("a popup window requires a parent window").into()); }, + None if is_dialog => { + return Err(InvalidInput::new("a dialog requires a parent window").into()); + }, None => (), } + let modal_parent = parent_window.clone().filter(|_| is_modal); let surface_resize_increments = match attrs .surface_resize_increments @@ -1027,7 +1046,9 @@ impl WindowDelegate { is_borderless_game: Cell::new(macos_attrs.borderless_game), window_type, anchored, + auto_placed, positioner: RefCell::new(attrs.positioner.unwrap_or_default()), + modal_parent, }); let delegate: Retained = unsafe { msg_send![super(delegate), init] }; @@ -1076,9 +1097,20 @@ impl WindowDelegate { // An anchored window's position is relative to the parent window, and the parent is // only attached above, so apply the (translated) position now. Default to the parent's - // content top-left when no position was given. + // content top-left when no position was given, or center a dialog over its parent. if anchored { - let position = attrs.position.unwrap_or_else(|| LogicalPosition::new(0.0, 0.0).into()); + let position = attrs.position.unwrap_or_else(|| match &parent_window { + Some(parent) if is_dialog => { + let parent_size = parent.contentRectForFrameRect(parent.frame()).size; + let size = window.frame().size; + LogicalPosition::new( + (parent_size.width - size.width) / 2.0, + (parent_size.height - size.height) / 2.0, + ) + .into() + }, + _ => LogicalPosition::new(0.0, 0.0).into(), + }); delegate.set_outer_position(position); } @@ -1091,7 +1123,9 @@ impl WindowDelegate { // state, since otherwise we'll briefly see the window at normal size // before it transitions. if attrs.visible { - if attrs.active { + if delegate.ivars().modal_parent.is_some() { + delegate.begin_modal_sheet(); + } else if attrs.active { // Tightly linked with `app_state::window_activation_hack` window.makeKeyAndOrderFront(None); } else { @@ -1264,7 +1298,31 @@ impl WindowDelegate { } } + /// Presents this modal dialog as a sheet on its parent, unless it already is. + fn begin_modal_sheet(&self) { + let Some(parent) = &self.ivars().modal_parent else { return }; + if self.window().sheetParent().is_none() { + parent.beginSheet_completionHandler(self.window(), None); + } + } + + /// Dismisses this modal dialog's sheet, if it is presented. This must happen before the window + /// is closed, otherwise the parent is left believing it still has a sheet attached. + pub(crate) fn end_modal_sheet(&self) { + let Some(parent) = &self.ivars().modal_parent else { return }; + if self.window().sheetParent().is_some() { + parent.endSheet(self.window()); + } + } + pub fn set_visible(&self, visible: bool) { + if self.ivars().modal_parent.is_some() { + match visible { + true => self.begin_modal_sheet(), + false => self.end_modal_sheet(), + } + return; + } match visible { true => self.window().makeKeyAndOrderFront(None), false => self.window().orderOut(None), @@ -1382,7 +1440,7 @@ impl WindowDelegate { /// this window isn't anchored. If it has no parent, the positioner is resolved relative to /// the screen instead of the parent's content area. pub(crate) fn reposition(&self) { - if !self.ivars().anchored { + if !self.ivars().auto_placed { return; } @@ -1436,7 +1494,7 @@ impl WindowDelegate { for child in children.iter() { let Some(child_delegate) = child.delegate() else { continue }; let Ok(child_delegate) = child_delegate.downcast::() else { continue }; - if child_delegate.ivars().anchored { + if child_delegate.ivars().auto_placed { child_delegate.reposition(); } } diff --git a/winit-core/src/error.rs b/winit-core/src/error.rs index bf769cae11..d50c59975d 100644 --- a/winit-core/src/error.rs +++ b/winit-core/src/error.rs @@ -59,6 +59,8 @@ impl From for EventLoopError { pub enum CreateWindowError { /// [`WindowType::Popup`] is not supported. PopupNotSupported, + /// [`WindowType::Dialog`] is not supported. + DialogNotSupported, /// Invalid input attribute InvalidAttribute(InvalidInput), /// Got unspecified OS specific error during the request. @@ -69,6 +71,7 @@ impl Display for CreateWindowError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::PopupNotSupported => write!(f, "WindowType::Popup is not supported"), + Self::DialogNotSupported => write!(f, "WindowType::Dialog is not supported"), Self::InvalidAttribute(InvalidInput { reason }) => { write!(f, "Invalid WindowAttributes: {reason}") }, diff --git a/winit-core/src/window.rs b/winit-core/src/window.rs index 2f6fe68801..318c923b95 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -70,6 +70,26 @@ pub enum WindowType { /// [`WindowAttributes::with_transparent`]) and render the round border yourself. /// - **X11, Web, Android, iOS, Orbital:** An error is returned because it is not implemented. Popup, + /// A native modal or non-modal dialog anchored to a parent window, requiring a parent set + /// via [`WindowAttributes::with_parent_window`]. + /// + /// See [`WindowAttributes::with_modal`] to control whether the dialog is modal. + /// + /// ## Platform-specific + /// + /// - **Windows:** An owned window (see MSDN's [owned windows]) that is always on top of its + /// parent. When modal, the parent is disabled via `EnableWindow` for as long as the dialog is + /// open, and re-enabled and reactivated once the dialog is closed. A position set via + /// [`WindowAttributes::with_position`] is relative to the parent's client area; without one, + /// the dialog is centered over its parent. Moving a modal dialog moves its parent along with + /// it, so the dialog keeps its position relative to the parent. + /// - **macOS:** A non-modal dialog is a child window of its parent (moving along with it), + /// positioned like on Windows. A modal dialog is presented as a sheet attached to its parent, + /// which AppKit positions itself, so it can't be moved and any position is ignored. + /// - **X11, Web, Android, iOS, Orbital:** An error is returned because it is not implemented. + /// + /// [owned windows]: https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#owned-windows + Dialog, } /// The positioner state backing a window's anchor-based placement. @@ -148,8 +168,6 @@ pub struct WindowAttributes { /// The initial position of the window in screen coordinates. /// /// For popups, this position is relative to the parent window. - /// - /// **Wayland:** See `WindowAttributesWayland` for more options to position a popup. pub position: Option, pub resizable: bool, pub enabled_buttons: WindowButtons, @@ -178,6 +196,7 @@ pub struct WindowAttributes { pub window_type: WindowType, /// See [`WindowAttributes::with_positioner`]. pub positioner: Option, + pub modal: bool, } impl WindowAttributes { @@ -468,9 +487,12 @@ impl WindowAttributes { /// /// ## Platform-specific /// - /// - **Windows** : A child window has the WS_CHILD style and is confined - /// to the client area of its parent window. For more information, see + /// - **Windows** : For [`WindowType::Window`], a child window has the WS_CHILD style and is + /// confined to the client area of its parent window. For more information, see /// + /// For [`WindowType::Popup`] and [`WindowType::Dialog`], the window instead becomes an + /// *owned* window (no WS_CHILD style) positioned relative to, and always on top of, this + /// parent -- see [`WindowType::Dialog`]'s platform-specific notes. /// - **X11**: A child window is confined to the client area of its parent window. /// - **Android / iOS / Wayland / Web:** Unsupported. #[inline] @@ -532,6 +554,25 @@ impl WindowAttributes { self.positioner = Some(positioner); self } + + /// Sets whether a [`WindowType::Dialog`] is modal, i.e. blocks interaction with its parent + /// while open. Has no effect on other [`WindowType`]s. + /// + /// The default is non-modal. + /// + /// ## Platform-specific + /// + /// - **Wayland:** Implemented via the `xdg_dialog` protocol's modal state. + /// - **Windows:** Implemented by disabling the parent window (`EnableWindow`) for as long as + /// the dialog is open. + /// - **macOS:** Implemented by presenting the dialog as a sheet on its parent window. + /// - **X11, Web, Android, iOS, Orbital:** No effect, since [`WindowType::Dialog`] is not + /// implemented. + #[inline] + pub fn with_modal(mut self, modal: bool) -> Self { + self.modal = modal; + self + } } impl Clone for WindowAttributes { @@ -561,6 +602,7 @@ impl Clone for WindowAttributes { platform: self.platform.as_ref().map(|platform| platform.box_clone()), window_type: self.window_type, positioner: self.positioner, + modal: self.modal, } } } @@ -593,6 +635,7 @@ impl Default for WindowAttributes { blur: Default::default(), window_type: Default::default(), positioner: Default::default(), + modal: Default::default(), } } } diff --git a/winit-orbital/src/window.rs b/winit-orbital/src/window.rs index 58c6a4ad0e..e4ce98c345 100644 --- a/winit-orbital/src/window.rs +++ b/winit-orbital/src/window.rs @@ -40,6 +40,7 @@ impl Window { match attrs.window_type() { window::WindowType::Window => (), window::WindowType::Popup => return Err(CreateWindowError::PopupNotSupported), + window::WindowType::Dialog => return Err(CreateWindowError::DialogNotSupported), _ => panic!("Unknown WindowType"), } diff --git a/winit-uikit/src/window.rs b/winit-uikit/src/window.rs index c7c50b2f38..6f3598a6ee 100644 --- a/winit-uikit/src/window.rs +++ b/winit-uikit/src/window.rs @@ -492,6 +492,7 @@ impl Window { match window_attributes.window_type() { WindowType::Window => (), WindowType::Popup => return Err(CreateWindowError::PopupNotSupported), + WindowType::Dialog => return Err(CreateWindowError::DialogNotSupported), _ => panic!("Unknown WindowType"), } diff --git a/winit-wayland/Cargo.toml b/winit-wayland/Cargo.toml index 4418a789f6..e80adcce03 100644 --- a/winit-wayland/Cargo.toml +++ b/winit-wayland/Cargo.toml @@ -42,7 +42,7 @@ sctk = { package = "smithay-client-toolkit", version = "0.21.0", default-feature ] } sctk-adwaita = { version = "0.12.0", default-features = false, optional = true } wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] } -wayland-client = "0.31.10" +wayland-client = "0.31.14" wayland-protocols = { version = "0.32.12", features = ["staging", "unstable"] } wayland-protocols-plasma = { version = "0.3.8", features = ["client"] } winit-common = { workspace = true, features = ["xkb", "wayland"] } diff --git a/winit-wayland/src/dialog.rs b/winit-wayland/src/dialog.rs new file mode 100644 index 0000000000..22c7099932 --- /dev/null +++ b/winit-wayland/src/dialog.rs @@ -0,0 +1,436 @@ +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; + +use dpi::{PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size}; +use rwh_06::RawWindowHandle; +use sctk::shell::WaylandSurface; +use sctk::shell::xdg::window::WindowDecorations; +use winit_core::cursor::Cursor; +use winit_core::error::{CreateWindowError, InvalidInput, NotSupportedError, RequestError}; +use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle}; +use winit_core::window::{ + CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme, + UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId, + WindowLevel, +}; + +use super::ActiveEventLoop; +use crate::WindowAttributesWayland; +use crate::window::common::WindowCommon; +use crate::window::state::{WindowState, WindowType}; +use crate::window::{Handles, finish_window_setup}; + +#[derive(Debug)] +pub struct Dialog { + common: WindowCommon, +} + +impl Dialog { + pub(crate) fn new( + event_loop_window_target: &ActiveEventLoop, + mut attributes: WindowAttributes, + ) -> Result { + let modal = attributes.modal; + let queue_handle = event_loop_window_target.queue_handle.clone(); + let mut state = event_loop_window_target.state.borrow_mut(); + let monitors = state.monitors.clone(); + let xdg_activation = + state.xdg_activation.as_ref().map(|activation_state| activation_state.global().clone()); + let parent_window_handle = attributes + .parent_window() + .ok_or(InvalidInput::new("Dialog without a parent is not supported"))?; + + let RawWindowHandle::Wayland(parent_window_handle) = parent_window_handle else { + return Err( + InvalidInput::new("A Dialog requires a parent wayland window handle").into() + ); + }; + + let parent_window_id = WindowId::from_raw(parent_window_handle.surface.as_ptr() as usize); + + let (dialog, dialog_state) = { + let windows = state.windows.borrow(); + let Some(parent_window_state) = windows.get(&parent_window_id) else { + return Err(InvalidInput::new("Unknown parent window id").into()); + }; + let parent_window_state = parent_window_state.lock().unwrap(); + let scale_factor = parent_window_state.scale_factor(); + let parent_xdg_toplevel = { + match &parent_window_state.window { + WindowType::Window { window, .. } => window.xdg_toplevel().clone(), + WindowType::Dialog { dialog, .. } => dialog.xdg_toplevel().clone(), + WindowType::Popup { .. } => { + return Err(InvalidInput::new( + "Parent of a dialog must be a window or a dialog", + ) + .into()); + }, + } + }; + + // We prefer server side decorations, however to not have decorations we ask for client + // side decorations instead. + let default_decorations = if attributes.decorations { + WindowDecorations::RequestServer + } else { + WindowDecorations::RequestClient + }; + + let surface = state.compositor_state.create_surface(&queue_handle); + let dialog = state + .xdg_shell + .create_dialog( + surface.clone(), + default_decorations, + &queue_handle, + &parent_xdg_toplevel, + ) + .map_err(|e| os_error!(e))?; + + let WindowAttributesWayland { activation_token, prefer_csd, .. } = *attributes + .platform + .take() + .and_then(|p| p.cast::().ok()) + .unwrap_or_default(); + + let mut dialog_state = WindowState::new( + event_loop_window_target, + &state, + attributes + .surface_size + .ok_or(InvalidInput::new("Missing surface size for Dialog window"))?, + WindowType::Dialog { dialog: dialog.clone(), last_configure: None }, + attributes.preferred_theme, + prefer_csd, + scale_factor, + Some(parent_window_id), + ); + + dialog.set_modal(modal); + + dialog_state.set_window_icon(attributes.window_icon); + + // Set transparency hint. + dialog_state.set_transparent(attributes.transparent); + + // Set blur. + let _ = dialog_state.set_blur(attributes.blur); + + // Set the decorations hint. + dialog_state.set_decorate(attributes.decorations); + + // Set the window title. + dialog_state.set_title(attributes.title); + + // Set the min and max sizes. We must set the hints upon creating a window, so + // we use the default `1.` scaling... + let min_size = attributes.min_surface_size.map(|size| size.to_logical(1.)); + let max_size = attributes.max_surface_size.map(|size| size.to_logical(1.)); + dialog_state.set_min_surface_size(min_size); + dialog_state.set_max_surface_size(max_size); + + // Non-resizable implies that the min and max sizes are set to the same value. + dialog_state.set_resizable(attributes.resizable); + + // Activate the window when the token is passed. + if let (Some(xdg_activation), Some(token)) = (xdg_activation.as_ref(), activation_token) + { + xdg_activation.activate(token.into_raw(), &surface); + } + + // Do initial commit + dialog.commit(); + + let dialog_state = Arc::new(Mutex::new(dialog_state)); + (dialog, dialog_state) + }; + + // Setup the event sync to insert `WindowEvents` right from the window. + let window_events_sink = state.window_events_sink.clone(); + + let (window_id, window_requests) = finish_window_setup( + event_loop_window_target, + &mut state, + dialog.wl_surface(), + &dialog_state, + Some("Dialog was dismissed by the compositor before configure"), + )?; + + let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); + + let windows = state.windows.borrow(); + if let Some(parent_window_state) = windows.get(&parent_window_id) { + let mut parent_window_state = parent_window_state.lock().unwrap(); + parent_window_state.add_child(super::make_wid(dialog.wl_surface())); + drop(parent_window_state); + }; + + Ok(Self { + common: WindowCommon { + state: Arc::downgrade(&dialog_state), + window_id, + display: event_loop_window_target.handle.connection.display().clone(), + handles: Handles { + queue_handle, + window_requests, + monitors, + event_loop_awakener, + window_events_sink, + xdg_activation, + attention_requested: Arc::new(AtomicBool::new(false)), + compositor: state.compositor_state.clone(), + }, + }, + }) + } +} + +impl CoreWindow for Dialog { + fn window_type(&self) -> winit_core::window::WindowType { + winit_core::window::WindowType::Dialog + } + + fn id(&self) -> WindowId { + self.common.id() + } + + fn request_redraw(&self) { + self.common.request_redraw(); + } + + #[inline] + fn title(&self) -> String { + self.common.title() + } + + fn pre_present_notify(&self) { + self.common.pre_present_notify(); + } + + fn reset_dead_keys(&self) { + self.common.reset_dead_keys(); + } + + fn surface_position(&self) -> PhysicalPosition { + (0, 0).into() + } + + fn outer_position(&self) -> Result, RequestError> { + Err(NotSupportedError::new("window position information is not available on Wayland") + .into()) + } + + fn set_outer_position(&self, _position: Position) { + // Not possible + } + + fn surface_size(&self) -> PhysicalSize { + self.common.surface_size() + } + + fn request_surface_size(&self, size: Size) -> Option> { + self.common.request_surface_size(size) + } + + fn outer_size(&self) -> PhysicalSize { + self.common.outer_size() + } + + fn safe_area(&self) -> PhysicalInsets { + self.common.safe_area() + } + + fn set_min_surface_size(&self, min_size: Option) { + self.common.set_min_surface_size(min_size); + } + + /// Set the maximum surface size for the window. + #[inline] + fn set_max_surface_size(&self, max_size: Option) { + self.common.set_max_surface_size(max_size); + } + + fn surface_resize_increments(&self) -> Option> { + self.common.surface_resize_increments() + } + + fn set_surface_resize_increments(&self, increments: Option) { + self.common.set_surface_resize_increments(increments); + } + + fn set_title(&self, title: &str) { + self.common.set_title(title); + } + + #[inline] + fn set_transparent(&self, transparent: bool) { + self.common.set_transparent(transparent); + } + + fn set_visible(&self, visible: bool) { + self.common.set_visible(visible); + } + + fn is_visible(&self) -> Option { + self.common.is_visible() + } + + fn set_resizable(&self, resizable: bool) { + self.common.set_resizable(resizable); + } + + fn is_resizable(&self) -> bool { + self.common.is_resizable() + } + + fn set_enabled_buttons(&self, _buttons: WindowButtons) { + // TODO(kchibisov) v5 of the xdg_shell allows that. + } + + fn enabled_buttons(&self) -> WindowButtons { + // TODO(kchibisov) v5 of the xdg_shell allows that. + // Minimize does not make any sense + WindowButtons::CLOSE | WindowButtons::MAXIMIZE + } + + fn set_minimized(&self, _minimized: bool) { + // A dialog cannot be minimized + } + + fn is_minimized(&self) -> Option { + // XXX clients don't know whether they are minimized or not. + None + } + + fn set_maximized(&self, maximized: bool) { + self.common.set_maximized(maximized); + } + + fn is_maximized(&self) -> bool { + self.common.is_maximized() + } + + fn set_fullscreen(&self, fullscreen: Option) { + self.common.set_fullscreen(fullscreen) + } + + fn fullscreen(&self) -> Option { + self.common.fullscreen() + } + + #[inline] + fn scale_factor(&self) -> f64 { + self.common.scale_factor() + } + + #[inline] + fn set_blur(&self, blur: bool) { + self.common.set_blur(blur); + } + + #[inline] + fn set_decorations(&self, decorate: bool) { + self.common.set_decorations(decorate); + } + + #[inline] + fn is_decorated(&self) -> bool { + self.common.is_decorated().unwrap_or_default() + } + + fn set_window_level(&self, level: WindowLevel) { + self.common.set_window_level(level) + } + + fn set_window_icon(&self, window_icon: Option) { + self.common.set_window_icon(window_icon); + } + + #[inline] + fn request_ime_update(&self, request: ImeRequest) -> Result<(), ImeRequestError> { + self.common.request_ime_update(request) + } + + #[inline] + fn ime_capabilities(&self) -> Option { + self.common.ime_capabilities() + } + + fn focus_window(&self) { + self.common.focus_window(); + } + + fn has_focus(&self) -> bool { + self.common.has_focus() + } + + fn request_user_attention(&self, request_type: Option) { + self.common.request_user_attention(request_type); + } + + fn set_theme(&self, theme: Option) { + self.common.set_theme(theme); + } + + fn theme(&self) -> Option { + self.common.theme() + } + + fn set_content_protected(&self, protected: bool) { + self.common.set_content_protected(protected); + } + + fn set_cursor(&self, cursor: Cursor) { + self.common.set_cursor(cursor); + } + + fn set_cursor_position(&self, position: Position) -> Result<(), RequestError> { + self.common.set_cursor_position(position) + } + + fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), RequestError> { + self.common.set_cursor_grab(mode) + } + + fn set_cursor_visible(&self, visible: bool) { + self.common.set_cursor_visible(visible); + } + + fn drag_window(&self) -> Result<(), RequestError> { + self.common.drag_window() + } + + fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError> { + self.common.drag_resize_window(direction) + } + + fn show_window_menu(&self, position: Position) { + self.common.show_window_menu(position) + } + + fn set_cursor_hittest(&self, hittest: bool) -> Result<(), RequestError> { + self.common.set_cursor_hittest(hittest) + } + + fn current_monitor(&self) -> Option { + self.common.current_monitor() + } + + fn available_monitors(&self) -> Box> { + self.common.available_monitors() + } + + fn primary_monitor(&self) -> Option { + self.common.primary_monitor() + } + + /// Get the raw-window-handle v0.6 display handle. + fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle { + &self.common + } + + /// Get the raw-window-handle v0.6 window handle. + fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { + &self.common + } +} diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 6b9e3bffe5..709b3ff205 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -755,6 +755,10 @@ impl RootActiveEventLoop for ActiveEventLoop { let popup = crate::Popup::new(self, window_attributes)?; Ok(Box::new(popup)) }, + WindowType::Dialog => { + let dialog = crate::dialog::Dialog::new(self, window_attributes)?; + Ok(Box::new(dialog)) + }, _ => panic!("Unknown WindowType"), } } diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 113e2b074c..0025631920 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -38,6 +38,7 @@ macro_rules! os_error { ($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }}; } +mod dialog; mod dnd; mod event_loop; mod output; diff --git a/winit-wayland/src/popup.rs b/winit-wayland/src/popup.rs index 1111f0a36e..d14fa45b06 100644 --- a/winit-wayland/src/popup.rs +++ b/winit-wayland/src/popup.rs @@ -21,10 +21,9 @@ use winit_core::window::{ use super::ActiveEventLoop; use crate::WindowAttributesWayland; -use crate::window::Handles; use crate::window::common::WindowCommon; -use crate::window::handles::WindowRequests; use crate::window::state::{WindowState, WindowType}; +use crate::window::{Handles, finish_window_setup}; #[derive(Debug)] pub struct Popup { @@ -65,7 +64,7 @@ impl Popup { } = attributes.positioner.unwrap_or_default(); let grab_keyboard = attributes.active; - let mut parent_window_state = parent_window_state.lock().unwrap(); + let parent_window_state = parent_window_state.lock().unwrap(); // Use the scale factor and xdg geometry of the parent. let scale_factor = parent_window_state.scale_factor(); @@ -131,7 +130,6 @@ impl Popup { &state.xdg_shell, ) .map_err(|e| os_error!(e))?; - parent_window_state.add_child(super::make_wid(popup.wl_surface())); drop(parent_window_state); drop(window_states); @@ -199,44 +197,25 @@ impl Popup { let popup_state = Arc::new(Mutex::new(popup_state)); - let window_id = super::make_wid(popup.wl_surface()); - state.windows.get_mut().insert(window_id, popup_state.clone()); - - let window_requests = WindowRequests { - redraw_requested: AtomicBool::new(true), - closed: AtomicBool::new(false), - }; - let window_requests = Arc::new(window_requests); - state.window_requests.get_mut().insert(window_id, window_requests.clone()); - // Setup the event sync to insert `WindowEvents` right from the window. let window_events_sink = state.window_events_sink.clone(); - let mut wayland_source = event_loop_window_target.wayland_dispatcher.as_source_mut(); - let event_queue = wayland_source.queue(); - // Do a roundtrip. - event_queue.roundtrip(&mut state).map_err(|err| os_error!(err))?; - - // XXX Wait for the initial configure to arrive. - while !popup_state.lock().unwrap().is_configured() { - event_queue.blocking_dispatch(&mut state).map_err(|err| os_error!(err))?; - // The compositor may dismiss a popup (e.g. invalid grab serial) by sending - // popup_done before configure. Detect that and bail out instead of looping forever. - if state - .window_compositor_updates - .iter() - .any(|u| u.window_id == window_id && u.close_window) - { - return Err(os_error!(PopupError( - "Popup was dismissed by the compositor before configure" - )) - .into()); - } - } + let (window_id, window_requests) = finish_window_setup( + event_loop_window_target, + &mut state, + popup.wl_surface(), + &popup_state, + Some("Popup was dismissed by the compositor before configure"), + )?; - // Wake-up event loop, so it'll send initial redraw requested. let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); - event_loop_awakener.ping(); + + let windows = state.windows.borrow(); + if let Some(parent_window_state) = windows.get(&parent_window_id) { + let mut parent_window_state = parent_window_state.lock().unwrap(); + parent_window_state.add_child(super::make_wid(popup.wl_surface())); + drop(parent_window_state); + }; Ok(Self { common: WindowCommon { @@ -649,7 +628,7 @@ fn from_constraint_adjustment( ConstraintAdjustment::from_bits_retain(value.bits()) } -/// Constructing a popup failed: dismissed by OS +/// A request on a popup failed because the popup is gone or not yet configured. #[derive(Debug, Copy, Clone, PartialEq, Eq)] struct PopupError(&'static str); diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index 3380db4864..dab87b9ca8 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -17,6 +17,7 @@ use sctk::seat::SeatState; use sctk::seat::pointer::ThemedPointer; use sctk::shell::WaylandSurface; use sctk::shell::xdg::XdgShell; +use sctk::shell::xdg::dialog::DialogHandler; use sctk::shell::xdg::popup::{Popup as XdgPopup, PopupConfigure, PopupHandler}; use sctk::shell::xdg::window::{Window, WindowConfigure, WindowHandler}; use sctk::shm::slot::SlotPool; @@ -292,30 +293,9 @@ impl WinitState { updates[pos].close_window = true; } -} - -impl ShmHandler for WinitState { - fn shm_state(&mut self) -> &mut Shm { - &mut self.shm - } -} - -impl WindowHandler for WinitState { - fn request_close(&mut self, _: &Connection, _: &QueueHandle, window: &Window) { - let window_id = super::make_wid(window.wl_surface()); - Self::queue_close(&mut self.window_compositor_updates, window_id); - } - - fn configure( - &mut self, - _: &Connection, - _: &QueueHandle, - window: &Window, - configure: WindowConfigure, - _serial: u32, - ) { - let window_id = super::make_wid(window.wl_surface()); + /// Handle an `xdg_toplevel` configure event, shared by `Window` and `Dialog` + fn configure_xdg_toplevel(&mut self, window_id: WindowId, configure: WindowConfigure) { let index = if let Some(index) = self.window_compositor_updates.iter().position(|update| update.window_id == window_id) { @@ -333,10 +313,10 @@ impl WindowHandler for WinitState { .expect("got configure for dead window.") .lock() .unwrap() - .configure_window(configure, &self.shm, &self.subcompositor_state); + .configure_xdg_toplevel(configure, &self.shm, &self.subcompositor_state); - // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of the - // users, since it can break a lot of things, thus it'll ask users to redraw instead. + // NOTE: configure demands wl_surface::commit, however winit doesn't commit on behalf of + // the users, since it can break a lot of things, thus it'll ask users to redraw instead. self.window_requests .get_mut() .get(&window_id) @@ -349,6 +329,31 @@ impl WindowHandler for WinitState { } } +impl ShmHandler for WinitState { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl WindowHandler for WinitState { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, window: &Window) { + let window_id = super::make_wid(window.wl_surface()); + Self::queue_close(&mut self.window_compositor_updates, window_id); + } + + fn configure( + &mut self, + _: &Connection, + _: &QueueHandle, + window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + let window_id = super::make_wid(window.wl_surface()); + self.configure_xdg_toplevel(window_id, configure); + } +} + impl PopupHandler for WinitState { fn configure( &mut self, @@ -400,6 +405,29 @@ impl PopupHandler for WinitState { } } +impl DialogHandler for WinitState { + fn configure( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + dialog: &sctk::shell::xdg::dialog::Dialog, + configure: sctk::shell::xdg::window::WindowConfigure, + _serial: u32, + ) { + let window_id = super::make_wid(dialog.wl_surface()); + self.configure_xdg_toplevel(window_id, configure); + } + fn request_close( + &mut self, + _: &Connection, + _: &QueueHandle, + window: &sctk::shell::xdg::dialog::Dialog, + ) { + let window_id = super::make_wid(window.wl_surface()); + Self::queue_close(&mut self.window_compositor_updates, window_id); + } +} + impl OutputHandler for WinitState { fn output_state(&mut self) -> &mut OutputState { &mut self.output_state diff --git a/winit-wayland/src/window/common.rs b/winit-wayland/src/window/common.rs index 91c171cdb2..925cfb4aa3 100644 --- a/winit-wayland/src/window/common.rs +++ b/winit-wayland/src/window/common.rs @@ -134,7 +134,7 @@ impl WindowCommon { } /// Only meaningful for a top-level surface. - pub(super) fn set_resizable(&self, resizable: bool) { + pub(crate) fn set_resizable(&self, resizable: bool) { let Some(s) = self.state.upgrade() else { return }; if s.lock().unwrap().set_resizable(resizable) { // NOTE: Requires commit to be applied. @@ -143,7 +143,7 @@ impl WindowCommon { } /// Only meaningful for a top-level surface. - pub(super) fn is_resizable(&self) -> bool { + pub(crate) fn is_resizable(&self) -> bool { let Some(s) = self.state.upgrade() else { return false; }; @@ -166,23 +166,23 @@ impl WindowCommon { /// Only meaningful for a top-level surface. #[inline] - pub(super) fn set_decorations(&self, decorate: bool) { + pub(crate) fn set_decorations(&self, decorate: bool) { let Some(s) = self.state.upgrade() else { return }; s.lock().unwrap().set_decorate(decorate) } /// Only meaningful for a top-level surface. #[inline] - pub(super) fn is_decorated(&self) -> Option { + pub(crate) fn is_decorated(&self) -> Option { let s = self.state.upgrade()?; Some(s.lock().unwrap().is_decorated()) } /// Only meaningful for a top-level surface. - pub(super) fn set_window_level(&self, _level: WindowLevel) {} + pub(crate) fn set_window_level(&self, _level: WindowLevel) {} /// Only meaningful for a top-level surface. - pub(super) fn set_window_icon(&self, window_icon: Option) { + pub(crate) fn set_window_icon(&self, window_icon: Option) { let Some(s) = self.state.upgrade() else { return }; s.lock().unwrap().set_window_icon(window_icon) } @@ -222,13 +222,13 @@ impl WindowCommon { } /// Only meaningful for a top-level surface. - pub(super) fn set_theme(&self, theme: Option) { + pub(crate) fn set_theme(&self, theme: Option) { let Some(s) = self.state.upgrade() else { return }; s.lock().unwrap().set_theme(theme) } /// Only meaningful for a top-level surface. - pub(super) fn theme(&self) -> Option { + pub(crate) fn theme(&self) -> Option { let s = self.state.upgrade()?; s.lock().unwrap().theme() } @@ -267,12 +267,12 @@ impl WindowCommon { } /// Only meaningful for a top-level surface. - pub(super) fn drag_window(&self) -> Result<(), RequestError> { + pub(crate) fn drag_window(&self) -> Result<(), RequestError> { self.state.upgrade().ok_or(os_error!(ResolveStateError))?.lock().unwrap().drag_window() } /// Only meaningful for a top-level surface. - pub(super) fn drag_resize_window( + pub(crate) fn drag_resize_window( &self, direction: ResizeDirection, ) -> Result<(), RequestError> { @@ -285,33 +285,33 @@ impl WindowCommon { } /// Only meaningful for a top-level surface. - pub(super) fn set_maximized(&self, maximized: bool) { + pub(crate) fn set_maximized(&self, maximized: bool) { let Some(s) = self.state.upgrade() else { return }; let s = s.lock().unwrap(); s.set_maximized(maximized); } /// Only meaningful for a top-level surface. - pub(super) fn is_maximized(&self) -> bool { + pub(crate) fn is_maximized(&self) -> bool { let Some(s) = self.state.upgrade() else { return false }; let s = s.lock().unwrap(); s.is_maximized() } /// Only meaningful for a top-level surface. - pub(super) fn fullscreen(&self) -> Option { + pub(crate) fn fullscreen(&self) -> Option { self.state.upgrade()?.lock().unwrap().fullscreen() } /// Only meaningful for a top-level surface. - pub(super) fn set_fullscreen(&self, fullscreen: Option) { + pub(crate) fn set_fullscreen(&self, fullscreen: Option) { let Some(s) = self.state.upgrade() else { return }; let s = s.lock().unwrap(); s.set_fullscreen(fullscreen) } /// Only meaningful for a top-level surface. - pub(super) fn show_window_menu(&self, position: Position) { + pub(crate) fn show_window_menu(&self, position: Position) { let Some(s) = self.state.upgrade() else { return }; let s = s.lock().unwrap(); let position = position.to_logical(s.scale_factor()); diff --git a/winit-wayland/src/window/mod.rs b/winit-wayland/src/window/mod.rs index fde91dfaa2..ab2105e969 100644 --- a/winit-wayland/src/window/mod.rs +++ b/winit-wayland/src/window/mod.rs @@ -1,5 +1,6 @@ //! The Wayland window. +use core::fmt; use std::ffi::c_void; use std::ptr::NonNull; use std::sync::atomic::AtomicBool; @@ -24,6 +25,7 @@ use winit_core::window::{ use super::ActiveEventLoop; use super::types::xdg_activation::XdgActivationTokenData; +use crate::state::WinitState; use crate::window::common::WindowCommon; use crate::{WindowAttributesWayland, output}; pub(crate) mod state; @@ -42,7 +44,7 @@ pub struct Window { /// Keeps the window's state alive for as long as this handle exists, independently of /// whatever the event loop's `windows` map does with its own reference. /// - /// Unlike `Popup`, which only ever holds a `Weak` (via `common.state`) so that a + /// Unlike `Dialog`/`Popup`, which only ever hold a `Weak` (via `common.state`) so that a /// compositor-initiated destroy can drop the state out from under a still-live handle, a /// top-level `Window` has no equivalent unilateral server-side destruction to react to -- /// it can only be closed by the application dropping this handle. This field is never read; @@ -53,6 +55,69 @@ pub struct Window { common: WindowCommon, } +/// Registers `window_state` with the event loop, waits for its initial configure, and wakes the +/// event loop for the first redraw. +/// +/// If `dismissed_error` is set, the wait loop bails out early with that message when the +/// compositor closes the surface before ever sending a configure +pub(crate) fn finish_window_setup( + event_loop_window_target: &ActiveEventLoop, + state: &mut WinitState, + surface: &WlSurface, + window_state: &Arc>, + dismissed_error: Option<&'static str>, +) -> Result<(WindowId, Arc), CreateWindowError> { + let window_id = super::make_wid(surface); + state.windows.get_mut().insert(window_id, window_state.clone()); + + let window_requests = Arc::new(WindowRequests { + redraw_requested: AtomicBool::new(true), + closed: AtomicBool::new(false), + }); + state.window_requests.get_mut().insert(window_id, window_requests.clone()); + + let mut wayland_source = event_loop_window_target.wayland_dispatcher.as_source_mut(); + let event_queue = wayland_source.queue(); + + // Do a roundtrip. + event_queue.roundtrip(state).map_err(|err| os_error!(err))?; + + // XXX Wait for the initial configure to arrive. + while !window_state.lock().unwrap().is_configured() { + event_queue.blocking_dispatch(state).map_err(|err| os_error!(err))?; + + // The compositor may dismiss the surface before it ever sends a configure. Detect + // that and bail out instead of looping forever. + if let Some(message) = dismissed_error { + if state + .window_compositor_updates + .iter() + .any(|u| u.window_id == window_id && u.close_window) + { + return Err(os_error!(DismissedError(message)).into()); + } + } + } + + // Wake-up event loop, so it'll send initial redraw requested. + event_loop_window_target.event_loop_awakener.ping(); + + Ok((window_id, window_requests)) +} + +/// Constructing a window/popup/dialog failed: the compositor dismissed the surface before it +/// ever sent an initial configure. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct DismissedError(&'static str); + +impl fmt::Display for DismissedError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for DismissedError {} + impl Window { pub(crate) fn new( event_loop_window_target: &ActiveEventLoop, @@ -173,33 +238,19 @@ impl Window { // Add the window and window requests into the state. let window_state = Arc::new(Mutex::new(window_state)); - let window_id = super::make_wid(&surface); - state.windows.get_mut().insert(window_id, window_state.clone()); - - let window_requests = WindowRequests { - redraw_requested: AtomicBool::new(true), - closed: AtomicBool::new(false), - }; - let window_requests = Arc::new(window_requests); - state.window_requests.get_mut().insert(window_id, window_requests.clone()); // Setup the event sync to insert `WindowEvents` right from the window. let window_events_sink = state.window_events_sink.clone(); - let mut wayland_source = event_loop_window_target.wayland_dispatcher.as_source_mut(); - let event_queue = wayland_source.queue(); - - // Do a roundtrip. - event_queue.roundtrip(&mut state).map_err(|err| os_error!(err))?; - - // XXX Wait for the initial configure to arrive. - while !window_state.lock().unwrap().is_configured() { - event_queue.blocking_dispatch(&mut state).map_err(|err| os_error!(err))?; - } + let (window_id, window_requests) = finish_window_setup( + event_loop_window_target, + &mut state, + &surface, + &window_state, + None, + )?; - // Wake-up event loop, so it'll send initial redraw requested. let event_loop_awakener = event_loop_window_target.event_loop_awakener.clone(); - event_loop_awakener.ping(); let state_weak = Arc::downgrade(&window_state); diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 78f2de75cd..2beb587e08 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -8,7 +8,6 @@ use foldhash::HashSet; use sctk::compositor::{CompositorState, Region, SurfaceData}; use sctk::globals::GlobalData; use sctk::reexports::client::backend::ObjectId; -use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_shm::WlShm; use sctk::reexports::client::{Proxy, QueueHandle}; use sctk::reexports::csd_frame::DecorationsFrame; @@ -22,6 +21,7 @@ use sctk::shell::xdg::XdgSurface; use sctk::shell::xdg::window::WindowConfigure; use sctk::shm::slot::SlotPool; use tracing::{info, warn}; +use wayland_client::protocol::wl_seat::WlSeat; use wayland_protocols::xdg::shell::client::xdg_toplevel; use wayland_protocols::xdg::toplevel_icon::v1::client::xdg_toplevel_icon_manager_v1::XdgToplevelIconManagerV1; use winit_core::error::{NotSupportedError, RequestError}; @@ -309,6 +309,7 @@ impl WindowState { pub fn drag_resize_window(&self, direction: ResizeDirection) -> Result<(), RequestError> { let xdg_toplevel = match &self.window { WindowType::Window { window, .. } => window.xdg_toplevel(), + WindowType::Dialog { dialog, .. } => dialog.xdg_toplevel(), WindowType::Popup { .. } => { return Err(RequestError::NotSupported(NotSupportedError::new( "drag_resize_window is not supported for WindowType::Popup", @@ -334,6 +335,7 @@ impl WindowState { pub fn drag_window(&self) -> Result<(), RequestError> { let xdg_toplevel = match &self.window { WindowType::Window { window, .. } => window.xdg_toplevel(), + WindowType::Dialog { dialog, .. } => dialog.xdg_toplevel(), WindowType::Popup { .. } => { return Err(RequestError::NotSupported(NotSupportedError::new( "drag_window is not supported for WindowType::Popup", @@ -437,6 +439,11 @@ impl WindowState { self.resize(surface_size.to_logical(self.scale_factor())) } }, + WindowType::Dialog { last_configure, .. } => { + if last_configure.as_ref().map(Self::is_stateless).unwrap_or(true) { + self.resize(surface_size.to_logical(self.scale_factor())) + } + }, WindowType::Popup { popup, xdg_positioner, .. } => { let size = surface_size.to_logical(self.scale_factor()); xdg_positioner.set_size(size.width, size.height); @@ -454,10 +461,14 @@ impl WindowState { self.size = surface_size; // Update the stateless size. - if let WindowType::Window { last_configure, .. } = &mut self.window { - if let Some(true) = last_configure.as_ref().map(Self::is_stateless) { - self.stateless_size = surface_size; - } + match &mut self.window { + WindowType::Window { last_configure, .. } + | WindowType::Dialog { last_configure, .. } => { + if let Some(true) = last_configure.as_ref().map(Self::is_stateless) { + self.stateless_size = surface_size; + } + }, + _ => (), } // Update the inner frame. @@ -508,7 +519,8 @@ impl WindowState { pub(crate) fn fullscreen(&self) -> Option { let is_fullscreen = match &self.window { - WindowType::Window { last_configure, .. } => last_configure + WindowType::Window { last_configure, .. } + | WindowType::Dialog { last_configure, .. } => last_configure .as_ref() .map(|last_configure| last_configure.is_fullscreen()) .unwrap_or_default(), @@ -545,6 +557,7 @@ impl WindowState { pub(crate) fn is_maximized(&self) -> bool { let last_configure = match &self.window { WindowType::Window { last_configure, .. } => last_configure, + WindowType::Dialog { last_configure, .. } => last_configure, WindowType::Popup { .. } => return false, }; last_configure diff --git a/winit-wayland/src/window/state/configure.rs b/winit-wayland/src/window/state/configure.rs index 61d4593944..1fe2a51cca 100644 --- a/winit-wayland/src/window/state/configure.rs +++ b/winit-wayland/src/window/state/configure.rs @@ -56,8 +56,8 @@ impl WindowState { /// Creates (or drops) the CSD frame per `configure`'s decoration mode, and computes the /// surface size to apply, accounting for borders, configure bounds, and resize increments. /// - /// Shared between `configure_window` and `configure_dialog`, since both configure an - /// `xdg_toplevel`-based surface from the same [`WindowConfigure`] event shape. + /// Used by [`Self::configure_xdg_toplevel`] to configure the `xdg_toplevel`-based `Window` + /// and `Dialog` surfaces from the same [`WindowConfigure`] event shape. fn configure_frame_and_size( &mut self, configure: &WindowConfigure, @@ -178,7 +178,12 @@ impl WindowState { new_size } - pub fn configure_window( + /// Update the last-received configure and resize if necessary. + /// + /// Used for both `Window` and `Dialog`, since both are backed by an `xdg_toplevel` and + /// receive the same [`WindowConfigure`] event shape (unlike `Popup`, handled by + /// [`Self::configure_popup`]). + pub fn configure_xdg_toplevel( &mut self, configure: WindowConfigure, shm: &Shm, @@ -187,33 +192,37 @@ impl WindowState { let new_size = self.configure_frame_and_size(&configure, shm, subcompositor); let new_state = configure.state; - if let WindowType::Window { last_configure, .. } = &mut self.window { - let old_state = last_configure.as_ref().map(|configure| configure.state); - - let state_change_requires_resize = old_state - .map(|old_state| { - !old_state - .symmetric_difference(new_state) - .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) - .is_empty() - }) - // NOTE: `None` is present for the initial configure, thus we must always resize. - .unwrap_or(true); - - // NOTE: Set the configure before doing a resize, since we query it during it. - *last_configure = Some(configure); + let last_configure = match &mut self.window { + WindowType::Window { last_configure, .. } + | WindowType::Dialog { last_configure, .. } => last_configure, + WindowType::Popup { .. } => { + tracing::error!( + "configure_xdg_toplevel called for a `Popup`. This should never happen, \ + because popups are configured through `configure_popup`" + ); + return false; + }, + }; - if state_change_requires_resize || new_size != self.surface_size() { - self.resize(new_size); - true - } else { - false - } + let old_state = last_configure.as_ref().map(|configure| configure.state); + + let state_change_requires_resize = old_state + .map(|old_state| { + !old_state + .symmetric_difference(new_state) + .difference(XdgWindowState::ACTIVATED | XdgWindowState::SUSPENDED) + .is_empty() + }) + // NOTE: `None` is present for the initial configure, thus we must always resize. + .unwrap_or(true); + + // NOTE: Set the configure before doing a resize, since we query it during it. + *last_configure = Some(configure); + + if state_change_requires_resize || new_size != self.surface_size() { + self.resize(new_size); + true } else { - tracing::error!( - "configure_window called for window type unequal of `Window`. This should never \ - happen, because we start configuring with a `Window`" - ); false } } diff --git a/winit-wayland/src/window/state/frame.rs b/winit-wayland/src/window/state/frame.rs index e68ebc8983..1af64701f7 100644 --- a/winit-wayland/src/window/state/frame.rs +++ b/winit-wayland/src/window/state/frame.rs @@ -129,7 +129,8 @@ impl WindowState { #[inline] pub fn is_decorated(&mut self) -> bool { match &mut self.window { - WindowType::Window { last_configure, .. } => { + WindowType::Window { last_configure, .. } + | WindowType::Dialog { last_configure, .. } => { let csd = last_configure .as_ref() .map(|configure| configure.decoration_mode == DecorationMode::Client) @@ -166,6 +167,12 @@ impl WindowState { fn request_decoration_mode(&self, mode: Option) { match &self.window { WindowType::Window { window, .. } => window.request_decoration_mode(mode), + WindowType::Dialog { dialog: _dialog, .. } => { + // TODO: Enable once a new sctk release was created with the following + // PR included + // https://github.com/Smithay/client-toolkit/pull/540 + // dialog.request_decoration_mode(mode) + }, WindowType::Popup { .. } => {}, } } @@ -180,7 +187,8 @@ impl WindowState { self.decorate = decorate; let last_configure = match &self.window { - WindowType::Window { last_configure, .. } => last_configure, + WindowType::Window { last_configure, .. } + | WindowType::Dialog { last_configure, .. } => last_configure, WindowType::Popup { .. } => return, // Popup does not have any decoration }; diff --git a/winit-wayland/src/window/state/window_type.rs b/winit-wayland/src/window/state/window_type.rs index da2e9bf219..0a1050f0d9 100644 --- a/winit-wayland/src/window/state/window_type.rs +++ b/winit-wayland/src/window/state/window_type.rs @@ -1,5 +1,6 @@ use dpi::LogicalPosition; use sctk::shell::WaylandSurface; +use sctk::shell::xdg::dialog::Dialog; use sctk::shell::xdg::popup::{Popup, PopupConfigure}; use sctk::shell::xdg::window::{Window, WindowConfigure}; use sctk::shell::xdg::{XdgPositioner, XdgSurface}; @@ -21,6 +22,10 @@ pub enum WindowType { positioner: WindowPositioner, }, + Dialog { + dialog: Dialog, + last_configure: Option, + }, } impl WindowType { @@ -28,12 +33,14 @@ impl WindowType { match self { Self::Window { last_configure, .. } => last_configure.is_some(), Self::Popup { last_configure, .. } => last_configure.is_some(), + Self::Dialog { last_configure, .. } => last_configure.is_some(), } } pub fn xdg_toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> { match self { WindowType::Window { window, .. } => Some(window.xdg_toplevel()), + WindowType::Dialog { dialog, .. } => Some(dialog.xdg_toplevel()), WindowType::Popup { .. } => None, } } @@ -44,6 +51,7 @@ impl WaylandSurface for WindowType { match self { Self::Window { window, .. } => window.wl_surface(), Self::Popup { popup, .. } => popup.wl_surface(), + Self::Dialog { dialog, .. } => dialog.wl_surface(), } } } @@ -53,6 +61,7 @@ impl XdgSurface for WindowType { match self { Self::Window { window, .. } => window.xdg_surface(), Self::Popup { popup, .. } => popup.xdg_surface(), + Self::Dialog { dialog, .. } => dialog.xdg_surface(), } } } diff --git a/winit-web/src/window.rs b/winit-web/src/window.rs index bb41150045..6eaec1d94e 100644 --- a/winit-web/src/window.rs +++ b/winit-web/src/window.rs @@ -49,6 +49,7 @@ impl Window { match attr.window_type() { WindowType::Window => (), WindowType::Popup => return Err(CreateWindowError::PopupNotSupported), + WindowType::Dialog => return Err(CreateWindowError::DialogNotSupported), _ => panic!("Unknown WindowType"), } diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 5fed2d0749..c480af6f32 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -1100,6 +1100,44 @@ unsafe fn lose_active_focus(window: HWND, userdata: &WindowData) { userdata.send_window_event(window, Focused(false)); } +/// When a modal dialog is moved, moves its (disabled) owner by the same delta, so the dialog keeps +/// its position relative to the owner instead of the other way around. +unsafe fn move_modal_owner(window: HWND, userdata: &WindowData, x: i32, y: i32) { + use windows_sys::Win32::UI::WindowsAndMessaging::{GetWindowRect, IsIconic, IsZoomed}; + + let (owner, previous) = { + let mut state = userdata.window_state_lock(); + let Some(owner) = state.modal_owner else { return }; + // Minimized windows are moved off-screen, don't drag the owner along. + if unsafe { IsIconic(window) } != 0 { + return; + } + (owner as HWND, state.last_outer_position.replace((x, y))) + }; + + let Some((previous_x, previous_y)) = previous else { return }; + let (dx, dy) = (x - previous_x, y - previous_y); + if (dx, dy) == (0, 0) || unsafe { IsZoomed(owner) } != 0 { + return; + } + + let mut rect = unsafe { std::mem::zeroed() }; + if unsafe { GetWindowRect(owner, &mut rect) } == 0 { + return; + } + unsafe { + SetWindowPos( + owner, + std::ptr::null_mut(), + rect.left + dx, + rect.top + dy, + 0, + 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE, + ); + } +} + /// Repositions any anchored windows owned by `parent`. Win32 does not reposition owned windows /// when their owner moves (unlike Wayland subsurfaces or X11's override-redirect popups), so this /// has to be done manually whenever `parent` receives a `WM_WINDOWPOSCHANGED` that moved it. @@ -1472,6 +1510,7 @@ unsafe fn public_window_callback_inner( unsafe { PhysicalPosition::new((*windowpos).x, (*windowpos).y) }; userdata.send_window_event(window, Moved(physical_position)); + unsafe { move_modal_owner(window, userdata, (*windowpos).x, (*windowpos).y) }; unsafe { reposition_owned_windows(window) }; } diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index 0bac9696a1..579671080e 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -36,18 +36,18 @@ use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ }; use windows_sys::Win32::UI::Input::Touch::{RegisterTouchWindow, TWF_WANTPALM}; use windows_sys::Win32::UI::WindowsAndMessaging::{ - CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, EnableMenuItem, FLASHW_ALL, - FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, FLASHWINFO, FlashWindowEx, GWLP_HINSTANCE, - GetClientRect, GetCursorPos, GetForegroundWindow, GetParent, GetSystemMenu, GetSystemMetrics, - GetWindowPlacement, GetWindowTextLengthW, GetWindowTextW, HTBOTTOM, HTBOTTOMLEFT, - HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsWindowVisible, - LoadCursorW, MENU_ITEM_STATE, MF_BYCOMMAND, MFS_DISABLED, MFS_ENABLED, NID_READY, PM_NOREMOVE, - PeekMessageW, PostMessageW, RegisterClassExW, SC_CLOSE, SC_MAXIMIZE, SC_MINIMIZE, SC_MOVE, - SC_RESTORE, SC_SIZE, SM_DIGITIZER, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOSIZE, - SWP_NOZORDER, SendMessageW, SetCursor, SetCursorPos, SetForegroundWindow, SetMenuDefaultItem, - SetWindowDisplayAffinity, SetWindowPlacement, SetWindowPos, SetWindowTextW, TPM_LEFTALIGN, - TPM_RETURNCMD, TrackPopupMenu, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, WM_NCLBUTTONDOWN, WM_SETICON, - WM_SYSCOMMAND, WNDCLASSEXW, + AdjustWindowRectEx, CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, CreateWindowExW, EnableMenuItem, + FLASHW_ALL, FLASHW_STOP, FLASHW_TIMERNOFG, FLASHW_TRAY, FLASHWINFO, FlashWindowEx, + GWLP_HINSTANCE, GetClientRect, GetCursorPos, GetForegroundWindow, GetParent, GetSystemMenu, + GetSystemMetrics, GetWindowPlacement, GetWindowTextLengthW, GetWindowTextW, HTBOTTOM, + HTBOTTOMLEFT, HTBOTTOMRIGHT, HTCAPTION, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, + IsWindowVisible, LoadCursorW, MENU_ITEM_STATE, MF_BYCOMMAND, MFS_DISABLED, MFS_ENABLED, + NID_READY, PM_NOREMOVE, PeekMessageW, PostMessageW, RegisterClassExW, SC_CLOSE, SC_MAXIMIZE, + SC_MINIMIZE, SC_MOVE, SC_RESTORE, SC_SIZE, SM_DIGITIZER, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, + SWP_NOSIZE, SWP_NOZORDER, SendMessageW, SetCursor, SetCursorPos, SetForegroundWindow, + SetMenuDefaultItem, SetWindowDisplayAffinity, SetWindowPlacement, SetWindowPos, SetWindowTextW, + TPM_LEFTALIGN, TPM_RETURNCMD, TrackPopupMenu, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, + WM_NCLBUTTONDOWN, WM_SETICON, WM_SYSCOMMAND, WNDCLASSEXW, }; use winit_common::positioner::place_window; use winit_core::cursor::Cursor; @@ -103,6 +103,10 @@ pub struct Window { // The events loop proxy. thread_executor: event_loop::EventLoopThreadExecutor, + + /// The owner window that was disabled to make this a modal [`WindowType::Dialog`], if any. + /// Re-enabled (and reactivated) when this window is dropped. + owner: Option, } impl Window { @@ -452,6 +456,15 @@ impl Drop for Window { self.set_fullscreen(None); } + // Re-enable and reactivate the owner we disabled to make this a modal dialog, before + // destroying this window, matching the order Win32 modal dialogs are expected to close in. + if let Some(owner) = self.owner { + unsafe { + EnableWindow(owner.hwnd(), 1); + SetForegroundWindow(owner.hwnd()); + } + } + unsafe { // The window must be destroyed from the same thread that created it, so we send a // custom message to be handled by our callback to do the actual work. @@ -1241,6 +1254,22 @@ fn outer_size_of(hwnd: HWND) -> PhysicalSize { .unwrap() } +/// Returns the screen position that centers a window with the given outer `size` over `parent`, +/// clamped to the monitor's work area. +fn centered_over_parent(parent: HWND, size: PhysicalSize) -> Option<(i32, i32)> { + let parent_rect = util::WindowArea::Outer.get_rect(parent).ok()?; + + let mut x = parent_rect.left + ((parent_rect.right - parent_rect.left) - size.width as i32) / 2; + let mut y = parent_rect.top + ((parent_rect.bottom - parent_rect.top) - size.height as i32) / 2; + + if let Some((work_pos, work_size)) = monitor::current_monitor(parent).work_area() { + x = x.min(work_pos.x + work_size.width as i32 - size.width as i32).max(work_pos.x); + y = y.min(work_pos.y + work_size.height as i32 - size.height as i32).max(work_pos.y); + } + + Some((x, y)) +} + fn surface_size_of(hwnd: HWND) -> PhysicalSize { util::WindowArea::Inner .get_rect(hwnd) @@ -1432,10 +1461,28 @@ impl InitData<'_> { unsafe { ImeContext::set_ime_allowed(window, false) }; + // If this is a modal dialog, disable its owner window; it's re-enabled when this + // `Window` is dropped. + let is_modal = + matches!(self.attributes.window_type, WindowType::Dialog) && self.attributes.modal; + let owner = is_modal + .then(|| self.attributes.parent_window()) + .flatten() + .and_then(|handle| match handle { + rwh_06::RawWindowHandle::Win32(handle) => Some(handle.hwnd.get() as HWND), + _ => None, + }) + .map(|owner_hwnd| { + unsafe { EnableWindow(owner_hwnd, 0) }; + window_state.lock().unwrap().modal_owner = Some(owner_hwnd as isize); + SyncWindowHandle(owner_hwnd) + }); + Window { window: SyncWindowHandle(window), window_state, thread_executor: self.runner.create_thread_executor(), + owner, } } @@ -1559,7 +1606,9 @@ impl InitData<'_> { // }; // dbg!(DwmExtendFrameIntoClientArea(win.hwnd(), &margins as *const _)); - if let Some(position) = attributes.position { + // Dialogs are already positioned at creation, see `init`. + let is_dialog = attributes.window_type == WindowType::Dialog; + if let Some(position) = attributes.position.filter(|_| !is_dialog) { win.set_outer_position(position); } @@ -1594,15 +1643,14 @@ unsafe fn init( let class_name = util::encode_wide(&win_attributes.class_name); unsafe { register_window_class(&class_name) }; - let is_popup = match attributes.window_type { - WindowType::Window => false, - WindowType::Popup => true, - _ => panic!("Unknown WindowType"), - }; + let is_popup = matches!(attributes.window_type, WindowType::Popup); + let is_dialog = matches!(attributes.window_type, WindowType::Dialog); // Whether this window is positioned relative to its parent via the anchor/gravity/ // positioner system -- either because it's a `WindowType::Popup`, or because anchor - // attributes were explicitly set on a `WindowType::Window` (Windows supports both). - let anchored = is_popup || attributes.positioner.is_some(); + // attributes were explicitly set on a `WindowType::Window` (Windows supports both). Dialogs + // only use the parent's coordinate frame; positioner auto-placement stays opt-in (see + // `WindowState::anchored`). + let anchored = is_popup || is_dialog || attributes.positioner.is_some(); let mut window_flags = WindowFlags::empty(); window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations); window_flags.set(WindowFlags::POPUP, is_popup); @@ -1637,7 +1685,7 @@ unsafe fn init( let parent = match attributes.parent_window() { Some(rwh_06::RawWindowHandle::Win32(handle)) => { - if !is_popup { + if !is_popup && !is_dialog { window_flags.set(WindowFlags::CHILD, true); } if win_attributes.menu.is_some() { @@ -1650,6 +1698,9 @@ unsafe fn init( if is_popup { return Err(InvalidInput::new("Popup without a parent is not supported!").into()); } + if is_dialog { + return Err(InvalidInput::new("Dialog without a parent is not supported!").into()); + } fallback_parent() }, }; @@ -1657,17 +1708,42 @@ unsafe fn init( let menu = win_attributes.menu; let fullscreen = attributes.fullscreen.clone(); let maximized = attributes.maximized; + let (style, ex_style) = window_flags.to_window_styles(); + + // Dialogs are placed at creation, relative to their parent (an explicit position is relative to + // the parent's client area, otherwise the dialog is centered over the parent). This can't be + // done afterwards: the parent isn't yet queryable via `GetParent` during `WM_CREATE`, and a + // position set before the first show is overridden by the `CW_USEDEFAULT` cascade. + let initial_position = parent.filter(|_| is_dialog).and_then(|parent| { + let scale_factor = dpi_to_scale_factor(unsafe { hwnd_dpi(parent) }); + if let Some(position) = attributes.position { + let position = position.to_physical::(scale_factor); + let mut point = POINT { x: position.x, y: position.y }; + unsafe { ClientToScreen(parent, &mut point) }; + return Some((point.x, point.y)); + } + + let size = attributes.surface_size.unwrap_or_else(|| PhysicalSize::new(800, 600).into()); + let size = size.to_physical::(scale_factor); + let mut rect = + RECT { left: 0, top: 0, right: size.width as i32, bottom: size.height as i32 }; + unsafe { AdjustWindowRectEx(&mut rect, style, 0, ex_style) }; + let outer = + PhysicalSize::new((rect.right - rect.left) as u32, (rect.bottom - rect.top) as u32); + centered_over_parent(parent, outer) + }); + let (x, y) = initial_position.unwrap_or((CW_USEDEFAULT, CW_USEDEFAULT)); + let mut initdata = InitData { runner, attributes, win_attributes, window_flags, window: None }; - let (style, ex_style) = window_flags.to_window_styles(); let handle = unsafe { CreateWindowExW( ex_style, class_name.as_ptr(), title.as_ptr(), style, - CW_USEDEFAULT, - CW_USEDEFAULT, + x, + y, CW_USEDEFAULT, CW_USEDEFAULT, parent.unwrap_or(ptr::null_mut()), @@ -1690,6 +1766,12 @@ unsafe fn init( // that we *must* have populated the `InitData.window` field. let win = initdata.window.unwrap(); + if win.owner.is_some() { + if let Ok(rect) = util::WindowArea::Outer.get_rect(win.hwnd()) { + win.window_state_lock().last_outer_position = Some((rect.left, rect.top)); + } + } + // Need to set FULLSCREEN or MAXIMIZED after CreateWindowEx // This is because if the size is changed in WM_CREATE, the restored size will be stored in that // size. diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index 27b007846a..32b7e5554f 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -60,6 +60,9 @@ pub(crate) struct WindowState { /// attributes were set on a [`WindowType::Window`]. Stored here (rather than only on the /// `Window` struct) so it stays reachable from just an `hwnd` via `GWL_USERDATA` -- e.g. to /// reposition an anchored window when its parent moves. + /// + /// Intentionally not set for [`WindowType::Dialog`]: dialogs use the parent's coordinate frame + /// (`WindowFlags::ANCHORED`) but are not auto-placed or moved along with the parent. pub anchored: bool, /// The positioner state backing `anchored` placement, meaningful only when `anchored` is @@ -81,6 +84,11 @@ pub(crate) struct WindowState { pub skip_taskbar: bool, pub use_system_wheel_speed: bool, + + /// For a modal dialog: its disabled owner (an `HWND`) + pub modal_owner: Option, + /// The dialog's last seen outer position, used to compute the delta to move `modal_owner` by. + pub last_outer_position: Option<(i32, i32)>, } #[derive(Clone)] @@ -157,7 +165,8 @@ bitflags! { /// Whether this window is positioned relative to its parent via the anchor/gravity/ /// positioner system. Independent of `POPUP`, which only selects the OS window style -- - /// a `WindowType::Window` can be `ANCHORED` too. Used to pick the coordinate frame in + /// a `WindowType::Window` can be `ANCHORED` too. Also set for `WindowType::Dialog`, which + /// only uses the parent's coordinate frame. Used to pick the coordinate frame in /// `translate_outer_position`/`translate_outer_position_to_parent`. const ANCHORED = 1 << 23; @@ -224,6 +233,9 @@ impl WindowState { skip_taskbar: false, use_system_wheel_speed: true, + + modal_owner: None, + last_outer_position: None, } } diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 678ae74168..59d46cec66 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -74,6 +74,7 @@ impl Window { Ok(Window(window)) }, WindowType::Popup => Err(CreateWindowError::PopupNotSupported), + WindowType::Dialog => Err(CreateWindowError::DialogNotSupported), _ => panic!("Unknown WindowType"), } } diff --git a/winit/examples/dialog.rs b/winit/examples/dialog.rs new file mode 100644 index 0000000000..48b56ed309 --- /dev/null +++ b/winit/examples/dialog.rs @@ -0,0 +1,192 @@ +//! This example shows the capabilities of dialogs +//! press 'N' to create a non modal dialog and 'M' to create a modal dialog + +#[cfg(any(x11_platform, macos_platform, windows_platform, wayland_platform))] +#[allow(deprecated)] +fn main() -> Result<(), impl std::error::Error> { + use std::collections::HashMap; + + use softbuffer::{Context, Surface}; + use tracing::info; + use winit::application::ApplicationHandler; + use winit::dpi::{LogicalPosition, LogicalSize, Position}; + use winit::event::{ElementState, KeyEvent, WindowEvent}; + use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle}; + use winit::raw_window_handle::HasRawWindowHandle; + use winit::window::{Window, WindowAttributes, WindowId}; + + #[path = "util/fill.rs"] + mod fill; + + #[derive(Debug)] + struct WindowData { + surface: Surface>, + color: u32, + } + + impl WindowData { + fn new(context: &Context, window: Box, color: u32) -> Self { + let surface = Surface::new(context, window).unwrap(); + Self { surface, color } + } + } + + #[derive(Debug)] + struct Application { + parent_window_id: Option, + main_window: Option, + windows: HashMap, + dialogs: Vec, + context: Context, + } + + impl ApplicationHandler for Application { + fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { + let attributes = WindowAttributes::default() + .with_title("parent window") + .with_position(Position::Logical(LogicalPosition::new(0.0, 0.0))) + .with_surface_size(LogicalSize::new(600.0f32, 600.0f32)) + .with_decorations(true); + let window = event_loop.create_window(attributes).unwrap(); + self.parent_window_id = Some(window.id()); + + if self.main_window.is_none() { + self.main_window = Some(window.id()); + } + self.windows.insert(window.id(), WindowData::new(&self.context, window, 0xffbbbbbb)); + } + + fn window_event( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: winit::window::WindowId, + event: WindowEvent, + ) { + use winit::keyboard::{KeyCode, PhysicalKey}; + + match event { + WindowEvent::CloseRequested => { + self.windows.remove(&window_id); + self.dialogs.retain_mut(|id| id != &window_id); + if self.main_window.unwrap() == window_id || self.windows.is_empty() { + event_loop.exit(); + } + }, + WindowEvent::PointerEntered { .. } => { + // On x11, println when the cursor entered in a window even if the child window + // is created by some key inputs. + // the child windows are always placed at (0, 0) with size (200, 200) in the + // parent window, so we also can see this log when we move + // the cursor around (200, 200) in parent window. + info!("cursor entered in the window {window_id:?}"); + }, + WindowEvent::PointerMoved { .. } => {}, + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Released, + physical_key: PhysicalKey::Code(code), + .. + }, + .. + } => { + match code { + KeyCode::KeyN | KeyCode::KeyM => { + let window_id = if let Some(dialog_id) = self.dialogs.last() { + dialog_id + } else { + // The mainwindow must exist, otherwise the event_loop was ended + &self.main_window.unwrap() + }; + + // Add a new Dialog + let child_index = self.windows.len() - 1; + let child_color = + 0xff000000 + 3_u32.pow((child_index + 2).rem_euclid(16) as u32); + + let parent_window = self.windows.get(window_id).unwrap(); + let child_window = spawn_dialog( + parent_window.surface.window().as_ref(), + event_loop, + child_index, + matches!(code, KeyCode::KeyM), + ); + let child_id = child_window.id(); + self.dialogs.push(child_id); + self.windows.insert( + child_id, + WindowData::new(&self.context, child_window, child_color), + ); + }, + KeyCode::KeyD => { + // Delete + if let Some(l) = self.dialogs.pop() { + self.windows.remove(&l); + } + + // // When deleting the first, it should not lead to a wayland protocol + // // error + // if let Some(l) = self.dialogs.first() { + // self.windows.remove(&l); + // } + }, + _ => (), + } + }, + WindowEvent::RedrawRequested => { + if let Some(window) = self.windows.get_mut(&window_id) { + if window_id == self.parent_window_id.unwrap() { + fill::fill(&mut window.surface); + } else { + fill::fill_with_color(&mut window.surface, window.color); + } + } + }, + _ => (), + } + } + } + + fn spawn_dialog( + parent: &dyn Window, + event_loop: &dyn ActiveEventLoop, + _child_count: usize, + modal: bool, + ) -> Box { + use winit::window::WindowType; + + let parent = parent.raw_window_handle().unwrap(); + + let mut window_attributes = WindowAttributes::default() + .with_title("child window") + .with_surface_size(LogicalSize::new(300.0f32, 300.0)) + .with_decorations(true) + .with_visible(true) + .with_active(true) + .with_window_type(WindowType::Dialog) + .with_modal(modal); + + // `with_parent_window` is unsafe. Parent window must be a valid window. + window_attributes = unsafe { window_attributes.with_parent_window(Some(parent)) }; + + event_loop.create_window(window_attributes).unwrap() + } + + let event_loop = EventLoop::new().unwrap(); + let context = Context::new(event_loop.owned_display_handle()).unwrap(); + event_loop.run_app(Application { + context, + parent_window_id: None, + windows: HashMap::new(), + dialogs: Vec::default(), + main_window: None, + }) +} + +#[cfg(not(any(x11_platform, macos_platform, windows_platform, wayland_platform)))] +fn main() { + panic!( + "This example is supported only on wayland, x11, macOS, and Windows, with the `rwh_06` \ + feature enabled." + ); +} diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index a0e7d3a474..fcfa914c71 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -40,6 +40,10 @@ changelog entry. ## Unreleased +### Added + +- Add `WindowType::Dialog` and `WindowAttributes::with_modal()`, implemented on Wayland, Windows and macOS. + ### Fixed - On Windows, fix a crash occurring when trying to create a DXGI surface on a window that is both fullscreen and always on top.