fix: clamp window size to screen resolution and fix physical/logical pixel mismatch

Two fixes for issue #139 (window opens larger than screen):

1. Screen resolution clamping at startup: detect monitor size via
   xdpyinfo/xrandr and clamp saved window dimensions to 95% of screen.
   Prevents windows from opening larger than the display even if the DB
   has valid-but-too-large values.

2. Physical-to-logical pixel conversion in resize handler: the Resized
   event gives physical pixels but inner_size() expects logical pixels.
   With scale factor > 1.0, the window would grow each restart because
   physical values were saved and restored as logical. Now divides by
   scale_factor before persisting.

Closes #139
This commit is contained in:
vicrodh committed 2026-02-28 17:56:42 -06:00
1 parent 37df510994
commit 421f993f82
1 file changed
+92 -6
+92 -6
View File
@@ -331,6 +331,66 @@ fn remove_kwin_window_rule() {
}
}
/// Get the primary screen resolution in logical pixels.
/// Tries multiple methods: Wayland (wlr-randr, wayland-info), X11 (xdpyinfo).
/// Returns (width, height) or None if detection fails.
#[cfg(target_os = "linux")]
fn get_screen_resolution() -> Option<(f64, f64)> {
use std::process::Command;
// Method 1: Try xdpyinfo (works on X11 and XWayland)
if let Ok(output) = Command::new("xdpyinfo").output() {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Look for "dimensions: WIDTHxHEIGHT pixels"
for line in stdout.lines() {
let trimmed = line.trim();
if trimmed.starts_with("dimensions:") {
// Parse "dimensions: 3840x2160 pixels (...)"
if let Some(dims) = trimmed.split_whitespace().nth(1) {
let parts: Vec<&str> = dims.split('x').collect();
if parts.len() == 2 {
if let (Ok(w), Ok(h)) = (parts[0].parse::<f64>(), parts[1].parse::<f64>()) {
log::info!("Screen resolution detected via xdpyinfo: {}x{}", w as u32, h as u32);
return Some((w, h));
}
}
}
}
}
}
}
// Method 2: Try xrandr (widely available)
if let Ok(output) = Command::new("xrandr").arg("--current").output() {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Look for line with " connected " and a resolution like "2560x1440+0+0"
for line in stdout.lines() {
if line.contains(" connected ") {
// Parse "DP-1 connected primary 2560x1440+0+0 ..."
for token in line.split_whitespace() {
if token.contains('x') && token.contains('+') {
// "2560x1440+0+0"
let res_part = token.split('+').next().unwrap_or("");
let parts: Vec<&str> = res_part.split('x').collect();
if parts.len() == 2 {
if let (Ok(w), Ok(h)) = (parts[0].parse::<f64>(), parts[1].parse::<f64>()) {
log::info!("Screen resolution detected via xrandr: {}x{}", w as u32, h as u32);
return Some((w, h));
}
}
}
}
}
}
}
}
log::warn!("Could not detect screen resolution");
None
}
pub fn run() {
// Load .env file if present (for development)
// Silently ignore if not found (production builds use compile-time env vars)
@@ -419,10 +479,34 @@ pub fn run() {
window_settings.is_maximized,
);
let use_system_titlebar = window_settings.use_system_titlebar;
let saved_win_width = window_settings.window_width;
let saved_win_height = window_settings.window_height;
let mut saved_win_width = window_settings.window_width;
let mut saved_win_height = window_settings.window_height;
let saved_win_maximized = window_settings.is_maximized;
// Safety: clamp window size to screen resolution.
// Prevents the window from opening larger than the display (issue #139).
// This also handles corrupt DB values that pass the 200..32767 validation
// but exceed the actual monitor dimensions.
#[cfg(target_os = "linux")]
{
// Read current screen resolution from xdpyinfo or Wayland
if let Some((screen_w, screen_h)) = get_screen_resolution() {
// Leave room for taskbar/panels (90% of screen)
let max_w = screen_w * 0.95;
let max_h = screen_h * 0.95;
if saved_win_width > max_w || saved_win_height > max_h {
log::warn!(
"Window size {}x{} exceeds screen {}x{}, clamping to {}x{}",
saved_win_width as u32, saved_win_height as u32,
screen_w as u32, screen_h as u32,
max_w as u32, max_h as u32
);
saved_win_width = saved_win_width.min(max_w);
saved_win_height = saved_win_height.min(max_h);
}
}
}
// One-time cleanup: remove the KWin SSD window rule written by QBZ 1.1.14.
// That version wrote a kwinrulesrc entry forcing server-side decorations; it
// was removed in 1.1.15 due to a GTK3/WebKit heap corruption bug. We silently
@@ -646,12 +730,14 @@ pub fn run() {
if size.width > 0 && size.height > 0 {
let maximized = win_for_events.is_maximized().unwrap_or(false);
if !maximized {
// Convert physical pixels to logical pixels
// inner_size() expects logical, Resized gives physical
let scale = win_for_events.scale_factor().unwrap_or(1.0);
let logical_w = size.width as f64 / scale;
let logical_h = size.height as f64 / scale;
if let Ok(guard) = store.lock() {
if let Some(s) = guard.as_ref() {
let _ = s.set_window_size(
size.width as f64,
size.height as f64,
);
let _ = s.set_window_size(logical_w, logical_h);
}
}
}