Windows: a maximized window stays clear of the taskbar (fixes #1977)

This commit is contained in:
Marco Arnulfo
2026-10-16 04:44:11 +07:00
committed by bflatastic
parent 52b072873d
commit 4b8a987445
+101 -2
View File
@@ -8,6 +8,13 @@
//! same in storytold/photocraft#2401). [`service`] strips the two styles and has Windows
//! recalculate the frame; winit puts them back whenever it changes the window flags (maximize,
//! restore, …), so the returned guard runs every frame.
//!
//! Windows sizes a maximize by the frame its styles describe, so a window maximized while its
//! caption is gone is sized without one: its rect covers the whole monitor, taskbar included, and
//! the shell reads a caption-less window covering its monitor as a full-screen app (#1977). The
//! guard therefore also trims a parked maximize's rect back to the work area plus its own frame,
//! clear of the taskbar, as PhotoCraft does (storytold/photocraft#3308). winit pins the client
//! area to the work area either way (`WM_NCCALCSIZE`), so the app's pixels do not move.
/// Run once per frame; a no-op once the window is borderless.
pub type WindowFrameFn = Box<dyn FnMut()>;
@@ -30,8 +37,15 @@ mod platform {
_ => return None,
};
let hwnd = find_window(target)?;
strip(&hwnd);
Some(Box::new(move || strip(&hwnd)))
reapply(&hwnd);
Some(Box::new(move || reapply(&hwnd)))
}
/// One pass of the per-frame guard: the styles winit put back come off again, and a parked
/// maximize stays clear of the taskbar.
fn reapply(hwnd: &HWND) {
strip(hwnd);
keep_clear_of_the_taskbar(hwnd);
}
/// The top-level window with the given raw handle: `EnumWindows` hands out real `HWND`s, so
@@ -65,6 +79,29 @@ mod platform {
co::SWP::FRAMECHANGED | co::SWP::NOMOVE | co::SWP::NOSIZE | co::SWP::NOZORDER | co::SWP::NOACTIVATE,
);
}
/// [`super::clamped_height`] for this frame's window: trim a maximized window whose rect runs
/// under the taskbar back to the work area plus its frame. No-op otherwise.
fn keep_clear_of_the_taskbar(hwnd: &HWND) {
if !hwnd.IsZoomed() {
return;
}
let (Ok(r), Ok(mi)) = (hwnd.GetWindowRect(), hwnd.MonitorFromWindow(co::MONITOR::DEFAULTTONEAREST).GetMonitorInfo()) else {
return;
};
let (m, w) = (mi.rcMonitor, mi.rcWork);
let Some(height) =
super::clamped_height((r.left, r.top, r.right, r.bottom), (m.left, m.top, m.right, m.bottom), (w.left, w.top, w.right, w.bottom))
else {
return;
};
let _ = hwnd.SetWindowPos(
HwndPlace::None,
POINT::new(),
SIZE { cx: r.right - r.left, cy: height },
co::SWP::NOMOVE | co::SWP::NOZORDER | co::SWP::NOACTIVATE,
);
}
}
#[cfg(not(all(windows, feature = "wgpu")))]
@@ -73,3 +110,65 @@ mod platform {
None
}
}
/// How far a parked maximize reaches past its monitor on the left, top and right: the resize border
/// (about 8 pt at 100 %, more with DPI). Past that, a maximized window's rect is not parked but
/// being dragged or snapped, and is left alone.
const FRAME: i32 = 32;
/// The height to trim a maximize to so its rect stops at the taskbar, or `None` when it already
/// does or `rect` (`left, top, right, bottom`, desktop coordinates) is not parked at its maximize
/// place. `monitor` is the monitor's full rect and `work` its work area (the monitor less the
/// taskbar) in the same coordinates. The parked check is against the full monitor, because that is
/// where Windows puts a caption-less maximize; the trim is against the work area. Platform-neutral
/// so it is tested everywhere.
#[cfg_attr(not(all(windows, feature = "wgpu")), allow(dead_code))]
fn clamped_height(rect: (i32, i32, i32, i32), monitor: (i32, i32, i32, i32), work: (i32, i32, i32, i32)) -> Option<i32> {
let (l, t, r, b) = rect;
let (ml, mt, mr, _) = monitor;
let (_, _, _, wb) = work;
let side = ml - l;
if !(0..=FRAME).contains(&side) || !(0..=FRAME).contains(&(mt - t)) || !(0..=FRAME).contains(&(r - mr)) {
return None;
}
if b <= wb + side {
return None;
}
Some(wb + side - t)
}
#[cfg(test)]
mod tests {
use super::clamped_height;
#[test]
fn a_maximize_covering_the_taskbar_is_trimmed_to_the_work_area() {
// 1920 × 1080 with a 34 px taskbar: a caption-less maximize covers the whole monitor,
// taskbar included (#1977). Trimmed so the rect ends at the work area plus the frame the
// window was parked with.
let monitor = (0, 0, 1920, 1080);
let work = (0, 0, 1920, 1046);
assert_eq!(clamped_height((-7, -7, 1927, 1087), monitor, work), Some(1046 + 7 - (-7)));
}
#[test]
fn a_maximize_already_clear_of_the_taskbar_is_left_alone() {
// What Windows sizes a framed maximize to: the work area plus the frame.
assert_eq!(clamped_height((-8, -8, 1928, 1054), (0, 0, 1920, 1080), (0, 0, 1920, 1046)), None);
}
#[test]
fn a_maximize_beside_a_side_taskbar_is_left_alone() {
// Taskbar on the left: the work area starts further right, but the monitor's bottom is
// still clear, so the rect reaches nothing.
assert_eq!(clamped_height((-8, -8, 1928, 1088), (0, 0, 1920, 1080), (48, 0, 1920, 1080)), None);
}
#[test]
fn a_maximize_being_dragged_away_is_left_alone() {
let monitor = (0, 0, 1920, 1080);
let work = (0, 0, 1920, 1046);
assert_eq!(clamped_height((100, 200, 1100, 900), monitor, work), None, "moved");
assert_eq!(clamped_height((-500, -7, 1427, 1087), monitor, work), None, "resized");
}
}