Compare commits

..
7 changed files with 77 additions and 247 deletions

No files matched your search

Generated
-1
View File
@@ -6207,7 +6207,6 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"windows-sys 0.61.2",
]
[[package]]
+59 -10
View File
@@ -215,13 +215,21 @@ fn fix_kind(
}
/// `vmArgs` of a launch by java-debug, which decodes the JVM's output as `encoding`
/// (its launch attribute, UTF-8 when unset) and sets `file.encoding` itself.
pub fn java_debug_vm_args(vm_args: &str, encoding: Option<&str>, windows: bool) -> Option<String> {
/// (its launch attribute, UTF-8 when unset). java-debug sets `file.encoding` only
/// when that attribute is nonblank; use the same defaults and user overrides as [`fix`].
pub fn java_debug_vm_args(
vm_args: &str,
vars: &BTreeMap<String, String>,
inherited: impl Fn(&str) -> Option<String>,
encoding: Option<&str>,
windows: bool,
) -> Option<String> {
let encoding = encoding.map(str::trim).filter(|e| !e.is_empty()).unwrap_or("UTF-8");
if (!windows && is_utf8(encoding)) || sets(vm_args, STD_PROPERTIES) {
let added = fix_kind(Kind::Java, Some(&[vm_args.to_owned()]), vars, inherited, encoding, windows).java_args;
if added.is_empty() {
return None;
}
let added = options(STD_PROPERTIES, encoding).join(" ");
let added = added.join(" ");
Some(match vm_args.trim() {
"" => added,
own => format!("{own} {added}"),
@@ -446,12 +454,53 @@ mod tests {
#[test]
fn java_debug_launches_write_what_java_debug_decodes() {
assert_eq!(java_debug_vm_args("", None, true).as_deref(), Some(TOOL_UTF8));
assert_eq!(java_debug_vm_args("-Xmx1g", Some(""), true), Some(format!("-Xmx1g {TOOL_UTF8}")));
assert_eq!(java_debug_vm_args("", Some("GBK"), true).unwrap().split(' ').next(), Some("-Dstdout.encoding=GBK"));
assert_eq!(java_debug_vm_args("-Dstdout.encoding=GBK", None, true), None);
assert_eq!(java_debug_vm_args("-Xmx1g", None, false), None);
assert!(java_debug_vm_args("", Some("GBK"), false).is_some());
let debug =
|args: &str, encoding, windows| java_debug_vm_args(args, &BTreeMap::new(), |_| None, encoding, windows);
for encoding in [None, Some(""), Some(" "), Some("UTF-8"), Some("utf8")] {
let streams = encoding.filter(|e| !e.trim().is_empty()).unwrap_or("UTF-8");
let utf8 = format!("{} -Dfile.encoding=UTF-8", options(STD_PROPERTIES, streams).join(" "));
assert_eq!(debug("", encoding, true), Some(utf8.clone()));
assert_eq!(debug("-Xmx1g", encoding, true), Some(format!("-Xmx1g {utf8}")));
assert_eq!(debug("-Xmx1g", encoding, false), None);
}
for windows in [true, false] {
assert_eq!(debug("", Some("GBK"), windows), Some(options(STD_PROPERTIES, "GBK").join(" ")));
}
}
#[test]
fn java_debug_preserves_user_options_independently() {
let debug = |args: &str| java_debug_vm_args(args, &BTreeMap::new(), |_| None, None, true);
assert_eq!(debug("-Dfile.encoding=GBK"), Some(format!("-Dfile.encoding=GBK {TOOL_UTF8}")));
for property in STD_PROPERTIES {
let args = format!("-D{property}=GBK");
assert_eq!(debug(&args), Some(format!("{args} -Dfile.encoding=UTF-8")));
assert_eq!(debug(&format!("{args} -Dfile.encoding=GBK")), None);
}
for key in ["JDK_JAVA_OPTIONS", TOOL_OPTIONS] {
for (own, expected) in [
("-Dfile.encoding=GBK", Some(TOOL_UTF8.to_owned())),
("-Dstdout.encoding=GBK", Some("-Dfile.encoding=UTF-8".to_owned())),
("-Dstdout.encoding=GBK -Dfile.encoding=GBK", None),
] {
assert_eq!(java_debug_vm_args("", &vars(&[(key, own)]), |_| None, None, true), expected);
assert_eq!(
java_debug_vm_args("", &BTreeMap::new(), |k| (k == key).then(|| own.to_owned()), None, true),
expected
);
}
// A launch environment value overrides the inherited one, as in Run.
assert_eq!(
java_debug_vm_args(
"",
&vars(&[(key, "")]),
|k| (k == key).then(|| "-Dfile.encoding=GBK".into()),
None,
true
),
Some(format!("{TOOL_UTF8} -Dfile.encoding=UTF-8"))
);
}
}
fn filtered(chunks: &[&str]) -> String {
+18 -3
View File
@@ -96,7 +96,13 @@ fn match_java_debug_encoding(mut config: LaunchConfig, windows: bool) -> LaunchC
};
let vm_args = extra.get("vmArgs").and_then(|v| v.as_str()).unwrap_or_default();
let encoding = extra.get("encoding").and_then(|v| v.as_str());
if let Some(vm_args) = ide_core::output_encoding::java_debug_vm_args(vm_args, encoding, windows) {
if let Some(vm_args) = ide_core::output_encoding::java_debug_vm_args(
vm_args,
&config.env,
|key| std::env::var(key).ok(),
encoding,
windows,
) {
extra.insert("vmArgs".into(), vm_args.into());
config.extra_json = Some(serde_json::Value::Object(extra).to_string());
}
@@ -452,7 +458,7 @@ mod tests {
program: None,
args: vec![],
cwd: None,
env: Default::default(),
env: [("JDK_JAVA_OPTIONS".into(), "".into()), ("JAVA_TOOL_OPTIONS".into(), "".into())].into(),
extra_json: Some(extra.to_string()),
}
}
@@ -468,7 +474,7 @@ mod tests {
let fixed = match_java_debug_encoding(launch("java-debug", "launch", extra.clone()), true);
assert_eq!(
vm_args(&fixed),
"-Xmx1g -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8"
"-Xmx1g -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -Dfile.encoding=UTF-8"
);
assert_eq!(
serde_json::from_str::<serde_json::Value>(fixed.extra_json.as_deref().unwrap()).unwrap()["mainClass"],
@@ -481,4 +487,13 @@ mod tests {
assert_eq!(vm_args(&match_java_debug_encoding(launch(adapter, request, extra.clone()), windows)), "-Xmx1g");
}
}
#[test]
fn java_debug_launches_respect_environment_encoding() {
for key in ["JDK_JAVA_OPTIONS", "JAVA_TOOL_OPTIONS"] {
let mut config = launch("java-debug", "launch", serde_json::json!({"vmArgs": "-Dstdout.encoding=GBK"}));
config.env.insert(key.into(), "-Dfile.encoding=GBK".into());
assert_eq!(vm_args(&match_java_debug_encoding(config, true)), "-Dstdout.encoding=GBK");
}
}
}
-8
View File
@@ -78,11 +78,3 @@ 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,8 +19,6 @@ 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;
@@ -72,9 +70,6 @@ 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
@@ -1,208 +0,0 @@
//! 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,18 +841,9 @@ 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);
@@ -881,9 +872,6 @@ pub(super) fn place_settings_window(window: &WebviewWindow, parent: &WebviewWind
)))?;
window.set_size(tauri::LogicalSize::new(width, height))?;
}
if maximized {
window.maximize()?;
}
Ok(())
}