diff --git a/breadman/src/main.rs b/breadman/src/main.rs index e165a9e..a32583c 100644 --- a/breadman/src/main.rs +++ b/breadman/src/main.rs @@ -506,7 +506,7 @@ fn build_app_window( let state_c = state.clone(); let window_c = window.clone(); new_note_btn.connect_clicked(move |_| { - show_add_note_window(&window_c, state_c.clone()); + show_add_note_window(&window_c, state_c.clone(), |_| {}); }); } @@ -527,7 +527,7 @@ fn build_app_window( stack.set_visible_child_name(initial); if let Some(req) = screenshot_req { - screenshot::dispatch(&window, req); + screenshot::dispatch(&window, req, state.clone(), new_note_btn.clone()); } window.present(); @@ -796,7 +796,7 @@ fn build_note_card(note: &Note, state: AppState) -> gtk4::Box { // ── Add note window ─────────────────────────────────────────────────────────── -fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) { +fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState, on_build: impl FnOnce(>k4::Window)) { let win = gtk4::Window::builder() .title("New Note") .transient_for(parent) @@ -981,6 +981,7 @@ fn show_add_note_window(parent: >k4::ApplicationWindow, state: AppState) { }); } + on_build(&win); win.present(); body_entry.grab_focus(); } diff --git a/breadman/src/screenshot.rs b/breadman/src/screenshot.rs index 66da64a..78c2bff 100644 --- a/breadman/src/screenshot.rs +++ b/breadman/src/screenshot.rs @@ -6,13 +6,25 @@ //! hand-rolled `mod args` instead of bolting on a second parser that would //! reject its real flags (`--view`, `done`, `upcoming --plain`). //! -//! `--screenshot ` doubles as the view selector — it's passed through -//! as `initial_view` (the same field `--view` already sets) rather than +//! `--screenshot ` doubles as the view selector for every named stack +//! page ("all", "upcoming", "todo", ...) — it's passed through as +//! `initial_view` (the same field `--view` already sets) rather than //! needing a separate mechanism, since breadman already supports opening //! directly to a named stack page. +//! +//! One view isn't a stack page at all: "editor" opens the per-note editor +//! popover (`editor::build_editor_popover`), normally only reachable by +//! clicking a real note card's edit button. Screenshot mode calls the same +//! builder function directly against the first real note in the store +//! (bypassing the button/click-handler entirely — there's no clean way to +//! synthesize a click on a button that only ever existed as a local inside +//! `build_note_card`, never stored anywhere else), with no-op save/delete/ +//! error callbacks since nothing here should actually persist a change. use gtk4::prelude::*; use std::path::PathBuf; +use std::rc::Rc; +use std::sync::Arc; use std::time::Duration; /// Extra settle time after `map` for the first frame to actually paint @@ -20,6 +32,11 @@ use std::time::Duration; /// anything has been drawn into it. const SETTLE_DELAY: Duration = Duration::from_millis(300); +/// Delay before popping the editor 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)] pub struct ScreenshotRequest { pub view: String, @@ -33,12 +50,81 @@ pub struct ScreenshotRequest { /// it never returns control to the normal note-manager UI. /// /// Unlike the other apps' `dispatch`, this doesn't validate `req.view` -/// against a known-views list — an invalid name just falls through to -/// breadman's own `unwrap_or("all")` default (see `build_app_window`), -/// same as `--view` already behaves for a normal run. -pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { +/// against a known-views list for the stack-page case — an invalid name +/// just falls through to breadman's own `unwrap_or("all")` default (see +/// `build_app_window`), same as `--view` already behaves for a normal run. +pub fn dispatch( + window: >k4::ApplicationWindow, + req: ScreenshotRequest, + state: crate::AppState, + editor_anchor: gtk4::Button, +) { let output = req.output; let (width, height) = (req.width as i32, req.height as i32); + + if req.view == "new-note" { + window.connect_map(move |root| { + let output = output.clone(); + let root = root.clone(); + let state = state.clone(); + gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { + crate::show_add_note_window(&root, state, move |dialog| { + let output = output.clone(); + dialog.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)); + }); + }); + }); + }); + }); + return; + } + + if req.view == "editor" { + window.connect_map(move |_| { + let output = output.clone(); + let state = state.clone(); + let editor_anchor = editor_anchor.clone(); + gtk4::glib::timeout_add_local_once(PRE_POPUP_DELAY, move || { + let Some(note) = state.notes.borrow().first().cloned() else { + eprintln!("breadman: no notes in the store to build the editor view from"); + std::process::exit(1); + }; + let morning = state.cfg.borrow().reminders.default_morning.clone(); + let store = Arc::new(state.write_store()); + let popover = crate::editor::build_editor_popover( + ¬e, + store, + morning, + Rc::new(|_| {}), + Rc::new(|| {}), + Rc::new(|_| {}), + ); + popover.set_parent(&editor_anchor); + // Parenting to the whole window (rather than a small, + // concretely-placed widget like the real edit-button call + // site does) left the popover positioned above the window + // entirely (GTK4's default Popover position is Top) — off + // the top of the canvas and clipped out of every capture. + // Anchoring to a real button plus an explicit Bottom + // position keeps it inside the visible canvas. + popover.set_position(gtk4::PositionType::Bottom); + popover.set_autohide(false); + let output = output.clone(); + 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)); + }); + }); + popover.popup(); + }); + }); + return; + } + window.connect_map(move |_| { let output = output.clone(); gtk4::glib::timeout_add_local_once(SETTLE_DELAY, move || { diff --git a/breadpad/src/main.rs b/breadpad/src/main.rs index 4e83f1e..7ba8c87 100644 --- a/breadpad/src/main.rs +++ b/breadpad/src/main.rs @@ -170,6 +170,21 @@ fn main() -> Result<()> { } let screenshot_req = args.screenshot_request(); + if let Some(req) = &screenshot_req { + if req.view == "reminder" || req.view == "reminder-snooze" { + // The real path (`fire `, 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) } @@ -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<()> { - let app = gtk4::Application::builder() - .application_id("com.breadway.breadpad.reminder") - .build(); +fn run_reminder_window( + note: breadpad_shared::types::Note, + cfg: &Config, + screenshot_req: Option, +) -> 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 cfg = Arc::new(cfg.clone()); 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::(&[]); @@ -381,6 +405,7 @@ fn build_reminder_window( app: >k4::Application, note: Arc, cfg: Arc, + screenshot_req: Option, ) { let window = gtk4::ApplicationWindow::builder() .application(app) @@ -557,6 +582,15 @@ fn build_reminder_window( outer.append(&btn_row); 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(); } diff --git a/breadpad/src/screenshot.rs b/breadpad/src/screenshot.rs index e88679f..c90873e 100644 --- a/breadpad/src/screenshot.rs +++ b/breadpad/src/screenshot.rs @@ -10,10 +10,13 @@ //! `--width`/`--height` are just three more fields on that same `Args` //! struct instead. //! -//! Only the "popup" view (the compose window from `run_popup`) is wired up -//! — the reminder window (`run_reminder_window`, reached via `fire `) -//! needs a real stored `Note` to render, which isn't worth fabricating for -//! a screenshot pass. +//! Three views: "popup" (the compose window from `run_popup`), "reminder" +//! (the alert window from `run_reminder_window`/`build_reminder_window`, +//! normally only reachable via a real due note through `fire `, built +//! 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 std::path::PathBuf; @@ -24,6 +27,11 @@ use std::time::Duration; /// anything has been drawn into it. 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)] pub struct ScreenshotRequest { pub view: String, @@ -48,12 +56,53 @@ pub fn dispatch(window: >k4::ApplicationWindow, req: ScreenshotRequest) { }); } other => { - eprintln!("breadpad: unknown screenshot view '{other}' (known: popup)"); + eprintln!("breadpad: unknown screenshot view '{other}' (known: popup, reminder, reminder-snooze)"); 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: >k4::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: >k4::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<()>) { match result { Ok(()) => std::process::exit(0),