← luna

Commit 4f8b0ce

3
files
+116
added
-4
deleted
@@ -209,6 +209,26 @@ fn main() -> Result<(), Box<dyn Error>> {
209 209 if let Some(ws) = &workspaces {
210 210 ws.activate(i);
211 211 }
212 + } else if !open && !power_open {
213 + // Klik w toast: wywołaj akcję (jeśli jest) i zamknij powiadomienie.
214 + let area = Rect::from_xywh(
215 + 0.0,
216 + THICKNESS + 8.0,
217 + size.width,
218 + size.height - THICKNESS,
219 + );
220 + if let Some(i) = notes.hit(area, pe.position) {
221 + if let Some(n) = notes.get(i) {
222 + if let Some(bus) = &bus {
223 + if let Some(key) = n.actions.first() {
224 + bus.signal_action(n.id, key);
225 + }
226 + // 2 = zamknięte przez użytkownika.
227 + bus.signal_closed(n.id, 2);
228 + }
229 + }
230 + notes.remove(i);
231 + }
212 232 }
213 233 }
214 234 Event::PointerButton(pe)
@@ -330,7 +350,10 @@ fn main() -> Result<(), Box<dyn Error>> {
330 350 fn notification_from(inc: Incoming) -> Notification {
331 351 let icon = icon_for(&inc.app_name);
332 352 let timeout = inc.timeout_secs();
333 - let mut n = Notification::new(inc.app_name, inc.summary, inc.body).with_icon(icon);
353 + let mut n = Notification::new(inc.app_name, inc.summary, inc.body)
354 + .with_icon(icon)
355 + .with_id(inc.id)
356 + .with_actions(inc.actions);
334 357 n.timeout = timeout;
335 358 n
336 359 }
@@ -18,6 +18,8 @@ use zbus::zvariant::OwnedValue;
18 18 /// Żądanie powiadomienia odebrane z magistrali.
19 19 #[derive(Debug, Clone, Default)]
20 20 pub struct Incoming {
21 + /// Identyfikator nadany przez nas (do `NotificationClosed`/`ActionInvoked`).
22 + pub id: u32,
21 23 /// Nazwa aplikacji wysyłającej.
22 24 pub app_name: String,
23 25 /// Podsumowanie (tytuł toasta).
@@ -26,6 +28,8 @@ pub struct Incoming {
26 28 pub body: String,
27 29 /// Czas życia w ms: `-1` = domyślny serwera, `0` = trwałe, `> 0` = jawny.
28 30 pub expire_ms: i32,
31 + /// Klucze akcji (bez etykiet) do `ActionInvoked`.
32 + pub actions: Vec<String>,
29 33 }
30 34
31 35 impl Incoming {
@@ -62,19 +66,23 @@ impl Server {
62 66 _app_icon: String,
63 67 summary: String,
64 68 body: String,
65 - _actions: Vec<String>,
69 + actions: Vec<String>,
66 70 _hints: HashMap<String, OwnedValue>,
67 71 expire_ms: i32,
68 72 ) -> u32 {
73 + let keys: Vec<String> = actions.chunks_exact(2).map(|c| c[0].clone()).collect();
69 74 let id = {
70 75 let mut q = self.queue.lock().unwrap_or_else(|e| e.into_inner());
76 + let id = q.iter().map(|i| i.id).max().unwrap_or(0) + 1;
71 77 q.push(Incoming {
78 + id,
72 79 app_name,
73 80 summary,
74 81 body,
75 82 expire_ms,
83 + actions: keys,
76 84 });
77 - q.len() as u32
85 + id
78 86 };
79 87 // Obudź pętlę powłoki, żeby nowy toast został narysowany.
80 88 self.waker.wake();
@@ -126,6 +134,30 @@ impl NotificationsBus {
126 134 let mut q = self.queue.lock().unwrap_or_else(|e| e.into_inner());
127 135 std::mem::take(&mut *q)
128 136 }
137 +
138 + /// Sygnalizuje zamknięcie powiadomienia (`NotificationClosed`).
139 + ///
140 + /// `reason`: 1 = wygasło, 2 = zamknięte przez użytkownika, 3 = `CloseNotification`.
141 + pub fn signal_closed(&self, id: u32, reason: u32) {
142 + let _ = self._conn.emit_signal(
143 + None::<&str>,
144 + "/org/freedesktop/Notifications",
145 + "org.freedesktop.Notifications",
146 + "NotificationClosed",
147 + &(id, reason),
148 + );
149 + }
150 +
151 + /// Sygnalizuje wywołanie akcji (`ActionInvoked`).
152 + pub fn signal_action(&self, id: u32, key: &str) {
153 + let _ = self._conn.emit_signal(
154 + None::<&str>,
155 + "/org/freedesktop/Notifications",
156 + "org.freedesktop.Notifications",
157 + "ActionInvoked",
158 + &(id, key),
159 + );
160 + }
129 161 }
130 162
131 163 #[cfg(test)]
@@ -5,7 +5,7 @@
5 5 //! `org.freedesktop.Notifications` to kolejny krok (patrz roadmapa).
6 6
7 7 use libluna_core::style::weight;
8 -use libluna_core::Rect;
8 +use libluna_core::{Point, Rect};
9 9 use libluna_widgets::{paint_icon, IconName, PaintCtx};
10 10
11 11 /// Czas animacji wejścia/wyjścia (s).
@@ -28,6 +28,10 @@ pub struct Notification {
28 28 pub icon: IconName,
29 29 /// Czas życia (s); 0 = nie zamykaj automatycznie.
30 30 pub timeout: f64,
31 + /// Identyfikator z magistrali D‑Bus (0 = powiadomienie lokalne).
32 + pub id: u32,
33 + /// Klucze akcji (`ActionInvoked`); puste, gdy brak akcji.
34 + pub actions: Vec<String>,
31 35 }
32 36
33 37 impl Notification {
@@ -38,6 +42,8 @@ impl Notification {
38 42 body: body.into(),
39 43 icon: IconName::Info,
40 44 timeout: 5.0,
45 + id: 0,
46 + actions: Vec::new(),
41 47 }
42 48 }
43 49
@@ -45,6 +51,18 @@ impl Notification {
45 51 self.icon = icon;
46 52 self
47 53 }
54 +
55 + /// Ustawia identyfikator z magistrali (do wysłania `NotificationClosed`).
56 + pub fn with_id(mut self, id: u32) -> Self {
57 + self.id = id;
58 + self
59 + }
60 +
61 + /// Ustawia klucze akcji (do wysłania `ActionInvoked`).
62 + pub fn with_actions(mut self, actions: Vec<String>) -> Self {
63 + self.actions = actions;
64 + self
65 + }
48 66 }
49 67
50 68 /// Kolejka powiadomień z animacją.
@@ -85,6 +103,25 @@ impl Notifications {
85 103 self.items.is_empty()
86 104 }
87 105
106 + /// Indeks powiadomienia pod punktem `p` (od wierzchu stosu).
107 + pub fn hit(&self, area: Rect, p: Point) -> Option<usize> {
108 + (0..self.items.len())
109 + .rev()
110 + .find(|&i| Self::rect_in(area, i).contains(p))
111 + }
112 +
113 + /// Powiadomienie o indeksie `i`.
114 + pub fn get(&self, i: usize) -> Option<&Notification> {
115 + self.items.get(i).map(|(n, _)| n)
116 + }
117 +
118 + /// Usuwa powiadomienie o indeksie `i` (np. po kliknięciu).
119 + pub fn remove(&mut self, i: usize) {
120 + if i < self.items.len() {
121 + self.items.remove(i);
122 + }
123 + }
124 +
88 125 /// Prostokąt `i`-tego powiadomienia w kolumnie zakotwiczonej w prawym-górnym
89 126 /// narożniku obszaru `area`.
90 127 pub fn rect_in(area: Rect, i: usize) -> Rect {
@@ -208,6 +245,26 @@ mod tests {
208 245 assert!(b.y() > a.y());
209 246 }
210 247
248 + #[test]
249 + fn hit_and_remove_by_point() {
250 + let mut n = Notifications::new();
251 + n.push(
252 + Notification::new("A", "t", "b")
253 + .with_id(7)
254 + .with_actions(vec!["default".to_owned()]),
255 + );
256 + n.advance(0.3);
257 + let area = Rect::from_xywh(0.0, 40.0, 1280.0, 200.0);
258 + let rect = Notifications::rect_in(area, 0);
259 + assert_eq!(n.hit(area, rect.center()), Some(0));
260 + assert_eq!(n.hit(area, Point::new(-10.0, -10.0)), None);
261 + let n0 = n.get(0).unwrap();
262 + assert_eq!(n0.id, 7);
263 + assert_eq!(n0.actions, vec!["default".to_owned()]);
264 + n.remove(0);
265 + assert!(n.is_empty());
266 + }
267 +
211 268 #[test]
212 269 fn paints_without_window() {
213 270 let theme = Theme::dark();