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
|
|
@ -6,6 +6,13 @@
|
|||
//! 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
|
||||
//! 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 gtk4::prelude::*;
|
||||
|
|
@ -17,15 +24,12 @@ use std::time::Duration;
|
|||
/// anything has been drawn into it.
|
||||
const SETTLE_DELAY: Duration = Duration::from_millis(300);
|
||||
|
||||
/// Settle time for the control-panel view specifically: longer than
|
||||
/// [`SETTLE_DELAY`] because the CPU/RAM/PWR/GPU/network labels there aren't
|
||||
/// populated by the popover's own load (that only covers volume/brightness/
|
||||
/// sinks, see `bar::control::spawn_load`) — they're refreshed by
|
||||
/// `bar::stats::spawn_poller`'s 2-second background loop, gated on
|
||||
/// `control_popover.is_visible()` at each tick. Capturing any sooner than one
|
||||
/// 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);
|
||||
/// Settle time for views whose content depends on `bar::stats::spawn_poller`'s
|
||||
/// 2-second background loop (control-panel's CPU/RAM/PWR/GPU/network labels,
|
||||
/// gated on popover visibility) or a similar live-data popover load
|
||||
/// (connectivity's wifi/bluetooth scan) — capturing any sooner leaves
|
||||
/// placeholder dashes/"Scanning…" instead of real content.
|
||||
const LIVE_DATA_SETTLE_DELAY: Duration = Duration::from_millis(2_200);
|
||||
|
||||
/// Delay between the bar's own `map` and calling `popover.popup()`. Calling
|
||||
/// `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.
|
||||
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)]
|
||||
#[command(name = "breadbar")]
|
||||
pub struct Cli {
|
||||
/// 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)]
|
||||
pub screenshot: Option<String>,
|
||||
|
||||
|
|
@ -80,20 +97,37 @@ impl Cli {
|
|||
}
|
||||
}
|
||||
|
||||
/// Wire up the given view's screenshot sequence. Called once from `init()`,
|
||||
/// after the window and its popovers exist but before the component finishes
|
||||
/// initializing — every path here ends by exiting the process, it never
|
||||
/// returns control to the normal bar UI.
|
||||
/// Every widget/window `dispatch` might need, gathered by `main.rs`'s
|
||||
/// `init()` — most of these are plain locals there that never otherwise
|
||||
/// outlive `init()` (never stored on `App`), so they have to be cloned out
|
||||
/// 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)` /
|
||||
/// `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`.
|
||||
const BAR_HEIGHT: i32 = 32;
|
||||
|
||||
pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, control_popover: gtk4::Popover) {
|
||||
pub fn dispatch(root: >k4::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() {
|
||||
"bar" => {
|
||||
let output = req.output;
|
||||
let width = req.width as i32;
|
||||
root.connect_map(move |_| {
|
||||
let output = output.clone();
|
||||
gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || {
|
||||
|
|
@ -102,35 +136,103 @@ pub fn dispatch(root: >k4::ApplicationWindow, req: ScreenshotRequest, control_
|
|||
});
|
||||
}
|
||||
"control-panel" => {
|
||||
let output = req.output;
|
||||
let (width, height) = (req.width as i32, req.height as i32);
|
||||
let popover_to_open = control_popover.clone();
|
||||
open_popover_on_root_map(root, handles.control_popover, LIVE_DATA_SETTLE_DELAY, output, width, height);
|
||||
}
|
||||
"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 |_| {
|
||||
// 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);
|
||||
let popover_to_open = popover_to_open.clone();
|
||||
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
|
||||
popover_to_open.popup();
|
||||
});
|
||||
});
|
||||
control_popover.connect_map(move |_| {
|
||||
let output = output.clone();
|
||||
gtk4::glib::timeout_add_local_once(CONTROL_PANEL_SETTLE_DELAY, move || {
|
||||
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||
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: bar, control-panel)");
|
||||
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: >k4::ApplicationWindow,
|
||||
popover: gtk4::Popover,
|
||||
settle: Duration,
|
||||
output: PathBuf,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) {
|
||||
let popover_to_open = popover.clone();
|
||||
root.connect_map(move |_| {
|
||||
popover_to_open.set_autohide(false);
|
||||
let popover_to_open = popover_to_open.clone();
|
||||
gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || {
|
||||
popover_to_open.popup();
|
||||
});
|
||||
});
|
||||
popover.connect_map(move |_| {
|
||||
let output = output.clone();
|
||||
gtk4::glib::timeout_add_local_once(settle, move || {
|
||||
finish(bread_screenshots::capture_region(0, 0, width, height, &output));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Shared shape for the standalone notification/OSD windows and the wifi
|
||||
/// 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<()>) {
|
||||
match result {
|
||||
Ok(()) => std::process::exit(0),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue