fix(windows): keep maximized windows within the work area (#644)

* fix(windows): keep maximized windows within the work area

* fix(windows): constrain final maximized outer placement

* Revert "fix(windows): constrain final maximized outer placement"

This reverts commit e481c900ca.
This commit is contained in:
l0ng-ai
2026-11-01 19:21:20 +08:00
committed by GitHub
parent 93328d7895
commit 4034853f91
5 changed files with 234 additions and 0 deletions
Generated
+1
View File
@@ -6208,6 +6208,7 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"windows-sys 0.61.2",
]
[[package]]
+8
View File
@@ -78,3 +78,11 @@ objc2-core-foundation = { version = "0.3", default-features = false, features =
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSNotification", "NSOperation", "NSString"] }
# Used only by the debug font-restriction override; Cargo dependency cfg cannot test debug_assertions.
objc2-web-kit = { version = "0.3", default-features = false, features = ["std", "WKWebViewConfiguration", "WKPreferences"] }
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
] }
+5
View File
@@ -19,6 +19,8 @@ mod theme_frame;
#[cfg(target_os = "macos")]
mod titlebar;
mod window_groups;
#[cfg(any(windows, test))]
mod window_work_area;
mod windows;
use parking_lot::Mutex;
@@ -70,6 +72,9 @@ pub fn run() {
// Single instance first: a second launch focuses this one and opens its path argument.
// Debug builds have a separate identifier per worktree.
let app = app.plugin(tauri_plugin_single_instance::init(windows::on_second_instance));
// Install before window-state restores maximized geometry during window creation.
#[cfg(windows)]
let app = app.plugin(window_work_area::init());
let app = app.plugin(tauri_plugin_opener::init()).plugin(tauri_plugin_dialog::init());
#[cfg(target_os = "linux")]
let app = app.plugin(linux_window_state::plugin());
+208
View File
@@ -0,0 +1,208 @@
//! Native maximize bounds for every Windows frameless window, including state restoration.
#[cfg(windows)]
pub use native::init;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Rect {
left: i32,
top: i32,
right: i32,
bottom: i32,
}
/// All coordinates are physical pixels in the selected monitor's coordinate space.
/// Auto-hide needs one exposed pixel, but an already reserved edge needs no extra inset.
fn bounds(monitor: Rect, work: Rect, auto_hide: [bool; 4], fullscreen: bool) -> Rect {
if fullscreen {
return monitor;
}
let [left, top, right, bottom] = auto_hide;
Rect {
left: work.left + i32::from(left && work.left == monitor.left),
top: work.top + i32::from(top && work.top == monitor.top),
right: work.right - i32::from(right && work.right == monitor.right),
bottom: work.bottom - i32::from(bottom && work.bottom == monitor.bottom),
}
}
fn maximize_geometry(monitor: Rect, area: Rect) -> (i32, i32, i32, i32) {
(area.left - monitor.left, area.top - monitor.top, area.right - area.left, area.bottom - area.top)
}
#[cfg(windows)]
mod native {
use super::{Rect, bounds, maximize_geometry};
use windows_sys::Win32::{
Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM},
Graphics::Gdi::{
GetMonitorInfoW, HMONITOR, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromRect, MonitorFromWindow,
},
UI::{
Shell::{
ABE_BOTTOM, ABE_LEFT, ABE_RIGHT, ABE_TOP, ABM_GETAUTOHIDEBAREX, APPBARDATA, DefSubclassProc,
RemoveWindowSubclass, SHAppBarMessage, SetWindowSubclass,
},
WindowsAndMessaging::{
GWL_STYLE, GetWindowLongPtrW, IsZoomed, MINMAXINFO, NCCALCSIZE_PARAMS, WM_GETMINMAXINFO, WM_NCCALCSIZE,
WM_NCDESTROY, WS_CAPTION,
},
},
};
const SUBCLASS: usize = 0x410;
pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> {
tauri::plugin::Builder::new("window-work-area")
.on_window_ready(|window| {
if window.is_decorated().unwrap_or(true) {
return;
}
let install = || -> tauri::Result<()> {
let hwnd = window.hwnd()?.0 as usize;
window.run_on_main_thread(move || {
// Subclasses must be installed and removed on the owning GUI thread.
if unsafe { SetWindowSubclass(hwnd as HWND, Some(window_proc), SUBCLASS, 0) } == 0 {
tracing::warn!(
"cannot install window work area handler: {}",
std::io::Error::last_os_error()
);
}
})
};
if let Err(error) = install() {
tracing::warn!("cannot install window work area handler: {error}");
}
})
.build()
}
fn rect(rect: RECT) -> Rect {
Rect { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }
}
fn native_rect(rect: Rect) -> RECT {
RECT { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }
}
unsafe fn monitor_bounds(monitor: HMONITOR, fullscreen: bool) -> Option<(Rect, Rect)> {
let mut info = MONITORINFO { cbSize: size_of::<MONITORINFO>() as u32, ..Default::default() };
if unsafe { GetMonitorInfoW(monitor, &mut info) } == 0 {
return None;
}
let mut auto_hide = [false; 4];
if !fullscreen {
for (index, edge) in [ABE_LEFT, ABE_TOP, ABE_RIGHT, ABE_BOTTOM].into_iter().enumerate() {
let mut data = APPBARDATA {
cbSize: size_of::<APPBARDATA>() as u32,
uEdge: edge,
rc: info.rcMonitor,
..Default::default()
};
auto_hide[index] = unsafe { SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &mut data) } != 0;
}
}
let monitor = rect(info.rcMonitor);
Some((monitor, bounds(monitor, rect(info.rcWork), auto_hide, fullscreen)))
}
unsafe extern "system" fn window_proc(
hwnd: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
_id: usize,
_data: usize,
) -> LRESULT {
// Tao keeps WS_CAPTION on frameless windows and removes it for native fullscreen.
// Read native styles here: a Tauri state query from its window procedure can deadlock.
let fullscreen = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) } as u32 & WS_CAPTION == 0;
match message {
WM_GETMINMAXINFO if !fullscreen => {
// Keep Tao's min/max tracking constraints, then set the outer maximize bounds.
let result = unsafe { DefSubclassProc(hwnd, message, wparam, lparam) };
let monitor = unsafe { MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST) };
if let Some((monitor, area)) = unsafe { monitor_bounds(monitor, false) } {
let (x, y, width, height) = maximize_geometry(monitor, area);
let info = unsafe { &mut *(lparam as *mut MINMAXINFO) };
info.ptMaxPosition.x = x;
info.ptMaxPosition.y = y;
info.ptMaxSize.x = width;
info.ptMaxSize.y = height;
}
result
}
WM_NCCALCSIZE if wparam != 0 && (fullscreen || unsafe { IsZoomed(hwnd) } != 0) => {
let params = unsafe { &mut *(lparam as *mut NCCALCSIZE_PARAMS) };
// Use the proposed rectangle: a move/snap/DPI change can target another monitor.
let monitor = unsafe { MonitorFromRect(&params.rgrc[0], MONITOR_DEFAULTTONEAREST) };
if let Some((_, area)) = unsafe { monitor_bounds(monitor, fullscreen) } {
// Replace Tao's client-only clamp and global auto-hide query. Fullscreen
// must retain the entire display, even when entered from a maximized window.
params.rgrc[0] = native_rect(area);
0
} else {
unsafe { DefSubclassProc(hwnd, message, wparam, lparam) }
}
}
WM_NCDESTROY => {
unsafe { RemoveWindowSubclass(hwnd, Some(window_proc), SUBCLASS) };
unsafe { DefSubclassProc(hwnd, message, wparam, lparam) }
}
_ => unsafe { DefSubclassProc(hwnd, message, wparam, lparam) },
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r(left: i32, top: i32, right: i32, bottom: i32) -> Rect {
Rect { left, top, right, bottom }
}
#[test]
fn taskbars_on_every_edge_and_secondary_displays() {
for (monitor, work, expected) in [
// Reported case: maximized window must leave the bottom taskbar visible.
(r(0, 0, 1920, 1080), r(0, 0, 1920, 1040), (0, 0, 1920, 1040)),
(r(0, 0, 1920, 1080), r(48, 0, 1920, 1080), (48, 0, 1872, 1080)),
(r(0, 0, 1920, 1080), r(0, 40, 1920, 1080), (0, 40, 1920, 1040)),
(r(0, 0, 1920, 1080), r(0, 0, 1872, 1080), (0, 0, 1872, 1080)),
// Negative origin, top taskbar, 150% display beside a 100% display.
(r(-2560, -1440, 0, 0), r(-2560, -1380, 0, 0), (0, 60, 2560, 1380)),
// 250% display: physical bounds must not be scaled with the previous monitor's DPI.
(r(1920, 0, 5760, 2160), r(1920, 0, 5760, 2060), (0, 0, 3840, 2060)),
] {
let area = bounds(monitor, work, [false; 4], false);
assert_eq!(area, work);
assert_eq!(maximize_geometry(monitor, area), expected);
}
}
#[test]
fn auto_hide_leaves_a_physical_pixel_only_on_its_own_monitor_edge() {
let monitor = r(-1920, 0, 0, 1080);
for (edges, expected) in [
([true, false, false, false], r(-1919, 0, 0, 1080)),
([false, true, false, false], r(-1920, 1, 0, 1080)),
([false, false, true, false], r(-1920, 0, -1, 1080)),
([false, false, false, true], r(-1920, 0, 0, 1079)),
([false; 4], monitor),
([true; 4], r(-1919, 1, -1, 1079)),
] {
assert_eq!(bounds(monitor, monitor, edges, false), expected);
}
let reserved = r(-1920, 0, 0, 1040);
assert_eq!(bounds(monitor, reserved, [false, false, false, true], false), reserved);
}
#[test]
fn fullscreen_covers_taskbars_including_when_entered_from_maximized() {
let monitor = r(1920, -100, 5760, 2060);
let work = r(1980, -100, 5760, 1960);
assert_eq!(bounds(monitor, work, [true; 4], true), monitor);
assert_eq!(bounds(monitor, work, [false; 4], false), work);
}
}
+12
View File
@@ -841,9 +841,18 @@ fn fit_settings_window(window: &WebviewWindow, parent: &WebviewWindow) -> tauri:
/// scale and its position is given in physical pixels.
#[cfg(not(target_os = "macos"))]
pub(super) fn place_settings_window(window: &WebviewWindow, parent: &WebviewWindow) -> tauri::Result<()> {
// Placement setters leave maximized mode. Fit the restored frame, then reapply the saved state.
// A restored fullscreen window already covers its display and must not be fitted to the work area.
if window.is_fullscreen()? {
return Ok(());
}
let Some(monitor) = parent.current_monitor()?.or(parent.primary_monitor()?) else {
return Ok(());
};
let maximized = window.is_maximized()?;
if maximized {
window.unmaximize()?;
}
let scale = monitor.scale_factor();
let area = monitor.work_area();
let origin = area.position.to_logical::<f64>(scale);
@@ -872,6 +881,9 @@ pub(super) fn place_settings_window(window: &WebviewWindow, parent: &WebviewWind
)))?;
window.set_size(tauri::LogicalSize::new(width, height))?;
}
if maximized {
window.maximize()?;
}
Ok(())
}