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();