@@ -4,8 +4,8 @@ |
| 4 | 4 | //! when multiple notifications are visible. |
| 5 | 5 | |
| 6 | 6 | 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}; |
| 9 | 9 | |
| 10 | 10 | /// Screen position for notifications |
| 11 | 11 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
@@ -57,12 +57,36 @@ pub enum StackDirection { |
| 57 | 57 | Up, |
| 58 | 58 | } |
| 59 | 59 | |
| 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 | + |
| 60 | 82 | /// Layout manager for notification popups |
| 61 | 83 | 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, |
| 66 | 90 | /// Notification width |
| 67 | 91 | notification_width: u32, |
| 68 | 92 | /// Margin from screen edges |
@@ -79,18 +103,32 @@ pub struct LayoutManager { |
| 79 | 103 | |
| 80 | 104 | impl LayoutManager { |
| 81 | 105 | /// 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); |
| 85 | 115 | |
| 86 | 116 | 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 |
| 89 | 126 | ); |
| 90 | 127 | |
| 91 | 128 | Self { |
| 92 | | - screen_width, |
| 93 | | - screen_height, |
| 129 | + monitor, |
| 130 | + monitor_selection, |
| 131 | + conn: conn.clone(), |
| 94 | 132 | notification_width: width, |
| 95 | 133 | margin: 16, |
| 96 | 134 | gap: 8, |
@@ -100,6 +138,62 @@ impl LayoutManager { |
| 100 | 138 | } |
| 101 | 139 | } |
| 102 | 140 | |
| 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 | + |
| 103 | 197 | /// Get the stack direction based on position |
| 104 | 198 | pub fn stack_direction(&self) -> StackDirection { |
| 105 | 199 | match self.position { |
@@ -155,28 +249,32 @@ impl LayoutManager { |
| 155 | 249 | Rect::new(x, y, self.notification_width, height) |
| 156 | 250 | } |
| 157 | 251 | |
| 158 | | - /// Calculate X position based on screen position |
| 252 | + /// Calculate X position based on monitor position |
| 159 | 253 | fn calculate_x(&self) -> i32 { |
| 254 | + let mon = &self.monitor.rect; |
| 160 | 255 | 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 | + } |
| 162 | 259 | 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 |
| 164 | 261 | } |
| 165 | 262 | 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 |
| 167 | 264 | } |
| 168 | 265 | } |
| 169 | 266 | } |
| 170 | 267 | |
| 171 | 268 | /// Calculate Y position based on slot and stack direction |
| 172 | 269 | fn calculate_y(&self, slot: usize, height: u32) -> i32 { |
| 270 | + let mon = &self.monitor.rect; |
| 173 | 271 | // Calculate offset from edge based on slot position |
| 174 | 272 | let slot_offset = self.calculate_slot_offset(slot, height); |
| 175 | 273 | |
| 176 | 274 | match self.stack_direction() { |
| 177 | | - StackDirection::Down => self.margin as i32 + slot_offset, |
| 275 | + StackDirection::Down => mon.y + self.margin as i32 + slot_offset, |
| 178 | 276 | 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 |
| 180 | 278 | } |
| 181 | 279 | } |
| 182 | 280 | } |