A state saved on a larger or rearranged screen (2720x1800 at x=3347 on a smaller second monitor) left the titlebar off-screen and the window could not be moved. After the window-state restore, the window is moved inside, and shrunk to, the work area of the monitor it is mostly on (the primary one when that monitor is gone). Maximized and full-screen windows are left to the OS. window_fit::fit is pure and unit tested.
108 lines
4.6 KiB
Rust
108 lines
4.6 KiB
Rust
//! A restored window fits the monitor it opens on. A saved state can come from a larger or a
|
|
//! differently arranged monitor (2720x1800 saved on a big screen, restored on a smaller second
|
|
//! one): the window then hangs past the work area, its titlebar out of reach, and it cannot be
|
|
//! moved. [`fit`] picks the monitor the window is mostly on (the primary one when it is on none),
|
|
//! shrinks the window to that monitor's work area when it is larger, and moves it inside.
|
|
//! Physical pixels throughout, as the OS reports monitors.
|
|
|
|
/// A rectangle in physical pixels (a window's outer frame, a monitor's work area).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct Rect {
|
|
pub x: i32,
|
|
pub y: i32,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
impl Rect {
|
|
pub const fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
|
|
Self { x, y, width, height }
|
|
}
|
|
|
|
fn right(&self) -> i64 {
|
|
i64::from(self.x) + i64::from(self.width)
|
|
}
|
|
|
|
fn bottom(&self) -> i64 {
|
|
i64::from(self.y) + i64::from(self.height)
|
|
}
|
|
|
|
/// The area two rectangles share (0 when they do not meet).
|
|
fn overlap(&self, other: &Rect) -> i64 {
|
|
let width = self.right().min(other.right()) - i64::from(self.x.max(other.x));
|
|
let height = self.bottom().min(other.bottom()) - i64::from(self.y.max(other.y));
|
|
if width <= 0 || height <= 0 { 0 } else { width * height }
|
|
}
|
|
}
|
|
|
|
/// Where `window` should be so it lies inside a work area: `None` when it already does.
|
|
///
|
|
/// The work area is the one of the monitor the window overlaps most; when it overlaps none (that
|
|
/// monitor is gone), `primary`, else the first one. The size shrinks to the work area (never
|
|
/// grows); the position moves just enough to bring the whole frame inside.
|
|
pub fn fit(window: Rect, work_areas: &[Rect], primary: Option<Rect>) -> Option<Rect> {
|
|
let best = work_areas
|
|
.iter()
|
|
.map(|area| (window.overlap(area), area))
|
|
.filter(|(shared, _)| *shared > 0)
|
|
.max_by_key(|(shared, _)| *shared)
|
|
.map(|(_, area)| *area);
|
|
let area = best.or(primary).or_else(|| work_areas.first().copied())?;
|
|
if area.width == 0 || area.height == 0 {
|
|
return None;
|
|
}
|
|
let width = window.width.min(area.width);
|
|
let height = window.height.min(area.height);
|
|
let clamp = |at: i32, len: u32, start: i32, room: u32| -> i32 {
|
|
let last = i64::from(start) + i64::from(room) - i64::from(len);
|
|
i64::from(at).clamp(i64::from(start), last.max(i64::from(start))) as i32
|
|
};
|
|
let fitted = Rect::new(clamp(window.x, width, area.x, area.width), clamp(window.y, height, area.y, area.height), width, height);
|
|
(fitted != window).then_some(fitted)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// A 2560x1600 main monitor (taskbar 48 px) and a smaller 1920x1080 one to its right.
|
|
const MAIN: Rect = Rect::new(0, 0, 2560, 1552);
|
|
const SECOND: Rect = Rect::new(2560, 0, 1920, 1032);
|
|
|
|
#[test]
|
|
fn a_window_saved_on_a_bigger_screen_shrinks_into_the_smaller_monitor_it_opens_on() {
|
|
// The reported case: 2720x1800 at x=3347, mostly on the second monitor; its titlebar was off-screen.
|
|
let fitted = fit(Rect::new(3347, 40, 2720, 1800), &[MAIN, SECOND], Some(MAIN)).unwrap();
|
|
assert_eq!(fitted, SECOND);
|
|
}
|
|
|
|
#[test]
|
|
fn a_window_hanging_past_an_edge_moves_inside_and_keeps_its_size() {
|
|
let fitted = fit(Rect::new(3800, -30, 1200, 800), &[MAIN, SECOND], Some(MAIN)).unwrap();
|
|
assert_eq!(fitted, Rect::new(2560 + 1920 - 1200, 0, 1200, 800));
|
|
// Past the start edges too.
|
|
assert_eq!(fit(Rect::new(-500, 900, 1200, 800), &[MAIN, SECOND], Some(MAIN)), Some(Rect::new(0, 1552 - 800, 1200, 800)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_window_whose_monitor_is_gone_opens_on_the_primary_one() {
|
|
let fitted = fit(Rect::new(6000, 200, 1400, 900), &[MAIN], Some(MAIN)).unwrap();
|
|
assert_eq!(fitted, Rect::new(2560 - 1400, 200, 1400, 900));
|
|
// No primary reported: the first monitor.
|
|
assert_eq!(fit(Rect::new(6000, 200, 1400, 900), &[SECOND], None), Some(Rect::new(2560 + 1920 - 1400, 1032 - 900, 1400, 900)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_window_inside_its_work_area_is_left_alone() {
|
|
assert_eq!(fit(Rect::new(100, 80, 1600, 1000), &[MAIN, SECOND], Some(MAIN)), None);
|
|
assert_eq!(fit(SECOND, &[MAIN, SECOND], Some(MAIN)), None);
|
|
assert_eq!(fit(Rect::new(100, 80, 1600, 1000), &[], None), None);
|
|
}
|
|
|
|
#[test]
|
|
fn monitors_left_of_and_above_the_main_one_work_too() {
|
|
let left = Rect::new(-1920, -200, 1920, 1040);
|
|
let fitted = fit(Rect::new(-2100, -260, 2000, 1200), &[MAIN, left], Some(MAIN)).unwrap();
|
|
assert_eq!(fitted, left);
|
|
}
|
|
}
|