simple_editor/
winpos.rs

1//! Window placement: open on the monitor the user is actually on.
2//!
3//! eframe persists the window rect and its stored position always wins over `ViewportBuilder::position`
4//! (epi_integration applies `WindowSettings::initialize_viewport_builder` last), so without this the app
5//! reopens on whichever monitor it was closed on. At startup we move the window onto the monitor under the
6//! mouse cursor — keeping the persisted size, and keeping the persisted position when it is already on
7//! that monitor (so a deliberate arrangement is never disturbed).
8
9use raw_window_handle::{HasWindowHandle, RawWindowHandle};
10use windows::Win32::Foundation::{HWND, POINT, RECT};
11use windows::Win32::Graphics::Gdi::{GetMonitorInfoW, MonitorFromPoint, MONITORINFO, MONITOR_DEFAULTTONEAREST};
12use windows::Win32::UI::WindowsAndMessaging::{
13    GetCursorPos, GetWindowRect, SetWindowPos, SWP_NOACTIVATE, SWP_NOOWNERZORDER, SWP_NOSIZE, SWP_NOZORDER,
14};
15
16fn hwnd_of(handle: &impl HasWindowHandle) -> Option<HWND> {
17    match handle.window_handle().ok()?.as_raw() {
18        RawWindowHandle::Win32(h) => Some(HWND(h.hwnd.get() as *mut std::ffi::c_void)),
19        _ => None,
20    }
21}
22
23/// Work area (excludes the taskbar) of the monitor containing `p`, in physical pixels.
24fn work_area(p: POINT) -> Option<RECT> {
25    unsafe {
26        let mon = MonitorFromPoint(p, MONITOR_DEFAULTTONEAREST);
27        let mut info = MONITORINFO { cbSize: std::mem::size_of::<MONITORINFO>() as u32, ..Default::default() };
28        GetMonitorInfoW(mon, &mut info).as_bool().then_some(info.rcWork)
29    }
30}
31
32fn contains(r: RECT, p: POINT) -> bool {
33    p.x >= r.left && p.x < r.right && p.y >= r.top && p.y < r.bottom
34}
35
36/// New top-left for a `w`×`h` window so it sits fully inside `area`, centred when it doesn't already fit.
37fn place(area: RECT, w: i32, h: i32) -> (i32, i32) {
38    let (aw, ah) = (area.right - area.left, area.bottom - area.top);
39    let x = area.left + ((aw - w) / 2).max(0);
40    let y = area.top + ((ah - h) / 2).max(0);
41    (x, y)
42}
43
44/// Move the window onto the monitor under the cursor (no-op when it is already there, or on any error).
45/// Call once, from `App::new` — before the first frame is painted, so there is no visible jump.
46pub fn place_on_cursor_monitor(handle: &impl HasWindowHandle) {
47    let Some(hwnd) = hwnd_of(handle) else { return };
48    unsafe {
49        let mut cursor = POINT::default();
50        if GetCursorPos(&mut cursor).is_err() {
51            return;
52        }
53        let Some(area) = work_area(cursor) else { return };
54        let mut rect = RECT::default();
55        if GetWindowRect(hwnd, &mut rect).is_err() {
56            return;
57        }
58        // already on this monitor? leave the user's arrangement alone
59        let centre = POINT { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 };
60        if contains(area, centre) {
61            return;
62        }
63        let (w, h) = (rect.right - rect.left, rect.bottom - rect.top);
64        let (x, y) = place(area, w, h);
65        let _ = SetWindowPos(hwnd, None, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    fn r(left: i32, top: i32, right: i32, bottom: i32) -> RECT {
74        RECT { left, top, right, bottom }
75    }
76
77    #[test]
78    fn places_inside_the_work_area() {
79        let area = r(1920, 0, 3840, 1080); // a second monitor to the right
80        let (x, y) = place(area, 1400, 860);
81        assert!(x >= area.left && x + 1400 <= area.right);
82        assert!(y >= area.top && y + 860 <= area.bottom);
83        // a window larger than the monitor still starts at the top-left corner (never off-screen)
84        let (x, y) = place(area, 4000, 2000);
85        assert_eq!((x, y), (area.left, area.top));
86    }
87
88    #[test]
89    fn contains_matches_win32_half_open_rects() {
90        let area = r(0, 0, 1920, 1080);
91        assert!(contains(area, POINT { x: 0, y: 0 }));
92        assert!(contains(area, POINT { x: 1919, y: 1079 }));
93        assert!(!contains(area, POINT { x: 1920, y: 500 }));
94        assert!(!contains(area, POINT { x: -1, y: 500 }));
95    }
96}