The libadwaita-based settings screen (previous commit) had two real bugs a closer look (and a screenshot from the real running app) caught: - AdwSpinRow's internal GtkSpinButton has no width constraint of its own, so once the row was widened to 900px, the spin button stretched to fill it - the digits and +/- buttons ended up stranded behind a huge empty bordered box, the exact bug the design review flagged, just worse. - bread-theme's shared `entry, spinbutton` rule outlines every field with an always-visible @overlay border, which on a light-cream overlay colour reads as a stark white outline against the dark theme. Rather than keep fighting libadwaita's internal row/spin-button opinions (there's no supported way to reach in and constrain them), settings.rs is now plain GTK4 mirroring bos-settings' own Row.svelte/NumberField.svelte/ TextField.svelte design exactly: same tokens (12/16px row padding, ch-width inputs, transparent-at-rest border, accent border only on focus), built on a plain `list.boxed-list` for the native rounded-corner-run + divider styling. Full control over sizing, no internal widget to hunt for. Also fixed a real ordering bug in the editor AdwDialog conversion: `open_editor` used to call `dialog.present()` internally before returning, so callers that connected `dialog.connect_map` afterward (screenshot mode) missed the signal entirely - it can fire synchronously inside `present`. Presentation now happens at each call site, after wiring `connect_map`. Also gave the dialog's content an explicit height/vexpand + min-content- height, since the ScrolledWindow had none and the whole dialog was collapsing to just its header bar. breadpad-shared's bread-theme dependency also gets fixed here: it was still pinned to an old GitHub-mirror tag (v0.2.8) while breadman pinned the same crate to git.breadway.dev's dev branch - two different copies of bread-theme compiled into the same binary, so breadman's actual runtime CSS (built through breadpad_shared::theme) never saw any of the shared stylesheet fixes above regardless of what breadman's own direct dependency resolved to.
136 lines
5.6 KiB
Rust
136 lines
5.6 KiB
Rust
//! `--screenshot` CLI mode: render the named view, capture it via
|
|
//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s
|
|
//! `bread-capture` orchestrator, or run standalone for one-off captures.
|
|
//!
|
|
//! No clap here, same reasoning as breadpad: extends breadman's own
|
|
//! hand-rolled `mod args` instead of bolting on a second parser that would
|
|
//! reject its real flags (`--view`, `done`, `upcoming --plain`).
|
|
//!
|
|
//! `--screenshot <view>` 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
|
|
//! dialog (`editor::open_editor`), normally only reachable by clicking a
|
|
//! real note row'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), with no-op save/delete/error
|
|
//! callbacks since nothing here should actually persist a change.
|
|
|
|
use gtk4::prelude::*;
|
|
use libadwaita::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
|
|
/// before grim runs — `map` fires once the surface exists, not once
|
|
/// 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,
|
|
pub output: PathBuf,
|
|
pub width: u32,
|
|
pub height: u32,
|
|
}
|
|
|
|
/// Wire up the given view's screenshot sequence against an already-built,
|
|
/// not-yet-presented window. Every path here ends by exiting the process —
|
|
/// 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 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,
|
|
) {
|
|
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, breadpad_shared::types::NoteType::Note, 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 |root| {
|
|
let output = output.clone();
|
|
let state = state.clone();
|
|
let root = root.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());
|
|
// AdwDialog handles its own presentation/centering - no more
|
|
// manual popover anchor/position/autohide juggling. Must
|
|
// connect `map` BEFORE presenting, or the signal (which can
|
|
// fire synchronously inside `present`) is missed entirely.
|
|
let dialog = crate::editor::open_editor(
|
|
¬e,
|
|
store,
|
|
morning,
|
|
Rc::new(|_| {}),
|
|
Rc::new(|| {}),
|
|
Rc::new(|_| {}),
|
|
);
|
|
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));
|
|
});
|
|
});
|
|
dialog.present(Some(root.upcast_ref::<gtk4::Widget>()));
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
|
|
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),
|
|
Err(e) => {
|
|
eprintln!("breadman: screenshot capture failed: {e}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|