gardesk/garnotify / fd3c96b

Browse files

ui: add popup manager for X11 coordination

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
fd3c96b178e6fa52dfcf90e074649ca3910589ef
Parents
4fb616b
Tree
a0fe197

1 changed file

StatusFile+-
A garnotify/src/ui/popup_manager.rs 246 0
garnotify/src/ui/popup_manager.rsadded
@@ -0,0 +1,246 @@
1
+//! Popup manager for coordinating notification windows
2
+//!
3
+//! Manages the lifecycle of notification popups, X11 event handling,
4
+//! and coordinates with the layout manager for positioning.
5
+
6
+use anyhow::{Context, Result};
7
+use gartk_x11::Connection;
8
+use std::collections::HashMap;
9
+use std::sync::Arc;
10
+use tokio::sync::mpsc;
11
+use tracing::{debug, error, info, warn};
12
+use x11rb::protocol::Event as X11Event;
13
+
14
+use crate::config::Config;
15
+use crate::notification::{CloseReason, Notification};
16
+use crate::ui::layout::{LayoutManager, NotificationPosition};
17
+use crate::ui::popup::{calculate_notification_height, NotificationPopup};
18
+use crate::ui::{PopupCommand, PopupEvent};
19
+
20
+/// Manager for notification popup windows
21
+pub struct PopupManager {
22
+    /// X11 connection
23
+    conn: Connection,
24
+    /// Configuration
25
+    config: Arc<Config>,
26
+    /// Layout manager for positioning
27
+    layout: LayoutManager,
28
+    /// Active popup windows by notification ID
29
+    popups: HashMap<u32, NotificationPopup>,
30
+    /// Channel to send events back to daemon
31
+    event_tx: mpsc::Sender<PopupEvent>,
32
+    /// Window ID to notification ID mapping
33
+    window_to_notification: HashMap<u32, u32>,
34
+}
35
+
36
+impl PopupManager {
37
+    /// Create a new popup manager
38
+    pub fn new(config: Arc<Config>, event_tx: mpsc::Sender<PopupEvent>) -> Result<Self> {
39
+        let conn = Connection::connect(None).context("Failed to connect to X11")?;
40
+
41
+        let position = NotificationPosition::from_str(&config.geometry.position);
42
+        let layout = LayoutManager::new(
43
+            &conn,
44
+            config.geometry.width,
45
+            position,
46
+            config.geometry.max_visible,
47
+        );
48
+
49
+        info!(
50
+            "PopupManager initialized: position={}, width={}, max_visible={}",
51
+            position, config.geometry.width, config.geometry.max_visible
52
+        );
53
+
54
+        Ok(Self {
55
+            conn,
56
+            config,
57
+            layout,
58
+            popups: HashMap::new(),
59
+            event_tx,
60
+            window_to_notification: HashMap::new(),
61
+        })
62
+    }
63
+
64
+    /// Handle a popup command
65
+    pub fn handle_command(&mut self, cmd: PopupCommand) -> Result<()> {
66
+        match cmd {
67
+            PopupCommand::Show(notification) => {
68
+                self.show_notification(notification)?;
69
+            }
70
+            PopupCommand::Update { id, notification } => {
71
+                self.update_notification(id, notification)?;
72
+            }
73
+            PopupCommand::Close { id, reason } => {
74
+                self.close_notification(id, reason)?;
75
+            }
76
+            PopupCommand::CloseAll => {
77
+                self.close_all_notifications()?;
78
+            }
79
+        }
80
+        Ok(())
81
+    }
82
+
83
+    /// Show a new notification popup
84
+    fn show_notification(&mut self, notification: Notification) -> Result<()> {
85
+        let id = notification.id;
86
+
87
+        // Check if we have an available slot
88
+        if !self.layout.has_available_slot() {
89
+            warn!(
90
+                "No available slots for notification {}, max_visible={}",
91
+                id,
92
+                self.config.geometry.max_visible
93
+            );
94
+            // Could queue it or close oldest, for now just warn
95
+            return Ok(());
96
+        }
97
+
98
+        // Allocate a slot
99
+        let slot = self.layout.allocate_slot(id).ok_or_else(|| {
100
+            anyhow::anyhow!("Failed to allocate slot for notification {}", id)
101
+        })?;
102
+
103
+        // Calculate height based on content
104
+        let height = calculate_notification_height(
105
+            &notification,
106
+            self.config.geometry.width,
107
+            &self.config.appearance,
108
+        );
109
+
110
+        // Get geometry from layout
111
+        let rect = self.layout.calculate_geometry(slot, height);
112
+
113
+        // Create popup window
114
+        let mut popup = NotificationPopup::new(
115
+            self.conn.clone(),
116
+            notification,
117
+            rect,
118
+            &self.config.appearance,
119
+        )?;
120
+
121
+        // Track window ID mapping
122
+        let window_id = popup.window_id();
123
+        self.window_to_notification.insert(window_id, id);
124
+
125
+        // Show the popup
126
+        popup.show()?;
127
+
128
+        info!(
129
+            "Showed notification {} in slot {} at ({}, {})",
130
+            id, slot, rect.x, rect.y
131
+        );
132
+
133
+        // Store popup
134
+        self.popups.insert(id, popup);
135
+
136
+        Ok(())
137
+    }
138
+
139
+    /// Update an existing notification
140
+    fn update_notification(&mut self, id: u32, notification: Notification) -> Result<()> {
141
+        if let Some(popup) = self.popups.get_mut(&id) {
142
+            popup.update_notification(notification)?;
143
+            info!("Updated notification {}", id);
144
+        } else {
145
+            // Notification doesn't exist, show it as new
146
+            self.show_notification(notification)?;
147
+        }
148
+        Ok(())
149
+    }
150
+
151
+    /// Close a notification popup
152
+    fn close_notification(&mut self, id: u32, reason: CloseReason) -> Result<()> {
153
+        if let Some(popup) = self.popups.remove(&id) {
154
+            // Remove window mapping
155
+            self.window_to_notification.remove(&popup.window_id());
156
+
157
+            // Release the slot
158
+            self.layout.release_slot(id);
159
+
160
+            // Hide and drop the popup (window destroyed on drop)
161
+            popup.hide()?;
162
+
163
+            info!("Closed notification {}: reason={:?}", id, reason);
164
+
165
+            // Send event back to daemon
166
+            let _ = self.event_tx.try_send(PopupEvent::Closed { id, reason });
167
+        }
168
+        Ok(())
169
+    }
170
+
171
+    /// Close all notification popups
172
+    fn close_all_notifications(&mut self) -> Result<()> {
173
+        let ids: Vec<u32> = self.popups.keys().copied().collect();
174
+        for id in ids {
175
+            self.close_notification(id, CloseReason::Closed)?;
176
+        }
177
+        Ok(())
178
+    }
179
+
180
+    /// Poll and handle X11 events (non-blocking)
181
+    pub fn poll_events(&mut self) -> Result<()> {
182
+        while let Ok(Some(event)) = self.conn.poll_event() {
183
+            self.handle_x11_event(event)?;
184
+        }
185
+        Ok(())
186
+    }
187
+
188
+    /// Handle an X11 event
189
+    fn handle_x11_event(&mut self, event: X11Event) -> Result<()> {
190
+        match event {
191
+            X11Event::Expose(e) => {
192
+                // Re-render the exposed window
193
+                if let Some(&id) = self.window_to_notification.get(&e.window) {
194
+                    if let Some(popup) = self.popups.get_mut(&id) {
195
+                        popup.render()?;
196
+                        debug!("Re-rendered notification {} on expose", id);
197
+                    }
198
+                }
199
+            }
200
+            X11Event::ButtonPress(e) => {
201
+                // Click to dismiss
202
+                if let Some(&id) = self.window_to_notification.get(&e.event) {
203
+                    info!("Click on notification {} - dismissing", id);
204
+                    self.close_notification(id, CloseReason::Dismissed)?;
205
+
206
+                    // Notify daemon
207
+                    let _ = self.event_tx.try_send(PopupEvent::Dismissed(id));
208
+                }
209
+            }
210
+            X11Event::EnterNotify(e) => {
211
+                if let Some(&id) = self.window_to_notification.get(&e.event) {
212
+                    if let Some(popup) = self.popups.get_mut(&id) {
213
+                        popup.on_enter();
214
+                    }
215
+                }
216
+            }
217
+            X11Event::LeaveNotify(e) => {
218
+                if let Some(&id) = self.window_to_notification.get(&e.event) {
219
+                    if let Some(popup) = self.popups.get_mut(&id) {
220
+                        popup.on_leave();
221
+                    }
222
+                }
223
+            }
224
+            _ => {
225
+                // Ignore other events
226
+            }
227
+        }
228
+        Ok(())
229
+    }
230
+
231
+    /// Get the X11 connection file descriptor for polling
232
+    pub fn x11_fd(&self) -> std::os::unix::io::RawFd {
233
+        use std::os::unix::io::AsRawFd;
234
+        self.conn.inner().stream().as_raw_fd()
235
+    }
236
+
237
+    /// Check if any popups are currently shown
238
+    pub fn has_popups(&self) -> bool {
239
+        !self.popups.is_empty()
240
+    }
241
+
242
+    /// Get the number of active popups
243
+    pub fn popup_count(&self) -> usize {
244
+        self.popups.len()
245
+    }
246
+}