gardesk/garnotify / 81eeb91

Browse files

ui: add layout manager for positioning

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
81eeb9160a81bf6744758246e169c2d6485fc049
Parents
78dddba
Tree
7227626

1 changed file

StatusFile+-
A garnotify/src/ui/layout.rs 205 0
garnotify/src/ui/layout.rsadded
@@ -0,0 +1,205 @@
1
+//! Layout management for notification positioning and stacking
2
+//!
3
+//! Handles positioning notifications on screen and managing the stack
4
+//! when multiple notifications are visible.
5
+
6
+use gartk_core::Rect;
7
+use gartk_x11::Connection;
8
+use tracing::debug;
9
+
10
+/// Screen position for notifications
11
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12
+pub enum NotificationPosition {
13
+    TopLeft,
14
+    #[default]
15
+    TopRight,
16
+    BottomLeft,
17
+    BottomRight,
18
+    TopCenter,
19
+    BottomCenter,
20
+}
21
+
22
+impl NotificationPosition {
23
+    /// Parse from string (for config)
24
+    pub fn from_str(s: &str) -> Self {
25
+        match s.to_lowercase().as_str() {
26
+            "top-left" | "topleft" => Self::TopLeft,
27
+            "top-right" | "topright" => Self::TopRight,
28
+            "bottom-left" | "bottomleft" => Self::BottomLeft,
29
+            "bottom-right" | "bottomright" => Self::BottomRight,
30
+            "top-center" | "topcenter" => Self::TopCenter,
31
+            "bottom-center" | "bottomcenter" => Self::BottomCenter,
32
+            _ => Self::TopRight,
33
+        }
34
+    }
35
+}
36
+
37
+impl std::fmt::Display for NotificationPosition {
38
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39
+        let s = match self {
40
+            Self::TopLeft => "top-left",
41
+            Self::TopRight => "top-right",
42
+            Self::BottomLeft => "bottom-left",
43
+            Self::BottomRight => "bottom-right",
44
+            Self::TopCenter => "top-center",
45
+            Self::BottomCenter => "bottom-center",
46
+        };
47
+        write!(f, "{}", s)
48
+    }
49
+}
50
+
51
+/// Direction notifications stack
52
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53
+pub enum StackDirection {
54
+    /// Stack downward (for top positions)
55
+    Down,
56
+    /// Stack upward (for bottom positions)
57
+    Up,
58
+}
59
+
60
+/// Layout manager for notification popups
61
+pub struct LayoutManager {
62
+    /// Screen width
63
+    screen_width: u32,
64
+    /// Screen height
65
+    screen_height: u32,
66
+    /// Notification width
67
+    notification_width: u32,
68
+    /// Margin from screen edges
69
+    margin: u32,
70
+    /// Gap between stacked notifications
71
+    gap: u32,
72
+    /// Position anchor
73
+    position: NotificationPosition,
74
+    /// Maximum visible notifications
75
+    max_visible: u32,
76
+    /// Currently occupied slots (notification IDs)
77
+    slots: Vec<Option<u32>>,
78
+}
79
+
80
+impl LayoutManager {
81
+    /// 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;
85
+
86
+        debug!(
87
+            "LayoutManager: screen={}x{}, position={}, width={}, max={}",
88
+            screen_width, screen_height, position, width, max_visible
89
+        );
90
+
91
+        Self {
92
+            screen_width,
93
+            screen_height,
94
+            notification_width: width,
95
+            margin: 16,
96
+            gap: 8,
97
+            position,
98
+            max_visible,
99
+            slots: vec![None; max_visible as usize],
100
+        }
101
+    }
102
+
103
+    /// Get the stack direction based on position
104
+    pub fn stack_direction(&self) -> StackDirection {
105
+        match self.position {
106
+            NotificationPosition::TopLeft
107
+            | NotificationPosition::TopRight
108
+            | NotificationPosition::TopCenter => StackDirection::Down,
109
+            NotificationPosition::BottomLeft
110
+            | NotificationPosition::BottomRight
111
+            | NotificationPosition::BottomCenter => StackDirection::Up,
112
+        }
113
+    }
114
+
115
+    /// Allocate a slot for a notification, returns the slot index
116
+    pub fn allocate_slot(&mut self, notification_id: u32) -> Option<usize> {
117
+        // Find first empty slot
118
+        for (i, slot) in self.slots.iter_mut().enumerate() {
119
+            if slot.is_none() {
120
+                *slot = Some(notification_id);
121
+                debug!("Allocated slot {} for notification {}", i, notification_id);
122
+                return Some(i);
123
+            }
124
+        }
125
+        debug!(
126
+            "No slots available for notification {} (max={})",
127
+            notification_id, self.max_visible
128
+        );
129
+        None
130
+    }
131
+
132
+    /// Release a slot
133
+    pub fn release_slot(&mut self, notification_id: u32) {
134
+        for slot in &mut self.slots {
135
+            if *slot == Some(notification_id) {
136
+                *slot = None;
137
+                debug!("Released slot for notification {}", notification_id);
138
+                return;
139
+            }
140
+        }
141
+    }
142
+
143
+    /// Get the slot index for a notification
144
+    pub fn get_slot(&self, notification_id: u32) -> Option<usize> {
145
+        self.slots
146
+            .iter()
147
+            .position(|s| *s == Some(notification_id))
148
+    }
149
+
150
+    /// Calculate notification geometry for a given slot and height
151
+    pub fn calculate_geometry(&self, slot: usize, height: u32) -> Rect {
152
+        let x = self.calculate_x();
153
+        let y = self.calculate_y(slot, height);
154
+
155
+        Rect::new(x, y, self.notification_width, height)
156
+    }
157
+
158
+    /// Calculate X position based on screen position
159
+    fn calculate_x(&self) -> i32 {
160
+        match self.position {
161
+            NotificationPosition::TopLeft | NotificationPosition::BottomLeft => self.margin as i32,
162
+            NotificationPosition::TopRight | NotificationPosition::BottomRight => {
163
+                self.screen_width as i32 - self.notification_width as i32 - self.margin as i32
164
+            }
165
+            NotificationPosition::TopCenter | NotificationPosition::BottomCenter => {
166
+                (self.screen_width as i32 - self.notification_width as i32) / 2
167
+            }
168
+        }
169
+    }
170
+
171
+    /// Calculate Y position based on slot and stack direction
172
+    fn calculate_y(&self, slot: usize, height: u32) -> i32 {
173
+        // Calculate offset from edge based on slot position
174
+        let slot_offset = self.calculate_slot_offset(slot, height);
175
+
176
+        match self.stack_direction() {
177
+            StackDirection::Down => self.margin as i32 + slot_offset,
178
+            StackDirection::Up => {
179
+                self.screen_height as i32 - height as i32 - self.margin as i32 - slot_offset
180
+            }
181
+        }
182
+    }
183
+
184
+    /// Calculate cumulative offset for a slot (accounts for varying heights)
185
+    fn calculate_slot_offset(&self, slot: usize, height: u32) -> i32 {
186
+        // For simplicity, assume uniform height for now
187
+        // In a full implementation, we'd track actual heights of each slot
188
+        (slot as i32) * (height as i32 + self.gap as i32)
189
+    }
190
+
191
+    /// Get the notification width
192
+    pub fn notification_width(&self) -> u32 {
193
+        self.notification_width
194
+    }
195
+
196
+    /// Check if there are available slots
197
+    pub fn has_available_slot(&self) -> bool {
198
+        self.slots.iter().any(|s| s.is_none())
199
+    }
200
+
201
+    /// Get number of active notifications
202
+    pub fn active_count(&self) -> usize {
203
+        self.slots.iter().filter(|s| s.is_some()).count()
204
+    }
205
+}