View > Export Slideshow... (and Export PDF / JPEG / Video in Settings > Slideshow) writes the photos in view as their composed slides, between the intro and ending screens that are on: - PDF: a page per slide (1024/1920/3840 px long side, JPEG quality), /Dur and /Trans fades per page, optional /PageMode /FullScreen. The contact sheet's writer moves to engine/src/pdf.rs and streams to disk. - JPEG: <name>-001.jpg... in a folder. - Video: AV1 (rav1e, speed 10, low latency) in MP4 or WebM written by our own muxers (codecs/src/video.rs); fades rendered as blended frames, held slides one key-frame sample with a long duration; 720p/1080p, 24/30 fps; no audio; capped at 2 hours. slideshow.export runs in the background with a row in the activity stack (activity.cancel stops it); outputs are written as .part and renamed when complete, so cancel or failure leaves nothing behind. A photo that can't be rendered is skipped with a warning. slideshow.exportStatus reports the result.
4.4 KiB
Long-running tasks and the activity stack
Everything that takes a while runs off the UI thread and shows up in one place: the activity stack in the
top-left corner under the top bar (issue #345). Each task has a row with its name, a progress bar (a sweep while
the amount of work isn't known), the count and what it's working on, and ✕ if it can stop. Quitting asks first while
a task that can stop is running. Agents see the same list through activity.list / activity.cancel, and
ui.inspect reports it as activity.
The registry is lightcraft_engine::activity (crates/engine/src/activity.rs, Session::activity). The stack is
crates/ui-egui/src/panels/activity.rs. Every job that can last more than a moment uses it; don't add a progress
window, panel or progress toast of your own. A dialog may still show progress inline (the Synchronize Folder
dialog counts files while it scans), and a toast at the end saying what happened is fine.
Adding a task
use lightcraft_engine::activity::{Cancel, Unit};
// when the job starts, with the cancel flag the worker already checks between files
let guard = app.session.activity.start("export", "Exporting", Cancel::Flag(cancel.clone()));
guard.progress(0, total as u64);
// every frame (or from the worker, through `guard.handle()`)
guard.progress(done as u64, total as u64);
guard.detail(&file_name);
- Keep the guard for exactly as long as the job runs. Put it in the struct the UI polls (
ExportTask,ScanTask,SyncRun…), or move it into the worker thread. Dropping it removes the row, also when the worker panics, so a dead job never leaves a stuck bar. A worker that only reports progress takes aTaskHandle(guard.handle(),Clone + Send). kindis a stable camelCase id that agents filter by (export,import,scan,sync,previews,download,faces…).labelis a short English name in sentence case ("Building previews"). The stack shows it translated, so add it to every catalog incrates/ui-egui/locales/.- Progress:
progress(done, total). Atotalof 0 means the amount of work isn't known yet, and the bar sweeps.set_unit(Unit::Bytes)makes the count read "12 of 340 MB";Unit::Percentreads "40 %". The defaultUnit::Countreads "3 of 25". - Detail: what the job is on (a file name, a model's name). Detail set by engine code is a name, never a sentence. UI code may set translated text.
- Cancel:
Cancel::Flag(Arc<AtomicBool>)adopts the flag the job already checks. The row's ✕,activity.canceland the job's own Cancel then all set the same atomic.Cancel::Yes: the job pollsguard.is_cancelled()itself.Cancel::No: the job can't stop; it gets no ✕ and doesn't make Quit ask.set_cancellable(false)for a phase that has to finish once started (a Lightroom import adding its photos).
- Quiet periodic work shows no row: the auto-import folder listing runs every few seconds through
tasks::spawn(.., None, ..).
Examples in the code
| Job | Where | Shows how to |
|---|---|---|
| Export, contact sheet | crates/ui-egui/src/export_task.rs |
adopt the job's flag; update the row each frame |
| Build Previews | crates/engine/src/cmd/previews.rs |
move the guard into the worker thread |
| Slideshow export | crates/engine/src/cmd/slideshow.rs, crates/engine/src/slideshow_export.rs |
an engine command that owns its worker and row; progress from the worker through a TaskHandle; results read back by command |
| Model downloads | crates/engine/src/face_download.rs |
report progress through a TaskHandle in shared state |
| Lightroom catalog import | crates/ui-egui/src/lightroom_import.rs |
stop being cancellable for a phase that must finish |
| Face scan, AI Denoise | crates/engine/src/cmd/face_recognize.rs, crates/engine/src/denoise.rs |
a continuous job with no ✕, kept in session state |
| Find Missing Photos | crates/ui-egui/src/tasks.rs |
a generic background task with a row (kind: Some(..)) |
Testing
Assert the row (session.activity.list(): kind, label, total, cancellable), that activity.cancel(id) stops the
job, and that the row is gone once the job ends. crates/ui-egui/src/tests_activity.rs has the helpers, and the
job-specific tests show the pattern (tests_sync.rs, export_task.rs, tests_denoise.rs…). A row only appears
on screen once its task is 0.5 s old, so tests that click its ✕ wait for activity:cancel:<id> first.