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_center_widget(Some(&center_area));
widgets.center_box.set_end_widget(Some(&stats_box)); widgets.center_box.set_end_widget(Some(&stats_box));
// Captured before `control_popover` moves into `model` below — needed // Captured before these move into `model` (or are otherwise dropped
// by the screenshot dispatch just before this function returns. // 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 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 { let model = App {
workspaces: vec![], workspaces: vec![],
@ -715,11 +722,43 @@ impl SimpleComponent for App {
bar::wifi::spawn_status_poller(sender.clone()); bar::wifi::spawn_status_poller(sender.clone());
bar::media::spawn_poller(sender.clone()); bar::media::spawn_poller(sender.clone());
widgets::client::spawn(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 { 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 } ComponentParts { model, widgets }
@ -1145,7 +1184,7 @@ impl App {
if saved { if saved {
bar::wifi::spawn_join(ssid_clone.clone()); bar::wifi::spawn_join(ssid_clone.clone());
} else { } else {
show_add_network_dialog(btn, ssid_clone.clone()); show_add_network_dialog(btn, ssid_clone.clone(), |_| {});
} }
close_parent_popover(btn); 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 /// Small modal prompting for a password, then saves + joins the network via
/// `breadcrumbs add` + `breadcrumbs join`. /// `breadcrumbs add` + `breadcrumbs join`. `on_build` runs on the freshly
fn show_add_network_dialog(anchor: &gtk4::Button, ssid: String) { /// 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(); let dialog = gtk4::Window::new();
dialog.set_title(Some(&format!("Add “{ssid}"))); dialog.set_title(Some(&format!("Add “{ssid}")));
dialog.set_resizable(false); dialog.set_resizable(false);
@ -1367,6 +1409,7 @@ fn show_add_network_dialog(anchor: &gtk4::Button, ssid: String) {
dialog_for_activate.close(); dialog_for_activate.close();
}); });
on_build(&dialog);
dialog.present(); dialog.present();
entry.grab_focus(); entry.grab_focus();
} }

View file

@ -172,8 +172,55 @@ impl NotifServer {
} }
} }
pub fn spawn() { /// 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,
}
impl SampleKind {
fn sample_event(&self) -> NotifEvent {
let urgency = match self {
SampleKind::Normal => Urgency::Normal,
SampleKind::Critical => Urgency::Critical,
};
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); 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(); let (conn_tx, conn_rx) = tokio::sync::oneshot::channel();
relm4::spawn(async move { relm4::spawn(async move {
@ -199,12 +246,17 @@ pub fn spawn() {
std::future::pending::<()>().await std::future::pending::<()>().await
}); });
let window_for_loop = window.clone();
relm4::spawn_local(async move { relm4::spawn_local(async move {
if let Ok(conn) = conn_rx.await { if let Ok(conn) = conn_rx.await {
popup::run(rx, conn).await; popup::run(window_for_loop, cards_box, rx, Some(conn)).await;
} }
}); });
} }
}
window
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View file

@ -23,7 +23,11 @@ mod close_reason {
pub const CLOSE_NOTIFICATION_CALL: u32 = 3; 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 window = create_window();
let cards_box = gtk4::Box::new(gtk4::Orientation::Vertical, 4); let cards_box = gtk4::Box::new(gtk4::Orientation::Vertical, 4);
cards_box.set_margin_top(8); 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_start(8);
cards_box.set_margin_end(8); cards_box.set_margin_end(8);
window.set_child(Some(&cards_box)); 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 cards: Cards = Rc::new(RefCell::new(HashMap::new()));
let generations: Generations = Rc::new(RefCell::new(HashMap::new())); let generations: Generations = Rc::new(RefCell::new(HashMap::new()));
@ -109,8 +127,10 @@ fn dismiss(cards_box: &gtk4::Box, window: &gtk4::Window, cards: &Cards, id: u32)
/// Emits the spec-mandated `NotificationClosed(id, reason)` signal. Sent /// Emits the spec-mandated `NotificationClosed(id, reason)` signal. Sent
/// directly over the connection rather than through the zbus interface /// directly over the connection rather than through the zbus interface
/// macro's generated helper, since the dismiss decision happens here in the /// macro's generated helper, since the dismiss decision happens here in the
/// popup task, not inside `NotifServer`'s own method bodies. /// popup task, not inside `NotifServer`'s own method bodies. No-op when
async fn emit_closed(conn: &zbus::Connection, id: u32, reason: u32) { /// `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 let result = conn
.emit_signal( .emit_signal(
None::<&str>, None::<&str>,

View file

@ -9,14 +9,50 @@ enum OsdEvent {
Brightness { pct: u8 }, Brightness { pct: u8 },
} }
pub fn spawn() { /// A fixed sample event for `--screenshot osd-volume`/`osd-brightness` —
/// substitutes for the real `pactl subscribe`/backlight-sysfs watchers so a
/// capture doesn't depend on this machine's actual volume/brightness at
/// capture time.
pub enum SampleKind {
Volume,
Brightness,
}
impl SampleKind {
fn sample_event(&self) -> OsdEvent {
match self {
SampleKind::Volume => OsdEvent::Volume { pct: 65, muted: false },
SampleKind::Brightness => OsdEvent::Brightness { pct: 80 },
}
}
}
/// Builds the OSD window synchronously (so a caller — screenshot mode, via
/// `sample`, in particular — has a real window to hook `connect_map` on
/// before the async event loop below ever runs) and spawns the event loop
/// that shows/updates/hides it.
///
/// `sample`: `Some` skips the real volume/brightness watchers entirely and
/// seeds the loop with one fixed sample event instead — screenshot mode
/// only, so a capture never depends on (or is disrupted by) this machine's
/// actual audio/backlight state.
pub fn spawn(sample: Option<SampleKind>) -> gtk4::Window {
let (tx, rx) = mpsc::channel::<OsdEvent>(8); let (tx, rx) = mpsc::channel::<OsdEvent>(8);
match sample {
Some(kind) => {
let _ = tx.try_send(kind.sample_event());
}
None => {
let tx1 = tx.clone(); let tx1 = tx.clone();
std::thread::spawn(move || volume_watcher(tx1)); std::thread::spawn(move || volume_watcher(tx1));
std::thread::spawn(move || brightness_watcher(tx)); std::thread::spawn(move || brightness_watcher(tx));
}
}
relm4::spawn_local(run_osd(rx)); let window = create_window();
relm4::spawn_local(run_osd(window.clone(), rx));
window
} }
fn volume_watcher(tx: mpsc::Sender<OsdEvent>) { fn volume_watcher(tx: mpsc::Sender<OsdEvent>) {
@ -119,9 +155,7 @@ fn brightness_watcher(tx: mpsc::Sender<OsdEvent>) {
} }
} }
async fn run_osd(mut rx: mpsc::Receiver<OsdEvent>) { async fn run_osd(window: gtk4::Window, mut rx: mpsc::Receiver<OsdEvent>) {
let window = create_window();
let container = gtk4::Box::new(gtk4::Orientation::Horizontal, 0); let container = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
container.set_margin_top(10); container.set_margin_top(10);
container.set_margin_bottom(10); container.set_margin_bottom(10);

View file

@ -6,6 +6,13 @@
//! grabbing pixels — the surface (or, for popover views, the popover itself) //! grabbing pixels — the surface (or, for popover views, the popover itself)
//! genuinely isn't on screen yet before that fires, so a fixed delay would //! genuinely isn't on screen yet before that fires, so a fixed delay would
//! either race a slow first paint or pad every fast one for nothing. //! either race a slow first paint or pad every fast one for nothing.
//!
//! breadbar is "a bar + the notification daemon + the OSD" (see its own
//! module docs), so its screenshot views span three separate top-level
//! surfaces, not just the bar: the bar itself and its popovers (this
//! module, anchored off `root`), plus the standalone notification and OSD
//! windows (`notifications::spawn`/`osd::spawn`, built and primed with
//! sample data by `main.rs` before `dispatch` runs — see [`Handles`]).
use clap::Parser; use clap::Parser;
use gtk4::prelude::*; use gtk4::prelude::*;
@ -17,15 +24,12 @@ use std::time::Duration;
/// anything has been drawn into it. /// anything has been drawn into it.
const SETTLE_DELAY: Duration = Duration::from_millis(300); const SETTLE_DELAY: Duration = Duration::from_millis(300);
/// Settle time for the control-panel view specifically: longer than /// Settle time for views whose content depends on `bar::stats::spawn_poller`'s
/// [`SETTLE_DELAY`] because the CPU/RAM/PWR/GPU/network labels there aren't /// 2-second background loop (control-panel's CPU/RAM/PWR/GPU/network labels,
/// populated by the popover's own load (that only covers volume/brightness/ /// gated on popover visibility) or a similar live-data popover load
/// sinks, see `bar::control::spawn_load`) — they're refreshed by /// (connectivity's wifi/bluetooth scan) — capturing any sooner leaves
/// `bar::stats::spawn_poller`'s 2-second background loop, gated on /// placeholder dashes/"Scanning…" instead of real content.
/// `control_popover.is_visible()` at each tick. Capturing any sooner than one const LIVE_DATA_SETTLE_DELAY: Duration = Duration::from_millis(2_200);
/// full poll interval after the popover opens leaves them at their initial
/// placeholder dashes.
const CONTROL_PANEL_SETTLE_DELAY: Duration = Duration::from_millis(2_200);
/// Delay between the bar's own `map` and calling `popover.popup()`. Calling /// Delay between the bar's own `map` and calling `popover.popup()`. Calling
/// `popup()` synchronously from inside the root window's `map` handler /// `popup()` synchronously from inside the root window's `map` handler
@ -36,11 +40,24 @@ const CONTROL_PANEL_SETTLE_DELAY: Duration = Duration::from_millis(2_200);
/// finish first is what makes it actually render. /// finish first is what makes it actually render.
const PRE_POPUP_DELAY: Duration = Duration::from_millis(300); const PRE_POPUP_DELAY: Duration = Duration::from_millis(300);
const KNOWN_VIEWS: &[&str] = &[
"bar",
"control-panel",
"connectivity-wifi",
"connectivity-bluetooth",
"media-popover",
"notification",
"notification-critical",
"osd-volume",
"osd-brightness",
"wifi-add-dialog",
];
#[derive(Parser)] #[derive(Parser)]
#[command(name = "breadbar")] #[command(name = "breadbar")]
pub struct Cli { pub struct Cli {
/// Render the named view, capture it, then exit instead of running /// Render the named view, capture it, then exit instead of running
/// normally. Known views: "bar", "control-panel". /// normally. See `screenshot::KNOWN_VIEWS` for the full list.
#[arg(long)] #[arg(long)]
pub screenshot: Option<String>, pub screenshot: Option<String>,
@ -80,20 +97,37 @@ impl Cli {
} }
} }
/// Wire up the given view's screenshot sequence. Called once from `init()`, /// Every widget/window `dispatch` might need, gathered by `main.rs`'s
/// after the window and its popovers exist but before the component finishes /// `init()` — most of these are plain locals there that never otherwise
/// initializing — every path here ends by exiting the process, it never /// outlive `init()` (never stored on `App`), so they have to be cloned out
/// returns control to the normal bar UI. /// before dispatch time same as `control_popover` always was.
pub struct Handles {
pub control_popover: gtk4::Popover,
pub connectivity_popover: gtk4::Popover,
pub wifi_tab_btn: gtk4::ToggleButton,
pub bt_tab_btn: gtk4::ToggleButton,
pub media_popover: gtk4::Popover,
pub media_widget: gtk4::Box,
pub media_track_lbl: gtk4::Label,
/// Already built and primed with sample content by `main.rs` (via
/// `notifications::spawn(Some(kind))`) when `req.view` calls for it —
/// `None` otherwise.
pub notification_window: Option<gtk4::Window>,
/// Same deal as `notification_window`, via `osd::spawn(Some(kind))`.
pub osd_window: Option<gtk4::Window>,
}
/// The bar's fixed height — matches `root.set_exclusive_zone(32)` / /// The bar's fixed height — matches `root.set_exclusive_zone(32)` /
/// `set_default_height: 32` in `main.rs`. Unlike the control-panel's full /// `set_default_height: 32` in `main.rs`. Unlike the other views' full
/// canvas, this never varies with `--width`/`--height`. /// canvas, this never varies with `--width`/`--height`.
const BAR_HEIGHT: i32 = 32; const BAR_HEIGHT: i32 = 32;
pub fn dispatch(root: &gtk4::ApplicationWindow, req: ScreenshotRequest, control_popover: gtk4::Popover) { pub fn dispatch(root: &gtk4::ApplicationWindow, req: ScreenshotRequest, handles: Handles) {
let output = req.output;
let (width, height) = (req.width as i32, req.height as i32);
match req.view.as_str() { match req.view.as_str() {
"bar" => { "bar" => {
let output = req.output;
let width = req.width as i32;
root.connect_map(move |_| { root.connect_map(move |_| {
let output = output.clone(); let output = output.clone();
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
@ -102,33 +136,101 @@ pub fn dispatch(root: &gtk4::ApplicationWindow, req: ScreenshotRequest, control_
}); });
} }
"control-panel" => { "control-panel" => {
let output = req.output; open_popover_on_root_map(root, handles.control_popover, LIVE_DATA_SETTLE_DELAY, output, width, height);
let (width, height) = (req.width as i32, req.height as i32); }
let popover_to_open = control_popover.clone(); "connectivity-wifi" => {
handles.wifi_tab_btn.set_active(true);
open_popover_on_root_map(root, handles.connectivity_popover, LIVE_DATA_SETTLE_DELAY, output, width, height);
}
"connectivity-bluetooth" => {
handles.bt_tab_btn.set_active(true);
open_popover_on_root_map(root, handles.connectivity_popover, LIVE_DATA_SETTLE_DELAY, output, width, height);
}
"media-popover" => {
// Real media state only shows the widget/text when something's
// actually playing (see AppInput::MediaUpdate) — an automated
// run has nothing playing, so fake enough of it directly on the
// widgets to get a representative capture.
handles.media_widget.set_visible(true);
handles.media_track_lbl.set_text("Sample Track — Sample Artist");
open_popover_on_root_map(root, handles.media_popover, SETTLE_DELAY, output, width, height);
}
"notification" | "notification-critical" => {
let Some(window) = handles.notification_window else {
eprintln!("breadbar: internal error — no notification window built for '{}'", req.view);
std::process::exit(1);
};
capture_standalone_window(window, output, width, height);
}
"osd-volume" | "osd-brightness" => {
let Some(window) = handles.osd_window else {
eprintln!("breadbar: internal error — no OSD window built for '{}'", req.view);
std::process::exit(1);
};
capture_standalone_window(window, output, width, height);
}
"wifi-add-dialog" => {
let anchor = handles.wifi_tab_btn;
root.connect_map(move |_| {
let output = output.clone();
let anchor = anchor.clone();
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
crate::show_add_network_dialog(&anchor, "Sample Network".to_string(), move |dialog| {
capture_standalone_window(dialog.clone(), output.clone(), width, height);
});
});
});
}
other => {
eprintln!(
"breadbar: unknown screenshot view '{other}' (known: {})",
KNOWN_VIEWS.join(", ")
);
std::process::exit(1);
}
}
}
/// Shared shape for every popover view: force it open shortly after the bar
/// maps (autohide disabled — a programmatic `popup()` has no real input
/// event serial to grab the Wayland seat with), then capture the whole
/// canvas after `settle` once the popover itself maps.
fn open_popover_on_root_map(
root: &gtk4::ApplicationWindow,
popover: gtk4::Popover,
settle: Duration,
output: PathBuf,
width: i32,
height: i32,
) {
let popover_to_open = popover.clone();
root.connect_map(move |_| { root.connect_map(move |_| {
// Autohide (the default) tries to grab the Wayland seat on
// popup, keyed to a real input event's serial — a
// programmatic popup() has no such event to grab with.
// Screenshot mode never needs the popover to dismiss itself
// anyway.
popover_to_open.set_autohide(false); popover_to_open.set_autohide(false);
let popover_to_open = popover_to_open.clone(); let popover_to_open = popover_to_open.clone();
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
popover_to_open.popup(); popover_to_open.popup();
}); });
}); });
control_popover.connect_map(move |_| { popover.connect_map(move |_| {
let output = output.clone(); let output = output.clone();
gtk4::glib::timeout_add_local_once(CONTROL_PANEL_SETTLE_DELAY, move || { gtk4::glib::timeout_add_local_once(settle, move || {
finish(bread_screenshots::capture_region(0, 0, width, height, &output)); finish(bread_screenshots::capture_region(0, 0, width, height, &output));
}); });
}); });
} }
other => {
eprintln!("breadbar: unknown screenshot view '{other}' (known: bar, control-panel)"); /// Shared shape for the standalone notification/OSD windows and the wifi
std::process::exit(1); /// add-network dialog: wait for `map`, settle, capture, exit. These are
} /// already-visible-or-about-to-be windows by the time this is called (their
} /// sample event is queued before `dispatch` even runs), so this is just the
/// capture half.
fn capture_standalone_window(window: gtk4::Window, output: PathBuf, width: i32, height: i32) {
window.connect_map(move |_| {
let output = output.clone();
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
});
});
} }
fn finish(result: anyhow::Result<()>) { fn finish(result: anyhow::Result<()>) {