diff --git a/CHANGELOG.md b/CHANGELOG.md index c7bcd96c..ea246d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ And please only add new entries to the top of this list, right below the `# Unre # Unreleased +- **Breaking:** Removed `Window::set_always_on_top` and related APIs in favor of `Window::set_window_level`. +- On Windows, MacOS and X11, add always on bottom APIs. - On Windows, fix the value in `MouseButton::Other`. - On macOS, add `WindowExtMacOS::is_document_edited` and `WindowExtMacOS::set_document_edited` APIs. - **Breaking:** Removed `WindowBuilderExtIOS::with_root_view_class`; instead, you should use `[[view layer] addSublayer: ...]` to add an instance of the desired layer class (e.g. `CAEAGLLayer` or `CAMetalLayer`). See `vulkano-win` or `wgpu` for examples of this. diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 9dc12507..b999f6c9 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -9,7 +9,7 @@ fn main() { dpi::{PhysicalPosition, PhysicalSize, Position, Size}, event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::EventLoop, - window::{CursorGrabMode, CursorIcon, Fullscreen, WindowBuilder}, + window::{CursorGrabMode, CursorIcon, Fullscreen, WindowBuilder, WindowLevel}, }; const WINDOW_COUNT: usize = 3; @@ -66,7 +66,9 @@ fn main() { let state = !modifiers.shift(); use VirtualKeyCode::*; match key { - A => window.set_always_on_top(state), + Key1 => window.set_window_level(WindowLevel::AlwaysOnTop), + Key2 => window.set_window_level(WindowLevel::AlwaysOnBottom), + Key3 => window.set_window_level(WindowLevel::Normal), C => window.set_cursor_icon(match state { true => CursorIcon::Progress, false => CursorIcon::Default, diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 93c81506..4315f6a4 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -24,7 +24,7 @@ use crate::{ error, event::{self, StartCause, VirtualKeyCode}, event_loop::{self, ControlFlow, EventLoopWindowTarget as RootELW}, - window::{self, CursorGrabMode, Theme}, + window::{self, CursorGrabMode, Theme, WindowLevel}, }; fn ndk_keycode_to_virtualkeycode(keycode: Keycode) -> Option { @@ -981,7 +981,7 @@ impl Window { true } - pub fn set_always_on_top(&self, _always_on_top: bool) {} + pub fn set_window_level(&self, _level: WindowLevel) {} pub fn set_window_icon(&self, _window_icon: Option) {} diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 670ac899..b3d645d6 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -24,7 +24,7 @@ use crate::{ }, window::{ CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, - WindowId as RootWindowId, + WindowId as RootWindowId, WindowLevel, }, }; @@ -282,8 +282,8 @@ impl Inner { true } - pub fn set_always_on_top(&self, _always_on_top: bool) { - warn!("`Window::set_always_on_top` is ignored on iOS") + pub fn set_window_level(&self, _level: WindowLevel) { + warn!("`Window::set_window_level` is ignored on iOS") } pub fn set_window_icon(&self, _icon: Option) { @@ -395,9 +395,7 @@ impl Window { if window_attributes.max_inner_size.is_some() { warn!("`WindowAttributes::max_inner_size` is ignored on iOS"); } - if window_attributes.always_on_top { - warn!("`WindowAttributes::always_on_top` is unsupported on iOS"); - } + // TODO: transparency, visible unsafe { diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index 3ffba899..a7f718a3 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -39,7 +39,7 @@ use crate::{ ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW, }, icon::Icon, - window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowLevel}, }; pub(crate) use crate::icon::RgbaIcon as PlatformIcon; @@ -476,10 +476,10 @@ impl Window { } #[inline] - pub fn set_always_on_top(&self, _always_on_top: bool) { + pub fn set_window_level(&self, _level: WindowLevel) { match self { #[cfg(feature = "x11")] - Window::X(ref w) => w.set_always_on_top(_always_on_top), + Window::X(ref w) => w.set_window_level(_level), #[cfg(feature = "wayland")] Window::Wayland(_) => (), } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index e67e5623..6c6f28ba 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -20,7 +20,9 @@ use crate::{ Fullscreen, MonitorHandle as PlatformMonitorHandle, OsError, PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, - window::{CursorGrabMode, CursorIcon, Icon, Theme, UserAttentionType, WindowAttributes}, + window::{ + CursorGrabMode, CursorIcon, Icon, Theme, UserAttentionType, WindowAttributes, WindowLevel, + }, }; use super::{ @@ -490,11 +492,10 @@ impl UnownedWindow { shared_state.restore_position = Some((x, y)); } } - if window_attrs.always_on_top { - window - .set_always_on_top_inner(window_attrs.always_on_top) - .queue(); - } + + window + .set_window_level_inner(window_attrs.window_level) + .queue(); } // We never want to give the user a broken window, since by then, it's too late to handle. @@ -919,16 +920,25 @@ impl UnownedWindow { self.xconn.set_motif_hints(self.xwindow, &hints) } - fn set_always_on_top_inner(&self, always_on_top: bool) -> util::Flusher<'_> { - let above_atom = unsafe { self.xconn.get_atom_unchecked(b"_NET_WM_STATE_ABOVE\0") }; - self.set_netwm(always_on_top.into(), (above_atom as c_long, 0, 0, 0)) + fn toggle_atom(&self, atom_bytes: &[u8], enable: bool) -> util::Flusher<'_> { + let atom = unsafe { self.xconn.get_atom_unchecked(atom_bytes) }; + self.set_netwm(enable.into(), (atom as c_long, 0, 0, 0)) + } + + fn set_window_level_inner(&self, level: WindowLevel) -> util::Flusher<'_> { + self.toggle_atom(b"_NET_WM_STATE_ABOVE\0", level == WindowLevel::AlwaysOnTop) + .queue(); + self.toggle_atom( + b"_NET_WM_STATE_BELOW\0", + level == WindowLevel::AlwaysOnBottom, + ) } #[inline] - pub fn set_always_on_top(&self, always_on_top: bool) { - self.set_always_on_top_inner(always_on_top) + pub fn set_window_level(&self, level: WindowLevel) { + self.set_window_level_inner(level) .flush() - .expect("Failed to set always-on-top state"); + .expect("Failed to set window-level state"); } fn set_icon_inner(&self, icon: Icon) -> util::Flusher<'_> { diff --git a/src/platform_impl/macos/appkit/window.rs b/src/platform_impl/macos/appkit/window.rs index 1f34e435..7f859746 100644 --- a/src/platform_impl/macos/appkit/window.rs +++ b/src/platform_impl/macos/appkit/window.rs @@ -296,6 +296,8 @@ pub struct NSWindowLevel(pub NSInteger); #[allow(dead_code)] impl NSWindowLevel { + #[doc(alias = "BelowNormalWindowLevel")] + pub const BELOW_NORMAL: Self = Self((kCGNormalWindowLevel - 1) as _); #[doc(alias = "NSNormalWindowLevel")] pub const Normal: Self = Self(kCGNormalWindowLevel as _); #[doc(alias = "NSFloatingWindowLevel")] diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index c3375264..4a420108 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -30,7 +30,7 @@ use crate::{ }, window::{ CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, - WindowId as RootWindowId, + WindowId as RootWindowId, WindowLevel, }, }; use core_graphics::display::{CGDisplay, CGPoint}; @@ -333,10 +333,6 @@ impl WinitWindow { this.setMovableByWindowBackground(true); } - if attrs.always_on_top { - this.setLevel(NSWindowLevel::Floating); - } - if let Some(increments) = attrs.resize_increments { let increments = increments.to_logical(this.scale_factor()); let (w, h) = (increments.width, increments.height); @@ -394,6 +390,8 @@ impl WinitWindow { this.set_max_inner_size(Some(dim)); } + this.set_window_level(attrs.window_level); + // register for drag and drop operations. this.registerForDraggedTypes(&NSArray::from_slice(&[ unsafe { NSFilenamesPboardType }.copy() @@ -1019,11 +1017,11 @@ impl WinitWindow { } #[inline] - pub fn set_always_on_top(&self, always_on_top: bool) { - let level = if always_on_top { - NSWindowLevel::Floating - } else { - NSWindowLevel::Normal + pub fn set_window_level(&self, level: WindowLevel) { + let level = match level { + WindowLevel::AlwaysOnTop => NSWindowLevel::Floating, + WindowLevel::AlwaysOnBottom => NSWindowLevel::BELOW_NORMAL, + WindowLevel::Normal => NSWindowLevel::Normal, }; util::set_level_async(self, level); } diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 39b27881..75f53c71 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -4,6 +4,7 @@ use crate::event; use crate::icon::Icon; use crate::window::{ CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowId as RootWI, + WindowLevel, }; use raw_window_handle::{RawDisplayHandle, RawWindowHandle, WebDisplayHandle, WebWindowHandle}; @@ -307,7 +308,7 @@ impl Window { } #[inline] - pub fn set_always_on_top(&self, _always_on_top: bool) { + pub fn set_window_level(&self, _level: WindowLevel) { // Intentionally a no-op, no window ordering } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 88dd1a38..1e258a60 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -71,7 +71,7 @@ use crate::{ window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, Fullscreen, Parent, PlatformSpecificWindowBuilderAttributes, WindowId, }, - window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes}, + window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType, WindowAttributes, WindowLevel}, }; /// The Win32 implementation of the main `Window` object. @@ -605,14 +605,21 @@ impl Window { } #[inline] - pub fn set_always_on_top(&self, always_on_top: bool) { + pub fn set_window_level(&self, level: WindowLevel) { let window = self.window.clone(); let window_state = Arc::clone(&self.window_state); self.thread_executor.execute_in_thread(move || { let _ = &window; WindowState::set_window_flags(window_state.lock().unwrap(), window.0, |f| { - f.set(WindowFlags::ALWAYS_ON_TOP, always_on_top) + f.set( + WindowFlags::ALWAYS_ON_TOP, + level == WindowLevel::AlwaysOnTop, + ); + f.set( + WindowFlags::ALWAYS_ON_BOTTOM, + level == WindowLevel::AlwaysOnBottom, + ); }); }); } @@ -985,7 +992,14 @@ where WindowFlags::MARKER_UNDECORATED_SHADOW, pl_attribs.decoration_shadow, ); - window_flags.set(WindowFlags::ALWAYS_ON_TOP, attributes.always_on_top); + window_flags.set( + WindowFlags::ALWAYS_ON_TOP, + attributes.window_level == WindowLevel::AlwaysOnTop, + ); + window_flags.set( + WindowFlags::ALWAYS_ON_BOTTOM, + attributes.window_level == WindowLevel::AlwaysOnBottom, + ); window_flags.set( WindowFlags::NO_BACK_BUFFER, pl_attribs.no_redirection_bitmap, diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 1f7ac631..f36ed935 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -12,14 +12,14 @@ use windows_sys::Win32::{ Graphics::Gdi::InvalidateRgn, UI::WindowsAndMessaging::{ AdjustWindowRectEx, GetMenu, GetWindowLongW, SendMessageW, SetWindowLongW, SetWindowPos, - ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS, - SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOREPOSITION, SWP_NOSIZE, SWP_NOZORDER, - SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, WINDOWPLACEMENT, WINDOW_EX_STYLE, - WINDOW_STYLE, 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, + ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_BOTTOM, HWND_NOTOPMOST, HWND_TOPMOST, + SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOREPOSITION, + SWP_NOSIZE, SWP_NOZORDER, SW_HIDE, SW_MAXIMIZE, SW_MINIMIZE, SW_RESTORE, SW_SHOW, + WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, 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, }, }; @@ -76,38 +76,39 @@ bitflags! { } bitflags! { pub struct WindowFlags: u32 { - const RESIZABLE = 1 << 0; - const VISIBLE = 1 << 1; - const ON_TASKBAR = 1 << 2; - const ALWAYS_ON_TOP = 1 << 3; - const NO_BACK_BUFFER = 1 << 4; - const TRANSPARENT = 1 << 5; - const CHILD = 1 << 6; - const MAXIMIZED = 1 << 7; - const POPUP = 1 << 8; + const RESIZABLE = 1 << 0; + const VISIBLE = 1 << 1; + const ON_TASKBAR = 1 << 2; + const ALWAYS_ON_TOP = 1 << 3; + const ALWAYS_ON_BOTTOM = 1 << 4; + const NO_BACK_BUFFER = 1 << 5; + const TRANSPARENT = 1 << 6; + const CHILD = 1 << 7; + const MAXIMIZED = 1 << 8; + const POPUP = 1 << 9; /// Marker flag for fullscreen. Should always match `WindowState::fullscreen`, but is /// included here to make masking easier. - const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 9; - const MARKER_BORDERLESS_FULLSCREEN = 1 << 10; + const MARKER_EXCLUSIVE_FULLSCREEN = 1 << 10; + const MARKER_BORDERLESS_FULLSCREEN = 1 << 11; /// The `WM_SIZE` event contains some parameters that can effect the state of `WindowFlags`. /// In most cases, it's okay to let those parameters change the state. However, when we're /// running the `WindowFlags::apply_diff` function, we *don't* want those parameters to /// effect our stored state, because the purpose of `apply_diff` is to update the actual /// window's state to match our stored state. This controls whether to accept those changes. - const MARKER_RETAIN_STATE_ON_SIZE = 1 << 11; + const MARKER_RETAIN_STATE_ON_SIZE = 1 << 12; - const MARKER_IN_SIZE_MOVE = 1 << 12; + const MARKER_IN_SIZE_MOVE = 1 << 13; - const MINIMIZED = 1 << 13; + const MINIMIZED = 1 << 14; - const IGNORE_CURSOR_EVENT = 1 << 14; + const IGNORE_CURSOR_EVENT = 1 << 15; /// Fully decorated window (incl. caption, border and drop shadow). - const MARKER_DECORATIONS = 1 << 15; + const MARKER_DECORATIONS = 1 << 16; /// Drop shadow for undecorated windows. - const MARKER_UNDECORATED_SHADOW = 1 << 16; + const MARKER_UNDECORATED_SHADOW = 1 << 17; const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; } @@ -301,13 +302,18 @@ impl WindowFlags { } } - if diff.contains(WindowFlags::ALWAYS_ON_TOP) { + if diff.intersects(WindowFlags::ALWAYS_ON_TOP | WindowFlags::ALWAYS_ON_BOTTOM) { unsafe { SetWindowPos( window, - match new.contains(WindowFlags::ALWAYS_ON_TOP) { - true => HWND_TOPMOST, - false => HWND_NOTOPMOST, + match ( + new.contains(WindowFlags::ALWAYS_ON_TOP), + new.contains(WindowFlags::ALWAYS_ON_BOTTOM), + ) { + (true, false) => HWND_TOPMOST, + (false, false) => HWND_NOTOPMOST, + (false, true) => HWND_BOTTOM, + (true, true) => unreachable!(), }, 0, 0, diff --git a/src/window.rs b/src/window.rs index d5f505c8..442084ff 100644 --- a/src/window.rs +++ b/src/window.rs @@ -132,11 +132,11 @@ pub(crate) struct WindowAttributes { pub visible: bool, pub transparent: bool, pub decorations: bool, - pub always_on_top: bool, pub window_icon: Option, pub preferred_theme: Option, pub resize_increments: Option, pub content_protected: bool, + pub window_level: WindowLevel, } impl Default for WindowAttributes { @@ -154,7 +154,7 @@ impl Default for WindowAttributes { visible: true, transparent: false, decorations: true, - always_on_top: false, + window_level: Default::default(), window_icon: None, preferred_theme: None, resize_increments: None, @@ -317,14 +317,16 @@ impl WindowBuilder { self } - /// Sets whether or not the window will always be on top of other windows. + /// Sets the window level. /// - /// The default is `false`. + /// This is just a hint to the OS, and the system could ignore it. /// - /// See [`Window::set_always_on_top`] for details. + /// The default is [`WindowLevel::Normal`]. + /// + /// See [`WindowLevel`] for details. #[inline] - pub fn with_always_on_top(mut self, always_on_top: bool) -> Self { - self.window.always_on_top = always_on_top; + pub fn with_window_level(mut self, level: WindowLevel) -> Self { + self.window.window_level = level; self } @@ -840,14 +842,13 @@ impl Window { self.window.is_decorated() } - /// Change whether or not the window will always be on top of other windows. + /// Change the window level. /// - /// ## Platform-specific + /// This is just a hint to the OS, and the system could ignore it. /// - /// - **iOS / Android / Web / Wayland:** Unsupported. - #[inline] - pub fn set_always_on_top(&self, always_on_top: bool) { - self.window.set_always_on_top(always_on_top) + /// See [`WindowLevel`] for details. + pub fn set_window_level(&self, level: WindowLevel) { + self.window.set_window_level(level) } /// Sets the window icon. @@ -1422,3 +1423,29 @@ impl Default for UserAttentionType { UserAttentionType::Informational } } + +/// A window level groups windows with respect to their z-position. +/// +/// The relative ordering between windows in different window levels is fixed. +/// The z-order of a window within the same window level may change dynamically on user interaction. +/// +/// ## Platform-specific +/// +/// - **iOS / Android / Web / Wayland:** Unsupported. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum WindowLevel { + /// The window will always be below normal windows. + /// + /// This is useful for a widget-based app. + AlwaysOnBottom, + /// The default. + Normal, + /// The window will always be on top of normal windows. + AlwaysOnTop, +} + +impl Default for WindowLevel { + fn default() -> Self { + Self::Normal + } +}