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

@@ -21,7 +21,7 @@ use windows_sys::Win32::Graphics::Dwm::{
use windows_sys::Win32::Graphics::Gdi::{
CDS_FULLSCREEN, ChangeDisplaySettingsExW, ClientToScreen, CreateRectRgn, DISP_CHANGE_BADFLAGS,
DISP_CHANGE_BADMODE, DISP_CHANGE_BADPARAM, DISP_CHANGE_FAILED, DISP_CHANGE_SUCCESSFUL,
DeleteObject, InvalidateRgn, RDW_INTERNALPAINT, RedrawWindow,
DeleteObject, InvalidateRgn, RDW_INTERNALPAINT, RedrawWindow, ScreenToClient,
};
use windows_sys::Win32::System::Com::{
CLSCTX_ALL, COINIT_APARTMENTTHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize,
@@ -36,7 +36,7 @@ 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, GetSystemMenu, GetSystemMetrics,
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,
@@ -48,13 +48,13 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
WM_SYSCOMMAND, WNDCLASSEXW,
};
use winit_core::cursor::Cursor;
use winit_core::error::RequestError;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::icon::{Icon, RgbaIcon};
use winit_core::monitor::{Fullscreen, MonitorHandle as CoreMonitorHandle, MonitorHandleProvider};
use winit_core::window::{
CursorGrabMode, ImeCapabilities, ImeRequest, ImeRequestError, ResizeDirection, Theme,
UserAttentionType, Window as CoreWindow, WindowAttributes, WindowButtons, WindowId,
WindowLevel,
WindowLevel, WindowType,
};
use crate::dark_mode::try_theme;
@@ -100,6 +100,9 @@ pub struct Window {
// The events loop proxy.
thread_executor: event_loop::EventLoopThreadExecutor,
/// The type of window of this
window_type: WindowType,
}
impl Window {
@@ -118,6 +121,49 @@ impl Window {
self.window_state.lock().unwrap()
}
// If we have a popup the position is relative to the parent window and not
// relative to the screen. Therefore we have to translate it from the parent
// coordinate system to the display coordinate system
fn translate_outer_position(&self, position: Position) -> PhysicalPosition<i32> {
let position = position.to_physical::<i32>(self.scale_factor());
let mut point = POINT { x: position.x, y: position.y };
let window_flags = self.window_state_lock().window_flags;
if window_flags.contains(WindowFlags::POPUP) && !window_flags.contains(WindowFlags::CHILD) {
let parent = unsafe { GetParent(self.hwnd()) };
if !parent.is_null() {
unsafe {
ClientToScreen(parent, &mut point);
}
}
}
PhysicalPosition::new(point.x, point.y)
}
// Inverse of `translate_outer_position`: if we have a popup the position is
// reported relative to the parent window instead of the screen. Therefore we
// translate it from the display coordinate system back to the parent
// coordinate system. Non-popup windows are left in screen coordinates.
fn translate_outer_position_to_parent(
&self,
position: PhysicalPosition<i32>,
) -> PhysicalPosition<i32> {
let mut point = POINT { x: position.x, y: position.y };
let window_flags = self.window_state_lock().window_flags;
if window_flags.contains(WindowFlags::POPUP) && !window_flags.contains(WindowFlags::CHILD) {
let parent = unsafe { GetParent(self.hwnd()) };
if !parent.is_null() {
unsafe {
ScreenToClient(parent, &mut point);
}
}
}
PhysicalPosition::new(point.x, point.y)
}
/// Returns the `hwnd` of this window.
pub fn hwnd(&self) -> HWND {
self.window.hwnd()
@@ -416,6 +462,10 @@ impl rwh_06::HasWindowHandle for Window {
}
impl CoreWindow for Window {
fn window_type(&self) -> WindowType {
self.window_type
}
fn set_title(&self, text: &str) {
let wide_text = util::encode_wide(text);
unsafe {
@@ -464,7 +514,10 @@ impl CoreWindow for Window {
fn outer_position(&self) -> Result<PhysicalPosition<i32>, RequestError> {
util::WindowArea::Outer
.get_rect(self.hwnd())
.map(|rect| Ok(PhysicalPosition::new(rect.left, rect.top)))
.map(|rect| {
Ok(self
.translate_outer_position_to_parent(PhysicalPosition::new(rect.left, rect.top)))
})
.expect(
"Unexpected GetWindowRect failure; please report this error to \
rust-windowing/winit",
@@ -483,7 +536,7 @@ impl CoreWindow for Window {
}
fn set_outer_position(&self, position: Position) {
let (x, y): (i32, i32) = position.to_physical::<i32>(self.scale_factor()).into();
let position = self.translate_outer_position(position);
let window_state = Arc::clone(&self.window_state);
let window = self.window;
@@ -498,8 +551,8 @@ impl CoreWindow for Window {
SetWindowPos(
self.hwnd(),
ptr::null_mut(),
x,
y,
position.x,
position.y,
0,
0,
SWP_ASYNCWINDOWPOS | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE,
@@ -1211,6 +1264,7 @@ impl InitData<'_> {
window: SyncWindowHandle(window),
window_state,
thread_executor: self.runner.create_thread_executor(),
window_type: self.attributes.window_type,
}
}
@@ -1369,8 +1423,10 @@ unsafe fn init(
let class_name = util::encode_wide(&win_attributes.class_name);
unsafe { register_window_class(&class_name) };
let is_popup = matches!(attributes.window_type, WindowType::Popup);
let mut window_flags = WindowFlags::empty();
window_flags.set(WindowFlags::MARKER_DECORATIONS, attributes.decorations);
window_flags.set(WindowFlags::POPUP, is_popup);
window_flags.set(WindowFlags::MARKER_UNDECORATED_SHADOW, win_attributes.decoration_shadow);
window_flags
.set(WindowFlags::ALWAYS_ON_TOP, attributes.window_level == WindowLevel::AlwaysOnTop);
@@ -1380,7 +1436,9 @@ unsafe fn init(
window_flags.set(WindowFlags::MARKER_ACTIVATE, attributes.active);
window_flags.set(WindowFlags::TRANSPARENT, attributes.transparent);
// WindowFlags::VISIBLE and MAXIMIZED are set down below after the window has been configured.
window_flags.set(WindowFlags::RESIZABLE, attributes.resizable);
// Popups are never resizable, matching the Wayland and macOS backends (and avoiding the
// thick `WS_SIZEBOX` resize frame).
window_flags.set(WindowFlags::RESIZABLE, attributes.resizable && !is_popup);
// Will be changed later using `window.set_enabled_buttons` but we need to set a default here
// so the diffing later can work.
window_flags.set(WindowFlags::CLOSABLE, true);
@@ -1399,14 +1457,23 @@ unsafe fn init(
let parent = match attributes.parent_window() {
Some(rwh_06::RawWindowHandle::Win32(handle)) => {
window_flags.set(WindowFlags::CHILD, true);
if !is_popup {
window_flags.set(WindowFlags::CHILD, true);
}
if win_attributes.menu.is_some() {
warn!("Setting a menu on a child window is unsupported");
}
Some(handle.hwnd.get() as HWND)
},
Some(raw) => unreachable!("Invalid raw window handle {raw:?} on Windows"),
None => fallback_parent(),
None => {
if is_popup {
return Err(RequestError::NotSupported(NotSupportedError::new(
"Popup without a parent is not supported!",
)));
}
fallback_parent()
},
};
let menu = win_attributes.menu;

View File

@@ -13,9 +13,9 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, SendMessageW, SetWindowLongW, SetWindowPos,
ShowWindow, WINDOW_EX_STYLE, WINDOW_STYLE, WINDOWPLACEMENT, WS_BORDER, WS_CAPTION, WS_CHILD,
WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED,
WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, WS_MAXIMIZE,
WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX,
WS_SYSMENU, WS_VISIBLE,
WS_EX_NOACTIVATE, WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT,
WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX,
WS_OVERLAPPEDWINDOW, WS_POPUP, WS_SIZEBOX, WS_SYSMENU, WS_VISIBLE,
};
use winit_core::icon::Icon;
use winit_core::keyboard::ModifiersState;
@@ -276,8 +276,18 @@ impl WindowFlags {
pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) {
// Required styles to properly support common window functionality like aero snap.
let mut style = WS_CAPTION | WS_BORDER | WS_CLIPSIBLINGS | WS_SYSMENU;
let mut style = WS_CLIPSIBLINGS;
let mut style_ex = WS_EX_WINDOWEDGE | WS_EX_ACCEPTFILES;
if self.contains(WindowFlags::POPUP) {
style |= WS_POPUP;
// Don't activate the popup (and thus don't deactivate the parent) when it is shown or
// clicked, unless the caller requested activation via `WindowAttributes::active`.
if !self.contains(WindowFlags::MARKER_ACTIVATE) {
style_ex |= WS_EX_NOACTIVATE;
}
} else {
style |= WS_CAPTION | WS_SYSMENU | WS_BORDER;
};
if self.contains(WindowFlags::RESIZABLE) {
style |= WS_SIZEBOX;
@@ -300,8 +310,8 @@ impl WindowFlags {
if self.contains(WindowFlags::NO_BACK_BUFFER) {
style_ex |= WS_EX_NOREDIRECTIONBITMAP;
}
if self.contains(WindowFlags::CHILD) {
style |= WS_CHILD; // This is incompatible with WS_POPUP if that gets added eventually.
if self.contains(WindowFlags::CHILD) && !self.contains(WindowFlags::POPUP) {
style |= WS_CHILD;
// Remove decorations window styles for child
if !self.contains(WindowFlags::MARKER_DECORATIONS) {
@@ -309,9 +319,6 @@ impl WindowFlags {
style_ex &= !WS_EX_WINDOWEDGE;
}
}
if self.contains(WindowFlags::POPUP) {
style |= WS_POPUP;
}
if self.contains(WindowFlags::MINIMIZED) {
style |= WS_MINIMIZE;
}