mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
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:
Generated
+1
@@ -6208,6 +6208,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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",
|
||||
] }
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(¶ms.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);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user