Wayland, Windows, MacOS: Popup Implementation (#4543)

Implement proper decorationless popups by specifying the type of the child window with with_type()

With this commit, different kind of child windows can be created

-  Popups: Special windows without any decoration which can be positioned relative to the parent
-  Window: Normal window with a parent or not

The type can be specified during creation of the Window using the window_attributes and the with_type() function. As default a normal Window is used. If Popup is choosen a parent must be specified, otherwise the Popup creation fails with an Error returned by the new() function.

Related issues: #403 and #4256
This commit is contained in:
Martin Marmsoler
2026-07-27 15:43:51 +02:00
committed by GitHub
parent 1fe178b2a6
commit 9674d8ceef
22 changed files with 2035 additions and 368 deletions

View File

@@ -15,7 +15,7 @@ use winit_core::icon::Icon;
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle};
use winit_core::window::{
ImeCapabilities, ImeRequest, ImeRequestError, Theme, UserAttentionType, Window as CoreWindow,
WindowAttributes, WindowButtons, WindowId, WindowLevel,
WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType,
};
use super::event_loop::ActiveEventLoop;
@@ -26,6 +26,7 @@ pub(crate) struct Window {
window: MainThreadBound<Retained<NSWindow>>,
/// The window only keeps a weak reference to this, so we must keep it around here.
delegate: MainThreadBound<Retained<WindowDelegate>>,
window_type: WindowType,
}
impl Window {
@@ -34,12 +35,14 @@ impl Window {
attributes: WindowAttributes,
) -> Result<Self, RequestError> {
let mtm = window_target.mtm;
let window_type = attributes.window_type;
let delegate =
autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?;
window_target.app_state.register_window(&delegate, mtm);
Ok(Window {
window: MainThreadBound::new(delegate.window().retain(), mtm),
delegate: MainThreadBound::new(delegate, mtm),
window_type,
})
}
@@ -95,6 +98,10 @@ impl rwh_06::HasWindowHandle for Window {
}
impl CoreWindow for Window {
fn window_type(&self) -> WindowType {
self.window_type
}
fn id(&self) -> winit_core::window::WindowId {
self.maybe_wait_on_main(|delegate| delegate.id())
}

View File

@@ -52,7 +52,7 @@ use winit_core::icon::Icon;
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider};
use winit_core::window::{
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel,
UserAttentionType, WindowAttributes, WindowButtons, WindowId, WindowLevel, WindowType,
};
use super::app_state::AppState;
@@ -112,6 +112,7 @@ pub(crate) struct State {
is_simple_fullscreen: Cell<bool>,
saved_style: Cell<Option<NSWindowStyleMask>>,
is_borderless_game: Cell<bool>,
is_popup: Cell<bool>,
}
define_class!(
@@ -655,6 +656,7 @@ fn new_window(
app_state: &Rc<AppState>,
attrs: &WindowAttributes,
macos_attrs: &WindowAttributesMacOS,
is_popup: bool,
mtm: MainThreadMarker,
) -> Option<Retained<NSWindow>> {
autoreleasepool(|_| {
@@ -681,6 +683,10 @@ fn new_window(
None => NSSize::new(800.0, 600.0),
};
let position = match attrs.position {
// A popup's position is parent-relative; it's applied in `WindowDelegate::new`
// (after the delegate exists) via the shared translation in
// `set_outer_position`.
_ if is_popup => NSPoint::new(0.0, 0.0),
Some(position) => {
let position = position.to_logical(scale_factor);
flip_window_screen_coordinates(NSRect::new(
@@ -738,6 +744,8 @@ fn new_window(
// confusing issues with the window not being properly activated.
//
// Winit ensures this by not allowing access to `ActiveEventLoop` before handling events.
// Panels (including popups) are non-activating so they don't steal key focus
// from their parent (matching menu/combobox semantics).
let window: Retained<NSWindow> = if macos_attrs.panel {
masks |= NSWindowStyleMask::NonactivatingPanel;
@@ -821,7 +829,8 @@ fn new_window(
if !macos_attrs.has_shadow {
window.setHasShadow(false);
}
if attrs.position.is_none() {
// Popups are positioned relative to their parent in `WindowDelegate::new`.
if attrs.position.is_none() && !is_popup {
window.center();
}
@@ -884,13 +893,27 @@ impl WindowDelegate {
mut attrs: WindowAttributes,
mtm: MainThreadMarker,
) -> Result<Retained<Self>, RequestError> {
let macos_attrs = attrs
let mut macos_attrs = attrs
.platform
.take()
.and_then(|attrs| attrs.cast::<WindowAttributesMacOS>().ok())
.unwrap_or_default();
let window = new_window(app_state, &attrs, &macos_attrs, mtm)
let is_popup = matches!(attrs.window_type(), WindowType::Popup);
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`
// instead of needing dedicated branches.
attrs.decorations = false;
attrs.enabled_buttons = WindowButtons::empty();
// Unless grab_keyboard is requested, use a non-activating panel so the popup doesn't
// steal keyboard focus from the parent window.
if !attrs.active {
macos_attrs.panel = true;
}
}
let window = new_window(app_state, &attrs, &macos_attrs, is_popup, mtm)
.ok_or_else(|| os_error!("couldn't create `NSWindow`"))?;
match attrs.parent_window() {
@@ -909,6 +932,11 @@ impl WindowDelegate {
unsafe { parent.addChildWindow_ordered(&window, NSWindowOrderingMode::Above) };
},
Some(raw) => panic!("invalid raw window handle {raw:?} on macOS"),
None if is_popup => {
return Err(RequestError::NotSupported(NotSupportedError::new(
"a popup window requires a parent window",
)));
},
None => (),
}
@@ -946,6 +974,7 @@ impl WindowDelegate {
is_simple_fullscreen: Cell::new(false),
saved_style: Cell::new(None),
is_borderless_game: Cell::new(macos_attrs.borderless_game),
is_popup: Cell::new(is_popup),
});
let delegate: Retained<WindowDelegate> = unsafe { msg_send![super(delegate), init] };
@@ -992,6 +1021,14 @@ impl WindowDelegate {
delegate.set_window_level(attrs.window_level);
// The popup 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.
if is_popup {
let position = attrs.position.unwrap_or_else(|| LogicalPosition::new(0.0, 0.0).into());
delegate.set_outer_position(position);
}
delegate.set_cursor(attrs.cursor);
// Set fullscreen mode after we setup everything
@@ -1147,7 +1184,9 @@ impl WindowDelegate {
pub fn outer_position(&self) -> Result<PhysicalPosition<i32>, RequestError> {
let position = flip_window_screen_coordinates(self.window().frame());
Ok(LogicalPosition::new(position.x, position.y).to_physical(self.scale_factor()))
let position =
self.translate_popup_position_to_parent(LogicalPosition::new(position.x, position.y));
Ok(position.to_physical(self.scale_factor()))
}
pub fn surface_position(&self) -> PhysicalPosition<i32> {
@@ -1171,6 +1210,7 @@ impl WindowDelegate {
pub fn set_outer_position(&self, position: Position) {
let position = position.to_logical(self.scale_factor());
let position = self.translate_popup_position(position);
let point = flip_window_screen_coordinates(NSRect::new(
NSPoint::new(position.x, position.y),
self.window().frame().size,
@@ -1178,6 +1218,40 @@ impl WindowDelegate {
self.window().setFrameOrigin(point);
}
/// Popups receive their position relative to the top-left of the parent window's
/// content area (matching the Win32 and Wayland backends). macOS positions windows
/// in global screen coordinates, so add the parent content area's origin.
fn translate_popup_position(&self, position: LogicalPosition<f64>) -> LogicalPosition<f64> {
if !self.ivars().is_popup.get() {
return position;
}
let Some(parent) = self.window().parentWindow() else {
return position;
};
let parent_origin =
flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame()));
LogicalPosition::new(parent_origin.x + position.x, parent_origin.y + position.y)
}
/// Inverse of [`Self::translate_popup_position`]. Popups report their position
/// relative to the top-left of the parent window's content area (matching the
/// Win32 and Wayland backends), so subtract the parent content area's origin from
/// the global screen coordinates. Non-popup windows are returned unchanged.
fn translate_popup_position_to_parent(
&self,
position: LogicalPosition<f64>,
) -> LogicalPosition<f64> {
if !self.ivars().is_popup.get() {
return position;
}
let Some(parent) = self.window().parentWindow() else {
return position;
};
let parent_origin =
flip_window_screen_coordinates(parent.contentRectForFrameRect(parent.frame()));
LogicalPosition::new(position.x - parent_origin.x, position.y - parent_origin.y)
}
#[inline]
pub fn surface_size(&self) -> PhysicalSize<u32> {
self.view().surface_size()