screen_app/recp/tray_positioning.rs
1//! M-RECP.1 / AUT-262 — Multi-display window positioning under the
2//! tray click.
3//!
4//! Pure-Rust helper that picks the right monitor for a given click
5//! position. The Tauri-side caller in `main.rs` invokes
6//! [`pick_monitor`] inside the `on_tray_icon_event` handler before
7//! showing the main window. OS hardware integration (querying real
8//! monitor bounds, applying the clamp) is the deferred follow-up.
9
10/// Axis-aligned rectangle in screen coordinates. Same shape as
11/// Tauri's `tauri::PhysicalRect` but lives here as a `Copy`-friendly
12/// pure-Rust struct so the picker is unit-testable without a Tauri
13/// runtime.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct MonitorBounds {
16 /// Top-left x.
17 pub x: i32,
18 /// Top-left y.
19 pub y: i32,
20 /// Width in pixels.
21 pub width: i32,
22 /// Height in pixels.
23 pub height: i32,
24}
25
26impl MonitorBounds {
27 /// `true` if the point `(x, y)` falls inside this monitor.
28 #[must_use]
29 pub fn contains(self, x: i32, y: i32) -> bool {
30 x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
31 }
32}
33
34/// Find the monitor whose bounds contain the click position. Returns
35/// the first monitor as a fallback if no bounds contain the click
36/// (defensive — shouldn't normally happen with valid monitor
37/// geometry, but multi-display setups can have negative coords).
38#[must_use]
39pub fn pick_monitor(
40 click_x: i32,
41 click_y: i32,
42 monitors: &[MonitorBounds],
43) -> Option<MonitorBounds> {
44 if monitors.is_empty() {
45 return None;
46 }
47 monitors
48 .iter()
49 .find(|m| m.contains(click_x, click_y))
50 .copied()
51 .or_else(|| monitors.first().copied())
52}
53
54/// Top-left position that anchors the popover's **top-right corner**
55/// to the monitor's top-right corner — flush against the screen edge,
56/// matching the macOS Control-Center / Notification-Center
57/// convention. The click position is only used by the caller to pick
58/// which monitor to anchor on; within that monitor the popover always
59/// lands top-right.
60///
61/// Returns top-left `(x, y)` in screen coordinates. macOS clamps the
62/// window's titlebar below the menubar automatically when `y` falls
63/// in the menubar region, so passing `monitor.y` directly is safe.
64#[must_use]
65pub fn position_window_top_right(window_width: i32, monitor: MonitorBounds) -> (i32, i32) {
66 let x = monitor.x + (monitor.width - window_width).max(0);
67 let y = monitor.y;
68 (x, y)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 fn mon(x: i32, y: i32, w: i32, h: i32) -> MonitorBounds {
76 MonitorBounds {
77 x,
78 y,
79 width: w,
80 height: h,
81 }
82 }
83
84 #[test]
85 fn contains_includes_top_left_excludes_bottom_right() {
86 let m = mon(0, 0, 100, 100);
87 assert!(m.contains(0, 0));
88 assert!(m.contains(50, 50));
89 assert!(!m.contains(100, 50));
90 assert!(!m.contains(50, 100));
91 }
92
93 #[test]
94 fn pick_monitor_returns_none_for_empty_list() {
95 assert!(pick_monitor(0, 0, &[]).is_none());
96 }
97
98 #[test]
99 fn pick_monitor_finds_the_one_containing_click() {
100 let mons = vec![mon(0, 0, 1920, 1080), mon(1920, 0, 1920, 1080)];
101 assert_eq!(pick_monitor(2000, 100, &mons), Some(mons[1]));
102 assert_eq!(pick_monitor(500, 100, &mons), Some(mons[0]));
103 }
104
105 #[test]
106 fn pick_monitor_falls_back_to_first_if_no_match() {
107 let mons = vec![mon(0, 0, 1920, 1080)];
108 // Negative click way out of bounds — fallback to first.
109 assert_eq!(pick_monitor(-100, -100, &mons), Some(mons[0]));
110 }
111
112 #[test]
113 fn position_window_top_right_anchors_to_monitor_top_right() {
114 let mon = mon(0, 0, 1920, 1080);
115 // Window's top-right should sit at monitor's top-right
116 // (1920, 0) → top-left at (1920 - 800, 0) = (1120, 0).
117 assert_eq!(position_window_top_right(800, mon), (1120, 0));
118 }
119
120 #[test]
121 fn position_window_top_right_respects_offset_monitor_origin() {
122 // Secondary display sitting to the right of the primary.
123 let mon = mon(1920, 0, 2560, 1440);
124 // Top-right of monitor is at (1920 + 2560, 0) = (4480, 0).
125 // Window top-left = (4480 - 600, 0) = (3880, 0).
126 assert_eq!(position_window_top_right(600, mon), (3880, 0));
127 }
128
129 #[test]
130 fn position_window_top_right_clamps_when_window_wider_than_monitor() {
131 // Pathological: window is wider than the monitor. Don't push
132 // the left edge into negative territory inside the monitor —
133 // clamp left edge to monitor.x so the window starts at the
134 // monitor's left edge (and overflows on the right, but the
135 // user will see *some* of it instead of all of it being
136 // pushed off the left side).
137 let mon = mon(0, 0, 800, 600);
138 assert_eq!(position_window_top_right(1000, mon), (0, 0));
139 }
140}