gardesk/garnotify / 3fec681

Browse files

feat(ui): add proper multi-monitor support via RandR

Use gartk-x11 monitor detection to position notifications on the
correct monitor. Supports primary, mouse, and named monitor selection.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3fec6813aff17fdbfd838ae3cc5e22135cd680d0
Parents
0c2303e
Tree
ab9e408

1 changed file

StatusFile+-
M garnotify/src/ui/layout.rs 117 19
garnotify/src/ui/layout.rsmodified
@@ -4,8 +4,8 @@
44
 //! when multiple notifications are visible.
55
 
66
 use gartk_core::Rect;
7
-use gartk_x11::Connection;
8
-use tracing::debug;
7
+use gartk_x11::{Connection, Monitor, detect_monitors, primary_monitor, monitor_at_pointer};
8
+use tracing::{debug, info, warn};
99
 
1010
 /// Screen position for notifications
1111
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -57,12 +57,36 @@ pub enum StackDirection {
5757
     Up,
5858
 }
5959
 
60
+/// Monitor selection mode
61
+#[derive(Debug, Clone, PartialEq, Eq)]
62
+pub enum MonitorSelection {
63
+    /// Use primary monitor
64
+    Primary,
65
+    /// Follow mouse pointer
66
+    Mouse,
67
+    /// Use specific monitor by name
68
+    Named(String),
69
+}
70
+
71
+impl MonitorSelection {
72
+    /// Parse from config string
73
+    pub fn from_str(s: &str) -> Self {
74
+        match s.to_lowercase().as_str() {
75
+            "primary" => Self::Primary,
76
+            "mouse" | "pointer" => Self::Mouse,
77
+            name => Self::Named(name.to_string()),
78
+        }
79
+    }
80
+}
81
+
6082
 /// Layout manager for notification popups
6183
 pub struct LayoutManager {
62
-    /// Screen width
63
-    screen_width: u32,
64
-    /// Screen height
65
-    screen_height: u32,
84
+    /// Current monitor bounds
85
+    monitor: Monitor,
86
+    /// Monitor selection mode
87
+    monitor_selection: MonitorSelection,
88
+    /// X11 connection for monitor updates
89
+    conn: Connection,
6690
     /// Notification width
6791
     notification_width: u32,
6892
     /// Margin from screen edges
@@ -79,18 +103,32 @@ pub struct LayoutManager {
79103
 
80104
 impl LayoutManager {
81105
     /// Create a new layout manager
82
-    pub fn new(conn: &Connection, width: u32, position: NotificationPosition, max_visible: u32) -> Self {
83
-        let screen_width = conn.screen_width() as u32;
84
-        let screen_height = conn.screen_height() as u32;
106
+    pub fn new(
107
+        conn: &Connection,
108
+        width: u32,
109
+        position: NotificationPosition,
110
+        max_visible: u32,
111
+        monitor_config: &str,
112
+    ) -> Self {
113
+        let monitor_selection = MonitorSelection::from_str(monitor_config);
114
+        let monitor = Self::get_target_monitor_static(conn, &monitor_selection);
85115
 
86116
         debug!(
87
-            "LayoutManager: screen={}x{}, position={}, width={}, max={}",
88
-            screen_width, screen_height, position, width, max_visible
117
+            "LayoutManager: monitor='{}' ({}x{} at {},{}) position={}, width={}, max={}",
118
+            monitor.name,
119
+            monitor.rect.width,
120
+            monitor.rect.height,
121
+            monitor.rect.x,
122
+            monitor.rect.y,
123
+            position,
124
+            width,
125
+            max_visible
89126
         );
90127
 
91128
         Self {
92
-            screen_width,
93
-            screen_height,
129
+            monitor,
130
+            monitor_selection,
131
+            conn: conn.clone(),
94132
             notification_width: width,
95133
             margin: 16,
96134
             gap: 8,
@@ -100,6 +138,62 @@ impl LayoutManager {
100138
         }
101139
     }
102140
 
141
+    /// Get target monitor based on selection mode (static version for construction)
142
+    fn get_target_monitor_static(conn: &Connection, selection: &MonitorSelection) -> Monitor {
143
+        match selection {
144
+            MonitorSelection::Primary => {
145
+                primary_monitor(conn).unwrap_or_else(|e| {
146
+                    warn!("Failed to get primary monitor: {}, using fallback", e);
147
+                    Self::fallback_monitor(conn)
148
+                })
149
+            }
150
+            MonitorSelection::Mouse => {
151
+                monitor_at_pointer(conn).unwrap_or_else(|e| {
152
+                    warn!("Failed to get monitor at pointer: {}, using primary", e);
153
+                    primary_monitor(conn).unwrap_or_else(|_| Self::fallback_monitor(conn))
154
+                })
155
+            }
156
+            MonitorSelection::Named(name) => {
157
+                detect_monitors(conn)
158
+                    .ok()
159
+                    .and_then(|monitors| {
160
+                        monitors.into_iter().find(|m| m.name == *name)
161
+                    })
162
+                    .unwrap_or_else(|| {
163
+                        warn!("Monitor '{}' not found, using primary", name);
164
+                        primary_monitor(conn).unwrap_or_else(|_| Self::fallback_monitor(conn))
165
+                    })
166
+            }
167
+        }
168
+    }
169
+
170
+    /// Create a fallback monitor from screen dimensions
171
+    fn fallback_monitor(conn: &Connection) -> Monitor {
172
+        Monitor {
173
+            name: "default".to_string(),
174
+            rect: Rect::new(0, 0, conn.screen_width() as u32, conn.screen_height() as u32),
175
+            primary: true,
176
+            width_mm: 0,
177
+            height_mm: 0,
178
+        }
179
+    }
180
+
181
+    /// Update monitor if using mouse-follow mode
182
+    pub fn update_monitor_if_needed(&mut self) {
183
+        if self.monitor_selection == MonitorSelection::Mouse {
184
+            let new_monitor = Self::get_target_monitor_static(&self.conn, &self.monitor_selection);
185
+            if new_monitor.name != self.monitor.name {
186
+                info!("Monitor changed: {} -> {}", self.monitor.name, new_monitor.name);
187
+                self.monitor = new_monitor;
188
+            }
189
+        }
190
+    }
191
+
192
+    /// Get current monitor info
193
+    pub fn monitor(&self) -> &Monitor {
194
+        &self.monitor
195
+    }
196
+
103197
     /// Get the stack direction based on position
104198
     pub fn stack_direction(&self) -> StackDirection {
105199
         match self.position {
@@ -155,28 +249,32 @@ impl LayoutManager {
155249
         Rect::new(x, y, self.notification_width, height)
156250
     }
157251
 
158
-    /// Calculate X position based on screen position
252
+    /// Calculate X position based on monitor position
159253
     fn calculate_x(&self) -> i32 {
254
+        let mon = &self.monitor.rect;
160255
         match self.position {
161
-            NotificationPosition::TopLeft | NotificationPosition::BottomLeft => self.margin as i32,
256
+            NotificationPosition::TopLeft | NotificationPosition::BottomLeft => {
257
+                mon.x + self.margin as i32
258
+            }
162259
             NotificationPosition::TopRight | NotificationPosition::BottomRight => {
163
-                self.screen_width as i32 - self.notification_width as i32 - self.margin as i32
260
+                mon.x + mon.width as i32 - self.notification_width as i32 - self.margin as i32
164261
             }
165262
             NotificationPosition::TopCenter | NotificationPosition::BottomCenter => {
166
-                (self.screen_width as i32 - self.notification_width as i32) / 2
263
+                mon.x + (mon.width as i32 - self.notification_width as i32) / 2
167264
             }
168265
         }
169266
     }
170267
 
171268
     /// Calculate Y position based on slot and stack direction
172269
     fn calculate_y(&self, slot: usize, height: u32) -> i32 {
270
+        let mon = &self.monitor.rect;
173271
         // Calculate offset from edge based on slot position
174272
         let slot_offset = self.calculate_slot_offset(slot, height);
175273
 
176274
         match self.stack_direction() {
177
-            StackDirection::Down => self.margin as i32 + slot_offset,
275
+            StackDirection::Down => mon.y + self.margin as i32 + slot_offset,
178276
             StackDirection::Up => {
179
-                self.screen_height as i32 - height as i32 - self.margin as i32 - slot_offset
277
+                mon.y + mon.height as i32 - height as i32 - self.margin as i32 - slot_offset
180278
             }
181279
         }
182280
     }