Merge feature/full-view-capture: capture every sidebar section
All checks were successful
dev release / build (push) Successful in 3m59s

This commit is contained in:
Breadway 2026-07-29 21:59:45 +08:00
commit a0bb9099cb
3 changed files with 80 additions and 29 deletions

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { listen } from "@tauri-apps/api/event";
import Sidebar from "$lib/components/Sidebar.svelte"; import Sidebar from "$lib/components/Sidebar.svelte";
import Placeholder from "$lib/components/Placeholder.svelte"; import Placeholder from "$lib/components/Placeholder.svelte";
import { DEFAULT_PAGE } from "$lib/sidebar"; import { DEFAULT_PAGE } from "$lib/sidebar";
@ -11,6 +12,15 @@
onMount(() => { onMount(() => {
initTheme(); initTheme();
// Screenshot mode only (src-tauri's screenshot.rs) — lets the Rust
// core drive which sidebar section is showing for a capture without
// a real user ever clicking the sidebar.
const unlisten = listen<string>("screenshot-set-view", (event) => {
activePage = event.payload;
});
return () => {
unlisten.then((f) => f());
};
}); });
</script> </script>

View file

@ -12,7 +12,7 @@ pub fn run() {
.setup(move |app| { .setup(move |app| {
commands::theme::watch_and_emit(app.handle()); commands::theme::watch_and_emit(app.handle());
if let Some(req) = screenshot_req { if let Some(req) = screenshot_req {
screenshot::dispatch(req); screenshot::dispatch(req, app.handle().clone());
} }
Ok(()) Ok(())
}) })

View file

@ -1,35 +1,69 @@
//! `--screenshot` CLI mode: capture the settings window via //! `--screenshot` CLI mode: switch the Svelte SPA to the named sidebar
//! `bread-screenshots`, then exit — driven by `bread-ecosystem`'s //! section, capture it via `bread-screenshots`, then exit — driven by
//! `bread-capture` orchestrator, or run standalone for one-off captures. //! `bread-ecosystem`'s `bread-capture` orchestrator, or run standalone for
//! one-off captures.
//! //!
//! First pass at a Tauri (webview, not raw GTK4) target: there's no //! There's no `connect_map`/`glib` signal to hook here the way every other
//! `connect_map`/`glib` signal to hook here the way every other bread- //! bread-ecosystem app's screenshot mode does, since the window is owned by
//! ecosystem app's screenshot mode does, since the window is owned by //! tao/wry (Tauri's Linux backend), not gtk4-rs directly. Instead this
//! tao/wry, not gtk4-rs directly. Instead this just waits a fixed //! waits a fixed [`INITIAL_SETTLE_DELAY`] on Tauri's own async runtime after
//! [`SETTLE_DELAY`] on Tauri's own async runtime after `setup()` runs — //! `setup()` runs for the page's first paint (JS bundle parse + Svelte
//! longer than the native apps' settle delays, since a webview's first //! mount) — longer than the native apps' settle delays, since a webview's
//! paint means a full page load (JS bundle parse + Svelte mount), not just //! first paint is a full page load, not just GTK widget layout — then emits
//! GTK widget layout. The window itself is a plain, non-layer-shell //! a `screenshot-set-view` event the frontend listens for
//! toplevel (per `tauri.conf.json`'s fixed 960x640 size), so — same //! (`+page.svelte`'s `onMount`) to switch `activePage` exactly like a real
//! reasoning as breadman/breadhelp — a full known-size canvas capture is //! sidebar click would, then waits [`VIEW_SETTLE_DELAY`] more for that
//! enough; no geometry to track. //! view's own data to load (each section fetches its own state over Tauri
//! commands on mount) before capturing. The window itself is a plain,
//! non-layer-shell toplevel (per `tauri.conf.json`'s fixed 960x640 size),
//! so — same reasoning as breadman/breadhelp — a full known-size canvas
//! capture is enough; no geometry to track.
//! //!
//! Only one view ("default", the settings window's initial landing //! View names match `frontend/src/lib/sidebar.ts`'s item ids exactly (see
//! section) is wired up so far. The frontend is a Svelte SPA with its own //! `KNOWN_VIEWS`) — every one of them has a real registered component (see
//! sidebar routing for each settings section (Appearance, Network, //! `frontend/src/lib/views/registry.ts`), no Placeholder fallbacks to skip.
//! Bluetooth, ...) — capturing one of those specifically would mean
//! passing a route via a URL/hash on window creation and is real, separate
//! frontend work, deferred past this first pass (which exists to prove the
//! isolated-headless-Sway pipeline works against a Tauri/webview window at
//! all, not to reach full view parity with the native GTK4 apps).
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use tauri::Emitter;
const SETTLE_DELAY: Duration = Duration::from_millis(2000); const INITIAL_SETTLE_DELAY: Duration = Duration::from_millis(2000);
const KNOWN_VIEWS: &[&str] = &["default"]; /// Applied after switching views — shorter than the initial load (no full
/// page/JS reload, just a component swap + that view's own Tauri-command
/// data fetch), but the About page alone needed 2s for its fetch to land
/// (see the initial-pass commit), so this stays generous rather than
/// re-guessing per view.
const VIEW_SETTLE_DELAY: Duration = Duration::from_millis(2000);
const KNOWN_VIEWS: &[&str] = &[
"network",
"breadcrumbs",
"bluetooth",
"firewall",
"sound",
"power",
"datetime",
"hyprland",
"keybinds",
"autostart",
"users",
"appearance",
"breadpaper",
"breadbar",
"breadbox",
"breadclip",
"breadpad",
"breadsearch",
"bread",
"packages",
"aur",
"firmware",
"snapshots",
"about",
];
pub struct ScreenshotRequest { pub struct ScreenshotRequest {
pub view: String,
pub output: PathBuf, pub output: PathBuf,
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
@ -73,13 +107,20 @@ pub fn parse(args: &[String]) -> Option<ScreenshotRequest> {
eprintln!("bos-settings: --screenshot requires --output"); eprintln!("bos-settings: --screenshot requires --output");
std::process::exit(1); std::process::exit(1);
}; };
Some(ScreenshotRequest { output: output.into(), width, height }) Some(ScreenshotRequest { view, output: output.into(), width, height })
} }
/// Schedule the capture-then-exit sequence. Called once from `setup()`. /// Schedule the switch-view-then-capture-then-exit sequence. Called once
pub fn dispatch(req: ScreenshotRequest) { /// from `setup()`, which is also where `app` (needed to emit the
/// `screenshot-set-view` event) comes from.
pub fn dispatch(req: ScreenshotRequest, app: tauri::AppHandle) {
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
tokio::time::sleep(SETTLE_DELAY).await; tokio::time::sleep(INITIAL_SETTLE_DELAY).await;
if let Err(e) = app.emit("screenshot-set-view", &req.view) {
eprintln!("bos-settings: failed to emit screenshot-set-view: {e}");
std::process::exit(1);
}
tokio::time::sleep(VIEW_SETTLE_DELAY).await;
finish(bread_screenshots::capture_region( finish(bread_screenshots::capture_region(
0, 0,
0, 0,