Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions winit-android/src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}

Expand Down
1 change: 1 addition & 0 deletions winit-appkit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions winit-appkit/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
}
Expand Down
88 changes: 73 additions & 15 deletions winit-appkit/src/window_delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WindowPositioner>,
/// For a modal [`WindowType::Dialog`]: the parent window it is presented on as a sheet.
modal_parent: Option<Retained<NSWindow>>,
}

define_class!(
Expand Down Expand Up @@ -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`
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<WindowDelegate> = unsafe { msg_send![super(delegate), init] };

Expand Down Expand Up @@ -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);
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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::<WindowDelegate>() else { continue };
if child_delegate.ivars().anchored {
if child_delegate.ivars().auto_placed {
child_delegate.reposition();
}
}
Expand Down
3 changes: 3 additions & 0 deletions winit-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ impl From<NotSupportedError> 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.
Expand All @@ -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}")
},
Expand Down
51 changes: 47 additions & 4 deletions winit-core/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly it would make sense to put modal: bool under WindowType::Dialog since it is not applicable to any other window type.

(The same may apply to popup positioning information, but if so best leave that to another PR.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a design decision to keep the WindowType a simple type instead of a complex one with properties

}

/// The positioner state backing a window's anchor-based placement.
Expand Down Expand Up @@ -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<Position>,
pub resizable: bool,
pub enabled_buttons: WindowButtons,
Expand Down Expand Up @@ -178,6 +196,7 @@ pub struct WindowAttributes {
pub window_type: WindowType,
/// See [`WindowAttributes::with_positioner`].
pub positioner: Option<WindowPositioner>,
pub modal: bool,
}

impl WindowAttributes {
Expand Down Expand Up @@ -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
/// <https://docs.microsoft.com/en-us/windows/win32/winmsg/window-features#child-windows>
/// 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]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -593,6 +635,7 @@ impl Default for WindowAttributes {
blur: Default::default(),
window_type: Default::default(),
positioner: Default::default(),
modal: Default::default(),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions winit-orbital/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}

Expand Down
1 change: 1 addition & 0 deletions winit-uikit/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}

Expand Down
2 changes: 1 addition & 1 deletion winit-wayland/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
Loading
Loading