notifications: add a dismiss button to the toast card

NOTIFICATION INTERACTION #A: a direct way to dismiss a toast, rather
than only auto-expiry or the D-Bus CloseNotification path. A small "x"
button floats in the card's top-right corner via a gtk4::Overlay
wrapping the existing content box, styled via the new
.notification-dismiss CSS class, so it doesn't add an extra header row
the approved demo's card layout never has.

dismiss_card emits NotificationClosed(id, DISMISSED_BY_USER) -- the
same freedesktop reason code (2) invoke_action/submit_reply already use
for their own user-initiated dismissals -- via the shared dismiss()
helper, which (as of the previous commit) already recomputes the
toast's clickable input region on every removal.
This commit is contained in:
Breadway 2026-08-26 19:31:01 +08:00
parent 3eada87c7d
commit c0a013c8ed

View file

@ -397,7 +397,34 @@ fn make_card(spec: CardSpec<'_>) -> gtk4::Box {
content.add_controller(gesture);
}
card.append(&content);
// NOTIFICATION INTERACTION #A: a direct dismiss control. Floated in
// the card's top-right corner via an Overlay rather than a full extra
// header row, so it doesn't add vertical bulk the approved demo's own
// card never has (see the `.notification-dismiss` CSS in theme.rs).
// `collect_interactive` (this module) picks it up the same way it
// picks up the action/reply buttons below, by walking the real widget
// tree — it doesn't need to know this button lives one level deeper,
// inside the overlay, than they do.
let overlay = gtk4::Overlay::new();
overlay.set_child(Some(&content));
let dismiss_btn = gtk4::Button::with_label("×");
dismiss_btn.add_css_class("notification-dismiss");
dismiss_btn.set_halign(gtk4::Align::End);
dismiss_btn.set_valign(gtk4::Align::Start);
let dismiss_invoke = Invoke {
conn: spec.conn.clone(),
cards: spec.cards.clone(),
cards_box: spec.cards_box.clone(),
window: spec.window.clone(),
id: spec.id,
};
dismiss_btn.connect_clicked(move |_| {
dismiss_card(dismiss_invoke.clone());
});
overlay.add_overlay(&dismiss_btn);
card.append(&overlay);
let visible: Vec<&Action> = spec
.actions
@ -500,6 +527,22 @@ fn invoke_action(invoke: Invoke, key: &str) {
});
}
/// NOTIFICATION INTERACTION #A: the card's own dismiss button. Unlike
/// `invoke_action`, this never emits `ActionInvoked` — there's no action
/// key here, the user just closed the toast unprompted — only the
/// spec-mandated `NotificationClosed(id, reason)`, with
/// `close_reason::DISMISSED_BY_USER` (freedesktop value 2, "dismissed by
/// the user") so clients are told properly, same reason code
/// `invoke_action` above and `submit_reply` below already use for their
/// own user-initiated dismissals.
fn dismiss_card(invoke: Invoke) {
relm4::spawn_local(async move {
if dismiss(&invoke.cards_box, &invoke.window, &invoke.cards, invoke.id) {
emit_closed(&invoke.conn, invoke.id, close_reason::DISMISSED_BY_USER).await;
}
});
}
fn submit_reply(entry: &gtk4::Entry, invoke: Invoke) {
let text = entry.text().to_string();
if text.trim().is_empty() {