breadbar: capture every view, not just bar/control-panel
breadbar is "a bar + the notification daemon + the OSD" — the screenshot mode only covered the bar and its control-panel popover, missing seven more distinct surfaces: the WiFi/Bluetooth connectivity popover (both tabs), the media-controls popover, the standalone notification window (both normal and critical urgency), the standalone OSD window (volume and brightness), and the wifi add-network dialog. All ten views are now --screenshot targets. Two real refactors needed to make the standalone notification/OSD windows screenshot-able at all, not just bigger match arms: - Both windows are built deep inside an async task (`run_osd`/ `popup::run`), only reachable after the real event loop starts — no window handle ever existed for a caller to hook `connect_map` on before that. Window construction is now synchronous in `osd::spawn`/ `notifications::spawn`, handed to the async loop as a parameter instead of created inside it. - Screenshot mode seeds each with one fixed sample event (SampleKind) via the same channel the real pactl/backlight/D-Bus sources feed, instead of waiting for real hardware/dbus activity. For notifications specifically this also means skipping the real org.freedesktop.Notifications D-Bus registration entirely in screenshot mode — claiming that well-known name would just race the real breadbar (if running) for it, for no benefit, since nothing external needs to reach a screenshot-only instance. show_add_network_dialog gained an `on_build` hook (called before `.present()`, the only point `connect_map` can still catch the map) so screenshot mode can capture it without changing its one real call site's behavior — and its `anchor` parameter widened from `&Button` to `&impl IsA<Widget>` since the screenshot path anchors off a `ToggleButton`, not a `Button`.
This commit is contained in:
parent
059e11cdeb
commit
566aeeed8b
5 changed files with 335 additions and 84 deletions
|
|
@ -172,38 +172,90 @@ impl NotifServer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn spawn() {
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
let (conn_tx, conn_rx) = tokio::sync::oneshot::channel();
|
||||
/// A fixed sample notification for `--screenshot notification`/
|
||||
/// `notification-critical` — substitutes for a real `Notify` D-Bus call so a
|
||||
/// capture doesn't depend on some external sender firing one at just the
|
||||
/// right moment.
|
||||
pub enum SampleKind {
|
||||
Normal,
|
||||
Critical,
|
||||
}
|
||||
|
||||
relm4::spawn(async move {
|
||||
let server = NotifServer {
|
||||
tx,
|
||||
next_id: AtomicU32::new(1),
|
||||
sync_tags: Mutex::new(HashMap::new()),
|
||||
impl SampleKind {
|
||||
fn sample_event(&self) -> NotifEvent {
|
||||
let urgency = match self {
|
||||
SampleKind::Normal => Urgency::Normal,
|
||||
SampleKind::Critical => Urgency::Critical,
|
||||
};
|
||||
// Builder failures here would only occur with invalid static strings — safe to unwrap.
|
||||
let conn = zbus::connection::Builder::session()
|
||||
.unwrap()
|
||||
.name("org.freedesktop.Notifications")
|
||||
.unwrap()
|
||||
.serve_at("/org/freedesktop/Notifications", server)
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to claim org.freedesktop.Notifications on D-Bus session bus");
|
||||
// Hand the connection to popup::run so it can emit `NotificationClosed`
|
||||
// (spec-mandated whenever a notification actually goes away) — the
|
||||
// dismiss decisions all happen over there, not in this interface impl.
|
||||
let _ = conn_tx.send(conn);
|
||||
std::future::pending::<()>().await
|
||||
});
|
||||
|
||||
relm4::spawn_local(async move {
|
||||
if let Ok(conn) = conn_rx.await {
|
||||
popup::run(rx, conn).await;
|
||||
NotifEvent::Show {
|
||||
id: 1,
|
||||
app_name: "Sample App".into(),
|
||||
summary: "Sample notification".into(),
|
||||
body: "This is what a notification card looks like.".into(),
|
||||
urgency,
|
||||
expire: Expire::Never,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the notification window synchronously (see
|
||||
/// `popup::build_window`'s doc comment) and spawns the event loop that
|
||||
/// shows/updates/hides it.
|
||||
///
|
||||
/// `sample`: `Some` skips real D-Bus registration entirely and seeds the
|
||||
/// loop with one fixed sample event instead — screenshot mode only. Doing
|
||||
/// the real `org.freedesktop.Notifications` registration in every
|
||||
/// screenshot run would race the real breadbar (if running) for the same
|
||||
/// well-known name for no benefit, since nothing needs to reach this
|
||||
/// instance externally.
|
||||
pub fn spawn(sample: Option<SampleKind>) -> gtk4::Window {
|
||||
let (window, cards_box) = popup::build_window();
|
||||
let (tx, rx) = mpsc::channel(32);
|
||||
|
||||
match sample {
|
||||
Some(kind) => {
|
||||
let _ = tx.try_send(kind.sample_event());
|
||||
let window_for_loop = window.clone();
|
||||
relm4::spawn_local(async move {
|
||||
popup::run(window_for_loop, cards_box, rx, None).await;
|
||||
});
|
||||
}
|
||||
None => {
|
||||
let (conn_tx, conn_rx) = tokio::sync::oneshot::channel();
|
||||
|
||||
relm4::spawn(async move {
|
||||
let server = NotifServer {
|
||||
tx,
|
||||
next_id: AtomicU32::new(1),
|
||||
sync_tags: Mutex::new(HashMap::new()),
|
||||
};
|
||||
// Builder failures here would only occur with invalid static strings — safe to unwrap.
|
||||
let conn = zbus::connection::Builder::session()
|
||||
.unwrap()
|
||||
.name("org.freedesktop.Notifications")
|
||||
.unwrap()
|
||||
.serve_at("/org/freedesktop/Notifications", server)
|
||||
.unwrap()
|
||||
.build()
|
||||
.await
|
||||
.expect("failed to claim org.freedesktop.Notifications on D-Bus session bus");
|
||||
// Hand the connection to popup::run so it can emit `NotificationClosed`
|
||||
// (spec-mandated whenever a notification actually goes away) — the
|
||||
// dismiss decisions all happen over there, not in this interface impl.
|
||||
let _ = conn_tx.send(conn);
|
||||
std::future::pending::<()>().await
|
||||
});
|
||||
|
||||
let window_for_loop = window.clone();
|
||||
relm4::spawn_local(async move {
|
||||
if let Ok(conn) = conn_rx.await {
|
||||
popup::run(window_for_loop, cards_box, rx, Some(conn)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ mod close_reason {
|
|||
pub const CLOSE_NOTIFICATION_CALL: u32 = 3;
|
||||
}
|
||||
|
||||
pub async fn run(mut rx: Receiver<NotifEvent>, conn: zbus::Connection) {
|
||||
/// Builds the notification window synchronously — so a caller (screenshot
|
||||
/// mode in particular) has a real window to hook `connect_map` on before
|
||||
/// `run`'s event loop, which needs an async `zbus::Connection` handshake in
|
||||
/// the real path, ever starts.
|
||||
pub fn build_window() -> (gtk4::Window, gtk4::Box) {
|
||||
let window = create_window();
|
||||
let cards_box = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
|
||||
cards_box.set_margin_top(8);
|
||||
|
|
@ -31,7 +35,21 @@ pub async fn run(mut rx: Receiver<NotifEvent>, conn: zbus::Connection) {
|
|||
cards_box.set_margin_start(8);
|
||||
cards_box.set_margin_end(8);
|
||||
window.set_child(Some(&cards_box));
|
||||
(window, cards_box)
|
||||
}
|
||||
|
||||
/// `conn`: `None` in screenshot mode, which skips real D-Bus registration
|
||||
/// entirely (see `super::spawn`) — there's no external client that needs to
|
||||
/// reach a screenshot-only instance, and registering the well-known name
|
||||
/// would just race the real breadbar for it. `NotificationClosed` is a
|
||||
/// spec-mandated signal for real clients only, so it's simply not emitted
|
||||
/// when there's no real connection to emit it on.
|
||||
pub async fn run(
|
||||
window: gtk4::Window,
|
||||
cards_box: gtk4::Box,
|
||||
mut rx: Receiver<NotifEvent>,
|
||||
conn: Option<zbus::Connection>,
|
||||
) {
|
||||
let cards: Cards = Rc::new(RefCell::new(HashMap::new()));
|
||||
let generations: Generations = Rc::new(RefCell::new(HashMap::new()));
|
||||
|
||||
|
|
@ -109,8 +127,10 @@ fn dismiss(cards_box: >k4::Box, window: >k4::Window, cards: &Cards, id: u32)
|
|||
/// Emits the spec-mandated `NotificationClosed(id, reason)` signal. Sent
|
||||
/// directly over the connection rather than through the zbus interface
|
||||
/// macro's generated helper, since the dismiss decision happens here in the
|
||||
/// popup task, not inside `NotifServer`'s own method bodies.
|
||||
async fn emit_closed(conn: &zbus::Connection, id: u32, reason: u32) {
|
||||
/// popup task, not inside `NotifServer`'s own method bodies. No-op when
|
||||
/// `conn` is `None` (screenshot mode — see `run`'s doc comment).
|
||||
async fn emit_closed(conn: &Option<zbus::Connection>, id: u32, reason: u32) {
|
||||
let Some(conn) = conn else { return };
|
||||
let result = conn
|
||||
.emit_signal(
|
||||
None::<&str>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue