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:
Breadway 2026-07-29 17:17:17 +08:00
parent 059e11cdeb
commit 566aeeed8b
5 changed files with 335 additions and 84 deletions

View file

@ -645,9 +645,16 @@ impl SimpleComponent for App {
widgets.center_box.set_center_widget(Some(&center_area));
widgets.center_box.set_end_widget(Some(&stats_box));
// Captured before `control_popover` moves into `model` below — needed
// by the screenshot dispatch just before this function returns.
// Captured before these move into `model` (or are otherwise dropped
// as bare locals, never stored on `App` at all) — needed by the
// screenshot dispatch just before this function returns.
let control_popover_for_screenshot = control_popover.clone();
let connectivity_popover_for_screenshot = connectivity_popover.clone();
let wifi_tab_btn_for_screenshot = wifi_tab_btn.clone();
let bt_tab_btn_for_screenshot = bt_tab_btn.clone();
let media_popover_for_screenshot = media_popover.clone();
let media_widget_for_screenshot = media_widget.clone();
let media_track_lbl_for_screenshot = media_track_lbl.clone();
let model = App {
workspaces: vec![],
@ -715,11 +722,43 @@ impl SimpleComponent for App {
bar::wifi::spawn_status_poller(sender.clone());
bar::media::spawn_poller(sender.clone());
widgets::client::spawn(sender.clone());
notifications::spawn();
osd::spawn();
// Screenshot mode primes these with sample content instead of the
// real D-Bus/pactl/backlight sources — see notifications::SampleKind
// and osd::SampleKind's doc comments.
let notif_sample = screenshot_req.as_ref().and_then(|r| match r.view.as_str() {
"notification" => Some(notifications::SampleKind::Normal),
"notification-critical" => Some(notifications::SampleKind::Critical),
_ => None,
});
let notification_window = notifications::spawn(notif_sample);
let osd_sample = screenshot_req.as_ref().and_then(|r| match r.view.as_str() {
"osd-volume" => Some(osd::SampleKind::Volume),
"osd-brightness" => Some(osd::SampleKind::Brightness),
_ => None,
});
let osd_window = osd::spawn(osd_sample);
if let Some(req) = screenshot_req {
screenshot::dispatch(&root, req, control_popover_for_screenshot);
let notification_window = matches!(req.view.as_str(), "notification" | "notification-critical")
.then_some(notification_window);
let osd_window = matches!(req.view.as_str(), "osd-volume" | "osd-brightness")
.then_some(osd_window);
screenshot::dispatch(
&root,
req,
screenshot::Handles {
control_popover: control_popover_for_screenshot,
connectivity_popover: connectivity_popover_for_screenshot,
wifi_tab_btn: wifi_tab_btn_for_screenshot,
bt_tab_btn: bt_tab_btn_for_screenshot,
media_popover: media_popover_for_screenshot,
media_widget: media_widget_for_screenshot,
media_track_lbl: media_track_lbl_for_screenshot,
notification_window,
osd_window,
},
);
}
ComponentParts { model, widgets }
@ -1145,7 +1184,7 @@ impl App {
if saved {
bar::wifi::spawn_join(ssid_clone.clone());
} else {
show_add_network_dialog(btn, ssid_clone.clone());
show_add_network_dialog(btn, ssid_clone.clone(), |_| {});
}
close_parent_popover(btn);
});
@ -1302,8 +1341,11 @@ fn close_parent_popover(widget: &gtk4::Button) {
}
/// Small modal prompting for a password, then saves + joins the network via
/// `breadcrumbs add` + `breadcrumbs join`.
fn show_add_network_dialog(anchor: &gtk4::Button, ssid: String) {
/// `breadcrumbs add` + `breadcrumbs join`. `on_build` runs on the freshly
/// built dialog *before* it's presented — screenshot mode's only hook point,
/// since `connect_map` registered any later would miss a map that already
/// happened. The real call site passes a no-op.
fn show_add_network_dialog(anchor: &impl IsA<gtk4::Widget>, ssid: String, on_build: impl FnOnce(&gtk4::Window)) {
let dialog = gtk4::Window::new();
dialog.set_title(Some(&format!("Add “{ssid}")));
dialog.set_resizable(false);
@ -1367,6 +1409,7 @@ fn show_add_network_dialog(anchor: &gtk4::Button, ssid: String) {
dialog_for_activate.close();
});
on_build(&dialog);
dialog.present();
entry.grab_focus();
}