diff --git a/CHANGELOG.md b/CHANGELOG.md index 5633f7b23d..e58aa234ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ And please only add new entries to the top of this list, right below the `# Unre - On Wayland and X11, implement `is_maximized` method on `Window`. - On Windows, prevent ghost window from showing up in the taskbar after either several hours of use or restarting `explorer.exe`. - On macOS, fix issue where `ReceivedCharacter` was not being emitted during some key repeat events. +- On Windows, fix aero-snap for borderless (undecorated) windows. - On Wayland, load cursor icons `hand2` and `hand1` for `CursorIcon::Hand`. - **Breaking:** On Wayland, Theme trait and its support types are dropped. - On Wayland, bump `smithay-client-toolkit` to 0.15.1. diff --git a/examples/window_debug.rs b/examples/window_debug.rs index 47a130178c..e1151e5d79 100644 --- a/examples/window_debug.rs +++ b/examples/window_debug.rs @@ -28,9 +28,11 @@ fn main() { eprintln!(" (Q) Quit event loop"); eprintln!(" (V) Toggle visibility"); eprintln!(" (X) Toggle maximized"); + eprintln!(" (B) Toggle borderless"); let mut minimized = false; let mut visible = true; + let mut borderless = false; event_loop.set_device_event_filter(DeviceEventFilter::Never); @@ -119,6 +121,10 @@ fn main() { let is_maximized = window.is_maximized(); window.set_maximized(!is_maximized); } + VirtualKeyCode::B => { + borderless = !borderless; + window.set_decorations(borderless); + } _ => (), }, Event::WindowEvent { diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 632fc854f9..c05cb41e80 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -55,21 +55,21 @@ use windows_sys::Win32::{ PeekMessageW, PostMessageW, PostThreadMessageW, RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, TranslateMessage, CREATESTRUCTW, GIDC_ARRIVAL, GIDC_REMOVAL, GWL_EXSTYLE, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, - MAPVK_VK_TO_VSC, MINMAXINFO, MSG, MWMO_INPUTAVAILABLE, PM_NOREMOVE, PM_QS_PAINT, - PM_REMOVE, PT_PEN, PT_TOUCH, QS_ALLEVENTS, RI_KEY_E0, RI_KEY_E1, RI_MOUSE_WHEEL, - SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, - SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, WM_CREATE, - WM_DESTROY, WM_DPICHANGED, WM_DROPFILES, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, + MAPVK_VK_TO_VSC, MINMAXINFO, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PM_NOREMOVE, + PM_QS_PAINT, PM_REMOVE, PT_PEN, PT_TOUCH, QS_ALLEVENTS, RI_KEY_E0, RI_KEY_E1, + RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, SWP_NOACTIVATE, SWP_NOMOVE, + SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WM_CAPTURECHANGED, WM_CHAR, WM_CLOSE, + WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_DROPFILES, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_INPUT_DEVICE_CHANGE, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, - WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCREATE, WM_NCDESTROY, - WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, - WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, - WM_SYSCHAR, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, - WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, - WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, - WS_VISIBLE, + WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, + WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, + WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, + WM_SETTINGCHANGE, WM_SIZE, WM_SYSCHAR, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, + WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, + WNDCLASSEXW, WS_CAPTION, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, + WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_SIZEBOX, WS_VISIBLE, }, }, }; @@ -1914,9 +1914,11 @@ unsafe fn public_window_callback_inner( let window_state = userdata.window_state.lock(); if window_state.min_size.is_some() || window_state.max_size.is_some() { + let is_decorated = window_state.window_flags.contains(WindowFlags::DECORATIONS); if let Some(min_size) = window_state.min_size { let min_size = min_size.to_physical(window_state.scale_factor); - let (width, height): (u32, u32) = util::adjust_size(window, min_size).into(); + let (width, height): (u32, u32) = + util::adjust_size(window, min_size, is_decorated).into(); (*mmi).ptMinTrackSize = POINT { x: width as i32, y: height as i32, @@ -1924,7 +1926,8 @@ unsafe fn public_window_callback_inner( } if let Some(max_size) = window_state.max_size { let max_size = max_size.to_physical(window_state.scale_factor); - let (width, height): (u32, u32) = util::adjust_size(window, max_size).into(); + let (width, height): (u32, u32) = + util::adjust_size(window, max_size, is_decorated).into(); (*mmi).ptMaxTrackSize = POINT { x: width as i32, y: height as i32, @@ -1948,7 +1951,7 @@ unsafe fn public_window_callback_inner( let new_scale_factor = dpi_to_scale_factor(new_dpi_x); let old_scale_factor: f64; - let allow_resize = { + let (allow_resize, is_decorated) = { let mut window_state = userdata.window_state.lock(); old_scale_factor = window_state.scale_factor; window_state.scale_factor = new_scale_factor; @@ -1957,12 +1960,23 @@ unsafe fn public_window_callback_inner( return 0; } - window_state.fullscreen.is_none() - && !window_state.window_flags().contains(WindowFlags::MAXIMIZED) + ( + window_state.fullscreen.is_none() + && !window_state.window_flags().contains(WindowFlags::MAXIMIZED), + window_state + .window_flags() + .contains(WindowFlags::DECORATIONS), + ) }; - let style = GetWindowLongW(window, GWL_STYLE) as u32; + let mut style = GetWindowLongW(window, GWL_STYLE) as u32; let style_ex = GetWindowLongW(window, GWL_EXSTYLE) as u32; + // if the window isn't decorated, remove `WS_SIZEBOX` and `WS_CAPTION` so + // `AdjustWindowRect*` functions doesn't account for the hidden caption and borders and + // calculates a correct size for the client area. + if !is_decorated { + style &= !(WS_CAPTION | WS_SIZEBOX); + } // New size as suggested by Windows. let suggested_rect = *(lparam as *const RECT); @@ -2177,6 +2191,33 @@ unsafe fn public_window_callback_inner( DefWindowProcW(window, msg, wparam, lparam) } + WM_NCCALCSIZE => { + let win_flags = userdata.window_state.lock().window_flags(); + + if !win_flags.contains(WindowFlags::DECORATIONS) { + let params = &mut *(lparam as *mut NCCALCSIZE_PARAMS); + + // adjust the maximized borderless window to fill the work area rectangle of the display monitor and doesn't cover the taskbar + if util::is_maximized(window) { + let monitor = monitor::current_monitor(window); + if let Ok(monitor_info) = monitor::get_monitor_info(monitor.hmonitor()) { + params.rgrc[0] = monitor_info.monitorInfo.rcWork; + } + // } else { + // let decoration_thickness = util::hwnd_decoration_thickness(window, true); + // let rect = &mut params.rgrc[0]; + // // We add `1` to the thickness to account for 1px of cross-monitor spill-over. + // // We should probably calculate this some way rather than just emedding a constant offset. + // rect.left += decoration_thickness.left + 1; + // rect.right -= decoration_thickness.right + 1; + // rect.bottom -= decoration_thickness.bottom + 1; + } + 0 // return 0 here to make the window borderless aka without decorations + } else { + DefWindowProcW(window, msg, wparam, lparam) + } + } + _ => { if msg == *DESTROY_MSG_ID { DestroyWindow(window); diff --git a/src/platform_impl/windows/event_loop/runner.rs b/src/platform_impl/windows/event_loop/runner.rs index af03005557..d530ca0214 100644 --- a/src/platform_impl/windows/event_loop/runner.rs +++ b/src/platform_impl/windows/event_loop/runner.rs @@ -10,13 +10,14 @@ use std::{ use windows_sys::Win32::{ Foundation::HWND, Graphics::Gdi::{RedrawWindow, RDW_INTERNALPAINT}, + UI::WindowsAndMessaging::GWL_USERDATA, }; use crate::{ dpi::PhysicalSize, event::{Event, StartCause, WindowEvent}, event_loop::ControlFlow, - platform_impl::platform::util, + platform_impl::platform::{get_window_long, util, window_state::WindowFlags}, window::WindowId, }; @@ -434,10 +435,20 @@ impl BufferedEvent { new_inner_size: &mut new_inner_size, }, }); + let is_decorated = unsafe { + let userdata = + get_window_long((window_id.0).0, GWL_USERDATA) as *mut super::WindowData; + (*userdata) + .window_state + .lock() + .window_flags() + .contains(WindowFlags::DECORATIONS) + }; util::set_inner_size_physical( (window_id.0).0, new_inner_size.width as _, new_inner_size.height as _, + is_decorated, ); } } diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index fd74b674f6..bdc837439e 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -24,18 +24,19 @@ use windows_sys::{ Input::KeyboardAndMouse::GetActiveWindow, WindowsAndMessaging::{ AdjustWindowRectEx, ClipCursor, GetClientRect, GetClipCursor, GetMenu, - GetSystemMetrics, GetWindowLongW, GetWindowRect, SetWindowPos, ShowCursor, + GetSystemMetrics, GetWindowPlacement, GetWindowRect, SetWindowPos, ShowCursor, GWL_EXSTYLE, GWL_STYLE, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM, IDC_NO, IDC_SIZEALL, IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN, SWP_ASYNCWINDOWPOS, SWP_NOACTIVATE, SWP_NOMOVE, - SWP_NOREPOSITION, SWP_NOZORDER, WINDOW_EX_STYLE, WINDOW_STYLE, + SWP_NOREPOSITION, SWP_NOZORDER, SW_MAXIMIZE, WINDOWPLACEMENT, WINDOW_EX_STYLE, + WINDOW_STYLE, WS_CAPTION, WS_SIZEBOX, }, }, }, }; -use crate::{dpi::PhysicalSize, window::CursorIcon}; +use crate::{dpi::PhysicalSize, platform_impl::platform::get_window_long, window::CursorIcon}; pub fn encode_wide(string: impl AsRef) -> Vec { string.as_ref().encode_wide().chain(once(0)).collect() @@ -93,7 +94,7 @@ pub fn get_client_rect(hwnd: HWND) -> Result { } } -pub fn adjust_size(hwnd: HWND, size: PhysicalSize) -> PhysicalSize { +pub fn adjust_size(hwnd: HWND, size: PhysicalSize, is_decorated: bool) -> PhysicalSize { let (width, height): (u32, u32) = size.into(); let rect = RECT { left: 0, @@ -101,11 +102,11 @@ pub fn adjust_size(hwnd: HWND, size: PhysicalSize) -> PhysicalSize { top: 0, bottom: height as i32, }; - let rect = adjust_window_rect(hwnd, rect).unwrap_or(rect); + let rect = adjust_window_rect(hwnd, rect, is_decorated).unwrap_or(rect); PhysicalSize::new((rect.right - rect.left) as _, (rect.bottom - rect.top) as _) } -pub(crate) fn set_inner_size_physical(window: HWND, x: u32, y: u32) { +pub(crate) fn set_inner_size_physical(window: HWND, x: u32, y: u32, is_decorated: bool) { unsafe { let rect = adjust_window_rect( window, @@ -115,6 +116,7 @@ pub(crate) fn set_inner_size_physical(window: HWND, x: u32, y: u32) { bottom: y as i32, right: x as i32, }, + is_decorated, ) .expect("adjust_window_rect failed"); @@ -133,10 +135,16 @@ pub(crate) fn set_inner_size_physical(window: HWND, x: u32, y: u32) { } } -pub fn adjust_window_rect(hwnd: HWND, rect: RECT) -> Option { +pub fn adjust_window_rect(hwnd: HWND, rect: RECT, is_decorated: bool) -> Option { unsafe { - let style = GetWindowLongW(hwnd, GWL_STYLE) as u32; - let style_ex = GetWindowLongW(hwnd, GWL_EXSTYLE) as u32; + let mut style = get_window_long(hwnd, GWL_STYLE) as u32; + // if the window isn't decorated, remove `WS_SIZEBOX` and `WS_CAPTION` so + // `AdjustWindowRect*` functions doesn't account for the hidden caption and borders and + // calculates a correct size for the client area. + if !is_decorated { + style &= !(WS_CAPTION | WS_SIZEBOX); + } + let style_ex = get_window_long(hwnd, GWL_EXSTYLE) as u32; adjust_window_rect_with_styles(hwnd, style, style_ex, rect) } } @@ -209,6 +217,63 @@ pub fn is_focused(window: HWND) -> bool { window == unsafe { GetActiveWindow() } } +pub fn is_maximized(window: HWND) -> bool { + unsafe { + let mut placement: WINDOWPLACEMENT = mem::zeroed(); + placement.length = mem::size_of::() as u32; + GetWindowPlacement(window, &mut placement); + placement.showCmd == SW_MAXIMIZE + } +} + +// pub fn hwnd_decoration_thickness(hwnd: HWND, border_only: bool) -> RECT { +// unsafe { +// let style = get_window_long(hwnd, GWL_STYLE) as u32; +// let style_ex = get_window_long(hwnd, GWL_EXSTYLE) as u32; + +// let adjust_style = if !border_only { +// style +// } else { +// style & !WS_CAPTION +// }; +// let mut decoration_thickness = RECT { +// left: 0, +// top: 0, +// right: 0, +// bottom: 0, +// }; +// if has_flag(style, WS_SIZEBOX) { +// #[allow(non_snake_case)] +// if let Some(AdjustWindowRectExForDpi) = *ADJUST_WINDOW_RECT_EX_FOR_DPI { +// AdjustWindowRectExForDpi( +// &mut decoration_thickness, +// adjust_style, +// false as _, +// style_ex, +// hwnd_dpi(hwnd), +// ); +// } else { +// AdjustWindowRectEx( +// &mut decoration_thickness, +// adjust_style, +// false as _, +// style_ex, +// ); +// } +// decoration_thickness.left *= -1; +// decoration_thickness.top *= -1; +// } else if has_flag(style, WS_BORDER) { +// decoration_thickness = RECT { +// left: 1, +// top: 1, +// right: 1, +// bottom: 1, +// }; +// } +// decoration_thickness +// } +// } + pub fn get_instance_handle() -> HINSTANCE { // Gets the instance handle by taking the address of the // pseudo-variable created by the microsoft linker: diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index e63075a833..ca2c08d3bc 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -203,6 +203,12 @@ impl Window { let (width, height) = size.to_physical::(scale_factor).into(); let window_state = Arc::clone(&self.window_state); + + let is_decorated = window_state + .lock() + .window_flags + .contains(WindowFlags::DECORATIONS); + let window = self.window.clone(); self.thread_executor.execute_in_thread(move || { let _ = &window; @@ -211,7 +217,7 @@ impl Window { }); }); - util::set_inner_size_physical(self.hwnd(), width, height); + util::set_inner_size_physical(self.hwnd(), width, height, is_decorated); } #[inline] diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index 21841f07c0..ad76c59bf3 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -14,7 +14,7 @@ use windows_sys::Win32::{ SendMessageW, SetWindowLongW, SetWindowPos, ShowWindow, GWL_EXSTYLE, GWL_STYLE, HWND_NOTOPMOST, HWND_TOPMOST, SWP_ASYNCWINDOWPOS, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, 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, + SW_SHOW, WINDOWPLACEMENT, WINDOW_EX_STYLE, WINDOW_STYLE, WS_CAPTION, WS_CHILD, WS_CLIPCHILDREN, WS_CLIPSIBLINGS, WS_EX_ACCEPTFILES, WS_EX_APPWINDOW, WS_EX_LAYERED, WS_EX_LEFT, WS_EX_NOREDIRECTIONBITMAP, WS_EX_TOPMOST, WS_EX_TRANSPARENT, WS_EX_WINDOWEDGE, WS_MAXIMIZE, WS_MAXIMIZEBOX, WS_MINIMIZE, WS_MINIMIZEBOX, WS_OVERLAPPED, @@ -105,7 +105,6 @@ bitflags! { const IGNORE_CURSOR_EVENT = 1 << 15; const EXCLUSIVE_FULLSCREEN_OR_MASK = WindowFlags::ALWAYS_ON_TOP.bits; - const NO_DECORATIONS_AND_MASK = !WindowFlags::RESIZABLE.bits; } } @@ -228,21 +227,21 @@ impl WindowFlags { if self.contains(WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN) { self |= WindowFlags::EXCLUSIVE_FULLSCREEN_OR_MASK; } - if !self.contains(WindowFlags::DECORATIONS) { - self &= WindowFlags::NO_DECORATIONS_AND_MASK; - } + self } pub fn to_window_styles(self) -> (WINDOW_STYLE, WINDOW_EX_STYLE) { let (mut style, mut style_ex) = (WS_OVERLAPPED, WS_EX_LEFT); + style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU | WS_CAPTION | WS_MINIMIZEBOX; + style_ex |= WS_EX_ACCEPTFILES; + if self.contains(WindowFlags::RESIZABLE) { style |= WS_SIZEBOX | WS_MAXIMIZEBOX; } if self.contains(WindowFlags::DECORATIONS) { - style |= WS_CAPTION | WS_MINIMIZEBOX | WS_BORDER; - style_ex = WS_EX_WINDOWEDGE; + style_ex |= WS_EX_WINDOWEDGE; } if self.contains(WindowFlags::VISIBLE) { style |= WS_VISIBLE; @@ -272,9 +271,6 @@ impl WindowFlags { style_ex |= WS_EX_TRANSPARENT | WS_EX_LAYERED; } - style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU; - style_ex |= WS_EX_ACCEPTFILES; - if self.intersects( WindowFlags::MARKER_EXCLUSIVE_FULLSCREEN | WindowFlags::MARKER_BORDERLESS_FULLSCREEN, ) {