breadpad: capture the reminder window + its snooze popover

Two more views: "reminder" (the alert window from run_reminder_window,
normally only reachable via a real due note through `fire <id>`) and
"reminder-snooze" (same window, snooze popover open).

Screenshot mode builds a throwaway sample Note directly instead of going
through cmd_fire's Store lookup — main() checks for these two view names
before ever reaching run_popup's dispatch, since the reminder window is
a completely separate GApplication entry point, not reachable through
the compose-popup path screenshot mode already covered.

Same NON_UNIQUE-on-screenshot-mode fix as run_popup already had, applied
to the reminder window's own Application builder.
This commit is contained in:
Breadway 2026-07-29 21:32:36 +08:00
parent 77b402833f
commit 47a4613b5a
2 changed files with 94 additions and 11 deletions

View file

@ -170,6 +170,21 @@ fn main() -> Result<()> {
} }
let screenshot_req = args.screenshot_request(); let screenshot_req = args.screenshot_request();
if let Some(req) = &screenshot_req {
if req.view == "reminder" || req.view == "reminder-snooze" {
// The real path (`fire <id>`, above) needs a real due note from
// the Store. A screenshot doesn't have one to work with — and
// shouldn't wait for one — so it builds a throwaway sample
// instead, never touching the Store at all.
let mut sample = Note::new(
"Sample reminder text".into(),
NoteType::from_str("reminder"),
None,
);
sample.time = Some(chrono::Utc::now());
return run_reminder_window(sample, &cfg, screenshot_req);
}
}
run_popup(args.note_type, args.no_classify, cfg, screenshot_req) run_popup(args.note_type, args.no_classify, cfg, screenshot_req)
} }
@ -326,19 +341,28 @@ fn cmd_fire(id: &str, cfg: &Config) -> Result<()> {
} }
} }
run_reminder_window(note, cfg) run_reminder_window(note, cfg, None)
} }
fn run_reminder_window(note: breadpad_shared::types::Note, cfg: &Config) -> Result<()> { fn run_reminder_window(
let app = gtk4::Application::builder() note: breadpad_shared::types::Note,
.application_id("com.breadway.breadpad.reminder") cfg: &Config,
.build(); screenshot_req: Option<screenshot::ScreenshotRequest>,
) -> Result<()> {
let mut builder = gtk4::Application::builder().application_id("com.breadway.breadpad.reminder");
if screenshot_req.is_some() {
// Same reasoning as run_popup's NON_UNIQUE: a screenshot run must
// get its own fresh window, never activate a real reminder that
// happens to already be showing.
builder = builder.flags(gtk4::gio::ApplicationFlags::NON_UNIQUE);
}
let app = builder.build();
let note = Arc::new(note); let note = Arc::new(note);
let cfg = Arc::new(cfg.clone()); let cfg = Arc::new(cfg.clone());
app.connect_activate(move |app| { app.connect_activate(move |app| {
build_reminder_window(app, note.clone(), cfg.clone()); build_reminder_window(app, note.clone(), cfg.clone(), screenshot_req.clone());
}); });
app.run_with_args::<String>(&[]); app.run_with_args::<String>(&[]);
@ -381,6 +405,7 @@ fn build_reminder_window(
app: &gtk4::Application, app: &gtk4::Application,
note: Arc<breadpad_shared::types::Note>, note: Arc<breadpad_shared::types::Note>,
cfg: Arc<Config>, cfg: Arc<Config>,
screenshot_req: Option<screenshot::ScreenshotRequest>,
) { ) {
let window = gtk4::ApplicationWindow::builder() let window = gtk4::ApplicationWindow::builder()
.application(app) .application(app)
@ -557,6 +582,15 @@ fn build_reminder_window(
outer.append(&btn_row); outer.append(&btn_row);
window.set_child(Some(&outer)); window.set_child(Some(&outer));
if let Some(req) = screenshot_req {
if req.view == "reminder-snooze" {
screenshot::capture_with_snooze_open(&window, &req, snooze_popover.clone());
} else {
screenshot::capture_window(&window, &req);
}
}
window.present(); window.present();
} }

View file

@ -10,10 +10,13 @@
//! `--width`/`--height` are just three more fields on that same `Args` //! `--width`/`--height` are just three more fields on that same `Args`
//! struct instead. //! struct instead.
//! //!
//! Only the "popup" view (the compose window from `run_popup`) is wired up //! Three views: "popup" (the compose window from `run_popup`), "reminder"
//! — the reminder window (`run_reminder_window`, reached via `fire <id>`) //! (the alert window from `run_reminder_window`/`build_reminder_window`,
//! needs a real stored `Note` to render, which isn't worth fabricating for //! normally only reachable via a real due note through `fire <id>`, built
//! a screenshot pass. //! here against a fabricated sample `Note` instead — see `main`'s
//! `screenshot_req.view == "reminder"` branch, which skips the Store lookup
//! entirely), and "reminder-snooze" (the same window with its snooze
//! popover open).
use gtk4::prelude::*; use gtk4::prelude::*;
use std::path::PathBuf; use std::path::PathBuf;
@ -24,6 +27,11 @@ 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);
/// Delay before popping the snooze popover open — same reasoning as every
/// other app's PRE_POPUP_DELAY: the parent window's own layout needs a beat
/// to settle first.
const PRE_POPUP_DELAY: Duration = Duration::from_millis(300);
#[derive(Clone)] #[derive(Clone)]
pub struct ScreenshotRequest { pub struct ScreenshotRequest {
pub view: String, pub view: String,
@ -48,12 +56,53 @@ pub fn dispatch(window: &gtk4::ApplicationWindow, req: ScreenshotRequest) {
}); });
} }
other => { other => {
eprintln!("breadpad: unknown screenshot view '{other}' (known: popup)"); eprintln!("breadpad: unknown screenshot view '{other}' (known: popup, reminder, reminder-snooze)");
std::process::exit(1); std::process::exit(1);
} }
} }
} }
/// Same shape as `dispatch`'s "popup" arm, for the reminder window itself
/// (view "reminder") — pulled out since `build_reminder_window` calls this
/// directly rather than going through `dispatch` (the reminder window is
/// built via a completely separate `run_reminder_window` entry point, not
/// `run_popup`'s).
pub fn capture_window(window: &gtk4::ApplicationWindow, req: &ScreenshotRequest) {
let output = req.output.clone();
let (width, height) = (req.width as i32, req.height as 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));
});
});
}
/// View "reminder-snooze": force the snooze popover open shortly after the
/// window maps, then capture once *it* maps.
pub fn capture_with_snooze_open(
window: &gtk4::ApplicationWindow,
req: &ScreenshotRequest,
snooze_popover: gtk4::Popover,
) {
let output = req.output.clone();
let (width, height) = (req.width as i32, req.height as i32);
let popover_to_open = snooze_popover.clone();
window.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();
});
});
snooze_popover.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<()>) {
match result { match result {
Ok(()) => std::process::exit(0), Ok(()) => std::process::exit(0),