collective

Browse / github.com/storytold/lightcraft / patch

Issue #3063: a test settle that times out fails with what was still busy; settle is must_use

683 of 730 `Headless::settle` calls ignore its timeout, so a slow test runs on for minutes and fails somewhere unrelated Part of #3057. ## Summary `Headless::settle(timeout)` returns `false` when the app hasn't settled in time, and almost every test ignores that. On a slow or loaded machine, a test sits out the full timeout at each call and carries on with an unsettled app. It then fails, if it fails at all, at some later assertion that has nothing to do with the wait. With 120 s timeouts and several calls per test, one slow test can take many minutes before it says anything. ## Evidence (main bb08a697) - **`crates/ui-egui/src/headless.rs:411`:** `pub fn settle(&mut self, timeout: Duration) -> bool`, which r

Status
open
Score
0.00
Adopters
0 other operators
Author
THEtheChad (imported from pull request #3120, open)
Base
662df66f1c08bdb1b1f1d0c1c638c4cef936d0b0
Size
+837 −721 in 63 files
Review
pending
Shared
2026-10-28
Id
589e78175bcaa7237df52cb7

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/examples/grid_bench.rs crates/ui-egui/examples/open_timeline.rs crates/ui-egui/src/headless.rs crates/ui-egui/src/headless/tests.rs crates/ui-egui/src/lightroom_import.rs crates/ui-egui/src/menus/menubar/tests.rs crates/ui-egui/src/pick.rs crates/ui-egui/src/shortcuts/tests.rs crates/ui-egui/src/tests/activity.rs crates/ui-egui/src/tests/album_picker.rs crates/ui-egui/src/tests/album_remove.rs crates/ui-egui/src/tests/appearance.rs crates/ui-egui/src/tests/before_after.rs crates/ui-egui/src/tests/browse_active.rs crates/ui-egui/src/tests/capture_time.rs crates/ui-egui/src/tests/compare.rs crates/ui-egui/src/tests/copy_paste_dialogs.rs crates/ui-egui/src/tests/crop_rotate.rs crates/ui-egui/src/tests/crop_tool.rs crates/ui-egui/src/tests/date_picker.rs crates/ui-egui/src/tests/dialog_fit.rs crates/ui-egui/src/tests/dialog_settings.rs crates/ui-egui/src/tests/dialog_tab.rs crates/ui-egui/src/tests/dialogs.rs crates/ui-egui/src/tests/docking_acceptance.rs crates/ui-egui/src/tests/filmstrip.rs crates/ui-egui/src/tests/focus_point.rs crates/ui-egui/src/tests/grading.rs crates/ui-egui/src/tests/grid.rs crates/ui-egui/src/tests/guided_guides.rs crates/ui-egui/src/tests/histogram_drag.rs crates/ui-egui/src/tests/info_dialogs.rs crates/ui-egui/src/tests/keymap.rs crates/ui-egui/src/tests/keyword_files.rs crates/ui-egui/src/tests/keyword_list.rs crates/ui-egui/src/tests/keyword_set.rs crates/ui-egui/src/tests/keyword_sidebar.rs crates/ui-egui/src/tests/keywording.rs crates/ui-egui/src/tests/labels.rs crates/ui-egui/src/tests/layout_image.rs crates/ui-egui/src/tests/library_problem.rs crates/ui-egui/src/tests/lifecycle_037.rs crates/ui-egui/src/tests/lights_out.rs crates/ui-egui/src/tests/masking.rs crates/ui-egui/src/tests/menubar.rs crates/ui-egui/src/tests/merge_preview.rs crates/ui-egui/src/tests/panels.rs crates/ui-egui/src/tests/preset_import.rs crates/ui-egui/src/tests/preview_limit.rs crates/ui-egui/src/tests/print.rs crates/ui-egui/src/tests/profile_menu.rs crates/ui-egui/src/tests/rename_person.rs crates/ui-egui/src/tests/rules_editor.rs crates/ui-egui/src/tests/scroll.rs crates/ui-egui/src/tests/slider_reset.rs crates/ui-egui/src/tests/slideshow.rs crates/ui-egui/src/tests/sync.rs crates/ui-egui/src/tests/temp_slider.rs crates/ui-egui/src/tests/titlebar.rs crates/ui-egui/src/tests/view_cost.rs crates/ui-egui/src/tests/wb_selector.rs crates/ui-egui/src/tests/zoom.rs crates/ui-egui/src/tests/zoom_keys.rs

diff --git a/crates/ui-egui/examples/grid_bench.rs b/crates/ui-egui/examples/grid_bench.rs--- a/crates/ui-egui/examples/grid_bench.rs+++ b/crates/ui-egui/examples/grid_bench.rs@@ -113,13 +113,17 @@ fn scenario(n: usize, view: ViewMode, local: bool, frames: usize) {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     h.app.ui.view = view;     h.app.ui.thumb_size = 160.0;-    h.settle(Duration::from_secs(120));+    if !h.settle(Duration::from_secs(120)) {+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(120), h.busy_description());+    }     // to the middle of the grid (and let the newly visible thumbnails fail / settle)     if let Some(mid) = h.app.session.visible().get(n / 2).copied() {         h.app.session.selection.active = Some(mid);         h.app.session.selection.ids = vec![mid];     }-    h.settle(Duration::from_secs(120));+    if !h.settle(Duration::from_secs(120)) {+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(120), h.busy_description());+    }     let setup = t0.elapsed();     let stats0 = h.app.caches.grid_stats.clone();     let mut frame_ms = Vec::with_capacity(frames);diff --git a/crates/ui-egui/examples/open_timeline.rs b/crates/ui-egui/examples/open_timeline.rs--- a/crates/ui-egui/examples/open_timeline.rs+++ b/crates/ui-egui/examples/open_timeline.rs@@ -19,12 +19,16 @@ fn main() {     let req = |h: &mut Headless, m: &str, p: Value| h.request(m, p, Duration::from_secs(60));     let cmd = |h: &mut Headless, c: &str| req(h, "engine.execute", json!({"command": c}))["result"].clone();     cmd(&mut h, "view.photoGrid");-    h.settle(Duration::from_secs(60));+    if !h.settle(Duration::from_secs(60)) {+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());+    }     cmd(&mut h, "library.selectNone");     let r = cmd(&mut h, "library.next");     eprintln!("{:9.1} first: {r}", ms());     let first = r["active"].as_u64().unwrap_or(1);-    h.settle(Duration::from_secs(60));+    if !h.settle(Duration::from_secs(60)) {+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());+    }     let gpu = req(&mut h, "ui.inspect", json!({}))["result"]["perf"]["gpu"].clone();     eprintln!("{:9.1} grid settled, gpu {gpu}", ms());     // what the loupe shows, every frame, until `photo` is rendered for good@@ -55,7 +59,9 @@ fn main() {     cmd(&mut h, "view.detail");     let open = watch(&mut h, "open", first);     let t = ms();-    h.settle(Duration::from_secs(60));+    if !h.settle(Duration::from_secs(60)) {+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());+    }     eprintln!("{:9.1} neighbours prefetched after {:.0} ms", ms(), ms() - t);     // step towards the side that has photos (the sort may put the first one last)     let mut dir = "library.next";@@ -69,6 +75,8 @@ fn main() {     let step1 = watch(&mut h, "next (prefetched)", n1);     let n2 = cmd(&mut h, dir)["active"].as_u64().unwrap_or(0);     let step2 = watch(&mut h, "next at once", n2);-    h.settle(Duration::from_secs(60));+    if !h.settle(Duration::from_secs(60)) {+        eprintln!("did not settle within {:?}: {}", Duration::from_secs(60), h.busy_description());+    }     eprintln!("{:9.1} summary: open {open:.0} ms, next prefetched {step1:.0} ms, next at once {step2:.0} ms", ms()); }diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs--- a/crates/ui-egui/src/headless.rs+++ b/crates/ui-egui/src/headless.rs@@ -26,6 +26,9 @@ use crate::{ControlRequest, LightcraftApp}; const CLEAR: Color32 = Color32::from_rgb(12, 12, 12); /// Simulated frame interval (deterministic animation time). const FRAME_DT: f64 = 1.0 / 60.0;+/// The shortest wait [`Headless::settled`] gives a test before failing it (see there).+#[cfg(test)]+const SETTLED_FLOOR: Duration = Duration::from_secs(120);  /// Validate the scaled screenshot dimensions before layout or pixel allocation. pub fn viewport_pixels(size: [f32; 2], pixels_per_point: f32) -> Result<[usize; 2], String> {@@ -388,26 +391,52 @@ impl Headless {         }     } +    /// What is still in progress, as `(what, how many)` with 0 for idle: renders, file-system+    /// checks the sidebar waits for, background work, queued input. [`Self::busy`] and+    /// [`Self::busy_description`] both read it, so they can't drift apart.+    fn busy_counts(&self) -> [(&'static str, usize); 13] {+        [+            ("renders in flight", self.app.renderer.in_flight()),+            ("file-system checks", crate::panels::left::fs_cached_running(&self.view.ctx)),+            ("merge", usize::from(self.app.merge.busy())),+            ("folder scan", usize::from(self.app.scan.is_some())),+            ("sync", usize::from(self.app.sync.is_some())),+            ("sync run", usize::from(self.app.sync_run.is_some())),+            ("import", usize::from(self.app.import.is_some())),+            ("export", usize::from(self.app.export.is_some())),+            ("AI denoise", usize::from(self.app.session.denoise_busy())),+            ("background tasks", usize::from(!self.app.tasks.is_empty())),+            ("synthetic input events", self.app.synthetic.len()),+            ("pending gestures", self.app.pending_gestures.len()),+            ("queued input events", self.events.len()),+        ]+    }+     /// Is anything still in progress (renders, file-system checks the sidebar waits for, queued     /// input)?     pub fn busy(&self) -> bool {-        self.app.renderer.in_flight() > 0-            || crate::panels::left::fs_cached_running(&self.view.ctx) > 0-            || self.app.merge.busy()-            || self.app.scan.is_some()-            || self.app.sync.is_some()-            || self.app.sync_run.is_some()-            || self.app.import.is_some()-            || self.app.export.is_some()-            || self.app.session.denoise_busy()-            || !self.app.tasks.is_empty()-            || !self.app.synthetic.is_empty()-            || !self.app.pending_gestures.is_empty()-            || !self.events.is_empty()+        self.busy_counts().iter().any(|&(_, n)| n > 0)+    }++    /// What keeps the session [`Self::busy`], for a person reading a timeout: for example+    /// `renders in flight: 2, background tasks: [Import]`, or `nothing` when idle.+    pub fn busy_description(&self) -> String {+        let parts: Vec<String> = self+            .busy_counts()+            .iter()+            .filter(|&&(_, n)| n > 0)+            .map(|&(what, n)| match what {+                "background tasks" => format!("{what}: {:?}", self.app.tasks.labels()),+                _ if n == 1 => what.to_string(),+                _ => format!("{what}: {n}"),+            })+            .collect();+        if parts.is_empty() { "nothing".to_string() } else { parts.join(", ") }     }      /// Run frames until nothing is pending (renders finished, input consumed) for a few frames in     /// a row, or `timeout` passes. Returns whether it settled.+    #[must_use = "`false` means it timed out with work still pending; tests use `settled`"]     pub fn settle(&mut self, timeout: Duration) -> bool {         let t0 = Instant::now();         let mut quiet = 0;@@ -429,6 +458,30 @@ impl Headless {         }     } +    /// [`Self::settle`], for tests: a timeout fails the test there and then, saying what it waited+    /// for (`what`), where (the caller's line) and what was still busy, instead of carrying on with+    /// an unsettled app and failing later at an unrelated assertion (issue #3063).+    ///+    /// It waits at least [`SETTLED_FLOOR`] (120 s), whatever `timeout` says. Tests passed short+    /// timeouts (5 or 10 s) back when a timeout was ignored, so none relies on a short wait giving+    /// up; a healthy app settles in well under a second, so the floor only lengthens waits that+    /// would otherwise fail the test on a slow or loaded machine. A test that is really stuck+    /// still fails, after 120 s, naming what was busy.+    #[cfg(test)]+    #[track_caller]+    pub(crate) fn settled(&mut self, timeout: Duration, what: &str) {+        self.settled_within(timeout.max(SETTLED_FLOOR), what);+    }++    /// [`Self::settled`] without the floor: gives up after exactly `timeout` (its own tests).+    #[cfg(test)]+    #[track_caller]+    fn settled_within(&mut self, timeout: Duration, what: &str) {+        if !self.settle(timeout) {+            panic!("did not settle within {timeout:?} ({what}); still busy: {}", self.busy_description());+        }+    }+     /// Run frames until `done` holds or `timeout` passes (a render that finishes on a busy machine     /// after a quiet spell, where [`Self::settle`] alone would stop too early). Returns `done`.     pub fn step_until(&mut self, timeout: Duration, done: impl Fn(&Self) -> bool) -> bool {@@ -448,9 +501,11 @@ impl Headless {         self.view.paint(&HashMap::new())     } -    /// Settle, then render the current UI.+    /// Settle, then render the current UI. A timeout still paints: the picture shows whatever is+    /// on screen by then (a placeholder for a render that never finished), which is what a+    /// snapshot of a stuck app should show.     pub fn snapshot(&mut self, timeout: Duration) -> ColorImage {-        self.settle(timeout);+        let _ = self.settle(timeout);         self.paint()     } diff --git a/crates/ui-egui/src/headless/tests.rs b/crates/ui-egui/src/headless/tests.rs--- a/crates/ui-egui/src/headless/tests.rs+++ b/crates/ui-egui/src/headless/tests.rs@@ -25,7 +25,7 @@ fn detail_offers_ai_denoise_for_raw_photos_and_leads_to_its_settings() {     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0]}}), t);     h.request("ui.set", json!({"view": "detail"}), t);     h.app.ui.open_sections = vec!["detail".to_string()];-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.open_sections");     h.step();     h.step();     let w = widgets(&mut h);@@ -36,7 +36,7 @@ fn detail_offers_ai_denoise_for_raw_photos_and_leads_to_its_settings() {     h.step();     h.step();     assert_eq!(h.app.ui.dialog, Some(Dialog::Settings { tab: "denoise".into() }));-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let listed = h.app.session.execute("denoise.models.list", &json!({})).unwrap();     assert_eq!(listed["models"].as_array().unwrap().len(), lightcraft_denoise::known::all().len(), "{listed}");     assert!(widgets(&mut h).contains("\"denoise:installFile\""));@@ -47,7 +47,7 @@ fn detail_offers_ai_denoise_for_raw_photos_and_leads_to_its_settings() {         json!({"command": "library.select", "params": {"ids": [h.app.session.catalog.photos().find(|p| p.id != id).unwrap().id.0]}}),         t,     );-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     h.step();     h.step();     assert!(!widgets(&mut h).contains("\"denoise:setup\""), "nothing is offered for a photo denoise does not apply to");@@ -77,7 +77,7 @@ fn a_rejected_model_file_says_why_in_settings() {     let picked = onnx.to_string_lossy().into_owned();     h.app.services.pick_denoise_model = Some(Box::new(move || vec![picked.clone()]));     h.app.ui.dialog = Some(Dialog::Settings { tab: "denoise".into() });-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.dialog");     let r = h.request("ui.clickWidget", json!({"id": "denoise:installFile"}), t);     assert_eq!(r["ok"], true, "{r}");     h.step();@@ -281,6 +281,55 @@ fn demo(size: [f32; 2]) -> Headless {     Headless::new(app, size, 1.0) } +/// An empty session with a background task that runs until `release` is dropped.+fn stuck(release: &mut Option<std::sync::mpsc::Sender<()>>) -> Headless {+    let mut h = Headless::new(LightcraftApp::new(lightcraft_engine::Session::new(), crate::Services::default()), [640.0, 480.0], 1.0);+    let (tx, rx) = std::sync::mpsc::channel::<()>();+    *release = Some(tx);+    let started = crate::tasks::spawn(&mut h.app, "stuck", None, move || rx.recv().is_ok(), |_, _, _| {});+    assert_eq!(started, Ok(()));+    h+}++/// A settle that times out fails the test at once and says what it waited for and what was+/// still busy (issue #3063), instead of letting the test carry on with an unsettled app.+#[test]+fn settled_fails_with_what_is_still_busy() {+    let mut release = None;+    let mut h = stuck(&mut release);+    let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| h.settled_within(Duration::from_millis(200), "the stuck task")));+    let message = failed.expect_err("a task that never ends can't settle");+    let message = message.downcast_ref::<String>().cloned().unwrap_or_default();+    assert!(message.starts_with("did not settle within 200ms (the stuck task); still busy: "), "{message}");+    assert!(message.contains("background tasks: [\"stuck\"]"), "{message}");+}++/// `settled` waits at least 120 s whatever timeout a test passes: tests passed short timeouts+/// back when a timeout was ignored, and a slow runner must not turn them into failures (#3063).+#[test]+fn settled_waits_past_a_short_timeout() {+    let mut h = Headless::new(LightcraftApp::new(lightcraft_engine::Session::new(), crate::Services::default()), [640.0, 480.0], 1.0);+    let started = crate::tasks::spawn(&mut h.app, "slow", None, || std::thread::sleep(Duration::from_secs(1)), |_, _, _| {});+    assert_eq!(started, Ok(()));+    assert!(h.busy());+    h.settled(Duration::from_millis(100), "a task that ends after a second");+    assert!(!h.busy(), "{}", h.busy_description());+}++/// The description lists what `busy` reads, and an idle session says so and settles.+#[test]+fn busy_description_names_pending_work() {+    let mut release = None;+    let mut h = stuck(&mut release);+    h.events.push(egui::Event::PointerGone);+    assert!(h.busy());+    assert_eq!(h.busy_description(), "background tasks: [\"stuck\"], queued input events");+    drop(release.take());+    h.settled(SETTLE, "the released task");+    assert!(!h.busy());+    assert_eq!(h.busy_description(), "nothing");+}+ /// Agents cut, copy and paste in a text field as the system does for ⌘X / ⌘C / ⌘V /// (`ui.clipboard`), and `ui.inspect` → `copied` says what the UI last copied. #[test]@@ -294,7 +343,7 @@ fn agents_cut_copy_and_paste_in_text_fields() {         h.step();         r     };-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     ask(&mut h, "ui.clickWidget", json!({"id": "field:search"}));     ask(&mut h, "ui.text", json!({"text": "wedding"}));     ask(&mut h, "ui.key", json!({"key": "a", "cmd": true}));@@ -314,14 +363,14 @@ fn agents_cut_copy_and_paste_in_text_fields() { fn the_clipboard_method_needs_a_text_field() {     let mut h = demo([1200.0, 760.0]);     let t = Duration::from_secs(10);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     let undo = h.app.session.undo.len();     for action in ["copy", "paste"] {         let r = h.request("ui.clipboard", json!({"action": action, "text": "travel"}), t);         assert_eq!(r["ok"], false, "{action}: {r}");         assert!(r["error"].as_str().is_some_and(|e| e.contains("text field")), "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clipboard");     assert_eq!(h.app.session.undo.len(), undo, "nothing was pasted"); } @@ -332,12 +381,12 @@ fn click_widget_right_clicks() {     let mut h = demo([1200.0, 760.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let cell = h.app.widgets.iter().find(|(w, _)| w.starts_with("film:")).map(|(w, _)| w.clone()).expect("a filmstrip photo");     assert!(!egui::Popup::is_any_open(&h.view.ctx));     let r = h.request("ui.clickWidget", json!({"id": cell, "button": "right"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     assert!(egui::Popup::is_any_open(&h.view.ctx), "its context menu"); } @@ -347,7 +396,7 @@ fn click_widget_right_clicks() { fn right_clicking_elsewhere_moves_the_context_menu() {     let mut h = demo([1200.0, 760.0]);     let t = Duration::from_secs(10);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     let thumbs: Vec<String> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("thumb:")).map(|(w, _)| w.clone()).collect();     let (Some(a), Some(b)) = (thumbs.first().cloned(), thumbs.get(1).cloned()) else { panic!("two grid photos: {thumbs:?}") };     let popup = |w: &str| {@@ -357,7 +406,7 @@ fn right_clicking_elsewhere_moves_the_context_menu() {     assert!(!egui::Popup::is_any_open(&h.view.ctx));     let r = h.request("ui.clickWidget", json!({"id": a, "button": "right"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&a)), "the first photo's menu is open");     // A real click: the press and the release reach the UI on different frames, with the     // pointer resting on the photo in between (`ui.clickWidget` sends both on one frame).@@ -380,7 +429,7 @@ fn right_clicking_elsewhere_moves_the_context_menu() {             assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "frame {frame}: the second photo's menu is open");         }     }-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "the second photo's menu stays open");     // The same photo again, elsewhere on it (the menu that is open belongs to this very     // widget): the menu moves to the pointer and stays.@@ -399,7 +448,7 @@ fn right_clicking_elsewhere_moves_the_context_menu() {             assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "frame {frame}: the menu is open after a right-click on the same photo");         }     }-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(egui::Popup::is_id_open(&h.view.ctx, popup(&b)), "the menu stays open");     let after = h.view.ctx.read_response(popup(&b)).map(|r| r.rect).expect("the menu's area");     assert!((after.left() - at.x).abs() < 4.0, "the menu moved to the pointer: {after:?} vs {at:?} (was {before:?})");@@ -411,17 +460,17 @@ fn up_and_down_move_through_the_grid_rows() {     let mut h = demo([1200.0, 760.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "grid"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let first = h.app.session.visible_cloned()[0];     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [first.0]}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     let rect = |h: &Headless| {         let a = h.app.session.selection.active.unwrap();         h.app.widgets.iter().find(|(w, _)| *w == format!("thumb:{}", a.0)).map(|(_, r)| *r).expect("grid cell")     };     let top = rect(&h);     h.request("ui.key", json!({"key": "down"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key down");     let below = rect(&h);     let pos = {         let a = h.app.session.selection.active;@@ -431,16 +480,16 @@ fn up_and_down_move_through_the_grid_rows() {     assert!(below.top() > top.bottom(), "the next row, at {pos}: {top:?} -> {below:?}");     assert!(below.left() < top.right() && below.right() > top.left(), "overlapping horizontally");     h.request("ui.key", json!({"key": "up"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key up");     assert_eq!(h.app.session.selection.active, Some(first), "back up");     h.request("ui.key", json!({"key": "up"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key up");     assert_eq!(h.app.session.selection.active, Some(first), "the top row stays put");     // the filmstrip (one row): Down is the next photo     h.request("ui.set", json!({"view": "detail"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h.request("ui.key", json!({"key": "down"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key down");     assert_eq!(         {             let a = h.app.session.selection.active;@@ -456,12 +505,12 @@ fn up_and_down_move_through_the_grid_rows() { fn the_search_field_pastes_from_its_menu() {     let mut h = demo([1200.0, 760.0]);     let t = Duration::from_secs(10);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     h.view.clipboard = "wedding".into();     for (id, button) in [("field:search", "right"), ("field:search:paste", "left")] {         let r = h.request("ui.clickWidget", json!({"id": id, "button": button}), t);         assert_eq!(r["ok"], true, "{id}: {r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.clickWidget");     }     assert_eq!(h.app.ui.search, "wedding");     assert_eq!(h.app.session.filter.text, "wedding", "and the library is filtered by it");@@ -473,19 +522,19 @@ fn the_search_field_pastes_from_its_menu() { fn clearing_the_loupe_filters_empties_the_search_field() {     let mut h = demo([1200.0, 760.0]);     let t = Duration::from_secs(10);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     for (method, params) in [("ui.clickWidget", json!({"id": "field:search"})), ("ui.text", json!({"text": "a"}))] {         let r = h.request(method, params, t);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after the request");     }     assert_eq!(h.app.session.filter.text, "a", "filtered while typing");     assert!(!h.app.session.visible_cloned().is_empty(), "the search finds photos, so the loupe has one to show");     h.request("engine.execute", json!({"command": "view.detail"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.detail");     let r = h.request("ui.clickWidget", json!({"id": "loupe:filters"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking loupe:filters");     assert_eq!((h.app.ui.search.as_str(), h.app.session.filter.text.as_str()), ("", "")); } @@ -494,16 +543,16 @@ fn clearing_the_loupe_filters_empties_the_search_field() { fn escape_in_the_search_field_gives_back_the_search() {     let mut h = demo([1200.0, 760.0]);     let t = Duration::from_secs(10);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     for (method, params) in [("ui.clickWidget", json!({"id": "field:search"})), ("ui.text", json!({"text": "travel"}))] {         let r = h.request(method, params, t);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after the request");     }     assert_eq!(h.app.session.filter.text, "travel", "filtered while typing");     let r = h.request("ui.key", json!({"key": "Escape"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_eq!((h.app.ui.search.as_str(), h.app.session.filter.text.as_str()), ("", "")); } @@ -537,7 +586,7 @@ fn snapshots_are_deterministic_and_control_requests_work() {         assert_eq!(r["ok"], true, "{r}");         // Compare only fully settled frames: a timed-out settle would compare half-rendered         // pictures and fail as a misleading pixel diff (seen on loaded CI machines).-        assert!(h.settle(Duration::from_secs(300)), "the demo detail view did not settle within 300 s");+        h.settled(Duration::from_secs(300), "the demo detail view");         h.paint()     };     let (a, b) = (shot(), shot());@@ -601,7 +650,7 @@ fn compare_survey_and_auto_advance_by_keyboard() {     h.request("ui.key", json!({"key": "escape"}), t);     assert_eq!(h.app.ui.view, crate::state::ViewMode::Detail);     // let in-flight renders finish: worker threads must not outlive the test process' TLS-    h.settle(SETTLE);+    h.settled(SETTLE, "after key escape"); }  /// Naming a face in the loupe: point at an unnamed face, click its "Add name" label, type, Enter. Needs the@@ -622,7 +671,7 @@ fn naming_a_face_in_the_loupe() {     }];     h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap();     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     // pointing at the box shows the invitation; clicking it opens the name box     h.request("ui.pointer", json!({"events": [{"kind": "move", "x": 0.42, "y": 0.4}]}), t);     h.step();@@ -675,7 +724,7 @@ fn adding_a_face_model_shows_its_terms_and_installs_only_once_accepted() {      // the dialog opens for the chosen file, with nothing accepted and nothing installed     assert_eq!(open(&mut h, &model)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after open");     h.step();     let Some(dlg) = h.app.ui.dialog.clone() else { panic!("no dialog") };     let Dialog::FaceModel { info, accepted, .. } = &dlg else { panic!("{dlg:?}") };@@ -768,7 +817,7 @@ fn download_shows_the_terms_first_and_fetches_nothing_until_accepted() {     std::fs::create_dir_all(&dir).unwrap();     h.app.session.face_models_dir = Some(dir.join("models"));     h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": "faces"}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after app.settings");     h.step();     let runtime = h.app.session.execute("faces.models.list", &json!({})).unwrap()["runtime"] == true;     let r = h.request("ui.clickWidget", json!({"id": "faces:download:sface-2021dec"}), t);@@ -826,7 +875,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {     let offers = |h: &mut Headless| h.request("ui.widgets", json!({}), t).to_string().contains("\"faces:setup\"");      h.request("engine.execute", json!({"command": "view.people"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.people");     h.step();     assert_eq!(h.app.ui.view, ViewMode::People);     if !runtime {@@ -845,7 +894,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {      // the loupe's name box offers it too, and its button leaves for Settings with the box closed     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h.request("ui.pointer", json!({"events": [{"kind": "move", "x": 0.42, "y": 0.4}]}), t);     h.step();     h.step();@@ -868,7 +917,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {     h.app.run("faces.models.select", json!({"id": installed["installed"]["id"]})).unwrap();     h.app.caches.faces_epoch += 1;     h.request("engine.execute", json!({"command": "view.people"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.people");     h.step();     assert!(offers(&mut h));     assert_eq!(h.app.session.execute("faces.models.list", &json!({})).unwrap()["enabled"], false);@@ -880,7 +929,7 @@ fn people_and_the_name_box_offer_to_set_face_recognition_up() {     assert_eq!(h.app.ui.dialog, None, "nothing to go to Settings for");     h.step();     assert!(!offers(&mut h), "once it is on the offer is gone");-    h.settle(SETTLE);+    h.settled(SETTLE, "after offers");     let _ = std::fs::remove_dir_all(&dir); } @@ -909,7 +958,7 @@ fn a_persons_page_shows_their_faces_and_back_returns_to_everyone() {     }     let open_people = |h: &mut Headless| {         h.request("engine.execute", json!({"command": "view.people"}), t);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.people");         h.step();     };     open_people(&mut h);@@ -992,7 +1041,7 @@ fn person_more_confirms_all_above_with_one_undo_step() {         h.app.session.commit("setup", Op::SetMeta { id: *id, meta: Box::new(meta) }).unwrap();     }     h.request("engine.execute", json!({"command": "view.person", "params": {"name": "Jane Doe"}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.person");     let page = crate::panels::person::PersonPage {         name: "Jane Doe".into(),         total: 1,@@ -1094,7 +1143,7 @@ fn unnamed_faces_are_selected_and_named_together() {         h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap();     }     h.request("engine.execute", json!({"command": "view.people"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.people");     h.step();     h.step();     let tiles = |h: &mut Headless| h.request("ui.widgets", json!({"filter": "unnamed-face:"}), t)["result"].as_array().map_or(0, Vec::len);@@ -1128,7 +1177,7 @@ fn unnamed_faces_are_selected_and_named_together() {     assert_eq!(named(&h, ids[1], 1), None, "a face that was not selected stays unnamed");     assert_eq!(h.app.session.undo.len(), undo + 1, "one step for both");     assert!(h.app.ui.unnamed_selected.is_empty() && h.app.ui.unnamed_name.is_empty());-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     h.step();     assert_eq!(tiles(&mut h), 2, "the named faces left the unnamed list");     assert_eq!(h.app.session.catalog.people().len(), 2, "and the new person is among the named");@@ -1167,7 +1216,7 @@ fn double_clicking_an_unnamed_face_opens_its_photo() {     }     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [ids[0].0]}}), t);     h.request("engine.execute", json!({"command": "view.people"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.people");     h.step();     h.step();     let tile = format!("unnamed-face:{}:0", ids[1].0);@@ -1213,7 +1262,7 @@ fn the_unnamed_faces_name_field_pastes_and_escape_clears() {     }];     h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap();     h.request("engine.execute", json!({"command": "view.people"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.people");     h.view.clipboard = "Wedding Guest".into();     for (widget, button) in [         (format!("unnamed-face:{}:0", id.0), "left"),@@ -1299,7 +1348,7 @@ fn a_screenful_of_small_faces_is_not_evicted_and_left_blank() { fn inspect_reports_the_face_scan_pace() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     h.step();     let r = h.request("ui.inspect", json!({}), t);     let scan = &r["result"]["faceScan"];@@ -1316,7 +1365,7 @@ fn profile_browser_previews_on_hover_and_applies_on_click() {     h.request("ui.set", json!({"view": "detail"}), t);     let r = h.request("engine.execute", json!({"command": "panel.profiles"}), t);     assert_eq!(r["result"]["open"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after panel.profiles");     let id = h.app.session.active().unwrap();     let photo = |h: &Headless| h.app.session.catalog.photo(id).unwrap().clone();     let before = photo(&h);@@ -1325,7 +1374,7 @@ fn profile_browser_previews_on_hover_and_applies_on_click() {     // hover: the loupe shows the look, nothing is committed     let r = h.request("ui.hoverWidget", json!({"id": "profileCell:lc.vivid"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.hoverWidget");     // the hover render can land after a quiet spell on a loaded machine (FreeBSD CI): wait for it     h.step_until(SETTLE, |h| h.app.loupe_shown.map(|l| l.1) == Some("hover"));     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Profile: Vivid"));@@ -1346,7 +1395,7 @@ fn profile_browser_previews_on_hover_and_applies_on_click() {     h.request("ui.clickWidget", json!({"id": "profileStar:lc.portrait"}), t);     assert_eq!(h.app.session.profile_favorites, ["lc.portrait"]);     assert_eq!(photo(&h).develop.profile.id, "lc.vivid", "the star doesn't apply");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking profileStar:lc.portrait"); }  /// Hovering the applied profile shows the applied look (no preview), not the last other profile hovered.@@ -1356,7 +1405,7 @@ fn hovering_the_applied_profile_drops_the_last_hover() {     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail"}), t);     h.request("engine.execute", json!({"command": "panel.profiles"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after panel.profiles");     h.request("ui.clickWidget", json!({"id": "profileCell:lc.vivid"}), t);     h.request("ui.hoverWidget", json!({"id": "profileCell:lc.portrait"}), t);     h.step();@@ -1374,7 +1423,7 @@ fn applied_preset_is_marked_again_on_returning_to_the_photo() {     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail"}), t);     h.request("engine.execute", json!({"command": "panel.presets"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after panel.presets");     let a = h.app.session.active().unwrap();     let b = h.app.session.visible_cloned().into_iter().find(|id| *id != a).unwrap();     let has = |h: &Headless, w: &str| h.app.widgets.iter().any(|(id, _)| id == w);@@ -1384,18 +1433,18 @@ fn applied_preset_is_marked_again_on_returning_to_the_photo() {     };     let r = h.request("ui.clickWidget", json!({"id": "preset:lc.warm-glow"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking preset:lc.warm-glow");     assert_eq!(marked(&h).as_deref(), Some("preset:lc.warm-glow"));     assert!(has(&h, "slider:presetAmount"));     for (photo, want) in [(b, None), (a, Some("preset:lc.warm-glow"))] {         h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [photo.0]}}), t);-        h.settle(SETTLE);+        h.settled(SETTLE, "after library.select");         h.step();         assert_eq!(marked(&h).as_deref(), want, "on photo {photo:?}");         assert_eq!(has(&h, "slider:presetAmount"), want.is_some(), "the Amount slider follows the mark");     }     h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after develop.set");     h.step();     assert_eq!(marked(&h), None, "an edit after the preset drops the mark"); }@@ -1408,7 +1457,7 @@ fn applied_preset_is_not_marked_on_an_active_photo_outside_the_selection() {     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail"}), t);     h.request("engine.execute", json!({"command": "panel.presets"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after panel.presets");     let a = h.app.session.active().unwrap();     let b = h.app.session.visible_cloned().into_iter().find(|id| *id != a).unwrap();     h.app.session.selection.ids = vec![b];@@ -1416,13 +1465,13 @@ fn applied_preset_is_not_marked_on_an_active_photo_outside_the_selection() {     let before = h.app.session.develop_of(a).unwrap();     let r = h.request("ui.clickWidget", json!({"id": "preset:lc.warm-glow"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking preset:lc.warm-glow");     h.step();     assert_eq!(h.app.session.active(), Some(a));     assert!(*h.app.session.develop_of(a).unwrap() == *before, "the active photo wasn't changed");     assert!(!h.app.widgets.iter().any(|(id, _)| id == "presetApplied"), "nor marked");     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [b.0]}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     h.step();     assert!(h.app.widgets.iter().any(|(id, _)| id == "presetApplied"), "the selected photo is"); }@@ -1435,12 +1484,12 @@ fn preset_hover_previews_and_create_preset_groups() {     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail"}), t);     h.request("engine.execute", json!({"command": "panel.presets"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after panel.presets");     let id = h.app.session.active().unwrap();     let photo = |h: &Headless| h.app.session.catalog.photo(id).unwrap().clone();     let before = photo(&h);     h.request("ui.hoverWidget", json!({"id": "preset:lc.bw-high-contrast"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.hoverWidget");     h.step_until(SETTLE, |h| h.app.loupe_shown.map(|l| l.1) == Some("hover"));     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: High Contrast B&W"));     assert_eq!(h.app.loupe_shown.map(|l| l.1), Some("hover"));@@ -1454,13 +1503,13 @@ fn preset_hover_previews_and_create_preset_groups() {     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: High Contrast B&W"));     // hovering another preset switches smoothly to it     h.request("ui.hoverWidget", json!({"id": "preset:lc.warm-glow"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.hoverWidget");     h.step();     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.label.as_str()), Some("Preset: Warm Glow"));     // thumbnails: variant textures for the visible presets     h.app.ui.preset_thumbs = true;     h.request("ui.move", json!({"x": 5, "y": 500}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.move");     h.step();     assert!(h.app.hover_preview.is_none());     assert!(h.app.renderer.variant_textures() >= 5, "{}", h.app.renderer.variant_textures());@@ -1483,7 +1532,7 @@ fn preset_hover_previews_and_create_preset_groups() {     assert_eq!(r["ok"], true, "{r}");     let p = h.app.session.presets.iter().find(|p| !p.builtin).expect("created").clone();     assert!(p.settings.get("light").is_none() && p.settings["effects"]["clarity"] == 25.0, "{}", p.settings);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm"); }  /// A collection of thousands of presets (a Lightroom library imported whole) lists only the@@ -1507,7 +1556,7 @@ fn presets_panel_with_thousands_of_presets_builds_only_visible_rows() {     h.request("ui.set", json!({"view": "detail"}), t);     h.request("engine.execute", json!({"command": "panel.presets"}), t);     h.app.ui.preset_thumbs = true;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.preset_thumbs");     h.step();     let rows = |h: &Headless| h.app.widgets.iter().filter(|(w, _)| w.starts_with("preset:")).map(|(w, _)| w.clone()).collect::<Vec<_>>();     let first = rows(&h);@@ -1552,7 +1601,7 @@ fn filter_bar_labels_from_label_and_labels() {     h.request("ui.clickWidget", json!({"id": "filter:label-green"}), t);     let f = h.app.session.filter.clone();     assert_eq!((f.label, f.labels), (None, vec![ColorLabel::Green]));-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking filter:label-green"); }  /// The filter bar drives `library.filter` and saves the view as a smart album.@@ -1580,7 +1629,7 @@ fn filter_bar_filters_and_saves_a_smart_album() {     h.request("ui.clickWidget", json!({"id": "button:filterClear"}), t);     assert_eq!(h.app.session.filter, Default::default());     assert_eq!(h.app.session.visible_cloned().len(), all);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:filterClear"); }  /// The photo grid shows date headers (registered as `group:<date>` widgets); clicking one@@ -1589,7 +1638,7 @@ fn filter_bar_filters_and_saves_a_smart_album() { fn grid_groups_by_capture_date() {     let mut h = demo([1300.0, 800.0]);     let t = Duration::from_secs(10);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     let groups = h.app.session.execute("library.groups", &json!({})).unwrap();     let first = groups[0].clone();     let key = first["key"].as_str().unwrap().to_string();@@ -1598,7 +1647,7 @@ fn grid_groups_by_capture_date() {     assert_eq!(h.app.session.selection.ids.len() as u64, first["count"].as_u64().unwrap());     // zoomed out: month headers     h.app.ui.thumb_size = 120.0;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.thumb_size");     let vis = h.app.session.visible_cloned();     let month = h.app.session.catalog.date_runs(&vis, h.app.session.sort.key, lightcraft_catalog::GroupBy::Month);     let r = h.request("ui.clickWidget", json!({"id": format!("group:{}", month[0].key)}), t);@@ -1606,7 +1655,7 @@ fn grid_groups_by_capture_date() {     h.request("engine.execute", json!({"command": "library.sort", "params": {"group": "none"}}), t);     let r = h.request("ui.clickWidget", json!({"id": format!("group:{}", month[0].key)}), t);     assert_eq!(r["ok"], false, "no headers: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget"); }  /// Keywords: the left-panel tree filters (children included), opens levels, the rename dialog@@ -1631,19 +1680,19 @@ fn keyword_list_filters_renames_and_suggests() {     h.request("ui.set", json!({"leftPanel": true}), t);     // clicks land on last frame's layout: let the keyword list settle first (on a loaded machine a     // row could still move, and the click then hit its neighbour, e.g. "sunrise")-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let r = h.request("ui.clickWidget", json!({"id": "source:keyword:travel"}), t);     assert_eq!(r["ok"], true, "{r}");     assert_eq!(h.app.session.filter.keyword.as_deref(), Some("travel"));     assert_eq!(h.app.session.visible_cloned().len(), 3);     // open the level, filter by the child     h.request("ui.clickWidget", json!({"id": "keywordToggle:travel"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking keywordToggle:travel");     let r = h.request("ui.clickWidget", json!({"id": "source:keyword:travel|italy"}), t);     assert_eq!(r["ok"], true, "{r}");     assert_eq!(h.app.session.visible_cloned().len(), 2);     h.app.ui.dialog = Some(crate::state::Dialog::RenameKeyword { from: "travel".into(), to: "trips".into() });-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.dialog");     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");     assert_eq!(h.app.session.filter.keyword.as_deref(), Some("trips|italy"));@@ -1653,11 +1702,11 @@ fn keyword_list_filters_renames_and_suggests() {     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[1]]}}), t);     ex(&mut h, "photo.setMeta", json!({"ids": [vis[0]], "addKeywords": ["gelato"]}));     h.request("ui.set", json!({"right": "keywords"}), t);-    assert!(h.settle(SETTLE), "keyword suggestions did not settle");+    h.settled(SETTLE, "keyword suggestions");     let r = h.request("ui.clickWidget", json!({"id": "kwSuggest:gelato"}), t);     assert_eq!(r["ok"], true, "{r}");     assert!(h.app.session.catalog.photo(lightcraft_catalog::PhotoId(vis[1])).unwrap().meta.keywords.contains(&"gelato".to_string()));-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking kwSuggest:gelato"); }  /// Rename Keyword's name field is a shared text field. It opens with the name selected, so@@ -1672,12 +1721,12 @@ fn the_rename_keyword_field_selects_pastes_and_cancels() {     let keywords = |h: &Headless| h.app.session.catalog.photo(lightcraft_catalog::PhotoId(first)).unwrap().meta.keywords.clone();     let open = |h: &mut Headless, name: &str| {         h.app.ui.dialog = Some(crate::state::Dialog::RenameKeyword { from: name.into(), to: name.into() });-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.dialog");     };     let ask = |h: &mut Headless, method: &str, params: serde_json::Value| {         let r = h.request(method, params.clone(), t);         assert_eq!(r["ok"], true, "{method} {params}: {r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after the request");     };     // typing replaces the selected name     open(&mut h, "travel");@@ -1715,11 +1764,11 @@ fn return_confirms_the_one_field_dialogs() {     let garden = album(&h, "Garden").expect("the demo's Garden album");     let confirm = |h: &mut Headless, dialog: Dialog, typed: &str| {         h.app.ui.dialog = Some(dialog);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.dialog");         for (method, params) in [("ui.text", json!({"text": typed})), ("ui.key", json!({"key": "Enter"}))] {             let r = h.request(method, params, t);             assert_eq!(r["ok"], true, "{r}");-            h.settle(SETTLE);+            h.settled(SETTLE, "after the request");         }         assert_eq!(h.app.ui.dialog, None, "Return confirmed the dialog ({typed})");     };@@ -1759,7 +1808,7 @@ fn settle_waits_for_file_system_checks() {         true     }     let mut h = demo([800.0, 600.0]);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     let check = |h: &mut Headless| {         let raw = HeadlessView::raw_input(h.size, 1.0, 0.0, vec![]);         let mut got = None;@@ -1769,7 +1818,7 @@ fn settle_waits_for_file_system_checks() {     assert_eq!(check(&mut h), None, "the answer is worked out off the UI thread");     assert!(h.busy(), "a pending file-system check is pending work");     RELEASE.store(true, Ordering::Release);-    assert!(h.settle(SETTLE));+    h.settled(SETTLE, "after check");     assert!(!h.busy());     assert_eq!(check(&mut h), Some(true)); }@@ -1784,13 +1833,13 @@ fn rename_dialog_renames_the_selection() {     let tree = h.request("ui.menu.tree", json!({}), t);     assert!(tree.to_string().contains("Rename 2 Photos…"), "live menu label");     h.request("engine.execute", json!({"command": "dialog.rename", "params": {"template": "Trip-{seq:2}", "start": 5}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after dialog.rename");     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");     let name = |id: u64| h.app.session.catalog.photo(lightcraft_catalog::PhotoId(id)).unwrap().file_name.clone();     assert!(name(vis[0]).starts_with("Trip-05."), "{}", name(vis[0]));     assert!(name(vis[1]).starts_with("Trip-06."), "{}", name(vis[1]));-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm"); }  /// The Export dialog hands its batch to a worker thread: the UI keeps drawing frames, shows@@ -1840,7 +1889,7 @@ fn export_dialog_previews_the_watermark() {         let r = h.request("ui.widgets", json!({}), t);         r["result"].as_array().and_then(|a| a.iter().find(|w| w["id"] == "image:exportWatermarkPreview")).map(|w| w["rect"].clone())     };-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.widgets");     assert!(preview(&mut h).is_none(), "no preview without a watermark");     if let Some(crate::state::Dialog::Export { opts, .. }) = &mut h.app.ui.dialog {         opts.watermark = Some(lightcraft_engine::export::Watermark { text: "© LightCraft".into(), ..Default::default() });@@ -1915,7 +1964,7 @@ fn export_dialog_runs_in_the_background() {     let ids: Vec<u64> = h.app.session.visible_cloned().iter().take(3).map(|p| p.0).collect();     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": ids}}), t);     h.request("engine.execute", json!({"command": "dialog.export", "params": {}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after dialog.export");     if let Some(crate::state::Dialog::Export { full_size, resize, dir, .. }) = &mut h.app.ui.dialog {         *full_size = false;         *resize = lightcraft_engine::export::Resize::long_edge(64);@@ -1962,14 +2011,14 @@ fn export_dialog_adds_to_apple_photos_on_macos() {     let ids: Vec<u64> = h.app.session.visible_cloned().iter().take(2).map(|p| p.0).collect();     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": ids}}), t);     h.request("engine.execute", json!({"command": "dialog.export", "params": {}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after dialog.export");     let shown = h.request("ui.widgets", json!({"filter": "exportAddToPhotos"}), t)["result"].as_array().map_or(0, Vec::len);     assert_eq!(shown > 0, cfg!(target_os = "macos"), "the option is offered where Photos is");     if !cfg!(target_os = "macos") {         return;     }     h.request("ui.clickWidget", json!({"id": "exportAddToPhotos"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking exportAddToPhotos");     if let Some(crate::state::Dialog::Export { opts, full_size, resize, dir, .. }) = &mut h.app.ui.dialog {         assert!(opts.add_to_photos, "the checkbox turns it on");         opts.photos_album = "Trip “2026”".into();@@ -2219,7 +2268,7 @@ fn drag_photos_onto_an_album() {     h.request("engine.execute", json!({"command": "view.leftPanel"}), t);     let r = h.request("engine.execute", json!({"command": "album.create", "params": {"name": "Dropped"}}), t);     let album = r["result"]["id"].as_u64().unwrap_or_else(|| panic!("{r}"));-    h.settle(SETTLE);+    h.settled(SETTLE, "after album.create");     let rect = |h: &mut Headless, id: &str| {         let w = h.request("ui.widgets", json!({"filter": id}), t);         let r =@@ -2232,7 +2281,7 @@ fn drag_photos_onto_an_album() {     let (tx, ty) = rect(&mut h, &format!("source:album:{album}"));     let r = h.request("ui.drag", json!({"x": x, "y": y, "toX": tx, "toY": ty, "steps": 12}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.drag");     let members = h.app.session.catalog.album(lightcraft_catalog::AlbumId(album)).unwrap().photos.clone();     assert_eq!(members, vec![lightcraft_catalog::PhotoId(first)]);     assert!(h.app.ui.dragging_photos.is_none(), "the drag ended");@@ -2261,7 +2310,7 @@ fn preview_only_raw_shows_badge_and_notices() {             preview_checked: lightcraft_catalog::APP_VERSION.into(),         })         .unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let ids = |h: &mut Headless, filter: &str| -> Vec<String> {         let w = h.request("ui.widgets", json!({"filter": filter}), t);         w["result"].as_array().unwrap_or_else(|| panic!("{w}")).iter().filter_map(|x| x["id"].as_str().map(str::to_string)).collect()@@ -2272,25 +2321,25 @@ fn preview_only_raw_shows_badge_and_notices() {     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [po.0]}}), t);     h.request("ui.set", json!({"view": "detail"}), t);     h.app.ui.right = crate::state::RightPanel::Edit;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.right");     let n = ids(&mut h, "notice:previewOnly:");     assert!(n.contains(&"notice:previewOnly:loupe".to_string()) && n.contains(&"notice:previewOnly:edit".to_string()), "{n:?}");     // with a Reload from Disk button (issue #662)     assert_eq!(ids(&mut h, "button:reloadPreviewOnly:"), vec!["button:reloadPreviewOnly:edit".to_string()]);     // Info     h.app.ui.right = crate::state::RightPanel::Info;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.right");     assert!(ids(&mut h, "notice:previewOnly:").contains(&"notice:previewOnly:info".to_string()));     assert_eq!(ids(&mut h, "button:reloadPreviewOnly:"), vec!["button:reloadPreviewOnly:info".to_string()]);     // …which re-reads the file and says what came of it (a demo photo has none to re-read)     let r = h.request("ui.clickWidget", json!({"id": "button:reloadPreviewOnly:info"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:reloadPreviewOnly:info");     assert_eq!(h.app.ui.toast.clone().map(|x| x.0).as_deref(), Some("This raw variant still can't be decoded"));     // a regular photo: no notice     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [other.0]}}), t);     h.app.ui.right = crate::state::RightPanel::Edit;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.right");     assert!(ids(&mut h, "notice:previewOnly:").is_empty()); } @@ -2306,7 +2355,7 @@ fn crop_default_browsing_is_read_only_and_drag_is_one_edit() {         h.app.run("library.select", json!({"ids": [photo.0]})).unwrap();         let before = h.app.session.catalog.photo(photo).unwrap().clone();         let undo = h.app.session.undo.len();-        h.settle(SETTLE);+        h.settled(SETTLE, "after library.select");         let after = h.app.session.catalog.photo(photo).unwrap();         assert_eq!(after.develop, before.develop);         assert_eq!(after.history.len(), before.history.len());@@ -2333,7 +2382,7 @@ fn crop_default_browsing_is_read_only_and_drag_is_one_edit() {     h.app.ui.crop_default_aspect = Some(json!("original"));     let p = h.app.session.catalog.photo(photo).unwrap();     assert_eq!(h.app.crop_tool_aspect(), Some((p.width * 100, p.height * 100)));-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.crop_default_aspect");     assert_eq!(h.app.session.develop_of(photo).unwrap(), before); } @@ -2373,7 +2422,7 @@ fn click_field_in_view(h: &mut Headless, key: &str, timeout: Duration) {         let dy = (panel.center().y - rect.center().y).clamp(-4000.0, 4000.0);         assert_eq!(h.request("ui.move", json!({"x": panel.center().x, "y": panel.center().y}), timeout)["ok"], true);         assert_eq!(h.request("ui.scroll", json!({"dy": dy}), timeout)["ok"], true);-        h.settle(timeout);+        h.settled(timeout, "after ui.scroll");     }     let rect = h.app.widgets.iter().find(|(w, _)| *w == id).map(|(_, r)| *r).expect("the field is laid out");     assert!(panel.contains(rect.center()), "the field must be scrolled into view before clicking: {rect:?} in {panel:?}");@@ -2388,7 +2437,7 @@ fn info_fields_keep_typing_and_save() {     let mut h = demo([1300.0, 1000.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     for (key, text) in [("altText", "A lake at dawn"), ("usageTerms", "Editorial use only"), ("city", "Zermatt")] {         click_field_in_view(&mut h, key, t);         h.request("ui.key", json!({"key": "A", "cmd": true}), t);@@ -2397,7 +2446,7 @@ fn info_fields_keep_typing_and_save() {         h.step();         // still being edited: not saved yet, not lost either         h.request("ui.key", json!({"key": "Tab"}), t);-        h.settle(Duration::from_secs(5));+        h.settled(Duration::from_secs(5), "after key Tab");     }     let m = &h.app.session.catalog.photo(h.app.session.active().unwrap()).unwrap().meta;     assert_eq!((m.alt_text.as_str(), m.city.as_str()), ("A lake at dawn", "Zermatt"));@@ -2415,7 +2464,7 @@ fn info_fields_take_lines_esc_and_the_menu() {     let mut h = demo([1300.0, 1000.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let meta = |h: &Headless| h.app.session.catalog.photo(h.app.session.active().unwrap()).unwrap().meta.clone();     let ask = |h: &mut Headless, method: &str, params: serde_json::Value| {         let r = h.request(method, params.clone(), t);@@ -2438,7 +2487,7 @@ fn info_fields_take_lines_esc_and_the_menu() {     ask(&mut h, "ui.clickWidget", json!({"id": "field:title"}));     ask(&mut h, "ui.text", json!({"text": " (draft)"}));     ask(&mut h, "ui.key", json!({"key": "Escape"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_eq!((meta(&h).title, h.app.session.undo.len()), (title, undo), "Esc saved nothing");     // the menu     h.view.clipboard = "Wedding in Sintra".into();@@ -2476,7 +2525,7 @@ fn info_shows_metadata_for_the_selected_photos() {     exec(&mut h, "photo.setMeta", json!({"ids": ids, "creator": "Ana"}));     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);     exec(&mut h, "library.select", json!({"ids": ids, "active": ids[0]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     let buttons = |h: &mut Headless| h.request("ui.widgets", json!({"filter": "infoFor"}), t)["result"].to_string();     assert!(buttons(&mut h).contains("button:infoFor-selected"), "the choice shows with two photos selected");     assert!(h.app.ui.settings.info_selected_photos, "Selected Photos by default");@@ -2490,14 +2539,14 @@ fn info_shows_metadata_for_the_selected_photos() {     let undo = h.app.session.undo.len();     click_field_in_view(&mut h, "title", t);     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert_eq!((meta(&h, a).title, meta(&h, b).title, h.app.session.undo.len()), ("Dawn!".into(), "Dusk".into(), undo));     // neither does switching the choice while a field is being edited, either way     for on in [false, true] {         click_field_in_view(&mut h, "title", t);         exec(&mut h, "view.infoSelectedPhotos", json!({"on": on}));         ask(&mut h, "ui.key", json!({"key": "Tab"}));-        h.settle(SETTLE);+        h.settled(SETTLE, "after key Tab");         assert_eq!((meta(&h, a).title, meta(&h, b).title, h.app.session.undo.len()), ("Dawn!".into(), "Dusk".into(), undo), "{on}");     }     // the mixed title starts empty; the shared creator keeps its value@@ -2506,28 +2555,28 @@ fn info_shows_metadata_for_the_selected_photos() {         ask(&mut h, "ui.key", json!({"key": "End"}));         ask(&mut h, "ui.text", json!({"text": typed}));         ask(&mut h, "ui.key", json!({"key": "Tab"}));-        h.settle(SETTLE);+        h.settled(SETTLE, "after key Tab");         let got = |h: &Headless, id| if key == "title" { meta(h, id).title } else { meta(h, id).creator };         assert_eq!((got(&h, a), got(&h, b)), (want.to_string(), want.to_string()), "{key}");     }     // a mixed field typed in and left empty clears it on every selected photo     exec(&mut h, "photo.setMeta", json!({"ids": [ids[0]], "caption": "Old"}));     exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "caption": "Other"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.setMeta");     click_field_in_view(&mut h, "caption", t);     ask(&mut h, "ui.text", json!({"text": "x"}));     ask(&mut h, "ui.key", json!({"key": "Backspace"}));     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert_eq!((meta(&h, a).caption, meta(&h, b).caption), (String::new(), String::new()));     // GPS: one photo's place and Show on Map only while the selected photos share it     let map = |h: &mut Headless| h.request("ui.widgets", json!({"filter": "showOnMap"}), t)["result"].to_string().contains("showOnMap");     exec(&mut h, "photo.setMeta", json!({"ids": [ids[0]], "gps": [41.15, -8.61]}));     exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "gps": [38.72, -9.14]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.setMeta");     assert!(!map(&mut h), "places differ");     exec(&mut h, "photo.setMeta", json!({"ids": ids, "gps": [41.15, -8.61]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.setMeta");     assert!(map(&mut h), "a shared place");     // the command: refuses a value that isn't true or false, and turns Target Photo back on     let r = h.request("engine.execute", json!({"command": "view.infoSelectedPhotos", "params": {"on": "yes"}}), t);@@ -2546,7 +2595,7 @@ fn info_shows_metadata_for_the_selected_photos() {     assert!(old.info_selected_photos);     // one photo selected: no choice to make     exec(&mut h, "library.select", json!({"ids": [ids[0]]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     assert!(!buttons(&mut h).contains("infoFor-"), "{}", buttons(&mut h)); } @@ -2572,12 +2621,12 @@ fn info_edits_of_a_multi_selection_start_from_mixed_fields_by_default() {     exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "caption": "Dusk"}));     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);     exec(&mut h, "library.select", json!({"ids": ids, "active": ids[0]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     click_field_in_view(&mut h, "caption", t);     ask(&mut h, "ui.key", json!({"key": "End"}));     ask(&mut h, "ui.text", json!({"text": "Harbor"}));     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert_eq!((caption(&h, 0), caption(&h, 1)), ("Harbor".into(), "Harbor".into())); } @@ -2603,12 +2652,12 @@ fn info_target_photo_edits_only_the_photo_it_shows() {     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);     exec(&mut h, "library.select", json!({"ids": ids, "active": ids[0]}));     exec(&mut h, "view.infoSelectedPhotos", json!({"on": false}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.infoSelectedPhotos");     click_field_in_view(&mut h, "caption", t);     ask(&mut h, "ui.key", json!({"key": "End"}));     ask(&mut h, "ui.text", json!({"text": "!"}));     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert_eq!((caption(&h, 0), caption(&h, 1)), ("Dawn!".into(), "Dusk".into())); } @@ -2661,12 +2710,12 @@ fn info_selected_photos_ignore_an_active_photo_outside_the_selection() {     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[2]}));     exec(&mut h, "view.infoSelectedPhotos", json!({"on": true}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.infoSelectedPhotos");     click_field_in_view(&mut h, "title", t);     ask(&mut h, "ui.key", json!({"key": "End"}));     ask(&mut h, "ui.text", json!({"text": "!"}));     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert_eq!((title(&h, 0), title(&h, 1), title(&h, 2)), ("Same!".into(), "Same!".into(), "Other".into())); } @@ -2691,34 +2740,34 @@ fn info_field_saves_to_the_photos_selected_when_the_edit_started() {     exec(&mut h, "photo.setMeta", json!({"ids": ids, "title": "Same", "gps": ""}));     h.request("ui.set", json!({"view": "detail", "right": "info"}), t);     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     click_field_in_view(&mut h, "title", t);     ask(&mut h, "ui.key", json!({"key": "End"}));     ask(&mut h, "ui.text", json!({"text": "!"}));     exec(&mut h, "library.select", json!({"ids": [ids[2]]}));     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert_eq!((meta(&h, 0).title, meta(&h, 1).title, meta(&h, 2).title), ("Same!".into(), "Same!".into(), "Same".into()));     // GPS too (the new selection is two photos as well, so the panel keeps its layout)     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     click_field_in_view(&mut h, "gps", t);     ask(&mut h, "ui.text", json!({"text": "41.15, -8.61"}));     exec(&mut h, "library.select", json!({"ids": [ids[2], ids[3]], "active": ids[2]}));     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     let gps: Vec<bool> = (0..4).map(|i| meta(&h, i).gps.is_some()).collect();     assert_eq!(gps, [true, true, false, false]);     // Esc gives a GPS edit up     exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     let before = meta(&h, 0).gps;     click_field_in_view(&mut h, "gps", t);     ask(&mut h, "ui.key", json!({"key": "End"}));     ask(&mut h, "ui.text", json!({"text": "5"}));     ask(&mut h, "ui.key", json!({"key": "Escape"}));     ask(&mut h, "ui.key", json!({"key": "Tab"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert_eq!((meta(&h, 0).gps, meta(&h, 1).gps), (before, before)); } @@ -2743,10 +2792,10 @@ fn browse_a_local_folder() {     // the folder is read in the background: the view switches at once, the photos follow     assert_eq!(r["result"]["scanning"], true, "{r}");     assert_eq!(h.app.session.source, lightcraft_engine::LibrarySource::Folder);-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     assert!(h.app.scan.is_none() && h.app.import.is_none());     assert_eq!(h.app.session.visible_cloned().len(), 1);-    assert!(h.settle(SETTLE), "a thumbnail that can't load must not keep the renderer busy");+    h.settled(SETTLE, "a thumbnail that can't load must not keep the renderer busy");     // this test session has no file hooks: the thumbnail fails once, is remembered, and isn't retried     let id = h.app.session.visible_cloned()[0];     assert!(h.app.renderer.failure(crate::render::Slot::Thumb(id)).is_some());@@ -2758,18 +2807,18 @@ fn browse_a_local_folder() {     let w = h.request("ui.widgets", json!({"filter": "crumb:"}), t);     assert!(w["result"].as_array().is_some_and(|a| !a.is_empty()), "breadcrumb: {w}");     assert_eq!(h.request("ui.clickWidget", json!({"id": "check:includeSubfolders"}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking check:includeSubfolders");     assert_eq!(h.app.session.visible_cloned().len(), 2);     assert_eq!(h.request("ui.clickWidget", json!({"id": "button:addToLibrary"}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:addToLibrary");     assert_eq!(h.app.session.catalog.photos().filter(|p| !p.local).count(), library_before + 2);     // Truncated ancestors remain clickable and still open the original, unabridged path.     let inner = dir.join("inner");     h.request("engine.execute", json!({"command": "library.browse", "params": {"path": inner.to_string_lossy()}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     let parent_index = dir.to_string_lossy().split(['/', '\\']).filter(|p| !p.is_empty()).count() - 1;     assert_eq!(h.request("ui.clickWidget", json!({"id": format!("crumb:{parent_index}")}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     // the same folder, whatever the platform's separators (issue #538)     assert_eq!(std::path::Path::new(&h.app.session.browse.as_ref().unwrap().path), dir.as_path());     let _ = std::fs::remove_dir_all(&dir);@@ -2806,7 +2855,7 @@ fn local_location_can_be_hidden_and_restored() {     assert_eq!(r["ok"], true, "{r}");     let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": path}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     let row = format!("source:local:{path}");     assert!(h.step_until(t, |h| h.app.widgets.iter().any(|(w, _)| *w == row)), "the browsed folder is listed: {:?}", ids(&mut h));     // the "Show hidden locations" row appears only while something is hidden@@ -2815,13 +2864,13 @@ fn local_location_can_be_hidden_and_restored() {     // hide it while it is still being browsed     let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after local.hide");     // give any listing a reveal would wait for time to arrive     for _ in 0..20 {         h.step();         std::thread::sleep(Duration::from_millis(5));     }-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let after = rects(&mut h);     let after_ids: Vec<&str> = after.iter().map(|(id, _)| id.as_str()).collect();     assert!(!after_ids.contains(&format!("source:local:{path}").as_str()), "{after_ids:?}");@@ -2840,38 +2889,38 @@ fn local_location_can_be_hidden_and_restored() {         let r = h.request("engine.execute", json!({"command": command, "params": params}), t);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after engine.execute");     let menu = |h: &mut Headless| -> Vec<String> {         let w = h.request("ui.widgets", json!({"filter": "local:restoreHidden:"}), t);         w["result"].as_array().unwrap().iter().map(|x| x["id"].as_str().unwrap().to_string()).collect()     };     assert!(menu(&mut h).is_empty(), "the list opens from the row");     assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking source:local:restoreHidden");     let items = menu(&mut h);     assert!(items.contains(&format!("local:restoreHidden:{path}")) && items.contains(&"local:restoreHidden:all".to_string()), "{items:?}");     assert_eq!(h.request("ui.clickWidget", json!({"id": format!("local:restoreHidden:{path}")}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     assert!(!h.app.ui.hidden_locations.iter().any(|x| x == &path), "{:?}", h.app.ui.hidden_locations);     assert!(h.app.ui.hidden_locations.iter().any(|x| x == &path2), "the other place stays hidden");     assert!(ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"), "still something to restore");     // two hidden again: Restore All brings every one back     let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after local.hide");     assert!(h.app.ui.hidden_locations.len() >= 2, "{:?}", h.app.ui.hidden_locations);     assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking source:local:restoreHidden");     assert_eq!(h.request("ui.clickWidget", json!({"id": "local:restoreHidden:all"}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking local:restoreHidden:all");     assert!(h.app.ui.hidden_locations.is_empty(), "{:?}", h.app.ui.hidden_locations);     assert!(!ids(&mut h).iter().any(|i| i == "source:local:restoreHidden"), "nothing left to restore");     // one hidden: the row brings it back itself, no list     let r = h.request("engine.execute", json!({"command": "local.hide", "params": {"path": path2}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after local.hide");     assert_eq!(h.request("ui.clickWidget", json!({"id": "source:local:restoreHidden"}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking source:local:restoreHidden");     assert!(menu(&mut h).is_empty(), "no list for a single hidden place");     assert!(h.app.ui.hidden_locations.is_empty(), "{:?}", h.app.ui.hidden_locations);     if !hidden_before {@@ -2913,7 +2962,7 @@ fn kept_local_root_stays_while_browsing_below_and_elsewhere() {     assert_eq!(exec(&mut h, "local.addRoot", json!({"path": photos}))["ok"], true);     assert_eq!(exec(&mut h, "local.addRoot", json!({"path": format!("{photos}/")}))["result"]["roots"].as_array().map(Vec::len), Some(1));     exec(&mut h, "library.browse", json!({"path": day1}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     h.step();     let row = format!("source:local:{day2}");     assert!(h.step_until(t, |h| h.app.widgets.iter().any(|(w, _)| *w == row)), "the sibling listing did not arrive");@@ -2923,11 +2972,11 @@ fn kept_local_root_stays_while_browsing_below_and_elsewhere() {     let (root_x, child_x) = (r[&format!("folderToggle:{photos}")], r[&format!("folderToggle:{day1}")]);     assert!(child_x > root_x, "the browsed folder is inside the root's tree, not a root of its own ({child_x} vs {root_x})");     assert_eq!(h.request("ui.clickWidget", json!({"id": format!("source:local:{day2}")}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     assert_eq!(h.app.session.browse.as_ref().map(|b| b.path.clone()), Some(day2.clone()), "the sibling is browsed");     // another location: the kept root stays listed     assert_eq!(exec(&mut h, "library.browse", json!({"path": other}))["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     h.step();     let row = format!("source:local:{other}");     assert!(h.step_until(t, |h| h.app.widgets.iter().any(|(w, _)| *w == row)), "the other location did not arrive");@@ -2938,7 +2987,7 @@ fn kept_local_root_stays_while_browsing_below_and_elsewhere() {     let back: crate::state::UiState = serde_json::from_str(&saved).unwrap();     assert_eq!(back.local_roots, vec![photos.clone()]);     let _ = std::fs::remove_dir_all(&base);-    h.settle(SETTLE);+    h.settled(SETTLE, "after rects"); }  /// Versions panel: resting on a version previews it in the loupe without changing the photo;@@ -2952,14 +3001,14 @@ fn versions_preview_on_hover_and_restore_on_click() {     exec(&mut h, "develop.set", json!({"values": {"light.exposure": 1.0}}));     exec(&mut h, "version.create", json!({"name": "Bright"}));     exec(&mut h, "develop.set", json!({"values": {"light.exposure": -1.0}}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after develop.set");     let exposure = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;     assert_eq!(h.request("ui.hoverWidget", json!({"id": "version:0"}), t)["ok"], true);     h.step();     assert_eq!(h.app.hover_preview.as_ref().map(|p| p.settings.light.exposure), Some(1.0));     assert_eq!(exposure(&h), -1.0, "hovering changes nothing");     assert_eq!(h.request("ui.clickWidget", json!({"id": "version:0"}), t)["ok"], true);-    h.settle(Duration::from_secs(5));+    h.settled(Duration::from_secs(5), "after clicking version:0");     assert_eq!(exposure(&h), 1.0); } @@ -2969,7 +3018,7 @@ fn slider_nudge_and_pick_advance() {     let mut h = demo([1300.0, 900.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let exposure = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;     assert_eq!(h.request("ui.hoverWidget", json!({"id": "slider:light.exposure"}), t)["ok"], true);     h.request("ui.key", json!({"key": "Up"}), t);@@ -2990,7 +3039,7 @@ fn slider_values_can_be_typed() {     let mut h = demo([1300.0, 900.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let active = h.app.session.active().unwrap();     let exposure = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;     let rating = |h: &Headless| h.app.session.catalog.photo(active).unwrap().rating;@@ -3065,17 +3114,17 @@ fn a_slider_value_pastes_from_its_menu() {     let mut h = demo([1300.0, 900.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h.view.clipboard = "0.75".into();     for (id, button) in         [("sliderValue:light.exposure", "left"), ("sliderField:light.exposure", "right"), ("sliderField:light.exposure:paste", "left")]     {         let r = h.request("ui.clickWidget", json!({"id": id, "button": button}), t);         assert_eq!(r["ok"], true, "{id}: {r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.clickWidget");     }     h.request("ui.key", json!({"key": "Enter"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Enter");     let exposure = h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().light.exposure;     assert!((exposure - 0.75).abs() < 1e-9, "{exposure}"); }@@ -3087,7 +3136,7 @@ fn section_eye_switches_a_section_off() {     let mut h = demo([1300.0, 900.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "edit"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let light_on = |h: &Headless| h.app.session.develop_of(h.app.session.active().unwrap()).unwrap().section_enabled("light");     for expected in [false, true] {         // the eye shows while the pointer is on the header@@ -3141,7 +3190,7 @@ fn add_folder_opens_the_import_review() {     std::fs::write(sub.join("b.png"), &png).unwrap();     let r = h.request("engine.execute", json!({"command": "file.addFolder", "params": {"path": dir.to_string_lossy()}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addFolder");     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review: {:?}", h.app.ui.dialog) };     assert_eq!(opts.candidates.len(), 2, "both files, the subfolder's too");     assert!(!opts.copy, "a folder is added in place by default");@@ -3155,7 +3204,7 @@ fn add_folder_opens_the_import_review() {     // The source facts were cached by a worker with the source-language default. They must     // still be rendered in German after switching, with the folder name kept verbatim.     assert_eq!(h.request("engine.execute", json!({"command": "app.language.german"}), t)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after app.language.german");     fn texts(shape: &egui::epaint::Shape, out: &mut Vec<String>) {         match shape {             egui::epaint::Shape::Text(shape) => out.push(shape.galley.job.text.clone()),@@ -3176,7 +3225,7 @@ fn add_folder_opens_the_import_review() {     h.app.ui.dialog = None;     let r = h.request("engine.execute", json!({"command": "file.addFromDevice", "params": {"path": sub.to_string_lossy()}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addFromDevice");     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };     assert!(opts.copy);@@ -3192,12 +3241,12 @@ fn settings_tabs_keep_the_window_in_place() {     let window = |h: &Headless| h.app.widgets.iter().find(|(w, _)| w == "dialog:window").map(|(_, r)| *r).expect("the dialog");     let r = h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": "general"}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after app.settings");     let first = window(&h);     let mut heights = Vec::new();     for tab in ["display", "import", "performance", "general", "interface", "display"] {         h.request("ui.clickWidget", json!({"id": format!("button:settingsTab-{tab}")}), t);-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.clickWidget");         let now = window(&h);         assert!((now.top() - first.top()).abs() < 1.0, "{tab}: top {} → {}", first.top(), now.top());         assert!(now.width() >= first.width() - 1.0, "{tab}: width {} → {}", first.width(), now.width());@@ -3277,7 +3326,7 @@ fn browsing_the_same_folder_again_does_not_restart() {     std::thread::sleep(Duration::from_millis(200));     assert_eq!(started.load(std::sync::atomic::Ordering::Relaxed), 1, "the second click must not start another read");     gate.store(true, std::sync::atomic::Ordering::Relaxed);-    h.settle(SETTLE);+    h.settled(SETTLE, "after browse");     assert_eq!(h.app.session.visible_cloned().len(), 1);     let _ = std::fs::remove_dir_all(&dir); }@@ -3299,7 +3348,7 @@ fn browsing_from_the_ui_records_the_folder() {     assert!(h.app.session.catalog.last_browsed(&root).is_none());     let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": root, "subfolders": true}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     assert_eq!(h.app.session.visible_cloned().len(), 1);     assert!(h.app.session.catalog.last_browsed(&root).is_some(), "the browsed folder");     assert!(h.app.session.catalog.last_browsed(&day_s).is_some(), "the folder its photo is in");@@ -3323,7 +3372,7 @@ fn browsing_again_from_the_ui_forgets_deleted_files() {     let browse = |h: &mut Headless| {         let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": path}}), t);         assert_eq!(r["result"]["scanning"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after library.browse");     };     browse(&mut h);     assert_eq!(h.app.session.visible_cloned().len(), 2);@@ -3359,7 +3408,7 @@ fn add_folder_scans_in_the_background() {     let r = h.request("engine.execute", json!({"command": "file.addFolder", "params": {"path": dir.to_string_lossy()}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");     assert!(h.app.scan.is_some() || h.app.ui.dialog.is_some());-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addFolder");     assert!(h.app.scan.is_none());     assert!(matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));     let _ = std::fs::remove_dir_all(&dir);@@ -3405,7 +3454,7 @@ fn folder_scan_cancel_and_busy() {     h.step();     assert!(h.app.scan.is_none(), "✕ closes the scan at once");     gate.store(true, std::sync::atomic::Ordering::Relaxed);-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(h.app.ui.dialog.is_none(), "a cancelled scan opens no review");     let _ = std::fs::remove_dir_all(&dir); }@@ -3423,21 +3472,21 @@ fn paste_selected_settings_and_find() {     exec(&mut h, "develop.copy", json!({"groups": ["light", "color"]}));     exec(&mut h, "library.select", json!({"ids": [ids[1]]}));     h.request("ui.key", json!({"key": "V", "cmd": true, "shift": true}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key V");     let Some(crate::state::Dialog::PasteSettings { groups }) = &h.app.ui.dialog else { panic!("dialog not open: {:?}", h.app.ui.dialog) };     assert_eq!(groups, &["light", "color"]);     let r = h.request("ui.clickWidget", json!({"id": "pasteGroup:color"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking pasteGroup:color");     assert_eq!(h.request("ui.dialog.confirm", json!({}), t)["ok"], true);     let d = h.app.session.develop_of(lightcraft_catalog::PhotoId(ids[1])).unwrap();     assert_eq!((d.light.exposure, d.color.vibrance), (1.0, 0.0));     // ⌘F, then typing filters the grid by text     h.request("ui.set", json!({"view": "grid"}), t);     h.request("ui.key", json!({"key": "F", "cmd": true}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key F");     h.request("ui.text", json!({"text": "zz-no-such-photo"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.text");     assert_eq!(h.app.ui.search, "zz-no-such-photo");     assert!(h.app.session.visible_cloned().is_empty()); }@@ -3457,7 +3506,7 @@ fn capture_time_dialog_shifts_time_zone() {     assert!(!panel.contains_rect(clipped_action), "the capture-time action starts below the visible panel: {clipped_action:?}");     h.request("ui.move", json!({"x": panel.center().x, "y": panel.center().y}), t);     h.request("ui.scroll", json!({"dy": -160.0}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.scroll");     let action = h.app.widgets.iter().find(|(id, _)| id == "icon:editCaptureTime").unwrap().1;     assert!(panel.contains_rect(action), "capture-time action must be visible before clicking: {action:?} in {panel:?}");     let r = h.request("ui.clickWidget", json!({"id": "icon:editCaptureTime"}), t);@@ -3469,13 +3518,13 @@ fn capture_time_dialog_shifts_time_zone() {     } else {         panic!("dialog not open: {:?}", h.app.ui.dialog);     }-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");     let after = h.app.session.catalog.photo(id).unwrap().captured.clone().unwrap();     let secs = lightcraft_catalog::dates::iso_seconds;     assert_eq!(secs(&after).unwrap() - secs(&before).unwrap(), -3 * 3600);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm"); }  /// Colour labels: Info-panel swatches set/clear the label, the names dialog renames them and@@ -3496,14 +3545,14 @@ fn label_swatches_and_names() {     } else {         panic!("no dialog");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");     let tree = h.request("ui.menu.tree", json!({}), t).to_string();     assert!(tree.contains("Approved (Green)"), "menu shows the name");     h.request("ui.clickWidget", json!({"id": "label:green"}), t);     assert_eq!(h.app.session.catalog.photo(id).unwrap().label, None, "clicking the current label clears it");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking label:green"); }  /// File → Import Photos… opens the import review: candidates with thumbnails, the duplicate is@@ -3531,7 +3580,7 @@ fn import_review_dialog() {     let undo0 = h.app.session.undo.len();     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.to_string_lossy()]}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addPhotos");     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));     h.step_until(SETTLE, |h| h.app.renderer.textures.keys().filter(|s| matches!(s, crate::render::Slot::Import(_))).count() >= 5);     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };@@ -3550,7 +3599,7 @@ fn import_review_dialog() {     }     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm");     assert!(h.app.import.is_none(), "finished");     assert_eq!(h.app.session.catalog.len(), n0 + 4);     let album = h.app.session.catalog.albums().find(|a| a.name == "Card 1").expect("album").id;@@ -3559,7 +3608,7 @@ fn import_review_dialog() {     assert_eq!(h.app.session.undo.len(), undo0 + 1, "one undo step");     h.request("engine.execute", json!({"command": "edit.undo"}), t);     assert_eq!(h.app.session.catalog.len(), n0);-    h.settle(SETTLE);+    h.settled(SETTLE, "after edit.undo");     let _ = std::fs::remove_dir_all(&dir); } @@ -3657,7 +3706,7 @@ fn import_review_checks_a_recently_deleted_file_by_hand() {     let Some(crate::state::Dialog::Import { opts }) = &mut h.app.ui.dialog else { panic!("no import review") };     let trashed = (0..opts.candidates.len()).find(|i| opts.is_trashed(*i)).expect("a file in Recently Deleted");     opts.set_on_deleted("");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let checked = |h: &Headless| matches!(&h.app.ui.dialog, Some(crate::state::Dialog::Import { opts }) if opts.checked[trashed]);     assert!(!checked(&h), "Leave them: unchecked");     let cell = |h: &mut Headless| {@@ -3668,16 +3717,16 @@ fn import_review_checks_a_recently_deleted_file_by_hand() {     // the checkbox: 16 pt, 6 pt in from the cell's top-left corner     let [x, y, ..] = cell(&mut h);     h.request("ui.click", json!({"x": x + 14.0, "y": y + 14.0}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(checked(&h), "a click on the checkbox checks the file");     // the badges ("Will be restored" at the top right, "In Recently Deleted" under the     // checkbox) don't swallow the click either     let [x, y, w, _] = cell(&mut h);     h.request("ui.click", json!({"x": x + w - 14.0, "y": y + 12.0}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(!checked(&h), "a click on the badge unchecks it again");     h.request("ui.click", json!({"x": x + w - 14.0, "y": y + 34.0}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(checked(&h));     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");@@ -3772,7 +3821,7 @@ fn readding_a_deleted_photo_shows_it_in_recently_deleted() {         assert!(h.app.import.is_none(), "import finished");     };     crate::import::start_paths(&mut h.app, paths.clone()).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "after start_paths");     let id = h.app.session.selection.active.expect("imported photo selected");     let n0 = h.app.session.catalog.len();     h.request("engine.execute", json!({"command": "photo.delete", "params": {"ids": [id.0]}}), t);@@ -3798,19 +3847,19 @@ fn readding_a_deleted_photo_shows_it_in_recently_deleted() {     assert!(h.app.session.catalog.photo(id).unwrap().deleted, "one undo step");     h.request("engine.execute", json!({"command": "library.source", "params": {"kind": "recentlyDeleted"}}), t);     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0]}}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     assert_eq!(h.app.session.source, lightcraft_engine::LibrarySource::RecentlyDeleted);     let items = photo_items(&h.app);     assert!(items.contains(&"photo.restore".to_string()) && items.contains(&"photo.deletePermanently".to_string()), "{items:?}");     assert!(!items.contains(&"photo.delete".to_string()), "{items:?}");     // a right-click on its filmstrip thumbnail opens the photo menu     h.request("ui.set", json!({"view": "detail"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let cell = h.app.widgets.iter().find(|(w, _)| *w == format!("film:{}", id.0)).map(|(_, r)| *r).expect("filmstrip cell");     assert!(!egui::Popup::is_any_open(&h.view.ctx));     let r = h.request("ui.click", json!({"x": cell.center().x, "y": cell.center().y, "button": "right"}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(egui::Popup::is_any_open(&h.view.ctx), "filmstrip context menu");      // Empty Recently Deleted is offered while the trash is the source@@ -3831,7 +3880,7 @@ fn readding_a_deleted_photo_shows_it_in_recently_deleted() {     h.request("engine.execute", json!({"command": "photo.deletePermanently", "params": {"ids": [id.0]}}), t);     assert!(h.app.session.catalog.photo(id).is_none());     crate::import::start_paths(&mut h.app, vec![file.to_string_lossy().to_string()]).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "after start_paths");     let fresh = h.app.session.selection.active.expect("re-imported photo selected");     assert_ne!(fresh, id);     assert!(!h.app.session.catalog.photo(fresh).unwrap().deleted);@@ -3856,14 +3905,14 @@ fn an_import_from_an_album_view_shows_the_first_new_photo() {     let album = h.app.session.catalog.albums().find(|a| !a.folder && !a.is_smart()).map(|a| a.id.0).expect("a demo album");     let r = h.request("engine.execute", json!({"command": "library.source", "params": {"kind": "album", "id": album}}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.source");     let before: Vec<lightcraft_catalog::PhotoId> = h.app.session.catalog.photos().map(|p| p.id).collect();     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.to_string_lossy()]}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm");     assert!(h.app.import.is_none(), "finished");     let active = h.app.session.selection.active.expect("a photo is active");     assert!(!before.contains(&active), "the active photo is a new one: {active:?}");@@ -3894,7 +3943,7 @@ fn import_copy_renames_into_destination() {     let t = Duration::from_secs(10);     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [src.to_string_lossy()]}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addPhotos");     h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. })));     let Some(crate::state::Dialog::Import { opts }) = &h.app.ui.dialog else { panic!("no import review") };     assert_eq!(opts.candidates.len(), 20);@@ -3912,7 +3961,7 @@ fn import_copy_renames_into_destination() {     }     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm");     assert!(h.app.import.is_none(), "finished");     let mut names: Vec<String> = std::fs::read_dir(&dest).unwrap().flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect();     names.sort();@@ -3950,7 +3999,7 @@ fn import_copy_into_a_custom_folder_template() {     let t = Duration::from_secs(10);     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [src.to_string_lossy()]}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addPhotos");     for id in ["button:importCopy", "button:importDest"] {         let r = h.request("ui.clickWidget", json!({"id": id}), t);         assert_eq!(r["ok"], true, "{id}: {r}");@@ -3975,7 +4024,7 @@ fn import_copy_into_a_custom_folder_template() {     assert!(r.to_string().contains("label:importExample"), "example shown");     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm");     assert!(dest.join("2026").join("20260114").join("IMG_01.png").is_file());     let _ = std::fs::remove_dir_all(&base); }@@ -4009,7 +4058,7 @@ fn import_move_into_a_folder_template() {     let t = Duration::from_secs(10);     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [src.to_string_lossy()]}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addPhotos");     for id in ["button:importMove", "button:importDest"] {         let r = h.request("ui.clickWidget", json!({"id": id}), t);         assert_eq!(r["ok"], true, "{id}: {r}");@@ -4029,7 +4078,7 @@ fn import_move_into_a_folder_template() {     assert!(!r.to_string().contains("check:importDng"), "no Copy as DNG when moving");     let r = h.request("ui.dialog.confirm", json!({}), t);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm");     assert!(h.app.import.is_none(), "finished");     let day = dest.join("2026").join("20260114");     assert!(day.join("20260114_001.png").is_file() && day.join("20260114_002.png").is_file());@@ -4056,14 +4105,14 @@ fn import_rename_tags_insert_at_the_cursor() {     let t = Duration::from_secs(10);     let r = h.request("engine.execute", json!({"command": "file.addPhotos", "params": {"paths": [dir.join("IMG_0007.png").to_string_lossy()]}}), t);     assert_eq!(r["result"]["scanning"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after file.addPhotos");     let r = h.request("ui.clickWidget", json!({"id": "button:importCopy"}), t);     assert_eq!(r["ok"], true, "{r}");     if let Some(crate::state::Dialog::Import { opts }) = &mut h.app.ui.dialog {         opts.rename = "Trip-_x".into();     }     // Paint the edited template before locating the Tags button in the resized dialog.-    assert!(h.settle(SETTLE), "Copy dialog layout should settle before locating Tags");+    h.settled(SETTLE, "Copy dialog layout should settle before locating Tags");     // the cursor sits after "Trip-"     let id = egui::Id::unique("import-rename");     let mut st = egui::text_edit::TextEditState::default();@@ -4136,7 +4185,7 @@ fn display_profile_converts_previews() {             && !h.app.renderer.is_pending(Slot::Main)     };     assert!(h.step_until(SETTLE, ready));-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let rgba = |c: &egui::ColorImage| lightcraft_raster::Rgba8 {         width: c.size[0],         height: c.size[1],@@ -4154,7 +4203,7 @@ fn display_profile_converts_previews() {     assert_eq!(h.app.ui.settings.display_profile, p3);     let d = h.app.renderer.display().cloned().unwrap();     assert!(h.step_until(SETTLE, |h| ready(h) && h.app.renderer.textures.contains_key(&thumb)));-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let (main1, thumb1) = (tex(&h, Slot::Main), tex(&h, thumb));     let mut want = thumb0.clone();     d.profile.from_srgb(&mut want).unwrap();@@ -4196,7 +4245,7 @@ fn display_profile_converts_previews() {     assert!(r["result"]["path"].is_null());     assert!(h.app.ui.settings.display_profile.is_empty() && h.app.display_error.is_none());     assert!(h.step_until(SETTLE, |h| ready(h) && h.app.renderer.textures.contains_key(&thumb)));-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert_eq!(tex(&h, thumb), thumb0, "back to the sRGB thumbnail");     let _ = std::fs::remove_dir_all(&dir); }@@ -4324,7 +4373,7 @@ fn settings_dialog_by_keyboard_and_clicks() {     let back = serde_json::from_str::<crate::UiState>(&saved).unwrap().sanitized();     assert_eq!(back.settings, h.app.ui.settings);     assert_eq!(back.view, crate::state::ViewMode::Detail, "startup view applied on load");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm"); }  /// F: full-screen preview (arrows step, I cycles the info overlay, Esc exits); ⌘I cycles the@@ -4363,7 +4412,7 @@ fn full_screen_preview_and_info_overlay_by_keyboard() {     assert!(h.app.window_is_fullscreen);     h.request("ui.key", json!({"key": "f", "cmd": true, "shift": true}), t);     assert!(!h.app.window_is_fullscreen);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key f"); }  /// #911 (toast labels only): cycling the overlay announces each mode in the UI language.@@ -4481,7 +4530,7 @@ fn click_zoom_ratio_animates() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert_eq!(h.app.ui.click_zoom, 100, "1:1 by default");     let r = h.request("engine.execute", json!({"command": "view.clickZoom", "params": {"ratio": 3}}), t);     assert_eq!(r["result"]["ratio"], 3, "{r}");@@ -4501,7 +4550,7 @@ fn click_zoom_ratio_animates() {         }     }     assert_eq!(keys.len(), 1, "one render for the whole animation: {keys:?}");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(!h.app.ui.zoom_anim, "animation finished");     assert!(h.app.image_rect.unwrap().width() > fit.width());     h.request("ui.clickWidget", json!({"id": "canvas:image", "fx": 0.5, "fy": 0.5}), t);@@ -4511,7 +4560,7 @@ fn click_zoom_ratio_animates() {     assert_eq!(h.app.ui.zoom, Zoom::Percent(300.0));     h.request("ui.key", json!({"key": "z"}), t);     assert_eq!(h.app.ui.zoom, Zoom::Fit);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key z"); }  /// A hover preview gets the loupe's display fit: it is the size the photo is drawn at, as the@@ -4523,7 +4572,7 @@ fn a_hover_preview_is_fitted_to_the_drawn_size() {     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail"}), t);     h.request("engine.execute", json!({"command": "panel.profiles"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after panel.profiles");     h.step_until(SETTLE, |h| !h.app.renderer.is_pending(Slot::Main));     let r = h.request("ui.hoverWidget", json!({"id": "profileCell:lc.vivid"}), t);     assert_eq!(r["ok"], true, "{r}");@@ -4542,9 +4591,9 @@ fn a_zoomed_view_keeps_its_whole_frame_render_canvas_sized() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h.request("ui.clickWidget", json!({"id": "canvas:image", "fx": 0.5, "fy": 0.5}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking canvas:image");     assert_eq!(h.app.ui.zoom, crate::state::Zoom::Percent(100.0));     h.step_until(SETTLE, |h| !h.app.renderer.is_pending(Slot::Main));     let main = h.app.renderer.textures.get(&Slot::Main).expect("whole-frame render").size;@@ -4557,7 +4606,7 @@ fn trackpad_navigation_keeps_the_image_cursor_between_events() {     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), t);     h.request("engine.execute", json!({"command": "view.zoom100"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.zoom100");     let anchor = h.app.canvas_rect.unwrap().center();     h.request("ui.move", json!({"x": anchor.x, "y": anchor.y}), t);     for (tool, expected) in [@@ -4603,7 +4652,7 @@ fn trackpad_pinch_anchors_fractional_zoom_and_scroll_pans() {     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none", "leftPanel": false, "filmstrip": false}), t);     h.request("engine.execute", json!({"command": "view.zoom100"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.zoom100");     let before = h.app.image_rect.unwrap();     let anchor = h.app.canvas_rect.unwrap().center() + egui::vec2(30.0, -20.0);     h.request("ui.move", json!({"x": anchor.x, "y": anchor.y}), t);@@ -4648,7 +4697,7 @@ fn mouse_wheel_zooms_the_loupe_with_and_without_ctrl() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none", "leftPanel": false, "filmstrip": false}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     for (unit, y, modifiers) in [         (egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::NONE),         (egui::MouseWheelUnit::Line, 1.0, egui::Modifiers::CTRL),@@ -4676,7 +4725,7 @@ fn trackpad_navigation_works_in_tools_compare_reference_and_before_after() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "leftPanel": false, "filmstrip": false}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let active = h.app.session.active().unwrap();     let settings = h.app.session.catalog.photo(active).unwrap().develop.clone();     for right in ["crop", "masking", "remove", "redEye", "none"] {@@ -4759,23 +4808,23 @@ fn navigator_pans_the_zoomed_loupe() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert!(!h.app.widgets.iter().any(|(w, _)| w == "canvas:navigator"), "hidden at Fit");     h.request("engine.execute", json!({"command": "view.zoom100"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.zoom100");     assert!(h.app.widgets.iter().any(|(w, _)| w == "canvas:navigator"), "navigator shown when zoomed");     h.request("ui.clickWidget", json!({"id": "canvas:navigator", "fx": 0.1, "fy": 0.2}), t);     let (u, v) = h.app.ui.pan;     assert!((u - 0.1).abs() < 0.03 && (v - 0.2).abs() < 0.03, "pan {:?}", h.app.ui.pan);     assert_eq!(h.app.ui.zoom, crate::state::Zoom::Percent(100.0), "the click didn't reach the loupe (which would zoom out)");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking canvas:navigator");     let nav = h.app.widgets.iter().rev().find(|(w, _)| w == "canvas:navigator").map(|(_, r)| *r).unwrap();     let to = nav.min + egui::vec2(nav.width() * 0.8, nav.height() * 0.7);     h.request("ui.dragWidget", json!({"id": "canvas:navigator", "fx": 0.5, "fy": 0.5, "toX": to.x, "toY": to.y}), t);     let (u, v) = h.app.ui.pan;     assert!((u - 0.8).abs() < 0.03 && (v - 0.7).abs() < 0.03, "drag pan {:?}", h.app.ui.pan);     h.request("engine.execute", json!({"command": "view.navigator"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.navigator");     assert!(!h.app.widgets.iter().any(|(w, _)| w == "canvas:navigator"), "toggled off"); } @@ -4790,7 +4839,7 @@ fn the_loupe_says_rendering_while_an_edit_renders() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert!(!pill(&h), "a finished render");     let open = Rc::new(Cell::new(false));     h.app.renderer.set_offload(Box::new(super::RenderGate::new(open.clone())));@@ -4826,7 +4875,7 @@ fn view_options_turn_the_rendering_message_off() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert!(h.app.ui.settings.loupe_messages, "on by default");     assert_eq!(h.request("ui.key", json!({"key": "j", "cmd": true}), t)["ok"], true);     h.step();@@ -4889,7 +4938,7 @@ fn the_loupe_says_so_when_the_render_behind_a_stand_in_fails() {     let mut h = demo([1000.0, 700.0]);     let t = Duration::from_secs(10);     h.request("ui.set", json!({"view": "detail", "right": "none"}), t);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert!(!has(&h, "loupe:renderFailed"));     let first = h.app.session.active().expect("a photo is open").0;     // every loupe render fails, as an undecodable raw's would; the rest renderdiff --git a/crates/ui-egui/src/lightroom_import.rs b/crates/ui-egui/src/lightroom_import.rs--- a/crates/ui-egui/src/lightroom_import.rs+++ b/crates/ui-egui/src/lightroom_import.rs@@ -569,7 +569,7 @@ mod tests {         let app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), crate::Services { png: None, ..Default::default() });         let mut h = crate::headless::Headless::new(app, [1200.0, 800.0], 1.0);         // no render left running at exit (see tests_activity::demo)-        h.settle(std::time::Duration::from_secs(20));+        h.settled(std::time::Duration::from_secs(20), "the new session");         h     } diff --git a/crates/ui-egui/src/menus/menubar/tests.rs b/crates/ui-egui/src/menus/menubar/tests.rs--- a/crates/ui-egui/src/menus/menubar/tests.rs+++ b/crates/ui-egui/src/menus/menubar/tests.rs@@ -472,7 +472,7 @@ fn a_refused_backup_from_the_menu_shows_why() {     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);     let mut h = crate::headless::Headless::new(app, [1400.0, 900.0], 1.0);     let t = std::time::Duration::from_secs(30);-    h.settle(t);+    h.settled(t, "the new session");     for id in ["menu:File", "menuitem:file.backupLibrary"] {         let r = h.request("ui.clickWidget", json!({"id": id}), t);         assert_eq!(r["ok"], true, "{id}: {r}");@@ -707,7 +707,7 @@ fn native_menu_commands_respect_the_blocking_window_policy() {     use crate::state::Dialog;      let mut h = crate::headless::Headless::new(app(), [1000.0, 760.0], 1.0);-    h.settle(std::time::Duration::from_secs(20));+    h.settled(std::time::Duration::from_secs(20), "the new session");     h.app.ui.view = ViewMode::PhotoGrid;     h.app.library_problem = Some(LibraryProblem::new("/no/such/library", "a test error")); diff --git a/crates/ui-egui/src/pick.rs b/crates/ui-egui/src/pick.rs--- a/crates/ui-egui/src/pick.rs+++ b/crates/ui-egui/src/pick.rs@@ -184,7 +184,7 @@ mod tests {             Ok(rx)         }));         let mut h = Headless::new(app, [1200.0, 800.0], 1.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "the new session");         (h, asked)     } @@ -200,7 +200,7 @@ mod tests {     /// The import review's sources once frames have run (the review opens after a background     /// scan of the chosen paths).     fn import_sources(h: &mut Headless) -> Option<Vec<String>> {-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");         match &h.app.ui.dialog {             Some(Dialog::Import { opts }) => Some(opts.sources.clone()),             _ => None,@@ -224,7 +224,7 @@ mod tests {         }         // asking again while it is up opens no second dialog         assert_eq!(h.app.run("file.addPhotos", json!({})).unwrap(), Value::Null);-        h.settle(SETTLE);+        h.settled(SETTLE, "after file.addPhotos");         assert_eq!(asked.lock().unwrap().len(), 1);         assert_eq!(h.app.pending_picks.len(), 1, "still waiting");         // the user chooses a photo: the command runs again with it and opens the import reviewdiff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs--- a/crates/ui-egui/src/shortcuts/tests.rs+++ b/crates/ui-egui/src/shortcuts/tests.rs@@ -194,7 +194,7 @@ fn classic_f8_hides_the_right_panel_and_brings_the_same_one_back() {     let mut h = library();     h.app.ui.view = crate::state::ViewMode::Detail;     h.app.ui.right = RightPanel::Keywords;-    h.settle(std::time::Duration::from_secs(10));+    h.settled(std::time::Duration::from_secs(10), "after setting ui.right");     let (filmstrip, left) = (h.app.ui.filmstrip, h.app.ui.left_panel);     key(&mut h, "F8", false);     assert_eq!(h.app.ui.right, RightPanel::None);@@ -215,7 +215,7 @@ fn classic_f8_shows_a_minimized_tools_window_and_keeps_its_mode() {     let mut h = library();     h.app.ui.view = crate::state::ViewMode::Detail;     h.app.ui.right = RightPanel::Edit;-    h.settle(std::time::Duration::from_secs(10));+    h.settled(std::time::Duration::from_secs(10), "after setting ui.right");     h.app.run("ui.dock", json!({"operation": "float", "panel": "tools", "rect": [400, 100, 320, 500]})).unwrap();     h.app.run("ui.dock", json!({"action": {"SetMinimized": {"panel": "tools", "minimized": true}}})).unwrap();     h.app.ui.right = RightPanel::Crop;diff --git a/crates/ui-egui/src/tests/activity.rs b/crates/ui-egui/src/tests/activity.rs--- a/crates/ui-egui/src/tests/activity.rs+++ b/crates/ui-egui/src/tests/activity.rs@@ -17,7 +17,7 @@ fn demo() -> Headless {     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);     // let the first renders finish: a render still running when the test binary exits can crash in the     // GPU driver's teardown (the preview pool doesn't join its workers)-    h.settle(T);+    h.settled(T, "the new session");     h } @@ -277,7 +277,7 @@ fn gated_import(tag: &str, n: usize, gate: Option<std::sync::Arc<std::sync::atom         })     }));     let mut h = Headless::new(LightcraftApp::new(s, Services { png: None, ..Default::default() }), [1200.0, 800.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     (h, dir) } @@ -356,7 +356,7 @@ fn bracket(tag: &str) -> (Headless, std::path::PathBuf) {     session.execute("library.select", &json!({"ids": ids})).unwrap();     let app = LightcraftApp::new(session, Services { png: None, ..Default::default() });     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     (h, dir) } @@ -445,7 +445,7 @@ fn batch_merge_runs_each_set_in_turn() {     // the merge ordering does not need the grid's nine thumbnails; one Detail render is enough     let r = h.request("ui.set", json!({"view": "detail", "filmstrip": false}), T);     assert_eq!(r["ok"], true, "{r}");-    assert!(h.settle(T), "the imported photos and the active preview settle");+    h.settled(T, "the imported photos and the active preview settle");     let r = crate::menus::run_ui_command(&mut h.app, "merge.hdrBatch", &json!({})).unwrap().unwrap();     assert_eq!(r["sets"], 3);     assert_eq!(h.app.merge.batch.len(), 2, "one running, two queued");diff --git a/crates/ui-egui/src/tests/album_picker.rs b/crates/ui-egui/src/tests/album_picker.rs--- a/crates/ui-egui/src/tests/album_picker.rs+++ b/crates/ui-egui/src/tests/album_picker.rs@@ -28,7 +28,7 @@ fn id(v: serde_json::Value) -> u64 { fn click(h: &mut Headless, id: &str) {     let r = h.request("ui.clickWidget", json!({"id": id}), T);     assert_eq!(r["ok"], true, "{id}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget"); }  /// Check the current frame registry for an exact widget ID.@@ -63,7 +63,7 @@ fn library() -> Library {     exec(&mut h, "dialog.smartAlbum", json!({"name": "Travel"}));     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     rules.rules = vec![serde_json::from_value(json!({"field": "album", "op": "isNot", "value": null})).unwrap()];-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     Library { h, utils, excluded, uncurated, trip } } @@ -91,13 +91,13 @@ fn typing_finds_and_keys_pick() {     click(&mut h, "albumPicker:rules-0");     let r = h.request("ui.text", json!({"text": "utils"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.text");     assert!(has(&h, &format!("albumPickerItem:{excluded}:rules-0")) && has(&h, &format!("albumPickerItem:{uncurated}:rules-0")));     assert!(!has(&h, &format!("albumPickerItem:{trip}:rules-0")), "Trip doesn't match");     for key in ["ArrowDown", "Enter"] {         let r = h.request("ui.key", json!({"key": key}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.key");     }     assert_eq!(value(&h), json!(uncurated), "the second match"); }@@ -112,7 +112,7 @@ fn albums_that_would_loop_are_greyed() {     exec(&mut h, "dialog.smartAlbum", json!({"id": excluded}));     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     rules.rules = vec![serde_json::from_value(json!({"field": "album", "op": "is", "value": null})).unwrap()];-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     click(&mut h, "albumPicker:rules-0");     assert!(has(&h, &format!("albumPickerItem:{travel}:rules-0")), "shown");     click(&mut h, &format!("albumPickerItem:{travel}:rules-0"));@@ -128,7 +128,7 @@ fn typing_after_a_folder_click_still_searches() {     assert!(has(&h, &format!("albumPickerItem:{excluded}:rules-0")));     let r = h.request("ui.text", json!({"text": "trip"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.text");     assert!(has(&h, &format!("albumPickerItem:{trip}:rules-0")) && !has(&h, &format!("albumPickerItem:{excluded}:rules-0")), "searching for trip"); } @@ -160,7 +160,7 @@ mod import_review {         let other = id(exec(&mut h, "album.create", json!({"name": "Favorites", "addSelected": false})));         let smart = id(exec(&mut h, "album.createSmart", json!({"name": "Smart travel", "parent": folder, "rules": {"rating": 4}})));         h.app.ui.dialog = Some(crate::state::Dialog::Import { opts: Box::new(crate::import::ImportDialog::new(Vec::new())) });-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.dialog");         Review { h, folder, nested, album, other, smart }     } @@ -194,14 +194,14 @@ mod import_review {     fn text(h: &mut Headless, value: &str) {         let r = h.request("ui.text", json!({"text": value}), T);         assert_eq!(r["ok"], true, "{r}");-        assert!(h.settle(SETTLE));+        h.settled(SETTLE, "after ui.text");     }      /// Send a public keyboard event and wait for its resulting UI state.     fn key(h: &mut Headless, key: &str) {         let r = h.request("ui.key", json!({"key": key}), T);         assert_eq!(r["ok"], true, "{r}");-        assert!(h.settle(SETTLE));+        h.settled(SETTLE, "after ui.key");     }      /// Replace the pending new album name using normal field selection and typing.@@ -322,7 +322,7 @@ mod import_review {                 exec(&mut h, "view.theme", json!({"theme": theme}));                 let r = h.request("ui.set", json!({"language": "de"}), T);                 assert_eq!(r["ok"], true, "{r}");-                assert!(h.settle(SETTLE));+                h.settled(SETTLE, "after ui.set");                 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, h.size);                 let rect = |h: &Headless, name: &str| h.app.widgets.iter().find(|(id, _)| id == name).unwrap().1;                 assert!(screen.contains_rect(rect(&h, "albumPicker:import-album")), "{theme} {size:?}: selected long path");@@ -351,7 +351,7 @@ mod import_review {         for locale in Locale::ALL {             let r = h.request("ui.set", json!({"language": locale.code()}), T);             assert_eq!(r["ok"], true, "{r}");-            assert!(h.settle(SETTLE));+            h.settled(SETTLE, "after ui.set");             assert!(button_text(&h, "albumPicker:import-album", locale.tr("None")), "{locale:?}: localized empty destination");             click(&mut h, "albumPicker:import-album");             assert!(button_text(&h, "importAlbum:new", locale.tr("New album…")), "{locale:?}: localized new-album option");@@ -369,7 +369,7 @@ mod import_review {         let app = LightcraftApp::new(lightcraft_engine::Session::new(), Services { png: None, ..Default::default() });         let mut h = Headless::new(app, [1000.0, 800.0], 1.0);         h.app.ui.dialog = Some(crate::state::Dialog::Import { opts: Box::new(crate::import::ImportDialog::new(Vec::new())) });-        assert!(h.settle(SETTLE));+        h.settled(SETTLE, "after setting ui.dialog");         click(&mut h, "albumPicker:import-album");         assert!(h.painted_text().iter().any(|t| t == "No albums yet"));         click(&mut h, "importAlbum:new");@@ -413,7 +413,7 @@ mod import_review {             h.app.ui.dialog = None;             exec(&mut h, "file.addPhotos", json!({"paths": [fixture.0.to_string_lossy()], "album": album}));             assert!(h.step_until(SETTLE, |h| matches!(h.app.ui.dialog, Some(crate::state::Dialog::Import { .. }))));-            assert!(h.settle(SETTLE));+            h.settled(SETTLE, "the UI");             assert_eq!(destination(&h).album, Some(album), "album-context import keeps its preselection");             click(&mut h, "albumPicker:import-album");             assert!(has(&h, &format!("albumPickerItem:{album}:import-album")), "preselection opens the right ancestors");@@ -429,7 +429,7 @@ mod import_review {                 "deleted" => {                     key(&mut h, "Escape");                     exec(&mut h, "album.delete", json!({"id": album}));-                    assert!(h.settle(SETTLE));+                    h.settled(SETTLE, "after album.delete");                     assert_eq!(destination(&h).album, None, "a deleted destination matches the displayed None choice");                     assert!(button_text(&h, "albumPicker:import-album", "None"));                 }@@ -439,7 +439,7 @@ mod import_review {             let r = h.request("ui.dialog.confirm", json!({}), T);             assert_eq!(r["ok"], true, "{r}");             assert!(h.step_until(SETTLE, |h| h.app.import.is_none()));-            assert!(h.settle(SETTLE));+            h.settled(SETTLE, "the UI");             let added: Vec<PhotoId> = h.app.session.catalog.photos().filter(|p| !before.contains(&p.id)).map(|p| p.id).collect();             assert_eq!(added.len(), 1, "{choice}: exactly the scanned photo is imported");             let receives = |id| h.app.session.catalog.album(lightcraft_catalog::AlbumId(id)).unwrap().photos.contains(&added[0]);@@ -455,7 +455,7 @@ mod import_review {             }             assert_eq!(h.app.session.undo.len(), undo + 1, "{choice}: one undo step");             exec(&mut h, "edit.undo", json!({}));-            assert!(h.settle(SETTLE));+            h.settled(SETTLE, "after edit.undo");             assert_eq!(h.app.session.catalog.len(), before.len());             if choice == "new" {                 assert!(h.app.session.catalog.albums().all(|a| a.name != "From card"), "undo removes the new album too");diff --git a/crates/ui-egui/src/tests/album_remove.rs b/crates/ui-egui/src/tests/album_remove.rs--- a/crates/ui-egui/src/tests/album_remove.rs+++ b/crates/ui-egui/src/tests/album_remove.rs@@ -20,7 +20,7 @@ fn demo() -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "photoGrid"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } diff --git a/crates/ui-egui/src/tests/appearance.rs b/crates/ui-egui/src/tests/appearance.rs--- a/crates/ui-egui/src/tests/appearance.rs+++ b/crates/ui-egui/src/tests/appearance.rs@@ -165,7 +165,7 @@ fn view_menu_lists_modes_and_themes_with_checks() { #[test] fn the_top_bar_button_and_settings_cards_drive_the_mode() {     let mut h = Headless::new(app(), [1400.0, 900.0], 1.0);-    h.settle(Duration::from_secs(60));+    h.settled(Duration::from_secs(60), "the new session");     assert_eq!(h.request("ui.clickWidget", json!({"id": "icon:appearance"}), T)["ok"], true);     h.step();     assert_eq!(h.app.ui.settings.appearance_mode, AppearanceMode::Auto);diff --git a/crates/ui-egui/src/tests/before_after.rs b/crates/ui-egui/src/tests/before_after.rs--- a/crates/ui-egui/src/tests/before_after.rs+++ b/crates/ui-egui/src/tests/before_after.rs@@ -18,7 +18,7 @@ fn click_at(h: &mut Headless, pos: egui::Pos2) {     h.app.synthetic.push(egui::Event::PointerMoved(pos));     h.app.synthetic.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });     h.app.synthetic.push(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input"); }  /// The white-balance selector picks the same point of the photo whichever view of a side-by-side@@ -38,10 +38,10 @@ fn wb_selector_picks_the_same_point_in_the_before_view() {         h.app.ui.before_after = mode;         h.app.ui.zoom = zoom;         h.app.ui.pan = Default::default();-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.pan");         let r = h.request("engine.execute", json!({"command": "tool.wbPicker"}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after tool.wbPicker");         (h.app.image_rect.expect("After view on screen"), h.app.canvas_rect.expect("canvas"))     };     for mode in [BeforeAfter::SideBySide, BeforeAfter::TopBottom] {@@ -86,7 +86,7 @@ fn a_click_in_the_before_view_zooms_in_on_the_point_clicked() {         h.app.ui.before_after = mode;         h.app.ui.zoom = Zoom::Fit;         h.app.ui.pan = Default::default();-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.pan");         (h.app.image_rect.expect("After view on screen"), h.app.canvas_rect.expect("canvas"))     };     for mode in [BeforeAfter::SideBySide, BeforeAfter::TopBottom] {@@ -155,9 +155,9 @@ fn animation_view(mode: BeforeAfter, zoom: Zoom) -> AnimationView {     h.app.ui.zoom = zoom;     h.app.ui.pan = (0.5, 0.5);     h.app.ui.click_zoom = 300;-    assert!(h.settle(SETTLE), "the single photo's source and Main render settle");+    h.settled(SETTLE, "the single photo's source and Main render settle");     h.app.ui.before_after = mode;-    assert!(h.settle(SETTLE), "both pane renders settled");+    h.settled(SETTLE, "both pane renders settled");     let before_id = h.app.renderer.textures[&Slot::Before].tex.id();     let before = h         .view@@ -233,7 +233,7 @@ fn check_click_animation(mode: BeforeAfter) {         }     }     open.set(true);-    assert!(h.settle(SETTLE));+    h.settled(SETTLE, "after opening the render gate"); }  #[test]@@ -287,7 +287,7 @@ fn check_region_animation(mode: BeforeAfter) {     assert!((h.app.image_rect.expect("settled image").width() - start.width() * 2.0).abs() < 1.0);     assert_eq!(keys.len(), 1, "region requests use the final target throughout the animation: {keys:?}");     open.set(true);-    assert!(h.settle(SETTLE));+    h.settled(SETTLE, "after opening the render gate"); }  #[test]@@ -332,7 +332,7 @@ fn zoomed_in_the_after_side_shows_the_edits() {         h.app.ui.zoom = zoom;         // (far enough from the middle of the photo that, zoomed in, it is off screen)         h.app.ui.pan = (0.25, 0.75);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.pan");         h.paint()     };     for zoom in [Zoom::Fit, Zoom::Percent(100.0), Zoom::Percent(400.0)] {@@ -379,11 +379,11 @@ fn show_original_does_not_light_the_before_after_button() {     let id = h.app.session.visible_cloned()[2];     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0], "active": id.0}}), T);     h.app.ui.view = ViewMode::Detail;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.view");     assert_eq!(h.request("engine.execute", json!({"command": "view.showOriginal"}), T)["ok"], true);     assert_eq!(h.app.ui.before_after, BeforeAfter::Original);     assert!(!h.app.ui.before_after.is_layout(), "the button is dark while the original is shown");-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.showOriginal");     assert_eq!(h.request("ui.clickWidget", json!({"id": "icon:beforeAfter"}), T)["ok"], true);     assert_eq!(h.app.ui.before_after, BeforeAfter::SideBySide, "a click on the dark button opens the layout");     assert_eq!(h.request("ui.clickWidget", json!({"id": "icon:beforeAfter"}), T)["ok"], true);diff --git a/crates/ui-egui/src/tests/browse_active.rs b/crates/ui-egui/src/tests/browse_active.rs--- a/crates/ui-egui/src/tests/browse_active.rs+++ b/crates/ui-egui/src/tests/browse_active.rs@@ -25,7 +25,7 @@ fn demo() -> Headless { fn exec(h: &mut Headless, command: &str, params: Value) -> Value {     let r = h.request("engine.execute", json!({"command": command, "params": params}), T);     assert_eq!(r["ok"], true, "{command} {params}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after engine.execute");     r } @@ -49,7 +49,7 @@ fn active(h: &Headless) -> Option<PhotoId> { #[test] fn browsing_a_local_folder_makes_its_first_photo_active() {     let mut h = demo();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     let dir = folder("browse-active", 3);     // a library photo is active: the folder doesn't show it     let before = h.app.session.visible_cloned()[0];@@ -62,7 +62,7 @@ fn browsing_a_local_folder_makes_its_first_photo_active() {     // the grid marks it, and Detail opens on it     assert!(h.app.widgets.iter().any(|(w, _)| *w == format!("thumb:{}", vis[0].0)));     h.request("ui.set", json!({"view": "detail"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert_eq!(active(&h), Some(vis[0]));      // the folder shows the current photo again: it stays active@@ -80,7 +80,7 @@ fn browsing_a_local_folder_makes_its_first_photo_active() { #[test] fn choosing_an_album_keeps_or_picks_the_active_photo() {     let mut h = demo();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new demo session");     let album = h.app.session.catalog.albums().find(|a| !a.folder && !a.is_smart() && a.photos.len() > 1).map(|a| (a.id, a.photos.clone()));     let (album, photos) = album.expect("the demo library has an album");     let outside = h.app.session.visible_cloned().into_iter().find(|p| !photos.contains(p)).expect("a photo outside the album");diff --git a/crates/ui-egui/src/tests/capture_time.rs b/crates/ui-egui/src/tests/capture_time.rs--- a/crates/ui-egui/src/tests/capture_time.rs+++ b/crates/ui-egui/src/tests/capture_time.rs@@ -31,7 +31,7 @@ fn open(h: &mut Headless, ids: &[lightcraft_catalog::PhotoId], active: lightcraf     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": ids, "active": active.0}}), T);     let r = h.request("ui.menu.invoke", json!({"id": "dialog.captureTime"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.menu.invoke"); }  #[test]@@ -48,12 +48,12 @@ fn shift_preview_says_the_photos_capture_time_cant_be_read() {         }         other => panic!("Edit Capture Time is not open: {other:?}"),     }-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(painted(&h, "2026-01-01 10:00:00 → 2026-01-01 11:00:00"), "a readable time previews its shift: {:?}", h.painted_text());      let bad = "2026-01-01T10:00:00 and more";     set_captured(&mut h, id, bad);-    h.settle(SETTLE);+    h.settled(SETTLE, "after set_captured");     assert!(painted(&h, "can't be read"), "the preview says the photo's time can't be read: {:?}", h.painted_text());     assert!(!painted(&h, "Enter a date"), "no date was typed, so the preview doesn't ask for one: {:?}", h.painted_text()); @@ -84,7 +84,7 @@ fn set_mode_refuses_moving_others_by_an_unreadable_time_and_repairs_it_alone() {             }             other => panic!("Edit Capture Time is not open: {other:?}"),         }-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");     };      // with another photo selected, OK can't move it by the unreadable time's change: it says so@@ -107,7 +107,7 @@ fn set_mode_refuses_moving_others_by_an_unreadable_time_and_repairs_it_alone() {     assert!(preview.ends_with(" → 2026-01-01 11:00:00"), "the preview shows the time it will get, nothing after: {preview:?}");     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:dialogOk");     assert!(h.app.ui.dialog.is_none(), "OK closes the dialog");     assert_eq!(captured(&h, bad).as_deref(), Some("2026-01-01T11:00:00"), "alone, it gets the time typed"); }@@ -126,7 +126,7 @@ fn errors_are_shown_and_the_dialog_stays_open() {         Some(Dialog::CaptureTime { time, .. }) => *time = "yesterday".into(),         other => panic!("Edit Capture Time is not open: {other:?}"),     }-    assert!(h.settle(SETTLE), "capture-time dialog renders settled");+    h.settled(SETTLE, "capture-time dialog renders settled");     assert_eq!(h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T)["ok"], true);     // The request consumes the click. Inspect its short-lived notification before waiting     // for unrelated rendering: settling advances the simulated clock and can expire the toast.@@ -141,7 +141,7 @@ fn errors_are_shown_and_the_dialog_stays_open() {         }         other => panic!("Edit Capture Time is not open: {other:?}"),     }-    assert!(h.settle(SETTLE), "capture-time dialog renders settled");+    h.settled(SETTLE, "capture-time dialog renders settled");     let r = h.request("ui.dialog.confirm", json!({}), T);     assert_ne!(r["ok"], true, "the shift is refused: {r}");     assert!(r.to_string().contains("9999"), "{r}");diff --git a/crates/ui-egui/src/tests/compare.rs b/crates/ui-egui/src/tests/compare.rs--- a/crates/ui-egui/src/tests/compare.rs+++ b/crates/ui-egui/src/tests/compare.rs@@ -28,12 +28,12 @@ fn filmstrip_click_in_compare_keeps_the_active_photo_on_screen() {         let r = h.request("engine.execute", json!({"command": "view.compare"}), T);         assert_eq!(r["ok"], true, "{r}");         h.app.ui.zoom = zoom;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         let target = vis[4];         let before = rating(&h, target);         let r = h.request("ui.clickWidget", json!({"id": format!("film:{target}")}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.clickWidget");         let (sel, cand) = h.app.ui.compare.expect("still comparing");         let active = h.app.session.selection.active.map(|p| p.0).expect("an active photo");         // a rating key must not change a photo that isn't shown@@ -55,11 +55,11 @@ fn filmstrip_click_replaces_the_active_side_in_compare() {     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);     let (sel, cand) = (r["result"]["select"].as_u64().unwrap(), r["result"]["candidate"].as_u64().unwrap());-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.compare");     let click_film = |h: &mut Headless, id: u64| {         let r = h.request("ui.clickWidget", json!({"id": format!("film:{id}")}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.clickWidget");     };     let active = |h: &Headless| h.app.session.selection.active.map(|p| p.0);     let others: Vec<u64> = vis.iter().copied().filter(|v| *v != sel && *v != cand).take(3).collect();@@ -94,11 +94,11 @@ fn modified_filmstrip_clicks_in_compare_change_the_selection_not_the_pair() {     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);     let (sel, cand) = (r["result"]["select"].as_u64().unwrap(), r["result"]["candidate"].as_u64().unwrap());-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.compare");     let click_film = |h: &mut Headless, id: u64, cmd: bool, shift: bool| {         let r = h.request("ui.clickWidget", json!({"id": format!("film:{id}"), "cmd": cmd, "shift": shift}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.clickWidget");     };     let selected = |h: &Headless| -> Vec<u64> {         let s = &h.app.session.selection;@@ -151,7 +151,7 @@ fn modified_filmstrip_clicks_in_compare_change_the_selection_not_the_pair() {     // a rating key acts on the active photo, which is on screen; a merely selected photo is left alone     let was = rating(&h, far);     h.request("ui.key", json!({"key": "3"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key 3");     assert_eq!(rating(&h, before.unwrap()), Some(3));     assert_eq!(rating(&h, far), was, "the hidden photo was rated");     // a plain click still replaces the active side@@ -171,7 +171,7 @@ fn a_compare_pane_says_rendering_while_its_edit_renders() {     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.compare");     assert!(!pill(&h), "finished renders");     let open = Rc::new(Cell::new(false));     h.app.renderer.set_offload(Box::new(crate::headless::RenderGate::new(open.clone())));@@ -199,7 +199,7 @@ fn a_compare_pane_says_when_its_render_failed() {     let vis: Vec<u64> = h.app.session.visible_cloned().iter().map(|p| p.0).collect();     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [vis[0], vis[1]], "active": vis[0]}}), T);     h.request("engine.execute", json!({"command": "view.compare"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.compare");     assert!(!failed(&h));     let open = Rc::new(Cell::new(true));     h.app.renderer.set_offload(Box::new(crate::headless::RenderGate::failing(open, |s| matches!(s, Slot::Compare(_)))));@@ -265,13 +265,13 @@ fn deleting_the_pinned_reference_stops_drawing_it_and_reentry_uses_a_live_photo(     let reference = h.app.session.visible_cloned()[0];     h.app.run("library.select", json!({"ids": [reference.0]})).unwrap();     h.app.run("view.reference", json!({})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.reference");     let texture = h.app.renderer.textures.get(&crate::render::Slot::Compare(0)).unwrap().tex.id();     let drawn = |h: &Headless| h.view.painted_shapes().iter().any(|s| matches!(s, egui::epaint::Shape::Mesh(mesh) if mesh.texture_id == texture));     assert!(drawn(&h), "the live reference is shown before deletion");     let active = h.app.session.active().unwrap();     h.app.run("photo.delete", json!({"ids": [reference.0]})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.delete");     assert!(!drawn(&h), "Recently Deleted photos must not remain on the reference canvas");     assert!(!h.app.renderer.textures.contains_key(&crate::render::Slot::Compare(0)), "the deleted reference's pane is released");     assert!(!h.app.renderer.textures.contains_key(&crate::render::Slot::Compare(1)), "and the active photo's pane beside it");@@ -281,7 +281,7 @@ fn deleting_the_pinned_reference_stops_drawing_it_and_reentry_uses_a_live_photo(     assert_eq!(h.app.ui.reference, Some(active.0));     h.app.run("photo.restore", json!({"ids": [reference.0]})).unwrap();     h.app.run("photo.setReference", json!({"id": reference.0})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.setReference");     assert!(h.app.renderer.textures.get(&crate::render::Slot::Compare(0)).is_some_and(|t| t.photo == reference)); } @@ -302,19 +302,19 @@ fn info_panel_label_in_compare_labels_the_active_photo_and_shows_it() {     assert_eq!(r["ok"], true, "{r}");     let (sel, cand) = (r["result"]["select"].as_u64().unwrap(), r["result"]["candidate"].as_u64().unwrap());     h.app.ui.right = crate::state::RightPanel::Info;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.right");     let photo = |h: &Headless, id: u64| h.app.session.catalog.photo(PhotoId(id)).cloned().expect("photo");     let widgets = |h: &mut Headless| h.request("ui.widgets", json!({}), T)["result"].to_string();     assert!(!widgets(&mut h).contains(&format!("cull:{cand}:label")), "no label yet");     let r = h.request("ui.clickWidget", json!({"id": "label:red"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking label:red");     assert_eq!(photo(&h, cand).label, Some(ColorLabel::Red), "the active photo (the candidate) is labelled");     assert_eq!(photo(&h, sel).label, None, "the select is not labelled with it");     assert!(widgets(&mut h).contains(&format!("cull:{cand}:label")), "the label shows under the candidate");     let r = h.request("ui.clickWidget", json!({"id": "star:info:3"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking star:info:3");     assert_eq!(photo(&h, cand).rating, 3);     assert_eq!(photo(&h, sel).rating, 0, "the select is not rated with it"); }diff --git a/crates/ui-egui/src/tests/copy_paste_dialogs.rs b/crates/ui-egui/src/tests/copy_paste_dialogs.rs--- a/crates/ui-egui/src/tests/copy_paste_dialogs.rs+++ b/crates/ui-egui/src/tests/copy_paste_dialogs.rs@@ -33,7 +33,7 @@ fn exec(h: &mut Headless, command: &str, params: Value) { fn key(h: &mut Headless, k: Value) {     let r = h.request("ui.key", k.clone(), T);     assert_eq!(r["ok"], true, "{k}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.key"); }  fn copy_dialog(h: &mut Headless) {diff --git a/crates/ui-egui/src/tests/crop_rotate.rs b/crates/ui-egui/src/tests/crop_rotate.rs--- a/crates/ui-egui/src/tests/crop_rotate.rs+++ b/crates/ui-egui/src/tests/crop_rotate.rs@@ -30,9 +30,9 @@ fn crop_view(app: LightcraftApp, assert_settle: bool) -> Headless {         assert_eq!(r["ok"], true, "{r}");     }     if assert_settle {-        assert!(h.settle(SETTLE), "the crop view settles before the snapshot");+        h.settled(SETTLE, "the crop view settles before the snapshot");     } else {-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");     }     h }@@ -60,7 +60,7 @@ fn crop_fits(h: &Headless) -> bool { fn hover(h: &mut Headless, p: egui::Pos2) {     let r = h.request("ui.move", json!({"x": p.x, "y": p.y}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.move"); }  /// Outside the crop box, where a drag rotates, the pointer becomes a curved double arrow drawn at@@ -87,12 +87,12 @@ fn rotating_shows_the_angle() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert!(angle(&h) != 0.0, "the drag rotated the crop");     assert!(has(&h, "cropAngleReadout"), "the angle is shown while rotating");     let r = h.request("ui.pointer", json!({"events": [{"kind": "up", "x": -0.05, "y": 0.3}]}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert!(!has(&h, "cropAngleReadout"), "and gone once released"); } @@ -102,7 +102,7 @@ fn the_straighten_value_takes_an_exact_angle() {     let mut h = crop_tool();     let r = h.request("ui.clickWidget", json!({"id": "sliderValue:crop.angle"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking sliderValue:crop.angle");     for key in ["a", "Backspace"] {         // select what is there and clear it         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);@@ -112,7 +112,7 @@ fn the_straighten_value_takes_an_exact_angle() {     assert_eq!(r["ok"], true, "{r}");     let r = h.request("ui.key", json!({"key": "Enter"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Enter");     assert!((angle(&h) - 2.5).abs() < 1e-9, "typed 2.5°, got {}", angle(&h));     assert!(crop_fits(&h), "the Straighten value shrinks the crop to fit the photo"); }@@ -150,7 +150,7 @@ fn command_held_shows_the_straighten_crosshair() {     let img = h.app.image_rect.expect("the photo on screen");     h.events.push(egui::Event::ModifiersChanged(egui::Modifiers::COMMAND));     h.events.push(egui::Event::PointerMoved(egui::pos2(img.left() - 24.0, img.center().y)));-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert_eq!(h.last_cursor, egui::CursorIcon::Crosshair);     assert!(!has(&h, "cropRotateCursor")); }@@ -161,7 +161,7 @@ fn the_crop_panel_has_an_angle_field() {     let mut h = crop_tool();     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking cropAngleField");     for key in ["a", "Backspace"] {         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);         assert_eq!(r["ok"], true, "{r}");@@ -170,7 +170,7 @@ fn the_crop_panel_has_an_angle_field() {     assert_eq!(r["ok"], true, "{r}");     let r = h.request("ui.key", json!({"key": "Enter"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Enter");     assert!((angle(&h) + 3.25).abs() < 1e-9, "typed -3.25°, got {}", angle(&h));     assert!(crop_fits(&h), "the Angle field shrinks the crop to fit the photo"); }@@ -190,7 +190,7 @@ fn the_readout_stays_on_the_canvas() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     let readout = h.app.widgets.iter().find(|(w, _)| w == "cropAngleReadout").map(|(_, r)| *r).expect("the readout while rotating");     assert!(canvas.contains_rect(readout), "{readout:?} in {canvas:?}"); }@@ -202,7 +202,7 @@ fn dragging_the_angle_field_is_one_undo_step() {     let undo = h.app.session.undo.len();     let r = h.request("ui.dragWidget", json!({"id": "cropAngleField", "dx": 40.0, "dy": 0.0, "steps": 6}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dragWidget");     assert!(angle(&h) > 0.0, "dragged right: {}", angle(&h));     assert_eq!(h.app.session.undo.len(), undo + 1, "one step for the whole drag"); }@@ -214,22 +214,22 @@ fn typing_in_the_angle_field_applies_on_return() {     let mut h = crop_tool();     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking cropAngleField");     for key in ["a", "Backspace"] {         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.key");     let (before, undo) = (angle(&h), h.app.session.undo.len());     for ch in ["-", "3", ".", "2", "5"] {         let r = h.request("ui.text", json!({"text": ch}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.text");     }     assert_eq!((angle(&h), h.app.session.undo.len()), (before, undo), "nothing applied while typing");     let r = h.request("ui.key", json!({"key": "Enter"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Enter");     assert!((angle(&h) + 3.25).abs() < 1e-9, "applied on Return: {}", angle(&h));     assert_eq!(h.app.session.undo.len(), undo + 1, "as one undo step"); }@@ -250,7 +250,7 @@ fn the_readout_follows_the_pointer_in_the_margin() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     let readout = h.app.widgets.iter().find(|(w, _)| w == "cropAngleReadout").map(|(_, r)| *r).expect("the readout while rotating");     assert!(readout.distance_to_pos(at) < 60.0, "{readout:?} near {at:?}"); }@@ -274,18 +274,18 @@ fn escape_in_the_angle_field_keeps_the_angle() {     let mut h = crop_tool();     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking cropAngleField");     for key in ["a", "Backspace"] {         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);         assert_eq!(r["ok"], true, "{r}");     }     let r = h.request("ui.text", json!({"text": "7"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.text");     let (before, undo) = (angle(&h), h.app.session.undo.len());     let r = h.request("ui.key", json!({"key": "Escape"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_eq!((angle(&h), h.app.session.undo.len()), (before, undo), "Esc applied nothing");     assert_eq!(h.app.ui.right, crate::state::RightPanel::Crop, "and only left the field"); }@@ -296,18 +296,18 @@ fn leaving_the_angle_field_applies_the_angle() {     let mut h = crop_tool();     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking cropAngleField");     for key in ["a", "Backspace"] {         let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);         assert_eq!(r["ok"], true, "{r}");     }     let r = h.request("ui.text", json!({"text": "4.5"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.text");     let undo = h.app.session.undo.len();     let r = h.request("ui.key", json!({"key": "Tab"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Tab");     assert!((angle(&h) - 4.5).abs() < 1e-9, "applied on leaving: {}", angle(&h));     assert_eq!(h.app.session.undo.len(), undo + 1); }@@ -323,7 +323,7 @@ fn moving_the_box_never_shows_rotation() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(h.last_cursor, egui::CursorIcon::Move, "still moving");     assert!(!has(&h, "cropRotateCursor"));     assert_eq!(angle(&h), 0.0, "nothing rotated");@@ -340,7 +340,7 @@ fn a_rotation_cut_short_leaves_nothing_behind() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert!(has(&h, "cropAngleReadout"), "rotating");     for (method, params) in [         ("engine.execute", json!({"command": "tool.done"})),@@ -349,7 +349,7 @@ fn a_rotation_cut_short_leaves_nothing_behind() {     ] {         let r = h.request(method, params, T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after the request");     }     let img = h.app.image_rect.expect("the photo on screen");     hover(&mut h, img.center());@@ -366,7 +366,7 @@ fn the_angle_field_reads_angles_as_people_type_them() {     for (typed, want) in [("-3,25", -3.25), ("1.5°", 1.5), ("60", 45.0)] {         let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after clicking cropAngleField");         for key in ["a", "Backspace"] {             let r = h.request("ui.key", json!({"key": key, "cmd": key == "a"}), T);             assert_eq!(r["ok"], true, "{r}");@@ -375,7 +375,7 @@ fn the_angle_field_reads_angles_as_people_type_them() {         assert_eq!(r["ok"], true, "{r}");         let r = h.request("ui.key", json!({"key": "Enter"}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after key Enter");         assert!((angle(&h) - want).abs() < 1e-9, "{typed:?}: want {want}, got {}", angle(&h));     } }@@ -386,7 +386,7 @@ fn the_angle_field_writes_the_angle_like_the_readout() {     let mut h = crop_tool();     let r = h.request("engine.execute", json!({"command": "crop.straighten", "params": {"angle": 2.5}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after crop.straighten");     let texts = h.painted_text();     // the field's number follows its label (its "°" is painted on its own)     let field = texts.iter().position(|t| t == "Angle").and_then(|i| texts.get(i + 1));@@ -406,7 +406,7 @@ fn rotating_the_crop_dims_nothing_inside_the_box() {     let plain = h.paint();     let r = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "crop.angle", "value": -45.0}}), T);     assert_eq!(r["ok"], true, "{r}");-    assert!(h.settle(SETTLE), "the rotated crop view settles before the snapshot");+    h.settled(SETTLE, "the rotated crop view settles before the snapshot");     assert_eq!(angle(&h), -45.0);     let rotated = h.paint();     let img = h.app.image_rect.expect("the photo on screen");@@ -451,7 +451,7 @@ fn drag_left_handle(dx: f32, step: f32) -> Headless {         .collect();     let r = h.request("ui.pointer", json!({ "events": events }), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     h } @@ -491,10 +491,10 @@ fn auto_straighten_says_when_it_finds_nothing() {         let r = h.request(method, params, T);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request");     let r = h.request("ui.clickWidget", json!({"id": "button:straightenAuto"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:straightenAuto");     assert!(h.app.ui.toast.as_ref().is_some_and(|t| t.0 == "No horizon or vertical lines to level by"), "{:?}", h.app.ui.toast);     assert_eq!(angle(&h), 0.0);     let _ = std::fs::remove_dir_all(dir);diff --git a/crates/ui-egui/src/tests/crop_tool.rs b/crates/ui-egui/src/tests/crop_tool.rs--- a/crates/ui-egui/src/tests/crop_tool.rs+++ b/crates/ui-egui/src/tests/crop_tool.rs@@ -24,16 +24,16 @@ fn escape_leaves_the_crop_tool_before_the_view() {     assert_eq!(r["ok"], true, "{r}");     let r = h.request("engine.execute", json!({"command": "crop.set", "params": {"rect": [0.1, 0.1, 0.8, 0.9]}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after crop.set");     assert_eq!(h.app.ui.right, RightPanel::Crop);     let crop = h.app.session.catalog.photo(id).map(|p| p.develop.crop.geometry.rect);     h.request("ui.key", json!({"key": "Escape"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_ne!(h.app.ui.right, RightPanel::Crop, "Esc closes the crop tool");     assert_eq!(h.app.ui.view, ViewMode::Detail, "and stays on the photo");     assert_eq!(h.app.session.catalog.photo(id).map(|p| p.develop.crop.geometry.rect), crop, "the crop is kept");     h.request("ui.key", json!({"key": "Escape"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_eq!(h.app.ui.view, ViewMode::PhotoGrid, "the next Esc goes back to the grid"); } @@ -48,18 +48,18 @@ fn escape_in_the_angle_field_only_leaves_the_field() {     h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0], "active": id.0}}), T);     h.app.ui.view = ViewMode::Detail;     h.request("engine.execute", json!({"command": "panel.crop"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after panel.crop");     let r = h.request("ui.clickWidget", json!({"id": "cropAngleField"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking cropAngleField");     h.request("ui.text", json!({"text": "7"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.text");     h.request("ui.key", json!({"key": "Escape"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_eq!((h.app.ui.view, h.app.ui.right), (ViewMode::Detail, RightPanel::Crop), "Esc only left the field");     // the field has let go: the next Esc is the crop tool's     h.request("ui.key", json!({"key": "Escape"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_eq!((h.app.ui.view, h.app.ui.right == RightPanel::Crop), (ViewMode::Detail, false)); } @@ -77,7 +77,7 @@ fn x_in_the_crop_tool_swaps_the_aspect_once() {     h.request("engine.execute", json!({"command": "panel.crop"}), T);     let r = h.request("engine.execute", json!({"command": "crop.aspect", "params": {"aspect": "3x4"}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after crop.aspect");     assert_eq!(h.app.ui.right, RightPanel::Crop);     let crop = |h: &Headless| {         let p = h.app.session.catalog.photo(id).unwrap();@@ -87,11 +87,11 @@ fn x_in_the_crop_tool_swaps_the_aspect_once() {     assert_eq!(crop(&h), (true, Some((400, 300))), "a 3:4 crop follows the landscape photo");     let undo = h.app.session.undo.len();     h.request("ui.key", json!({"key": "x"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key x");     assert_eq!(crop(&h), (false, Some((300, 400))), "X turns the crop portrait");     assert_eq!(h.app.session.undo.len(), undo + 1, "one swap, one undo step");     assert_eq!(h.app.session.catalog.photo(id).unwrap().flag, lightcraft_catalog::Flag::None, "X doesn't reject while cropping");     h.request("ui.key", json!({"key": "x"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key x");     assert_eq!(crop(&h), (true, Some((400, 300))), "X again turns it back"); }diff --git a/crates/ui-egui/src/tests/date_picker.rs b/crates/ui-egui/src/tests/date_picker.rs--- a/crates/ui-egui/src/tests/date_picker.rs+++ b/crates/ui-egui/src/tests/date_picker.rs@@ -19,14 +19,14 @@ fn editor(rules: serde_json::Value) -> Headless {     assert_eq!(r["ok"], true, "{r}");     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     *r = serde_json::from_value(json!({"rules": rules})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     h }  fn click(h: &mut Headless, id: &str) {     let r = h.request("ui.clickWidget", json!({"id": id}), T);     assert_eq!(r["ok"], true, "{id}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget"); }  fn has(h: &Headless, id: &str) -> bool {@@ -83,7 +83,7 @@ fn between_dates_stay_in_order() { fn an_unreadable_date_opens_on_this_month() {     let mut h = editor(json!([{"field": "captureDate", "op": "after", "value": "banana"}]));     h.app.session.clock = Box::new(|| "2026-10-09T12:00:00".to_string());-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting session.clock");     click(&mut h, "datePicker:rules-0");     assert!(has(&h, "datePickerCell:2026-10-09:rules-0"));     assert_eq!(value(&h, 0), json!("banana"), "opening doesn't change the rule");@@ -97,7 +97,7 @@ fn escape_closes_only_the_calendar() {     assert!(has(&h, "datePickerCell:2026-01-20:rules-0"));     let r = h.request("ui.key", json!({"key": "Escape"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert!(!has(&h, "datePickerCell:2026-01-20:rules-0"), "the calendar closed");     assert!(h.app.ui.dialog.is_some(), "the editor stays open"); }@@ -110,7 +110,7 @@ fn escape_closes_only_a_dropdown() {     assert!(has(&h, "ruleFieldItem:rating:rules-0"));     let r = h.request("ui.key", json!({"key": "Escape"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert!(!has(&h, "ruleFieldItem:rating:rules-0"), "the menu closed");     assert!(h.app.ui.dialog.is_some(), "the editor stays open"); }diff --git a/crates/ui-egui/src/tests/dialog_fit.rs b/crates/ui-egui/src/tests/dialog_fit.rs--- a/crates/ui-egui/src/tests/dialog_fit.rs+++ b/crates/ui-egui/src/tests/dialog_fit.rs@@ -15,7 +15,7 @@ fn demo(size: [f32; 2]) -> Headless {     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);     let mut h = Headless::new(app, size, 1.0);     // let the first renders finish (a render still running at exit can crash in GPU teardown)-    h.settle(T);+    h.settled(T, "the new session");     h } @@ -27,7 +27,7 @@ fn open(h: &mut Headless, command: &str) {     let r = h.request("engine.execute", json!({"command": command, "params": {}}), T);     assert_eq!(r["ok"], true, "{command}: {r}");     assert!(h.app.ui.dialog.is_some(), "{command} opens a dialog");-    h.settle(T);+    h.settled(T, "after engine.execute"); }  /// The dialog lies inside the window, and so do its buttons (`button:dialogCancel` when it has one).@@ -54,7 +54,7 @@ fn export_dialog(size: [f32; 2]) -> Headless {         *resize = lightcraft_engine::export::Resize::long_edge(64);         *dir = "/lc-test-out".into();     }-    h.settle(T);+    h.settled(T, "the UI");     h } @@ -121,7 +121,7 @@ fn long_dialogs_fit_a_short_window() {         open(&mut h, command);         assert_fits(&h, command);         h.app.ui.dialog = None;-        h.settle(T);+        h.settled(T, "after setting ui.dialog");     } } diff --git a/crates/ui-egui/src/tests/dialog_settings.rs b/crates/ui-egui/src/tests/dialog_settings.rs--- a/crates/ui-egui/src/tests/dialog_settings.rs+++ b/crates/ui-egui/src/tests/dialog_settings.rs@@ -22,7 +22,7 @@ const SIZES: [[f32; 2]; 3] = [[1600.0, 1000.0], [900.0, 560.0], [640.0, 480.0]]; fn headless(session: lightcraft_engine::Session, size: [f32; 2]) -> Headless {     let mut h = Headless::new(LightcraftApp::new(session, Services::default()), size, 1.0);     // let the first renders finish (a render still running at exit can crash in GPU teardown)-    h.settle(T);+    h.settled(T, "the new session");     h } @@ -34,7 +34,7 @@ fn demo(size: [f32; 2]) -> Headless { fn show(h: &mut Headless, tab: &str) {     let r = h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": tab}}), T);     assert_eq!(r["ok"], true, "app.settings {tab}: {r}");-    h.settle(T);+    h.settled(T, "after app.settings");     for _ in 0..4 {         h.step();     }@@ -205,7 +205,7 @@ fn a_long_library_folder_is_cut_short_and_its_tooltip_has_all_of_it() {         h.request("ui.scroll", json!({"dy": -400.0}), T);         h.step();     }-    h.settle(T);+    h.settled(T, "the UI");     let (folder, room) = (rect(&h, "text:settingsAutoFolder"), rect(&h, "settings:pageRoom"));     assert!(room.intersects(folder), "the watched folder {folder:?} is in view in {room:?}");     assert!(folder.height() < 24.0 && folder.right() <= room.right(), "the watched folder {folder:?} is one line inside {room:?}");@@ -216,7 +216,7 @@ fn a_long_library_folder_is_cut_short_and_its_tooltip_has_all_of_it() {         let path = format!("{path}/{}/{name}", "d".repeat(2 + 3 * n));         std::fs::create_dir_all(&path).unwrap();         assert!(h.app.run("library.autoImport", json!({"folder": path})).is_ok());-        h.settle(T);+        h.settled(T, "after library.autoImport");         for _ in 0..4 {             h.step();         }@@ -247,7 +247,7 @@ fn library_default_stays_on_the_line_of_the_smart_previews_folder_whatever_its_l             h.request("ui.scroll", json!({"dy": -400.0}), T);             h.step();         }-        h.settle(T);+        h.settled(T, "the UI");         // Then "(library default)" stays inside the room, on the line of the path, after it         let room = rect(&h, "settings:pageRoom");         let (smart, note) = (rect(&h, "text:settingsSmartFolder"), rect(&h, "text:settingsSmartDefault"));@@ -327,7 +327,7 @@ fn a_window_too_short_for_a_page_of_its_own_scrolls_tabs_and_page_together() {         h.request("ui.scroll", json!({"dy": -120.0}), T);         h.step();     }-    h.settle(T);+    h.settled(T, "the UI");     // Then the tabs and the page move as one, so every setting can still be reached     let (tab_now, page_now) = (rect(&h, "button:settingsTab-import"), rect(&h, "check:settings.perCamera"));     assert!(page_now.top() < page.top() - 50.0, "the page scrolled: {page:?} -> {page_now:?}");@@ -349,7 +349,7 @@ fn the_tabs_stay_put_while_a_page_scrolls() {         h.request("ui.scroll", json!({"dy": -120.0}), T);         h.step();     }-    h.settle(T);+    h.settled(T, "the UI");     // Then the page moved     let page_after = rect(&h, "check:settings.perCamera");     assert!(page_after.top() < page_before.top() - 50.0, "the page scrolled: {page_before:?} -> {page_after:?}");@@ -374,7 +374,7 @@ fn each_tab_remembers_how_far_it_was_scrolled() {         h.request("ui.scroll", json!({"dy": -120.0}), T);         h.step();     }-    h.settle(T);+    h.settled(T, "the UI");     let scrolled = rect(&h, "check:settings.perCamera").top();     assert!(scrolled < top - 50.0);     // When another tab is opened, it starts at its top@@ -386,7 +386,7 @@ fn each_tab_remembers_how_far_it_was_scrolled() {     assert!((rect(&h, "check:settings.perCamera").top() - scrolled).abs() < 0.5, "Import is where it was left");     // And it is still there when Settings is closed and opened again (documented in What's New)     assert_eq!(h.request("ui.dialog.cancel", json!({}), T)["ok"], true);-    h.settle(T);+    h.settled(T, "after ui.dialog.cancel");     show(&mut h, "import");     assert!((rect(&h, "check:settings.perCamera").top() - scrolled).abs() < 0.5, "Import is where it was left after a reopen"); }diff --git a/crates/ui-egui/src/tests/dialog_tab.rs b/crates/ui-egui/src/tests/dialog_tab.rs--- a/crates/ui-egui/src/tests/dialog_tab.rs+++ b/crates/ui-egui/src/tests/dialog_tab.rs@@ -16,7 +16,7 @@ fn demo_with_selection() -> Headless {     let mut session = lightcraft_engine::Session::with_demo();     session.selection.ids = session.catalog.photos().take(2).map(|p| p.id).collect();     let mut h = Headless::new(LightcraftApp::new(session, Services::default()), [1400.0, 900.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     h } @@ -105,7 +105,7 @@ fn the_name_still_takes_typing_and_enter_after_opening() {     assert_eq!(r["ok"], true, "{r}");     h.step();     key(&mut h, json!({"key": "Enter"}));-    h.settle(T);+    h.settled(T, "after key Enter");     assert!(h.app.ui.dialog.is_none(), "Enter confirmed the dialog");     assert!(h.app.session.catalog.albums().any(|a| a.name == "Holiday"), "the typed name made the album"); }@@ -134,7 +134,7 @@ fn a_click_on_a_check_box_leaves_return_confirming_the_name() {     h.step();     h.step();     key(&mut h, json!({"key": "Enter"}));-    h.settle(T);+    h.settled(T, "after key Enter");     assert!(h.app.ui.dialog.is_none(), "Enter confirmed the dialog after the click");     assert!(h.app.session.catalog.albums().any(|a| a.name == "Trip")); }diff --git a/crates/ui-egui/src/tests/dialogs.rs b/crates/ui-egui/src/tests/dialogs.rs--- a/crates/ui-egui/src/tests/dialogs.rs+++ b/crates/ui-egui/src/tests/dialogs.rs@@ -13,7 +13,7 @@ fn demo(size: [f32; 2], ui: serde_json::Value) -> Headless {     let mut h = Headless::new(app, size, 1.0);     let r = h.request("ui.set", ui, T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(Duration::from_secs(120));+    h.settled(Duration::from_secs(120), "after ui.set");     h } diff --git a/crates/ui-egui/src/tests/docking_acceptance.rs b/crates/ui-egui/src/tests/docking_acceptance.rs--- a/crates/ui-egui/src/tests/docking_acceptance.rs+++ b/crates/ui-egui/src/tests/docking_acceptance.rs@@ -107,7 +107,7 @@ fn crop_canvas_double_click_reveals_edit_in_actual_custom_workspace() {     }     h.app.ui.view = crate::state::ViewMode::Detail;     h.app.run("panel.crop", json!({})).unwrap();-    assert!(h.settle(std::time::Duration::from_secs(120)), "the file-backed photo settles before pointer input");+    h.settled(std::time::Duration::from_secs(120), "the file-backed photo settles before pointer input");     let active = h.app.session.active().expect("the fixture photo is active");     assert!(         h.app.renderer.textures.get(&crate::render::Slot::Main).is_some_and(|tex| tex.photo == active && tex.size[0] > 0 && tex.size[1] > 0),diff --git a/crates/ui-egui/src/tests/filmstrip.rs b/crates/ui-egui/src/tests/filmstrip.rs--- a/crates/ui-egui/src/tests/filmstrip.rs+++ b/crates/ui-egui/src/tests/filmstrip.rs@@ -23,7 +23,7 @@ fn filmstrip_shows_a_cjk_file_name() {     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);     let r = h.request("ui.set", json!({"view": "detail"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert!(h.app.ui.settings.film_names);     let film = format!("film:{}", first.0);     assert!(h.app.widgets.iter().any(|(w, _)| *w == film), "{film} drawn in the filmstrip");@@ -44,18 +44,18 @@ fn filmstrip_stack_badge_expands_and_collapses_the_stack() {     assert_eq!(r["ok"], true, "{r}");     let r = h.request("ui.set", json!({"view": "detail"}), T);     assert_eq!(r["ok"], true, "{r}");-    assert!(h.settle(SETTLE), "the filmstrip settles before clicking its stack badge");+    h.settled(SETTLE, "the filmstrip settles before clicking its stack badge");     assert!(!h.app.session.visible_cloned().contains(&member), "collapsed: only the top shows");     let badge = format!("filmstack:{}", top.0);     let r = h.request("ui.clickWidget", json!({"id": badge}), T);     assert_eq!(r["ok"], true, "{r}");-    assert!(h.settle(SETTLE), "the expanded stack settles");+    h.settled(SETTLE, "the expanded stack settles");     assert!(!h.app.session.catalog.stack(StackId(1)).unwrap().collapsed, "a click on the badge expands the stack");     assert!(h.app.session.visible_cloned().contains(&member));     assert_eq!(h.app.session.selection.active, Some(loose), "the loupe keeps its photo");     let r = h.request("ui.clickWidget", json!({"id": badge}), T);     assert_eq!(r["ok"], true, "{r}");-    assert!(h.settle(SETTLE), "the collapsed stack settles");+    h.settled(SETTLE, "the collapsed stack settles");     assert!(h.app.session.catalog.stack(StackId(1)).unwrap().collapsed, "a second click collapses it");     assert!(!h.app.session.visible_cloned().contains(&member)); }diff --git a/crates/ui-egui/src/tests/focus_point.rs b/crates/ui-egui/src/tests/focus_point.rs--- a/crates/ui-egui/src/tests/focus_point.rs+++ b/crates/ui-egui/src/tests/focus_point.rs@@ -38,7 +38,7 @@ fn detail_with_focus(f: Option<FocusPoint>) -> (Headless, PhotoId) {     assert_eq!(r["ok"], true, "{r}");     let r = h.request("ui.set", json!({"view": "detail", "right": "info"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     (h, id) } @@ -49,7 +49,7 @@ fn widget(h: &Headless, id: &str) -> Option<egui::Rect> { fn run(h: &mut Headless, cmd: &str, params: serde_json::Value) -> serde_json::Value {     let r = h.request("engine.execute", json!({"command": cmd, "params": params}), T);     assert_eq!(r["ok"], true, "{cmd}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after engine.execute");     r["result"].clone() } @@ -87,7 +87,7 @@ fn toggle_zoom_centres_on_the_focus_point() {     assert_eq!(h.app.ui.zoom, Zoom::Fit);     let r = h.request("ui.key", json!({"key": "z"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key z");     assert!(matches!(h.app.ui.zoom, Zoom::Percent(_)), "{:?}", h.app.ui.zoom);     let (u, v) = h.app.ui.pan;     assert!((u - 0.72).abs() < 1e-3 && (v - 0.23).abs() < 1e-3, "pan {u} {v}");diff --git a/crates/ui-egui/src/tests/grading.rs b/crates/ui-egui/src/tests/grading.rs--- a/crates/ui-egui/src/tests/grading.rs+++ b/crates/ui-egui/src/tests/grading.rs@@ -23,7 +23,7 @@ fn demo() -> Headless {     if !h.app.ui.flyout_open("grading") {         h.app.ui.toggle_flyout("grading");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } diff --git a/crates/ui-egui/src/tests/grid.rs b/crates/ui-egui/src/tests/grid.rs--- a/crates/ui-egui/src/tests/grid.rs+++ b/crates/ui-egui/src/tests/grid.rs@@ -42,7 +42,7 @@ fn grid(view: ViewMode) -> Headless {     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);     h.app.ui.view = view;     h.app.ui.thumb_size = 160.0;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.thumb_size");     h } @@ -53,7 +53,7 @@ fn widget(h: &Headless, id: &str) -> Option<egui::Rect> { fn select(h: &mut Headless, id: PhotoId) {     let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0]}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select"); }  /// The photo's cell is drawn inside the grid's canvas.@@ -110,7 +110,7 @@ fn grid_headers_selection_scrolling_stacks_and_shape_changes() {     assert!(widget(&h, "stack:2002").is_some(), "stack member badge drawn");     // keyboard: the next photo becomes active and stays in view     h.request("ui.key", json!({"key": "right"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key right");     let next = vis[i + 1];     assert_eq!(h.app.session.selection.active, Some(next));     assert!(on_screen(&h, next));@@ -125,7 +125,7 @@ fn grid_headers_selection_scrolling_stacks_and_shape_changes() {     let layouts = h.app.caches.grid_stats.layout_builds;     let r = h.request("engine.execute", json!({"command": "photo.rotateRight", "params": {"ids": [landscape.0]}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.rotateRight");     assert!(h.app.caches.grid_stats.layout_builds > layouts, "a shape change re-lays the grid");     let after = widget(&h, &format!("thumb:{}", landscape.0)).unwrap();     assert!(after.height() > after.width(), "{after:?}");@@ -194,7 +194,7 @@ fn double_clicking_a_stack_badge_toggles_the_stack_once() {     select(&mut h, top);     let r = h.request("engine.execute", json!({"command": "stack.toggle", "params": {"ids": [top.0]}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after stack.toggle");     assert!(collapsed(&h));     for want in [false, true] {         let badge = widget(&h, &format!("stack:{}", top.0)).expect("stack badge drawn").center();@@ -204,7 +204,7 @@ fn double_clicking_a_stack_badge_toggles_the_stack_once() {         let moved = widget(&h, &format!("stack:{}", top.0)).expect("stack badge drawn");         assert!(moved.contains(badge), "the badge moved from under the pointer: {moved:?}");         click_at(&mut h, badge);-        h.settle(SETTLE);+        h.settled(SETTLE, "after click_at");         assert_eq!(collapsed(&h), want, "a double-click toggles the stack once");         assert_eq!(h.app.ui.view, ViewMode::PhotoGrid, "a double-click on the badge doesn't open the photo");         // the next double-click is apart from this one@@ -214,7 +214,7 @@ fn double_clicking_a_stack_badge_toggles_the_stack_once() {     }     let badge = widget(&h, &format!("stack:{}", top.0)).unwrap().center();     click_at(&mut h, badge);-    h.settle(SETTLE);+    h.settled(SETTLE, "after click_at");     assert!(!collapsed(&h), "a single click toggles it"); } @@ -232,7 +232,7 @@ fn grid_stack_badge_toggles_a_stack_that_is_not_selected() {     assert!(widget(&h, &format!("stack:{}", top.0)).is_some(), "badge drawn");     let r = h.request("ui.clickWidget", json!({"id": format!("stack:{}", top.0)}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     assert!(h.app.session.catalog.stack_of(top).unwrap().collapsed, "a click on an unselected stack's badge toggles it");     assert_eq!((h.app.session.selection.ids.clone(), h.app.session.selection.active), (vec![loose], Some(loose)), "and leaves the selection alone"); }diff --git a/crates/ui-egui/src/tests/guided_guides.rs b/crates/ui-egui/src/tests/guided_guides.rs--- a/crates/ui-egui/src/tests/guided_guides.rs+++ b/crates/ui-egui/src/tests/guided_guides.rs@@ -24,7 +24,7 @@ fn guided() -> Headless {         let r = h.request(method, params, T);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request");     h } @@ -49,7 +49,7 @@ fn a_click_near_a_guide_selects_it() {     let id = h.app.session.active().expect("a photo");     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert_eq!(h.app.ui.selected_guide, Some((id.0, 0)), "the clicked guide is selected");     assert_eq!(guides_len(&h), 2, "selecting removes nothing"); }@@ -60,11 +60,11 @@ fn delete_removes_the_selected_guide() {     let mut h = guided();     let r = h.request("ui.click", json!({"x": guide_at(&h, 1).x, "y": guide_at(&h, 1).y}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert_eq!(h.app.ui.selected_guide.map(|(_, i)| i), Some(1));     let r = h.request("ui.key", json!({"key": "Delete"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Delete");     assert_eq!(guides_len(&h), 1, "one guide is gone");     assert_eq!(h.app.ui.selected_guide, None, "and nothing is selected afterwards");     // the survivor is the first guide: the second one's endpoints are gone@@ -83,13 +83,13 @@ fn a_click_away_clears_the_selection() {     let mut h = guided();     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(h.app.ui.selected_guide.is_some());     let img = h.app.image_rect.expect("the photo on screen");     // below both guides, far from them     let r = h.request("ui.click", json!({"x": img.center().x, "y": img.top() + 0.95 * img.height()}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert_eq!(h.app.ui.selected_guide, None);     assert_eq!(guides_len(&h), 2, "and removes nothing"); }@@ -101,7 +101,7 @@ fn delete_with_no_selection_removes_nothing() {     let undo = h.app.session.undo.len();     let r = h.request("ui.key", json!({"key": "Delete"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Delete");     assert_eq!(guides_len(&h), 2, "no guide is removed");     assert_eq!(h.app.session.undo.len(), undo, "no edit either");     assert!(h.app.session.active().is_some(), "the photo is still active");@@ -113,7 +113,7 @@ fn drawing_a_new_guide_clears_the_selection() {     let mut h = guided();     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(h.app.ui.selected_guide.is_some());     let img = h.app.image_rect.expect("the photo on screen");     // a drag across the middle of the photo (clear of both guides) draws a third guide@@ -123,7 +123,7 @@ fn drawing_a_new_guide_clears_the_selection() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.drag");     assert_eq!(guides_len(&h), 3, "the drawn guide is added");     assert_eq!(h.app.ui.selected_guide, None, "and the click's selection is gone"); }@@ -149,7 +149,7 @@ fn dragging_a_guide_endpoint_moves_it() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(guides_len(&h), 2, "no guide is added");     let d = h.app.session.catalog.photo(id).expect("the photo");     let (a, b) = d.develop.geometry.guides[0];@@ -171,7 +171,7 @@ fn clicking_an_endpoint_selects_it_without_changing_it() {     let y = ((at.y - img.top()) / img.height()) as f64;     let r = h.request("ui.pointer", json!({"events": [{"kind": "down", "x": x, "y": y}, {"kind": "up", "x": x, "y": y}]}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(h.app.ui.selected_guide, Some((id.0, 1)), "the endpoint's guide is selected");     assert_eq!(guides_len(&h), 2);     let d = h.app.session.catalog.photo(id).expect("the photo");@@ -199,7 +199,7 @@ fn a_new_guide_starts_at_the_press_point() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(guides_len(&h), 3, "the drawn guide is added");     assert_eq!(h.app.ui.selected_guide, None, "drawing a new guide leaves nothing selected");     let rect = guide_rect(&h, 2);@@ -224,7 +224,7 @@ fn a_drag_whose_press_and_release_share_one_frame_still_draws() {     h.events.push(egui::Event::PointerButton { pos: p1, button: egui::PointerButton::Primary, pressed: true, modifiers: egui::Modifiers::NONE });     h.events.push(egui::Event::PointerMoved(p2));     h.events.push(egui::Event::PointerButton { pos: p2, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert_eq!(guides_len(&h), 3, "the drag that shared one frame still draws its guide");     assert_eq!(h.app.session.undo.len(), undo + 1, "one edit was written");     let rect = guide_rect(&h, 2);@@ -239,7 +239,7 @@ fn a_right_click_on_a_guide_selects_nothing() {     let mut h = guided();     let r = h.request("ui.click", json!({"x": guide_at(&h, 0).x, "y": guide_at(&h, 0).y, "button": "right"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert_eq!(h.app.ui.selected_guide, None, "the right button selects nothing");     assert_eq!(guides_len(&h), 2, "and draws nothing"); }@@ -251,7 +251,7 @@ fn a_press_on_the_navigator_draws_no_guide() {     let mut h = guided();     let r = h.request("engine.execute", json!({"command": "view.zoom100"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.zoom100");     let nav = h.app.widgets.iter().rev().find(|(w, _)| *w == "canvas:navigator").map(|(_, r)| *r).expect("navigator shown when zoomed");     let img = h.app.image_rect.expect("the photo on screen");     let c = nav.center();@@ -262,7 +262,7 @@ fn a_press_on_the_navigator_draws_no_guide() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(guides_len(&h), 2, "the press on the navigator drew no guide");     assert_eq!(h.app.ui.selected_guide, None, "and selected nothing"); }@@ -280,13 +280,13 @@ fn a_press_on_a_menu_over_a_guide_selects_nothing() {     // at the pointer (gap 0), so its frame covers the guide from 30px inside every edge     let r = h.request("ui.click", json!({"x": line.x - 30.0, "y": line.y - 30.0, "button": "right"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");     let img = h.app.image_rect.expect("the photo on screen");     let (lx, ly) = ((line.x - img.left()) / img.width(), (line.y - img.top()) / img.height());     let r = h.request("ui.pointer", json!({"events": [{"kind": "down", "x": lx, "y": ly}, {"kind": "up", "x": lx, "y": ly}]}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(h.app.ui.selected_guide, None, "the press under the menu selects nothing");     assert!(h.app.gesture.is_none(), "and starts no gesture");     assert_eq!(guides_len(&h), 2, "and draws nothing");@@ -309,7 +309,7 @@ fn a_press_on_a_menu_over_an_endpoint_moves_nothing() {     let end = guide_rect(&h, 0).left_top();     let r = h.request("ui.click", json!({"x": end.x - 30.0, "y": end.y - 30.0, "button": "right"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");     let img = h.app.image_rect.expect("the photo on screen");     let (ex, ey) = ((end.x - img.left()) / img.width(), (end.y - img.top()) / img.height());@@ -319,7 +319,7 @@ fn a_press_on_a_menu_over_an_endpoint_moves_nothing() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(h.app.ui.selected_guide, None, "the endpoint's press under the menu selects nothing");     assert!(h.app.gesture.is_none(), "and starts no gesture");     assert_eq!(guides_len(&h), 2, "and draws nothing");@@ -343,7 +343,7 @@ fn a_press_that_leaves_the_menu_in_one_frame_draws_nothing() {     // right-click up-left of the endpoint: the menu opens with its top-left at the pointer     let r = h.request("ui.click", json!({"x": end.x - 30.0, "y": end.y - 30.0, "button": "right"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert!(h.view.ctx.any_popup_open(), "the context menu is open over the photo");     // one batch: go down 1px inside the menu, then straight off it to bare photo, far below     // both guides, and release there — the current pointer never sees the menu again@@ -359,7 +359,7 @@ fn a_press_that_leaves_the_menu_in_one_frame_draws_nothing() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(guides_len(&h), 2, "the press that left the menu drew no guide");     assert_eq!(h.app.ui.selected_guide, None, "and selected nothing");     assert!(h.app.gesture.is_none(), "and leaves no gesture behind");diff --git a/crates/ui-egui/src/tests/histogram_drag.rs b/crates/ui-egui/src/tests/histogram_drag.rs--- a/crates/ui-egui/src/tests/histogram_drag.rs+++ b/crates/ui-egui/src/tests/histogram_drag.rs@@ -24,7 +24,7 @@ fn edit_panel() -> Headless {     assert_eq!(r["ok"], true, "{r}");     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } @@ -68,14 +68,14 @@ fn drag(h: &mut Headless, fx: f32, dx: f32, shift: bool) {     let p = json!({"id": "histogram", "fx": fx, "fy": 0.5, "dx": dx, "steps": 8, "shift": shift});     let r = h.request("ui.dragWidget", p.clone(), T);     assert_eq!(r["ok"], true, "{p}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dragWidget"); }  /// A double-click on the histogram at `fx` (the plot's own rows, `fy` 0.5). fn double_click(h: &mut Headless, fx: f32) {     let r = h.request("ui.clickWidget", json!({"id": "histogram", "fx": fx, "fy": 0.5, "count": 2}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking histogram"); }  /// Hover the histogram at `fx` and let the frame settle.@@ -183,7 +183,7 @@ fn a_drag_below_the_plot_leaves_the_tones_alone() {     let p = json!({"id": "histogram", "fx": 0.5, "fy": 0.97, "dx": w * 0.2, "steps": 6});     let r = h.request("ui.dragWidget", p.clone(), T);     assert_eq!(r["ok"], true, "{p}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dragWidget");     assert_eq!(values(&h), [0.0; 5], "no zone was dragged");     assert_eq!(h.app.session.undo.len(), undo, "and nothing was recorded");     assert!(h.app.session.interaction.is_none(), "no interaction was opened");@@ -199,7 +199,7 @@ fn a_drag_starting_on_a_clipping_chevron_changes_no_develop_value() {     let p = json!({"id": "histogram:shadows", "dx": 40.0, "steps": 8});     let r = h.request("ui.dragWidget", p.clone(), T);     assert_eq!(r["ok"], true, "{p}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dragWidget");     assert_eq!(values(&h), [0.0; 5], "dragging from the shadow chevron moves no tone");     assert_eq!(h.app.session.undo.len(), undo, "and leaves undo alone");     assert!(h.app.session.interaction.is_none(), "no interaction was opened");@@ -208,7 +208,7 @@ fn a_drag_starting_on_a_clipping_chevron_changes_no_develop_value() {     let p = json!({"id": "histogram:highlights", "dx": -40.0, "steps": 8});     let r = h.request("ui.dragWidget", p.clone(), T);     assert_eq!(r["ok"], true, "{p}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dragWidget");     assert_eq!(values(&h), [0.0; 5], "dragging from the highlight chevron moves no tone");     assert_eq!(h.app.session.undo.len(), undo, "undo remains untouched");     assert!(h.app.session.interaction.is_none(), "no interaction was opened");@@ -288,7 +288,7 @@ fn a_drag_dispatches_live_and_commits_on_the_release() {         pressed: false,         modifiers: egui::Modifiers::NONE,     });-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert!(h.app.session.interaction.is_none(), "the release commits the interaction");     assert!(h.app.ui.dragging_control.is_none(), "and no control is left dragged");     assert_eq!(h.app.session.undo.len() - before, 1, "the whole drag is one undo step");diff --git a/crates/ui-egui/src/tests/info_dialogs.rs b/crates/ui-egui/src/tests/info_dialogs.rs--- a/crates/ui-egui/src/tests/info_dialogs.rs+++ b/crates/ui-egui/src/tests/info_dialogs.rs@@ -26,7 +26,7 @@ fn open(h: &mut Headless, id: &str) { fn information_dialogs_end_with_one_close() {     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });     let mut h = Headless::new(app, [1300.0, 900.0], 1.0);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new session");     for id in ["app.systemInfo", "dialog.allMetadata", "app.whatsNew"] {         open(&mut h, id);         assert!(!has(&h, "button:dialogCancel"), "{id}: a Cancel button, though there is nothing to confirm");diff --git a/crates/ui-egui/src/tests/keymap.rs b/crates/ui-egui/src/tests/keymap.rs--- a/crates/ui-egui/src/tests/keymap.rs+++ b/crates/ui-egui/src/tests/keymap.rs@@ -19,7 +19,7 @@ fn demo() -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "detail"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } @@ -176,11 +176,11 @@ fn clipboard_events_run_the_copy_and_paste_shortcuts() {     let mut h = demo();     assert!(h.app.session.clipboard.is_none());     h.events.push(egui::Event::Copy);-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert!(h.app.session.clipboard.is_some(), "Ctrl+C copied the edit settings");     let shift_cmd = egui::Modifiers::SHIFT | egui::Modifiers::COMMAND;     h.events.extend([egui::Event::ModifiersChanged(shift_cmd), egui::Event::Paste("text".into())]);-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert!(matches!(h.app.ui.dialog, Some(Dialog::PasteSettings { .. })), "⇧Ctrl+V: {:?}", h.app.ui.dialog); } @@ -190,10 +190,10 @@ fn clipboard_events_run_the_copy_and_paste_shortcuts() { fn shift_insert_pastes_plainly() {     let mut h = demo();     h.events.push(egui::Event::Copy);-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     let undo = h.app.session.undo.len();     h.events.extend([egui::Event::ModifiersChanged(egui::Modifiers::SHIFT), egui::Event::Paste("text".into())]);-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert_eq!(h.app.ui.dialog, None, "no Paste Selected Settings");     assert_eq!(h.app.session.undo.len(), undo + 1, "the settings were pasted"); }@@ -217,7 +217,7 @@ fn cmd_alt_shift_c_and_v_copy_and_paste_metadata() {         p["key"] = json!(k);         let r = h.request("ui.key", p, T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.key");     };      h.app.run("library.select", json!({"ids": [a.0]})).unwrap();@@ -233,11 +233,11 @@ fn cmd_alt_shift_c_and_v_copy_and_paste_metadata() {     let all = egui::Modifiers::COMMAND | egui::Modifiers::ALT | egui::Modifiers::SHIFT;     h.app.run("library.select", json!({"ids": [a.0]})).unwrap();     h.events.extend([egui::Event::ModifiersChanged(all), egui::Event::Copy]);-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert!(h.app.session.meta_clipboard.is_some(), "the Copy event with ⌥⇧ held copied the metadata");     h.app.run("library.select", json!({"ids": [b.0]})).unwrap();     h.events.extend([egui::Event::ModifiersChanged(all), egui::Event::Paste("text".into())]);-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input");     assert_eq!(title(&h, b), "Dawn", "the Paste event with ⌥⇧ held pasted the metadata");     assert!(h.app.session.clipboard.is_none(), "no edit settings were copied");     assert_eq!(h.app.ui.dialog, None, "no Paste Selected Settings");@@ -254,7 +254,7 @@ fn opening_numeric_release_keeps_keys_in_the_editor() {         h.app.run("app.setShortcut", json!({"id": "photo.increaseRating", "shortcut": "Enter"})).unwrap();         let r = h.request("ui.set", json!({"view": "detail", "right": "edit", "openSections": ["light"], "histogram": false}), T);         assert_eq!(r["ok"], true, "{r}");-        assert!(h.settle(SETTLE));+        h.settled(SETTLE, "after ui.set");         assert!(h.app.fonts_ready);         assert!(h.app.recording_shortcut.is_none() && h.app.ui.name_edit.is_none());         assert!(h.app.native_shortcuts.is_empty() && h.app.synthetic.is_empty() && h.events.is_empty());diff --git a/crates/ui-egui/src/tests/keyword_files.rs b/crates/ui-egui/src/tests/keyword_files.rs--- a/crates/ui-egui/src/tests/keyword_files.rs+++ b/crates/ui-egui/src/tests/keyword_files.rs@@ -13,7 +13,7 @@ const SETTLE: Duration = Duration::from_secs(120);  fn app(library: &SyntheticLibrary) -> Headless {     let mut h = Headless::new(library.app(), [1200.0, 800.0], 1.0);-    assert!(h.settle(SETTLE), "keyword fixture did not settle");+    h.settled(SETTLE, "keyword fixture did not settle");     h } diff --git a/crates/ui-egui/src/tests/keyword_list.rs b/crates/ui-egui/src/tests/keyword_list.rs--- a/crates/ui-egui/src/tests/keyword_list.rs+++ b/crates/ui-egui/src/tests/keyword_list.rs@@ -28,21 +28,21 @@ fn keywords_panel_at(size: [f32; 2]) -> (Headless, Vec<u64>) {         let r = h.request(method, params, T);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request");     (h, ids) }  fn run(h: &mut Headless, command: &str, params: serde_json::Value) -> serde_json::Value {     let r = h.request("engine.execute", json!({"command": command, "params": params}), T);     assert_eq!(r["ok"], true, "{command}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after engine.execute");     r }  fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {     let r = h.request(method, params.clone(), T);     assert_eq!(r["ok"], true, "{method} {params}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request"); }  fn has(h: &Headless, id: &str) -> bool {@@ -358,7 +358,7 @@ fn press_and_move(h: &mut Headless, from: egui::Pos2, to: egui::Pos2) {  fn release(h: &mut Headless, at: egui::Pos2) {     h.events.push(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: false, modifiers: egui::Modifiers::NONE });-    h.settle(SETTLE);+    h.settled(SETTLE, "after the injected input"); }  /// Esc gives up a keyword drag: released on another keyword afterwards, it stays where it is.@@ -492,7 +492,7 @@ fn ticks_in_quick_succession_land_on_their_rows() {         let r = h.request("ui.clickWidget", json!({"id": format!("keywordCheck:{k}")}), T);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     let mut have = keywords_of(&h, ids[0]);     have.sort();     assert_eq!(have, picked);diff --git a/crates/ui-egui/src/tests/keyword_set.rs b/crates/ui-egui/src/tests/keyword_set.rs--- a/crates/ui-egui/src/tests/keyword_set.rs+++ b/crates/ui-egui/src/tests/keyword_set.rs@@ -23,14 +23,14 @@ fn keywords_panel() -> Headless {         let r = h.request(method, params, T);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request");     h }  fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {     let r = h.request(method, params.clone(), T);     assert_eq!(r["ok"], true, "{method} {params}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request"); }  fn sets(h: &mut Headless) -> serde_json::Value {@@ -67,7 +67,7 @@ fn editing_recent_keywords_saves_them_as_a_set() { fn editing_a_named_set_renames_it() {     let mut h = keywords_panel();     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony", "reception"]})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new keywords_panel session");     open_editor(&mut h);     let Some(crate::state::Dialog::KeywordSet { replaces, name, .. }) = h.app.ui.dialog.clone() else { panic!("{:?}", h.app.ui.dialog) };     assert_eq!((replaces.as_deref(), name.as_str()), (Some("Weddings"), "Weddings"));@@ -94,7 +94,7 @@ fn a_set_needs_a_name() { fn an_empty_slot_is_an_idle_button() {     let mut h = keywords_panel();     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony", "", "reception"]})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new keywords_panel session");     assert!(h.app.widgets.iter().any(|(w, _)| w == "kwSetEmpty:2"), "slot 2 is shown empty"); } @@ -119,7 +119,7 @@ fn a_taken_name_says_so() {     let mut h = keywords_panel();     h.app.session.execute("keyword.saveSet", &json!({"name": "Travel", "keywords": ["harbour"]})).unwrap();     h.app.session.execute("keyword.useSet", &json!({"name": "Recent Keywords"})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new keywords_panel session");     open_editor(&mut h);     ask(&mut h, "ui.text", json!({"text": "travel"}));     ask(&mut h, "ui.clickWidget", json!({"id": "button:dialogOk"}));@@ -136,7 +136,7 @@ fn a_taken_name_says_so() { fn a_set_deleted_meanwhile_is_saved_anew() {     let mut h = keywords_panel();     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new keywords_panel session");     open_editor(&mut h);     h.app.session.execute("keyword.deleteSet", &json!({"name": "Weddings"})).unwrap();     ask(&mut h, "ui.clickWidget", json!({"id": "button:dialogOk"}));@@ -150,7 +150,7 @@ fn a_set_deleted_meanwhile_is_saved_anew() { fn save_as_a_new_set_keeps_the_original() {     let mut h = keywords_panel();     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new keywords_panel session");     open_editor(&mut h);     ask(&mut h, "ui.text", json!({"text": "Ceremonies"}));     ask(&mut h, "ui.clickWidget", json!({"id": "check:keywordSetAsNew"}));@@ -168,7 +168,7 @@ fn alt_0_and_alt_shift_0_cycle_the_keyword_sets() {     let mut h = keywords_panel();     h.app.session.execute("keyword.saveSet", &json!({"name": "Weddings", "keywords": ["ceremony"]})).unwrap();     h.app.session.execute("keyword.useSet", &json!({"name": "Recent Keywords"})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the new keywords_panel session");     ask(&mut h, "ui.key", json!({"key": "0", "alt": true}));     assert_eq!(sets(&mut h)["current"], "Weddings", "⌥0: the next set");     ask(&mut h, "ui.key", json!({"key": "1", "alt": true}));diff --git a/crates/ui-egui/src/tests/keyword_sidebar.rs b/crates/ui-egui/src/tests/keyword_sidebar.rs--- a/crates/ui-egui/src/tests/keyword_sidebar.rs+++ b/crates/ui-egui/src/tests/keyword_sidebar.rs@@ -31,7 +31,7 @@ fn app(photos: u64, keywords: u64) -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } diff --git a/crates/ui-egui/src/tests/keywording.rs b/crates/ui-egui/src/tests/keywording.rs--- a/crates/ui-egui/src/tests/keywording.rs+++ b/crates/ui-egui/src/tests/keywording.rs@@ -26,14 +26,14 @@ fn two_selected() -> (Headless, Vec<u64>) {         let r = h.request(method, params, T);         assert_eq!(r["ok"], true, "{r}");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request");     (h, ids) }  fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {     let r = h.request(method, params.clone(), T);     assert_eq!(r["ok"], true, "{method} {params}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request"); }  fn has(h: &Headless, id: &str) -> bool {@@ -218,7 +218,7 @@ fn chips_wrap_whole_inside_the_panel() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.setMeta");     let panel = h.app.widgets.iter().find(|(w, _)| w == "panel:right_panel").map(|(_, r)| *r).expect("the right panel");     let chips: Vec<(String, egui::Rect)> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("keywordChip:")).cloned().collect();     assert!(chips.len() >= 6, "{chips:?}");@@ -253,7 +253,7 @@ fn the_active_photo_alone_is_the_selection() { fn a_keyword_shows_that_one_inside_it_is_applied() {     let (mut h, ids) = two_selected();     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": ids, "addKeywords": ["travel|Italy|Rome"]}}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.setMeta");     // folded: `travel` is the only row of the three, and says there is something inside     assert!(has(&h, "keywordRow:travel") && !has(&h, "keywordRow:travel|Italy"), "folded");     assert!(has(&h, "keywordInside:travel"));@@ -263,17 +263,17 @@ fn a_keyword_shows_that_one_inside_it_is_applied() {     assert!(rect(&h, "keywordCheck:travel").contains_rect(rect(&h, "keywordInside:travel")));     // opened down to it: each level above has the dot, the keyword itself the tick     h.app.ui.keyword_list_open.extend(["travel".to_string(), "travel|italy".to_string()]);-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(has(&h, "keywordInside:travel") && has(&h, "keywordInside:travel|Italy"));     assert!(has(&h, "keywordRow:travel|Italy|Rome") && !has(&h, "keywordInside:travel|Italy|Rome"));     // a keyword the selection has itself shows its own state: the tick, or the dash when only some photos have it     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": ids, "addKeywords": ["travel"]}}));     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": [ids[0]], "addKeywords": ["travel|Italy"]}}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.setMeta");     assert!(!has(&h, "keywordInside:travel") && !has(&h, "keywordInside:travel|Italy"));     // with nothing selected nothing is applied, inside or not     ask(&mut h, "engine.execute", json!({"command": "library.select", "params": {"ids": []}}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     assert!(has(&h, "keywordRow:travel") && !h.app.widgets.iter().any(|(w, _)| w.starts_with("keywordInside:"))); } @@ -345,7 +345,7 @@ fn grouped_keywords_share_a_line_naming_their_parent() {     let whole = rect(&h, "keywordChip:Places|Italy|Rome").width();      h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");     let heading = rect(&h, "keywordGroup:Places|Italy");     let (rome, venice) = (rect(&h, "keywordChip:Places|Italy|Rome"), rect(&h, "keywordChip:Places|Italy|Venice"));     assert!(rome.top() >= heading.bottom(), "under its heading: {rome:?} {heading:?}");@@ -373,7 +373,7 @@ fn a_long_grouped_list_is_shortened_until_asked_for() {     let list_before = rect(&h, "field:keywordFilter").top();      h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");     assert_eq!(count(&h, "keywordChip:"), 7, "a heading and seven chips");     assert!(has(&h, "keywordChipsMore") && !has(&h, "keywordChipsFewer"));     assert!(rect(&h, "field:keywordFilter").top() < list_before - 80.0, "{} from {list_before}", rect(&h, "field:keywordFilter").top());@@ -412,7 +412,7 @@ fn a_selection_without_keywords_says_so() {     give(&mut h, &ids, &[]);     assert!(has(&h, "keywordChipsEmpty"));     h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");     assert!(has(&h, "keywordChipsEmpty")); } @@ -449,7 +449,7 @@ fn grouped_suggestions_name_the_keyword_alone() {     ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": [ids[0]], "addKeywords": ["Places|Portugal|Lisbon"]}}));     let whole = rect(&h, "kwSuggest:Places|Portugal|Lisbon").width();     h.app.ui.settings.keyword_chips = crate::state::KeywordChips::Grouped;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.settings.keyword_chips");     assert!(rect(&h, "kwSuggest:Places|Portugal|Lisbon").width() < whole);     ask(&mut h, "ui.clickWidget", json!({"id": "kwSuggest:Places|Portugal|Lisbon"}));     assert!(ids.iter().all(|id| keywords_of(&h, *id).iter().any(|k| k == "Places|Portugal|Lisbon")));@@ -646,7 +646,7 @@ fn any_name_fits_the_panel() {     give(&mut h, &ids, &keywords);     for layout in [crate::state::KeywordChips::Paths, crate::state::KeywordChips::Grouped] {         h.app.ui.settings.keyword_chips = layout;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.settings.keyword_chips");         let panel = rect(&h, "panel:right_panel");         let chips: Vec<(String, egui::Rect)> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("keywordChip:")).cloned().collect();         assert_eq!(chips.len(), keywords.len(), "{layout:?}");@@ -840,7 +840,7 @@ fn an_offer_out_of_sight_is_not_taken() {             assert_eq!(r["ok"], true, "{r}");         }     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Enter");     ask(&mut h, "ui.key", json!({"key": "Enter"}));     assert!(none_has(&h, &ids, "Places|Portugal|Lisbon"), "{:?}", keywords_of(&h, ids[0])); }diff --git a/crates/ui-egui/src/tests/labels.rs b/crates/ui-egui/src/tests/labels.rs--- a/crates/ui-egui/src/tests/labels.rs+++ b/crates/ui-egui/src/tests/labels.rs@@ -27,7 +27,7 @@ fn app(view: ViewMode) -> Headless {     // Parallel UI tests can leave a quiet spell before thumbnail jobs complete. Require the     // fixture's real textures before comparing photo pixels, as well as settled layout.     assert!(h.step_until(SETTLE, |h| h.app.renderer.thumb_textures() == 6), "all fixture thumbnails must be ready");-    assert!(h.settle(SETTLE), "label fixture did not settle");+    h.settled(SETTLE, "the label fixture");     h } diff --git a/crates/ui-egui/src/tests/layout_image.rs b/crates/ui-egui/src/tests/layout_image.rs--- a/crates/ui-egui/src/tests/layout_image.rs+++ b/crates/ui-egui/src/tests/layout_image.rs@@ -23,7 +23,7 @@ fn loupe() -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } diff --git a/crates/ui-egui/src/tests/library_problem.rs b/crates/ui-egui/src/tests/library_problem.rs--- a/crates/ui-egui/src/tests/library_problem.rs+++ b/crates/ui-egui/src/tests/library_problem.rs@@ -238,7 +238,7 @@ fn an_unrelated_open_dialog_does_not_own_the_launch_list_or_its_barrier() {         [1200.0, 900.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     assert!(h.app.run("app.settings", json!({})).is_ok(), "Settings opens first");     h.step();     let mut problem = LibraryProblem::choose(&settings);@@ -281,7 +281,7 @@ fn the_launch_list_still_quits_on_escape_without_a_notice() { fn creation_frame_drops_background_text(problem: LibraryProblem, button: &str) {     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });     let mut h = Headless::new(app, [1200.0, 760.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     let r = h.request("ui.clickWidget", json!({"id": "field:search"}), T);     assert_eq!(r["ok"], true, "the search field takes focus: {r}");     let r = h.request("ui.text", json!({"text": "base"}), T);@@ -328,7 +328,7 @@ fn creation_after_raw_input_cancels_injected_text(problem: LibraryProblem) {         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     let (tx, rx) = std::sync::mpsc::channel();     h.app.admit_gesture(&h.view.ctx.clone(), "ui.text".into(), json!({"text": "leak"}), tx).expect("the text gesture is admitted");     h.step(); // resolve: one Text event is queued@@ -363,7 +363,7 @@ fn alt_launch_creation_after_raw_input_cancels_an_injected_gesture() {         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     let (tx, rx) = std::sync::mpsc::channel();     h.app.admit_gesture(&h.view.ctx.clone(), "ui.text".into(), json!({"text": "leak"}), tx).expect("the text gesture is admitted");     h.step();@@ -412,7 +412,7 @@ fn a_held_background_press_is_released_when_a_library_window_appears_after_raw_i fn a_library_window_first_seen_after_begin_pass_drops_background_input() {     let mut h =         Headless::new(LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }), [800.0, 600.0], 1.0);-    h.settle(T);+    h.settled(T, "the UI");     let _ = pass_log(&mut h);     let at = egui::pos2(30.0, 40.0);     h.events.extend([@@ -452,7 +452,7 @@ fn a_library_window_first_seen_after_begin_pass_drops_background_input() { fn touch_release_clears_egui_touch_state_when_the_library_window_appears_after_begin_pass(phase: egui::TouchPhase) {     let mut h =         Headless::new(LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }), [800.0, 600.0], 1.0);-    h.settle(T);+    h.settled(T, "the UI");     let at = egui::pos2(30.0, 40.0);     let touch = |phase| egui::Event::Touch { device_id: egui::TouchDeviceId(1), id: egui::TouchId(1), phase, pos: at, force: None };     h.events.push(touch(egui::TouchPhase::Start));@@ -522,7 +522,7 @@ fn a_notice_dismissal_shields_the_library_windows_first_return_frame() {         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     let photo = select_first_photo(&mut h);     let before = rating(&h, photo);     h.app.library_problem = Some(LibraryProblem::new("/no/such/library", "a test error"));@@ -567,7 +567,7 @@ fn a_library_window_created_under_a_notice_shields_the_first_frame_after_dismiss         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     let photo = select_first_photo(&mut h);     let before = rating(&h, photo);     h.app.notices.push("The settings file could not be read and was reset.".into());@@ -607,11 +607,11 @@ fn closing_the_new_library_dialog_shields_the_launch_lists_first_return_frame()         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     let photo = select_first_photo(&mut h);     let before = rating(&h, photo);     h.app.library_problem = Some(LibraryProblem::choose(&settings));-    h.settle(T);+    h.settled(T, "after setting library_problem");     assert!(has(&h, "button:libraryOpen"), "the launch list is settled");     assert!(h.app.run("app.newLibrary", json!({})).is_ok(), "New Library opens its name prompt");     h.step();@@ -655,7 +655,7 @@ fn the_banner_reopen_shields_the_first_input_frame() {         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     let photo = select_first_photo(&mut h);     let before = rating(&h, photo);     h.app.library_problem = Some(LibraryProblem { dismissed: true, ..LibraryProblem::new("/no/such/library", "a test error") });@@ -708,9 +708,9 @@ fn clipboard_pastes_into_the_launch_lists_own_text_prompt() {         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     h.app.library_problem = Some(LibraryProblem::choose(&settings));-    h.settle(T);+    h.settled(T, "after setting library_problem");     assert!(h.app.run("app.newLibrary", json!({})).is_ok(), "New Library opens its name prompt");     h.step();     h.step();@@ -735,7 +735,7 @@ fn clipboard_pastes_into_the_launch_lists_own_text_prompt() { fn a_settled_library_window_keeps_a_background_dialogs_text_draft_unchanged() {     let files = SyntheticLibrary::new("library-text-draft");     let mut h = Headless::new(files.app(), [1200.0, 760.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     h.app.ui.dialog = Some(crate::state::Dialog::NewAlbum { name: "Trip".into(), folder: false, parent: None, include_selected: false });     for _ in 0..4 {         h.step();@@ -788,9 +788,9 @@ fn typing_reaches_the_launch_lists_own_new_library_prompt() {         [1200.0, 760.0],         1.0,     );-    h.settle(T);+    h.settled(T, "the new session");     h.app.library_problem = Some(LibraryProblem::choose(&settings));-    h.settle(T);+    h.settled(T, "after setting library_problem");     assert!(h.app.run("app.newLibrary", json!({})).is_ok(), "New Library opens its name prompt");     h.step();     h.step();diff --git a/crates/ui-egui/src/tests/lifecycle_037.rs b/crates/ui-egui/src/tests/lifecycle_037.rs--- a/crates/ui-egui/src/tests/lifecycle_037.rs+++ b/crates/ui-egui/src/tests/lifecycle_037.rs@@ -1126,7 +1126,7 @@ fn an_overlay_that_appears_after_the_press_cancels_the_release() {     assert!(h.app.quit_prompt.is_some(), "the prompt was not activated");     // Remove the overlay: the same target is clickable again (a positive control after refusal).     h.app.quit_prompt = None;-    assert!(h.settle(Duration::from_secs(60)), "the Settings dialog settles after the overlay");+    h.settled(Duration::from_secs(60), "the Settings dialog settles after the overlay");     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);     assert_eq!(r["ok"], true, "{r}");     h.step();@@ -1215,7 +1215,7 @@ fn a_nonprompt_overlay_that_appears_after_the_press_cancels_the_release() {     );     assert!(h.app.library_problem.is_some(), "the overlay was not activated");     h.app.library_problem = None;-    assert!(h.settle(Duration::from_secs(60)), "Settings settles after the overlay");+    h.settled(Duration::from_secs(60), "Settings settles after the overlay");     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);     assert_eq!(r["ok"], true, "{r}");     h.step();@@ -1236,7 +1236,7 @@ fn a_library_problem_overlay_blocks_a_control_click_in_settings() {     let id = "check:settings.confirmDelete";     let before = h.app.ui.settings.confirm_delete;     h.app.library_problem = Some(crate::panels::library_problem::LibraryProblem::new("/no/such/library", "test error"));-    assert!(h.settle(Duration::from_secs(60)), "the library problem window settles");+    h.settled(Duration::from_secs(60), "the library problem window settles");     assert!(h.app.widgets.iter().any(|(widget, _)| widget == "button:libraryRetry"), "the library window is laid out");     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);     assert_eq!(r["ok"], false, "{r}");@@ -1247,7 +1247,7 @@ fn a_library_problem_overlay_blocks_a_control_click_in_settings() {     assert!(h.app.library_problem.is_some(), "the overlay was not activated");     // Positive control: the same control works once the covering window is gone.     h.app.library_problem = None;-    assert!(h.settle(Duration::from_secs(60)), "Settings settles after the overlay");+    h.settled(Duration::from_secs(60), "Settings settles after the overlay");     let r = h.request("ui.clickWidget", serde_json::json!({"id": id}), t);     assert_eq!(r["ok"], true, "{r}");     h.step();@@ -2141,13 +2141,13 @@ fn hover_widget_refuses_clipped_and_covered_targets() {     // The library window is a non-modal overlay (its dim is the modal layer). A target below it     // must be refused before a pointer move is queued, then work again after the window closes.     h.app.library_problem = Some(crate::panels::library_problem::LibraryProblem::new("/no/such/library", "test error"));-    assert!(h.settle(Duration::from_secs(60)), "the library window settles");+    h.settled(Duration::from_secs(60), "the library window settles");     assert!(h.app.widgets.iter().any(|(widget, _)| widget == "button:libraryRetry"), "the library window is laid out");     let r = h.request("ui.hoverWidget", serde_json::json!({"id": id}), t);     assert_eq!(r["ok"], false, "{r}");     assert!(r["error"].as_str().unwrap_or_default().contains(id), "{r}");     h.app.library_problem = None;-    assert!(h.settle(Duration::from_secs(60)), "Settings settles after the overlay");+    h.settled(Duration::from_secs(60), "Settings settles after the overlay");     let r = h.request("ui.hoverWidget", serde_json::json!({"id": id}), t);     assert_eq!(r["ok"], true, "the same hover works once the overlay is gone: {r}"); }@@ -2159,7 +2159,7 @@ fn hover_widget_refuses_clipped_and_covered_targets() { fn a_system_paste_under_a_new_barrier_is_refused() {     let mut h = Headless::new(app(), [1200.0, 760.0], 1.0);     let t = Duration::from_secs(20);-    h.settle(Duration::from_secs(60));+    h.settled(Duration::from_secs(60), "the new session");     let r = h.request("ui.clickWidget", serde_json::json!({"id": "field:search"}), t);     assert_eq!(r["ok"], true, "{r}");     h.view.clipboard = "blocked".into();diff --git a/crates/ui-egui/src/tests/lights_out.rs b/crates/ui-egui/src/tests/lights_out.rs--- a/crates/ui-egui/src/tests/lights_out.rs+++ b/crates/ui-egui/src/tests/lights_out.rs@@ -19,7 +19,7 @@ fn loupe() -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } diff --git a/crates/ui-egui/src/tests/masking.rs b/crates/ui-egui/src/tests/masking.rs--- a/crates/ui-egui/src/tests/masking.rs+++ b/crates/ui-egui/src/tests/masking.rs@@ -106,7 +106,7 @@ fn subtract_brush_removes_sky_coverage_and_add_restores_it() {     brush_at(&mut h, 0.25, 0.5);     assert!(mask_coverage(&h, 0, 25, 50) > 0.99, "Add must be composed after Subtract");     assert_eq!(develop(&h).masks[0].components.len(), 3);-    h.settle(SETTLE);+    h.settled(SETTLE, "after brush_at"); }  #[test]@@ -130,7 +130,7 @@ fn new_brush_starts_a_separate_mask_in_paint_mode() {     exec(&mut h, "edit.undo", json!({}));     assert!(mask_coverage(&h, 1, 50, 50) > 0.99);     assert_eq!(develop(&h).masks[0], original);-    h.settle(SETTLE);+    h.settled(SETTLE, "after edit.undo"); }  #[test]@@ -150,7 +150,7 @@ fn mask_adjustments_hide_overlay_without_disabling_the_mask() {     assert!(!panel.contains_rect(slider), "exposure initially needs scrolling: {slider:?} in {panel:?}");     h.request("ui.move", json!({"x": panel.center().x, "y": panel.center().y}), T);     h.request("ui.scroll", json!({"dy": -160.0}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.scroll");     let slider = h.app.widgets.iter().find(|(id, _)| id == "slider:exposure").unwrap().1;     assert!(panel.contains_rect(slider), "exposure is visible before clicking: {slider:?} in {panel:?}");     let r = h.request("ui.clickWidget", json!({"id": "slider:exposure", "fx": 0.7}), T);@@ -189,7 +189,7 @@ fn mask_adjustments_hide_overlay_without_disabling_the_mask() {         exec(&mut h, "mask.adjust", json!({"values": values}));         assert!(!h.app.ui.mask_overlay);     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after mask.adjust"); }  /// A slider hides the overlay (above); a new mask, an added part, a duplicate or picking a mask in@@ -220,7 +220,7 @@ fn a_new_or_picked_mask_shows_the_overlay_hidden_by_a_slider() {         exec(&mut h, cmd, params);         assert!(shown(&h), "{cmd} shows the overlay");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "after exec"); }  #[test]@@ -360,7 +360,7 @@ fn mask_overlay_keys_and_pins() {     exec(&mut h, "view.maskPins", json!({"show": false}));     pointer(&mut h, json!([{"kind": "down", "x": 0.5, "y": 0.6}, {"kind": "up", "x": 0.5, "y": 0.6}]));     assert_eq!(h.app.session.active_mask, Some(2));-    h.settle(SETTLE);+    h.settled(SETTLE, "after pointer"); }  /// #2210: Ctrl+Shift+O (the all-masks overlay) draws the white outline of every visible@@ -377,7 +377,7 @@ fn all_masks_overlay_outlines_every_visible_mask_but_only_the_selected_one_is_dr     assert_eq!(h.app.session.active_mask, Some(2));     // Pins add their own <maskPin:…> elements; keep them out of the handle count.     exec(&mut h, "view.maskPins", json!({"show": false}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.maskPins");      let handles_of = |h: &Headless, id: usize| h.app.widgets.iter().filter(|(w, _)| w.starts_with(&format!("maskHandle:{id}:"))).count();     // The selected mask keeps its 4 edge handles and its rotation grip.@@ -389,7 +389,7 @@ fn all_masks_overlay_outlines_every_visible_mask_but_only_the_selected_one_is_dr     exec(&mut h, "mask.addComponent", json!({"kind": "radial", "op": "add", "center": [0.5, 0.8], "rx": 0.08, "ry": 0.08}));     assert_eq!(h.app.session.active_mask, Some(3));     assert_eq!(handles_of(&h, 1), 0);-    h.settle(SETTLE);+    h.settled(SETTLE, "after mask.addComponent"); }  /// #2210: the outlines of *unselected* masks are painted, not registered as widgets, so the way to@@ -411,7 +411,7 @@ fn all_masks_overlay_paints_the_outline_of_an_unselected_mask() {             exec(h, "view.maskOverlayAll", json!({}));         }         assert_eq!(h.app.ui.mask_overlay_all, on);-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");         let px = h.view.paint(&Default::default());         px.pixels.iter().filter(|p| p.r() > 200 && p.g() > 200 && p.b() > 200).count()     };@@ -443,7 +443,7 @@ fn holding_the_brush_still_does_not_harden_the_spot() {         assert!((quick - held).abs() < 0.01, "{dx} px from the centre: quick {quick}, held {held}");     }     assert!(mask_coverage(&h, 0, 70 + 6, 50) < 0.9, "the edge stays feathered");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  /// A ⇧-click with the brush paints a straight line from where the last stroke ended (issue #106 of the Classic@@ -469,7 +469,7 @@ fn shift_click_paints_a_straight_line_from_the_last_stroke() {     let d = develop(&h);     let MaskShape::Brush { strokes } = &d.masks[0].components[0].shape else { panic!("brush") };     assert_eq!(strokes.len(), 2, "one undo step takes the line back");-    h.settle(SETTLE);+    h.settled(SETTLE, "after edit.undo"); }  #[test]@@ -499,7 +499,7 @@ fn brush_strokes_carry_auto_mask() {     assert_eq!(strokes.len(), 2, "{strokes:?}");     assert!(strokes[1].erase, "⌥ erases");     assert!(!h.app.ui.brush_erase, "the brush mode is unchanged");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -561,7 +561,7 @@ fn remove_spots_by_pointer_and_keyboard() {     assert_eq!(h.app.session.visible_cloned().len(), photos);     h.request("ui.key", json!({"key": "delete"}), T);     assert_eq!(spots(&h).len(), 1, "nothing selected: nothing deleted");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key delete"); }  /// Masks list: double-click renames in place, the hover eye hides a mask, the overlay colour@@ -571,22 +571,22 @@ fn mask_list_rename_hide_and_overlay_colour() {     let mut h = detail("panel.masking");     exec(&mut h, "mask.add", json!({"kind": "linear"}));     exec(&mut h, "mask.add", json!({"kind": "radial"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after mask.add");     let first = develop(&h).masks[0].id;     let r = h.request("ui.clickWidget", json!({"id": format!("mask:{first}"), "count": 2}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(Duration::from_secs(5));+    h.settled(Duration::from_secs(5), "after ui.clickWidget");     assert_eq!(h.app.ui.renaming_mask.as_ref().map(|r| r.0), Some(first), "double-click starts renaming");     h.request("ui.key", json!({"key": "A", "cmd": true}), T);     h.request("ui.text", json!({"text": "Sky"}), T);     h.request("ui.key", json!({"key": "Enter"}), T);-    h.settle(Duration::from_secs(5));+    h.settled(Duration::from_secs(5), "after key Enter");     assert!(h.app.ui.renaming_mask.is_none());     assert_eq!(develop(&h).masks[0].name, "Sky");     // hover the row, then click its eye     let r = h.request("ui.hoverWidget", json!({"id": format!("mask:{first}")}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(Duration::from_secs(5));+    h.settled(Duration::from_secs(5), "after ui.hoverWidget");     let r = h.request("ui.clickWidget", json!({"id": format!("maskVisible:{first}")}), T);     assert_eq!(r["ok"], true, "{r}");     assert!(!develop(&h).masks[0].visible);@@ -613,14 +613,14 @@ fn command_drag_straightens_in_crop() {     assert!(angle != before && (1.0..15.0).contains(&angle.abs()), "a slightly tilted line straightens: {angle}");     assert_eq!(h.app.ui.tool, tool, "the crop tool stays active");     assert!(h.app.gesture.is_none());-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test] fn crop_drag_activates_the_remembered_ratio_and_undo_restores_the_photo() {     let mut h = detail("panel.crop");     h.app.ui.crop_default_aspect = Some(json!([1, 1]));-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.crop_default_aspect");     let before = develop(&h);     let undo = h.app.session.undo.len();     assert!(before.crop.aspect.is_none());@@ -635,7 +635,7 @@ fn crop_drag_activates_the_remembered_ratio_and_undo_restores_the_photo() {         T,     );     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert_eq!(develop(&h).crop.aspect, Some((100, 100)));     assert_eq!(h.app.session.undo.len(), undo + 1);     exec(&mut h, "edit.undo", json!({}));@@ -649,7 +649,7 @@ fn crop_drag_activates_the_remembered_ratio_and_undo_restores_the_photo() { fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {     let mut h = detail("panel.crop");     h.app.ui.crop_default_aspect = None;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.crop_default_aspect");     let before = develop(&h);     assert!(before.crop.aspect.is_none());     let p = h.app.session.catalog.photo(h.app.session.active().unwrap()).unwrap().clone();@@ -663,7 +663,7 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {     ]});     let r = h.request("ui.pointer", drag.clone(), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.pointer");     assert_eq!(develop(&h).crop.aspect, Some(own), "the drag kept the photo's proportions");     assert_eq!(h.app.ui.crop_default_aspect, None, "nothing was chosen explicitly");     exec(&mut h, "edit.undo", json!({}));@@ -671,9 +671,9 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {     // Lock off once: remembered as free, and from then on the same drag crops without a ratio     let mut h = detail("panel.crop");     h.app.ui.crop_default_aspect = None;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.crop_default_aspect");     h.app.run("crop.aspect", json!({"aspect": "toggle"})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "after crop.aspect");     assert_eq!(h.app.ui.crop_default_aspect, Some(json!("free")));     assert_eq!(h.app.crop_tool_aspect(), None);     let before = develop(&h);@@ -684,7 +684,7 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() {     ] {         let r = h.request("ui.pointer", json!({ "events": events }), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.pointer");     }     assert_ne!(develop(&h).crop, before.crop, "the drag cropped");     assert_eq!(develop(&h).crop.aspect, None, "and left the ratio free");@@ -697,7 +697,7 @@ fn a_fresh_crop_starts_locked_to_original_until_the_lock_is_switched_off() { fn a_bad_remembered_ratio_is_dropped_and_the_drag_still_crops() {     let mut h = detail("panel.crop");     h.app.ui.crop_default_aspect = Some(json!([0, -3]));-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.crop_default_aspect");     let before = develop(&h);     // a rotating drag outside the box (any crop drag starts cropping); the release on its own     for events in [@@ -706,7 +706,7 @@ fn a_bad_remembered_ratio_is_dropped_and_the_drag_still_crops() {     ] {         let r = h.request("ui.pointer", json!({ "events": events }), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.pointer");     }     assert_eq!(h.app.ui.crop_default_aspect, None, "the bad value is forgotten");     assert_ne!(develop(&h).crop, before.crop, "the drag cropped");@@ -716,12 +716,12 @@ fn a_bad_remembered_ratio_is_dropped_and_the_drag_still_crops() { #[test] fn double_click_in_crop_box_applies_the_crop() {     let mut h = detail("panel.crop");-    h.settle(SETTLE);+    h.settled(SETTLE, "the new detail session");     assert_eq!(h.app.ui.right, crate::state::RightPanel::Crop);     let c = h.app.image_rect.expect("image on screen").center();     let r = h.request("ui.click", json!({"x": c.x, "y": c.y, "count": 2}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert_eq!(h.app.ui.right, crate::state::RightPanel::Edit, "double-click leaves the crop tool"); } @@ -738,7 +738,7 @@ fn option_digit_toggles_keyword_from_set() {     let r = h.request("ui.clickWidget", json!({"id": "kwSet:1"}), T);     assert_eq!(r["ok"], true, "{r}");     assert!(!has(&h), "its button removes it again");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking kwSet:1"); }  fn develop_photo_keywords(h: &Headless) -> Vec<String> {@@ -769,7 +769,7 @@ fn smart_album_rule_editor_creates_and_edits() {     assert_eq!(r["ok"], true, "{r}");     let n3 = h.app.session.catalog.photos().filter(|p| !p.deleted && p.rating >= 3).count();     assert_eq!(h.app.session.catalog.album_count(a.id), n3);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.dialog.confirm"); }  /// Given the rule editor, the field menu shows the top-level fields and one submenu per group;@@ -781,7 +781,7 @@ fn smart_album_field_menu_groups_fields_in_submenus() {     let has = |h: &Headless, id: &str| h.app.widgets.iter().any(|(w, _)| w == id);     let r = h.request("ui.clickWidget", json!({"id": "ruleField:rules-0"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking ruleField:rules-0");     assert!(has(&h, "ruleFieldItem:rating:rules-0"), "top-level fields are in the menu itself");     for (group, _) in lightcraft_catalog::rules::FIELD_GROUPS {         assert!(has(&h, &format!("ruleFieldGroup:{group}:rules-0")), "no {group} submenu");@@ -790,10 +790,10 @@ fn smart_album_field_menu_groups_fields_in_submenus() {     // clicking a group row (a touch, or a click faster than hover) opens it, not closes the menu     let r = h.request("ui.clickWidget", json!({"id": "ruleFieldGroup:File:rules-0"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking ruleFieldGroup:File:rules-0");     let r = h.request("ui.clickWidget", json!({"id": "ruleFieldItem:filePath:rules-0"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking ruleFieldItem:filePath:rules-0");     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };     let Rule::Field { field, op, .. } = &rules.rules[0] else { panic!("not a field rule") };     assert_eq!((field.as_str(), op.as_str()), ("filePath", "contains"), "Rating's ≥ isn't a text operator");@@ -808,7 +808,7 @@ fn smart_album_editor_keeps_an_unreadable_yes_no_value() {     exec(&mut h, "dialog.smartAlbum", json!({"name": "Odd"}));     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     rules.rules[0] = serde_json::from_value(json!({"field": "edited", "op": "is", "value": "maybe"})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };     let Rule::Field { value, .. } = &rules.rules[0] else { panic!("not a field rule") };     assert_eq!(value, &json!("maybe"), "drawing the editor doesn't rewrite the value");@@ -825,7 +825,7 @@ fn smart_album_editor_marks_bad_rules_and_blocks_ok() {     let set = |h: &mut Headless, rules: serde_json::Value| {         let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };         *r = serde_json::from_value(json!({"rules": rules})).unwrap();-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");     };     let has = |h: &Headless, id: &str| h.app.widgets.iter().any(|(w, _)| w == id);     set(@@ -843,7 +843,7 @@ fn smart_album_editor_marks_bad_rules_and_blocks_ok() {     assert!(serde_json::to_string(rules).unwrap().contains(r#""value":9"#), "drawing doesn't clamp the rating to 5");     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:dialogOk");     assert!(h.app.session.catalog.albums().all(|a| a.name != "Checked"), "OK is disabled");     let r = h.request("ui.dialog.confirm", json!({}), T);     assert!(h.app.session.catalog.albums().all(|a| a.name != "Checked"), "confirm refuses too: {r}");@@ -853,7 +853,7 @@ fn smart_album_editor_marks_bad_rules_and_blocks_ok() {     assert!(!h.app.widgets.iter().any(|(w, _)| w.starts_with("ruleProblem:")));     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:dialogOk");     assert!(h.app.session.catalog.albums().any(|a| a.name == "Checked")); } @@ -866,17 +866,17 @@ fn smart_album_editor_picks_an_album_from_a_list() {     exec(&mut h, "dialog.smartAlbum", json!({"name": "In Trip"}));     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     rules.rules[0] = serde_json::from_value(json!({"field": "album", "op": "is", "value": 0})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(h.app.widgets.iter().any(|(w, _)| w == "ruleProblem:rules-0"), "no album chosen yet");     let r = h.request("ui.clickWidget", json!({"id": "albumPicker:rules-0"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking albumPicker:rules-0");     let has = |h: &Headless, id: String| h.app.widgets.iter().any(|(w, _)| *w == id);     assert!(has(&h, format!("albumPickerItem:{trip}:rules-0")), "plain albums are offered");     assert!(has(&h, format!("albumPickerItem:{smart}:rules-0")), "smart albums too");     let r = h.request("ui.clickWidget", json!({"id": format!("albumPickerItem:{trip}:rules-0")}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget");     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };     let Rule::Field { value, .. } = &rules.rules[0] else { panic!("not a field rule") };     assert_eq!(value, &json!(trip));@@ -899,7 +899,7 @@ fn smart_album_editor_never_rewrites_what_it_draws() {     ]});     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     *r = serde_json::from_value(rules.clone()).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };     assert_eq!(serde_json::to_value(r).unwrap()["rules"], rules["rules"], "unchanged by drawing");     let marked: Vec<&str> = h.app.widgets.iter().filter_map(|(w, _)| w.strip_prefix("ruleProblem:")).collect();@@ -915,21 +915,21 @@ fn smart_album_edit_with_bad_rules_keeps_the_name() {     let Some(crate::state::Dialog::SmartRules { name, rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     *name = "Renamed".into();     *rules = serde_json::from_value(json!({"rules": [{"field": "captureDate", "op": "is", "value": "banana"}]})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:dialogOk");     let _ = h.request("ui.dialog.confirm", json!({}), T);     let album = h.app.session.catalog.album(lightcraft_catalog::AlbumId(id)).expect("album");     assert_eq!(album.name, "Keep", "neither OK nor confirm renamed it");     // fixed: OK renames it and sets the rules in one undo step     let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("the editor stays open") };     *rules = serde_json::from_value(json!({"rules": [{"field": "captureDate", "op": "is", "value": "2026"}]})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let undo = h.app.session.undo.len();     let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking button:dialogOk");     let album = h.app.session.catalog.album(lightcraft_catalog::AlbumId(id)).expect("album");     assert_eq!((album.name.as_str(), h.app.session.undo.len()), ("Renamed", undo + 1));     exec(&mut h, "edit.undo", json!({}));@@ -947,12 +947,12 @@ fn sidebar_marks_smart_albums_with_problems() {     let id = exec(&mut h, "album.createSmart", json!({"name": "In Trip", "rules": rules}))["id"].as_u64().unwrap();     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert!(h.app.widgets.iter().any(|(w, _)| *w == format!("source:album:{id}")), "the album's row is on screen");     let marked = |h: &Headless| h.app.widgets.iter().any(|(w, _)| *w == format!("albumProblem:{id}"));     assert!(!marked(&h));     exec(&mut h, "album.delete", json!({"id": trip}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after album.delete");     assert!(marked(&h), "its album is gone"); } @@ -969,24 +969,24 @@ fn smart_album_editor_keeps_albums_from_including_themselves() {     let set = |h: &mut Headless, value: serde_json::Value| {         let Some(crate::state::Dialog::SmartRules { rules, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };         rules.rules = vec![serde_json::from_value(json!({"field": "album", "op": "is", "value": value})).unwrap()];-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");     };     set(&mut h, json!(null));     let r = h.request("ui.clickWidget", json!({"id": "albumPicker:rules-0"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking albumPicker:rules-0");     let has = |h: &Headless, id: String| h.app.widgets.iter().any(|(w, _)| *w == id);     assert!(has(&h, format!("albumPickerItem:{trip}:rules-0")));     // itself and an album that tests it are shown, greyed: clicking them picks nothing     for blocked in [excluded, travel] {         let r = h.request("ui.clickWidget", json!({"id": format!("albumPickerItem:{blocked}:rules-0")}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.clickWidget");         let Some(crate::state::Dialog::SmartRules { rules, .. }) = &h.app.ui.dialog else { panic!("no rule editor") };         assert!(matches!(&rules.rules[0], Rule::Field { value, .. } if value.is_null()), "{blocked} wasn't picked");     }     let _ = h.request("ui.key", json!({"key": "Escape"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     set(&mut h, json!(travel));     assert!(has(&h, "ruleProblem:rules-0".to_string()), "a loop is marked"); }@@ -1007,14 +1007,14 @@ fn g_toggles_grids_and_shift_g_starts_guided_upright() {     key(&mut h, true);     assert_eq!((h.app.ui.view, h.app.ui.right, h.app.ui.tool.as_str()), (crate::state::ViewMode::Detail, RightPanel::Crop, "guidedUpright"));     assert_eq!(develop(&h).geometry.upright, lightcraft_develop::Upright::Guided);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key"); }  #[test] fn luminance_range_drag_keeps_the_grabbed_handle() {     let mut h = detail("panel.masking");     exec(&mut h, "mask.add", json!({"kind": "luminanceRange", "lo": 0.2, "hi": 0.8}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after mask.add");     let original = develop(&h);     for (start, dx, steps) in [(0.2, 180.0, 10), (0.8, -180.0, 10), (0.2, 120.0, 1), (0.8, -120.0, 1)] {         let undo0 = h.app.session.undo.len();@@ -1039,7 +1039,7 @@ fn luminance_range_drag_keeps_the_grabbed_handle() { fn luminance_range_controls_and_map() {     let mut h = detail("panel.masking");     exec(&mut h, "mask.add", json!({"kind": "luminanceRange", "lo": 0.6, "hi": 1.0}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after mask.add");     let lum = |h: &Headless| match &develop(h).masks[0].components[0].shape {         MaskShape::LuminanceRange { lo, hi, lo_feather, .. } => (*lo, *hi, *lo_feather),         s => panic!("{s:?}"),@@ -1059,7 +1059,7 @@ fn luminance_range_controls_and_map() {     let r = h.request("ui.clickWidget", json!({"id": "check:lumMap0"}), T);     assert_eq!(r["ok"], true, "{r}");     assert_ne!(h.app.ui.mask_overlay_mode, "colorOnBw");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking check:lumMap0"); }  #[test]@@ -1076,7 +1076,7 @@ fn b_adds_to_quick_collection_in_the_grid_and_brushes_in_edit() {     assert_eq!(r["ok"], true, "{r}");     let q = h.app.session.catalog.quick_collection().expect("quick collection");     assert_eq!(h.app.session.catalog.album_count(q), 1);-    h.settle(SETTLE);+    h.settled(SETTLE, "after key b"); }  #[test]@@ -1090,7 +1090,7 @@ fn local_folder_tree_expands_and_browses() {     h.hide_home_above(&base);     exec(&mut h, "local.addRoot", json!({"path": base.to_string_lossy()}));     exec(&mut h, "library.browse", json!({"path": base.to_string_lossy()}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     let base_s = base.to_string_lossy().to_string();     let r = h.request("ui.clickWidget", json!({"id": format!("folderToggle:{base_s}")}), T);     assert_eq!(r["ok"], true, "{r}");@@ -1105,7 +1105,7 @@ fn local_folder_tree_expands_and_browses() {     let r = h.request("ui.clickWidget", json!({"id": format!("source:local:{hidden}")}), T);     assert_ne!(r["ok"], true, "hidden folders are not listed");     let _ = std::fs::remove_dir_all(&base);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.clickWidget"); }  /// A folder listed twice in Local (Pictures as a place of its own and again inside Home) opens@@ -1124,7 +1124,7 @@ fn local_folder_listed_twice_opens_and_closes_each_row() {     let sub = inner.join("Sub").to_string_lossy().to_string();     exec(&mut h, "local.addRoot", json!({"path": base_s}));     exec(&mut h, "local.addRoot", json!({"path": inner_s}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after local.addRoot");     let r = h.request("ui.clickWidget", json!({"id": format!("folderToggle:{base_s}")}), T);     assert_eq!(r["ok"], true, "{r}");     let toggle = format!("folderToggle:{inner_s}");@@ -1143,11 +1143,11 @@ fn local_folder_listed_twice_opens_and_closes_each_row() {         let at = h.app.widgets.iter().filter(|(w, _)| *w == toggle).nth(copy).map(|(_, r)| r.center()).unwrap();         let r = h.request("ui.click", json!({"x": at.x, "y": at.y}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.click");         assert_eq!(rows(&h, &sub_row), 0, "copy {copy} closes again");     }     let _ = std::fs::remove_dir_all(&base);-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -1161,7 +1161,7 @@ fn geometry_slider_drag_marks_the_grid() {     let stop = crate::widgets::SliderOut { value: None, drag_started: false, drag_stopped: true, reset: false };     crate::panels::edit::apply_slider_out(&mut h.app, spec, stop, |_, _| Ok(serde_json::Value::Null));     assert!(h.app.ui.dragging_control.is_none());-    h.settle(SETTLE);+    h.settled(SETTLE, "after apply_slider_out"); }  #[test]@@ -1189,7 +1189,7 @@ fn edit_in_external_editor_opens_the_copy() {     assert!(calls[0].0.ends_with("-Edit.tif") && std::path::Path::new(&calls[0].0).exists(), "{calls:?}");     assert_eq!(calls[0].1, "PhotoCraft");     let _ = std::fs::remove_dir_all(&dir);-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.editInExternal"); }  /// An editor that won't open the copy: the copy is in the library already, so the command succeeds@@ -1212,7 +1212,7 @@ fn edit_in_reports_an_editor_that_wont_open_without_failing() {     let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();     assert!(toast.contains("the editor quit"), "{toast}");     let _ = std::fs::remove_dir_all(&dir);-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.editInExternal"); }  /// Edit in PhotoCraft: a PSD copy (what PhotoCraft saves back to), handed to "PhotoCraft" for@@ -1272,7 +1272,7 @@ fn edit_in_photocraft_opens_a_psd_copy() {     assert!(last.0.ends_with(".tif") && last.1 == "/opt/pc/photocraft", "{last:?}");     assert!(crate::menus::ui_enabled(&h.app, "photo.editInPhotoCraft"));     let _ = std::fs::remove_dir_all(&dir);-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.editInPhotoCraft"); }  /// Open Original in Other Editor: each selected photo's own file (a raw stays a raw) goes to the@@ -1355,7 +1355,7 @@ fn open_original_in_external_editor_hands_over_the_raw_itself() {     }     assert_eq!(opened.lock().unwrap().len(), calls);     let _ = std::fs::remove_dir_all(&dir);-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.openOriginalInExternal"); }  /// Issue #2135: a library record whose original is a program (an imported `.lrcat` names the@@ -1419,7 +1419,7 @@ fn open_original_refuses_anything_that_is_not_a_photo_file() {     assert_eq!(handed[0].len(), 1, "{handed:?}");     assert!(std::path::Path::new(&handed[0][0]).is_absolute() && handed[0][0].ends_with("DSC_0001.NEF"), "{handed:?}");     let _ = std::fs::remove_dir_all(&dir);-    h.settle(SETTLE);+    h.settled(SETTLE, "after photo.openOriginalInExternal"); }  #[test]@@ -1427,7 +1427,7 @@ fn second_window_shows_the_active_photo() {     let mut h = detail("panel.edit");     exec(&mut h, "view.photoGrid", json!({}));     exec(&mut h, "view.secondWindow", json!({"show": true}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.secondWindow");     // headless has no native windows: it's embedded, and renders the photo for its own slot     let tex = h.app.renderer.textures.get(&crate::render::Slot::Second).map(|t| t.photo);     assert_eq!(tex, h.app.session.active(), "the second window has its own render of the active photo");@@ -1435,7 +1435,7 @@ fn second_window_shows_the_active_photo() {     assert_eq!(r["ok"], true, "on screen: {r}");     exec(&mut h, "view.secondWindow", json!({}));     assert!(!h.app.ui.second_window);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.secondWindow"); }  /// Frame time of the photo grid on a 100k-photo library (ignored:@@ -1483,7 +1483,7 @@ fn keyword_painter_toggles_on_click() {     let mut h = detail("panel.edit");     exec(&mut h, "tool.keywordPainter", json!({"keyword": "harbour"}));     assert_eq!(h.app.ui.view, crate::state::ViewMode::PhotoGrid);-    h.settle(SETTLE);+    h.settled(SETTLE, "after tool.keywordPainter");     let target = h.app.session.visible_cloned()[0];     let has = |h: &Headless| h.app.session.catalog.photo(target).unwrap().meta.keywords.iter().any(|k| k == "harbour");     let sel = h.app.session.selection.clone();@@ -1506,7 +1506,7 @@ fn keyword_painter_toggles_on_click() {     assert!(!has(&h), "a second click takes it away");     h.request("ui.key", json!({"key": "escape"}), T);     assert!(h.app.ui.keyword_painter.is_none());-    h.settle(SETTLE);+    h.settled(SETTLE, "after key escape"); }  #[test]@@ -1519,7 +1519,7 @@ fn grid_info_cycles_caption() {     assert_ne!(r["ok"], true);     exec(&mut h, "view.squareGrid", json!({}));     exec(&mut h, "view.gridInfo", json!({"info": "exposure"}));-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.gridInfo");     // Photo Grid has no captions: picking one shows it in Square Grid     exec(&mut h, "view.photoGrid", json!({}));     exec(&mut h, "view.gridInfo", json!({"info": "date"}));@@ -1541,7 +1541,7 @@ fn reference_view_pins_a_photo_beside_the_active_one() {     exec(&mut h, "develop.set", json!({"control": "light.exposure", "value": 0.7}));     assert_eq!(h.app.session.develop_of(active).unwrap().light.exposure, 0.7);     assert_ne!(h.app.session.develop_of(first).unwrap().light.exposure, 0.7);-    h.settle(SETTLE);+    h.settled(SETTLE, "after develop.set");     assert!(h.app.renderer.textures.get(&crate::render::Slot::Compare(0)).is_some_and(|t| t.photo == first), "the reference is drawn"); } @@ -1559,7 +1559,7 @@ fn soft_proofing_flags_out_of_gamut_colours_and_makes_proof_copies() {         let t = h.app.renderer.textures.get(&crate::render::Slot::Main).expect("loupe render");         t.pixels.as_ref().expect("pixels kept").pixels.iter().filter(|c| c.r() == 255 && c.g() == 0 && c.b() == 0).count()     };-    h.settle(SETTLE);+    h.settled(SETTLE, "after develop.set");     let before = red(&h);     // S in the loupe: soft proofing on; the warning is the proof's own setting     let r = h.request("ui.key", json!({"key": "s"}), T);@@ -1570,7 +1570,7 @@ fn soft_proofing_flags_out_of_gamut_colours_and_makes_proof_copies() {         st,         json!({"on": true, "space": "srgb", "profile": null, "intent": "perceptual", "paperInk": false, "destWarning": true, "displayWarning": false})     );-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.softProof");     assert!(red(&h) > before, "out-of-gamut colours are painted red");     let r = h.request("engine.execute", json!({"command": "view.softProof", "params": {"space": "cmyk"}}), T);     assert_ne!(r["ok"], true);@@ -1583,7 +1583,7 @@ fn soft_proofing_flags_out_of_gamut_colours_and_makes_proof_copies() {     assert_eq!((st["intent"].as_str(), st["paperInk"].as_bool()), (Some("relative"), Some(true)), "{st}");     assert!(st["profile"]["name"].as_str().is_some_and(|n| !n.is_empty()), "{st}");     assert_ne!(h.app.ui.proof.profile, 0);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.softProof");     assert!(red(&h) > before, "the device gamut is flagged too");     let r = h.request("engine.execute", json!({"command": "view.softProof", "params": {"profile": "/nonexistent.icc"}}), T);     assert_ne!(r["ok"], true);@@ -1705,7 +1705,7 @@ fn color_range_pick_button_samples_the_photo() {     let mut h = detail("panel.masking");     exec(&mut h, "mask.add", json!({"kind": "colorRange"}));     h.app.ui.tool.clear();-    h.settle(SETTLE);+    h.settled(SETTLE, "after mask.add");     click(&mut h, "button:colorPick0");     assert_eq!(h.app.ui.tool, "colorRange", "Pick arms the eyedropper");     brush_at(&mut h, 0.25, 0.5);@@ -1723,7 +1723,7 @@ fn color_range_pick_button_samples_the_photo() {     exec(&mut h, "crop.set", json!({"rect": [0.2, 0.1, 0.9, 0.8]}));     h.app.ui.zoom = crate::state::Zoom::Percent(200.0);     h.app.ui.pan = (0.5, 0.5);-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.pan");     brush_at(&mut h, 0.5, 0.5);     let ui_sample = color_samples(&h);     assert_eq!(ui_sample.as_array().map(Vec::len), Some(1));@@ -1734,7 +1734,7 @@ fn color_range_pick_button_samples_the_photo() {     // Pick again ends picking     click(&mut h, "button:colorPick0");     assert!(h.app.ui.tool.is_empty());-    h.settle(SETTLE);+    h.settled(SETTLE, "after click button:colorPick0"); }  #[test]@@ -1743,7 +1743,7 @@ fn a_selected_develop_slider_accepts_wheel_nudges() {     // the demo may open on the Edit panel already, which `panel.edit` then toggles shut     let r = h.request("ui.set", json!({"right": "edit"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let name = "slider:light.exposure";     let panel = h.app.widgets.iter().find(|(id, _)| id == "panel:right_panel").unwrap().1;     let track = h.app.widgets.iter().find(|(id, _)| id == name).unwrap().1;@@ -1803,7 +1803,7 @@ fn sideways_scrolling_adjusts_the_slider_under_the_pointer() {     let mut h = detail("panel.edit");     let r = h.request("ui.set", json!({"right": "edit"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let exposure = |h: &Headless| lightcraft_develop::controls::get(&develop(h), "light.exposure").expect("exposure");     let scroll = |h: &mut Headless, params: serde_json::Value| {         // the pointer rests on the track, the slider is not clicked@@ -1831,13 +1831,13 @@ fn sideways_scrolling_adjusts_the_slider_under_the_pointer() { #[test] fn a_fast_brush_stroke_shows_without_gaps_before_the_button_is_released() {     let mut h = detail("panel.masking");-    h.settle(SETTLE);+    h.settled(SETTLE, "the new detail session");     exec(&mut h, "tool.brush", json!({"new": true}));     let r = h.request("ui.set", json!({"brushSize": 0.03, "brushFeather": 50.0, "brushFlow": 100.0}), T);     assert_eq!(r["ok"], true, "{r}");     // the photo before painting, and where a normalized image point is on screen     pointer(&mut h, json!([{"kind": "move", "x": 0.02, "y": 0.02}]));-    h.settle(SETTLE);+    h.settled(SETTLE, "after pointer");     let photo = h.paint();     let img = h.app.image_rect.expect("the photo is on screen");     let ppp = h.pixels_per_point;@@ -1875,7 +1875,7 @@ fn a_fast_brush_stroke_shows_without_gaps_before_the_button_is_released() {     let MaskShape::Brush { strokes } = &d.masks[0].components[0].shape else { panic!("brush") };     assert!(strokes[0].points.len() >= 3, "{strokes:?}");     // committed: the rendered overlay covers what the live stroke did, feather included-    h.settle(SETTLE);+    h.settled(SETTLE, "after pointer");     let done = h.paint();     for (x, y) in [(0.3, a.1), (0.6, a.1), (0.3, a.1 + 0.02), (0.6, a.1 - 0.025), (0.45, a.1 + 0.06)] {         let (l, k) = (px(&held, x, y), px(&done, x, y));diff --git a/crates/ui-egui/src/tests/menubar.rs b/crates/ui-egui/src/tests/menubar.rs--- a/crates/ui-egui/src/tests/menubar.rs+++ b/crates/ui-egui/src/tests/menubar.rs@@ -13,7 +13,7 @@ const SETTLE: Duration = Duration::from_secs(10); fn app(size: [f32; 2]) -> Headless {     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });     let mut h = Headless::new(app, size, 1.0);-    h.settle(SETTLE);+    h.settled(SETTLE, "the new session");     h } @@ -59,7 +59,7 @@ fn no_menu_or_submenu_covers_the_menu_bar() {             assert!(widget(&h, "menu-level:1").is_none(), "{what}: a menu is open before any click");             let r = h.request("ui.clickWidget", json!({"id": format!("menu:{top}")}), T);             assert_eq!(r["ok"], true, "{what}: {r}");-            h.settle(SETTLE);+            h.settled(SETTLE, "after ui.clickWidget");             check_bar_clear(&h, "menu-level:1", &what);             // Hover each submenu row in view, top to bottom: rows low in the menu open upward.             let level = widget(&h, "menu-level:1").unwrap_or_else(|| panic!("{what}: menu"));@@ -76,7 +76,7 @@ fn no_menu_or_submenu_covers_the_menu_bar() {                 let fy = (at.y - full.top()) / full.height();                 let r = h.request("ui.hoverWidget", json!({"id": row, "fx": fx, "fy": fy}), T);                 assert_eq!(r["ok"], true, "{what}: {r}");-                h.settle(SETTLE);+                h.settled(SETTLE, "after ui.hoverWidget");                 if widget(&h, "menu-level:2").is_none() {                     continue;                 }@@ -94,7 +94,7 @@ fn a_tall_menu_scrolls_instead_of_growing_past_the_window() {     let mut h = app([1160.0, 420.0]);     let r = h.request("ui.clickWidget", json!({"id": "menu:Photo"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking menu:Photo");     check_bar_clear(&h, "menu-level:1", "420 pt window");     let level = widget(&h, "menu-level:1").expect("Photo menu");     // Not every row fits: some are scrolled out of view, none drawn outside the menu.@@ -114,7 +114,7 @@ fn open_menu(h: &Headless) -> Option<String> { fn go(h: &mut Headless, method: &str, id: &str) {     let r = h.request(method, json!({"id": id}), T);     assert_eq!(r["ok"], true, "{method} {id}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request"); }  #[test]@@ -140,7 +140,7 @@ fn clicking_another_title_opens_it_in_one_click() {     // move away from the titles first so only the click can be what switches     let r = h.request("ui.move", json!({"x": 640.0, "y": 400.0}), T);     assert_eq!(r["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.move");     go(&mut h, "ui.clickWidget", "menu:View");     assert_eq!(open_menu(&h).as_deref(), Some("menu:View"));     // clicking the open title closes it@@ -155,13 +155,13 @@ fn click_outside_and_escape_close_the_open_menu() {     assert!(open_menu(&h).is_some());     let r = h.request("ui.click", json!({"x": 640.0, "y": 600.0}), T);     assert_eq!(r["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.click");     assert_eq!(open_menu(&h), None, "click outside");     go(&mut h, "ui.clickWidget", "menu:Photo");     assert!(open_menu(&h).is_some());     let r = h.request("ui.key", json!({"key": "Escape"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after key Escape");     assert_eq!(open_menu(&h), None, "Escape"); } diff --git a/crates/ui-egui/src/tests/merge_preview.rs b/crates/ui-egui/src/tests/merge_preview.rs--- a/crates/ui-egui/src/tests/merge_preview.rs+++ b/crates/ui-egui/src/tests/merge_preview.rs@@ -21,7 +21,7 @@ fn click_at(h: &mut Headless, pos: egui::Pos2) {     h.app.synthetic.push(egui::Event::PointerMoved(pos));     press(h, pos, true);     press(h, pos, false);-    h.settle(T);+    h.settled(T, "after the injected input"); }  #[test]@@ -40,10 +40,10 @@ fn a_click_on_the_merge_preview_zooms_in_and_a_drag_pans() {     let ids: Vec<u64> = r["imported"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();     session.execute("library.select", &json!({"ids": ids})).unwrap();     let mut h = Headless::new(LightcraftApp::new(session, Services { png: None, ..Default::default() }), [1200.0, 800.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     crate::merge::open(&mut h.app, "merge.hdr").unwrap();     assert!(h.step_until(T, |h| h.app.merge.preview.is_some() && h.app.merge.preview_task.is_none()), "the preview rendered");-    h.settle(T);+    h.settled(T, "the UI");     assert_eq!(h.app.merge.zoom, None, "the preview opens fitted");      let area = rect(&h, "merge.preview");@@ -64,7 +64,7 @@ fn a_click_on_the_merge_preview_zooms_in_and_a_drag_pans() {         h.step();     }     press(&mut h, to, false);-    h.settle(T);+    h.settled(T, "after press");     let (u2, v2) = h.app.merge.zoom.expect("still zoomed in after a drag");     assert!(u2 > u + 0.01 && (v2 - v).abs() < 1e-3, "the drag panned right ({u}, {v}) → ({u2}, {v2})"); diff --git a/crates/ui-egui/src/tests/panels.rs b/crates/ui-egui/src/tests/panels.rs--- a/crates/ui-egui/src/tests/panels.rs+++ b/crates/ui-egui/src/tests/panels.rs@@ -18,7 +18,7 @@ fn demo(size: [f32; 2], ui: serde_json::Value) -> Headless {     let mut h = Headless::new(app, size, 1.0);     let r = h.request("ui.set", ui, T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } @@ -159,13 +159,13 @@ fn a_narrow_window_shrinks_the_panels_without_forgetting_their_width() {     let mut h = demo([1400.0, 900.0], json!({"view": "detail", "right": "edit", "leftPanel": true, "rightWidth": 480.0, "leftWidth": 400.0}));     assert_eq!(widget(&h, "panel:right_panel").width(), 480.0);     h.request("ui.resize", json!({"width": 1000.0, "height": 800.0}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.resize");     let (left, right) = (widget(&h, "panel:left_panel"), widget(&h, "panel:right_panel"));     assert!(right.left() - left.right() >= MIN_PHOTO_WIDTH - 1.0, "{left:?} {right:?}");     // the chosen widths come back when the window does     assert_eq!((h.app.ui.left_width, h.app.ui.right_width), (400.0, 480.0));     h.request("ui.resize", json!({"width": 1400.0, "height": 900.0}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.resize");     assert_eq!(widget(&h, "panel:right_panel").width(), 480.0); } @@ -267,7 +267,7 @@ fn the_sidebar_draws_its_sections_as_arranged() {     assert!(has(&h, "sidebarEmpty:folders") && !has(&h, "sidebarEmpty:keywords"));     let r = h.request("ui.set", json!({"sidebar": [{"id": "keywords"}, {"id": "byDate", "hidden": true}, {"id": "albums"}]}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     let drawn = sections_drawn(&h);     assert_eq!(listed(&drawn), ["keywords", "albums", "folders"], "{drawn:?}");     assert!(!h.app.widgets.iter().any(|(w, _)| w.starts_with("source:date:")), "a hidden section draws no rows");@@ -721,7 +721,7 @@ fn the_header_of_a_long_section_stays_in_sight() {     assert!(h.app.widgets.iter().any(|(w, r)| w == "icon:albumNew" && pinned.contains_rect(*r)), "with its buttons");     // folded from the pinned header: Albums' own header is what the sidebar shows now     click(&mut h, "sidebarSectionPinned:albums");-    h.settle(SETTLE);+    h.settled(SETTLE, "after click sidebarSectionPinned:albums");     assert!(h.app.ui.sidebar_section_collapsed("albums"));     assert!(!has(&h, "sidebarSectionPinned:albums"), "a folded section has no rows to pin its header over");     let own = header(&h, "albums");@@ -890,7 +890,7 @@ fn the_keywords_header_creates_a_keyword() {     h.step();     assert!(has(&h, "source:keyword:zebra crossing"), "the new keyword is listed");     // the sidebar scrolls to it: it is listed last, below what the panel shows-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let (row, panel) = (widget(&h, "source:keyword:zebra crossing"), widget(&h, "panel:left_panel"));     assert!(panel.contains_rect(row), "the new keyword is in sight: {row:?} in {panel:?}"); }@@ -1506,7 +1506,7 @@ fn folders_app_with_services(paths: &[&str], size: [f32; 2], services: Services)     let mut h = Headless::new(app, size, 1.0);     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } @@ -1699,7 +1699,7 @@ fn show_in_folders_scrolls_the_folder_into_view() {     let mut h = folders_app_sized(&refs, [1400.0, 500.0]);     activate(&mut h, "/pics/f59/a.jpg");     h.app.run("app.showInFolders", json!({})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "after app.showInFolders");     let row = widget(&h, "source:libfolder:/pics/f59");     assert!(row.top() >= widget(&h, "sidebarTitle").bottom() && row.bottom() <= 500.0, "in view: {row:?}"); }@@ -1915,7 +1915,7 @@ fn deep_folders_app(deep: &str, size: [f32; 2]) -> Headless {     }     let last = format!("libraryFolderToggle:{}", levels[levels.len() - 1]);     assert!(h.step_until(T, |h| has(h, &last)), "{last} is drawn");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     h } @@ -1978,7 +1978,7 @@ fn a_deep_library_path_narrows_the_indent_instead_of_cutting_the_names() {     // a wide panel has room for full steps again     let r = h.request("ui.set", json!({"leftWidth": 480.0}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     assert_eq!(crate::panels::left::indent_step(&h.view.ctx), full, "a wide panel indents by the full step");     let xs = chevron_xs(&h, deep);     assert!(xs.windows(2).all(|w| (w[1] - w[0] - full).abs() < 0.5), "{xs:?}");@@ -2055,7 +2055,7 @@ fn widening_the_sidebar_shows_more_of_a_long_name() {     let narrow = widget(&h, &id).width();     let r = h.request("ui.set", json!({"leftWidth": 480.0}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h.step();     assert!(widget(&h, &id).width() > narrow + 100.0, "a wider panel shows more of it"); }@@ -2120,7 +2120,7 @@ fn date_and_keyword_rows_show_their_photos_from_any_source() {     let (row, key) = (format!("source:date:{year}"), "date");     let r = h.request("engine.execute", json!({"command": "library.source", "params": {"kind": "recentlyDeleted"}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.source");     assert!(h.app.session.visible().is_empty(), "nothing deleted in the demo");     let r = h.request("ui.clickWidget", json!({"id": row}), T);     assert_eq!(r["ok"], true, "{r}");@@ -2215,7 +2215,7 @@ fn recognise_faces_stays_off_until_a_model_is_chosen() {     assert_eq!(h.app.session.execute("faces.gpu", &json!({})).unwrap()["cpuOnly"], true);     let r = h.request("engine.execute", json!({"command": "app.settings", "params": {"tab": "faces"}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after app.settings");     // (the first request switches the widget list on, the next frame fills it)     h.request("ui.widgets", json!({}), T);     h.step();@@ -2266,7 +2266,7 @@ fn local_photos_cannot_be_added_to_albums_until_they_join_my_photos() {     std::fs::write(dir.join("a.png"), lightcraft_engine::export::encode_image(&img, &o).unwrap()).unwrap();     let r = h.request("engine.execute", json!({"command": "library.browse", "params": {"path": dir.to_string_lossy()}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.browse");     let vis = h.app.session.visible_cloned();     assert_eq!(vis.len(), 1, "{vis:?}");     let local = vis[0];@@ -2315,7 +2315,7 @@ fn local_photos_cannot_be_added_to_albums_until_they_join_my_photos() {     let row = widget(&h, &format!("source:album:{album}"));     let r = h.request("ui.drag", json!({"x": thumb.center().x, "y": thumb.center().y, "toX": row.center().x, "toY": row.center().y, "steps": 12}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.drag");     assert!(h.app.session.catalog.album(album_id).unwrap().photos.is_empty(), "nothing invisible was added");     assert!(h.app.ui.dragging_photos.is_none(), "the drag ended");     let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();@@ -2406,7 +2406,7 @@ fn an_album_menu_imports_into_that_album() {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     // the demo's top-level album and album folder, both shown in the sidebar     let id_of = |name: &str| h.app.session.catalog.albums().find(|a| a.name == name).map(|a| a.id.0).expect(name);     let (album, folder) = (id_of("Garden"), id_of("Travel 2026"));diff --git a/crates/ui-egui/src/tests/preset_import.rs b/crates/ui-egui/src/tests/preset_import.rs--- a/crates/ui-egui/src/tests/preset_import.rs+++ b/crates/ui-egui/src/tests/preset_import.rs@@ -15,7 +15,7 @@ fn app() -> Headless {     // Profile import needs no photos. Avoid unrelated demo renders while testing its notice.     let app = LightcraftApp::new(lightcraft_engine::Session::new(), Services { png: None, ..Default::default() });     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);-    assert!(h.settle(SETTLE), "the empty library settles before importing profiles");+    h.settled(SETTLE, "the empty library settles before importing profiles");     h } diff --git a/crates/ui-egui/src/tests/preview_limit.rs b/crates/ui-egui/src/tests/preview_limit.rs--- a/crates/ui-egui/src/tests/preview_limit.rs+++ b/crates/ui-egui/src/tests/preview_limit.rs@@ -162,7 +162,7 @@ mod in_the_loupe {         let mut h = Headless::new(app, size, 1.0);         let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.set");         let native = {             let id = h.app.session.active().expect("a photo is open");             let p = h.app.session.catalog.photo(id).expect("it is in the catalog");@@ -188,7 +188,7 @@ mod in_the_loupe {         let mut h = Headless::new(app, size, 1.0);         let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.set");         let (native, dims) = {             let id = h.app.session.active().expect("a photo is open");             let p = h.app.session.catalog.photo(id).expect("it is in the catalog");@@ -211,7 +211,7 @@ mod in_the_loupe {     fn one_to_one_is_a_canvas_sized_frame_plus_a_native_window() {         let (_fx, mut h, native) = detail();         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let main = rendered_long_edge(&h);         assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}");         let region = h.app.region_view.expect("a window render");@@ -234,13 +234,13 @@ mod in_the_loupe {         h.request("engine.execute", json!({"command": "view.zoom100"}), T);         h.app.ui.pan = (0.3, 0.4);         h.app.ui.mask_overlay = false;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.mask_overlay");         let (zoom, pan) = (h.app.ui.zoom, h.app.ui.pan);         assert_eq!(zoom, crate::state::Zoom::Percent(100.0));         let plain = h.app.region_view.expect("a window render without the overlay");          h.app.ui.mask_overlay = true;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.mask_overlay");         let id = h.app.session.active().expect("a photo");         let d = h.app.session.develop_of(id).expect("settings");         assert!(matches!(crate::panels::detail::view_overlay(&h.app, &d), lightcraft_pipeline::Overlay::Mask { .. }), "the mask overlay is shown");@@ -253,7 +253,7 @@ mod in_the_loupe {         assert_ne!(shown.settings, plain.settings, "the overlay is part of the window's look");          h.app.ui.mask_overlay = false;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.mask_overlay");         assert_eq!((h.app.ui.zoom, h.app.ui.pan), (zoom, pan), "hiding it keeps them too");         assert_eq!(h.app.region_view.map(|r| r.settings), Some(plain.settings), "back to the plain window");     }@@ -277,7 +277,7 @@ mod in_the_loupe {         assert_eq!(r["ok"], true, "{r}");         assert!(h.app.ui.mask_overlay_all, "the all-masks overlay is on");         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let id = h.app.session.active().expect("a photo");         let d = h.app.session.develop_of(id).expect("settings");         assert!(@@ -299,11 +299,11 @@ mod in_the_loupe {     fn soft_proofing_at_one_to_one_is_a_window() {         let (_fx, mut h, native) = detail();         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let plain = h.app.region_view.expect("a window render without the proof");         let r = h.request("engine.execute", json!({"command": "view.softProof", "params": {"on": true, "destWarning": true}}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.softProof");         assert!(h.app.ui.soft_proof);         let main = rendered_long_edge(&h);         assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}");@@ -311,7 +311,7 @@ mod in_the_loupe {         assert_eq!(proofed.full.0.max(proofed.full.1), native, "cut from the photo's own pixels");         assert_ne!(proofed.settings, plain.settings, "the proof is part of the window's look");         h.request("engine.execute", json!({"command": "view.softProof", "params": {"on": false}}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.softProof");         assert_eq!(h.app.region_view.map(|r| r.settings), Some(plain.settings), "back to the plain window");     } @@ -321,11 +321,11 @@ mod in_the_loupe {     fn visualize_spots_at_one_to_one_is_a_window() {         let (_fx, mut h, native) = detail();         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let plain = h.app.region_view.expect("a window render without the overlay");         let r = h.request("ui.menu.invoke", json!({"id": "view.visualizeSpots"}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.menu.invoke");         assert!(h.app.ui.visualize_spots);         let main = rendered_long_edge(&h);         assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}");@@ -341,7 +341,7 @@ mod in_the_loupe {         assert!(native > 1600, "the demo photo must be larger than the cap for this to mean anything (is {native})");         h.app.ui.settings.preview_limit = 1600;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(rendered_long_edge(&h) <= 1600, "rendered {}", rendered_long_edge(&h));     } @@ -364,7 +364,7 @@ mod in_the_loupe {         let (_fx, mut h, native) = detail();         h.app.ui.settings.preview_limit = 1600;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let (w, hh) = region_tile(&h).expect("a window render");         // it covers what is on screen (the 1400×900 window less its chrome), and is not the frame         assert!(w >= 1300 && hh >= 700, "{w}×{hh}");@@ -378,13 +378,13 @@ mod in_the_loupe {     fn eight_to_one_renders_only_what_is_visible() {         let (_fx, mut h, native) = detail();         h.app.ui.zoom = crate::state::Zoom::Percent(800.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         let (w, hh) = region_tile(&h).expect("a window render");         assert!(w.max(hh) <= crate::region::MAX_SPAN && w < native * 8 / 2, "{w}×{hh}");         // …and panning far away asks for another window         let before = h.app.region_view.unwrap().window;         h.app.ui.pan = (0.9, 0.9);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.pan");         assert_ne!(h.app.region_view.unwrap().window, before);     } @@ -396,7 +396,7 @@ mod in_the_loupe {         h.app.renderer.keep_pixels = true;         h.app.ui.settings.preview_limit = 1600;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let view = h.app.region_view.expect("a window render");         let tile = h.app.renderer.textures.get(&Slot::Region).and_then(|t| t.pixels.clone()).expect("tile pixels");         let main = h.app.renderer.textures.get(&Slot::Main).and_then(|t| t.pixels.clone()).expect("main pixels");@@ -439,10 +439,10 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.settings.preview_limit = 1600;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(region_tile(&h).is_some());         h.request("engine.execute", json!({"command": "view.zoomFit"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoomFit");         assert_eq!(region_tile(&h), None);         assert_eq!(h.app.region_view, None);     }@@ -454,7 +454,7 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some());         h.request("ui.set", json!({"view": "photoGrid"}), T);         h.step();@@ -469,10 +469,10 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.settings.preview_limit = 1600;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let first = h.app.region_view.expect("a window");         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 1.0}}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after develop.set");         let second = h.app.region_view.expect("a window");         assert_ne!(first.settings, second.settings);         assert_ne!(first.key, second.key);@@ -494,7 +494,7 @@ mod in_the_loupe {         h.app.session.set_develop(id, s, "Spot").unwrap();         h.app.ui.settings.preview_limit = 1600;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(region_tile(&h).is_some(), "a window render");     } @@ -505,10 +505,10 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.max_texture_side = 2048;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         for zoom in [crate::state::Zoom::Fit, crate::state::Zoom::Percent(100.0), crate::state::Zoom::Percent(800.0)] {             h.app.ui.zoom = zoom;-            h.settle(SETTLE);+            h.settled(SETTLE, "after setting ui.zoom");             for (slot, t) in &h.app.renderer.textures {                 assert!(t.size[0] <= 2048 && t.size[1] <= 2048, "{zoom:?} {slot:?}: {}×{}", t.size[0], t.size[1]);             }@@ -525,7 +525,7 @@ mod in_the_loupe {         let (_fx, mut h, native) = detail_in([2600.0, 1200.0]);         assert!(native > 2600, "the photo is wider than the canvas at 1:1: {native}");         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let canvas = h.app.canvas_rect.expect("the loupe was drawn");         let image = h.app.image_rect.expect("the photo was drawn");         let region = h.app.region_view.expect("a window");@@ -545,10 +545,10 @@ mod in_the_loupe {     fn a_slider_drag_when_zoomed_in_renders_canvas_sized_drafts() {         let (_fx, mut h, native) = detail();         h.app.ui.zoom = crate::state::Zoom::Percent(400.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         h.app.session.begin_interaction("Exposure").unwrap();         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after develop.set");         let main = rendered_long_edge(&h);         // canvas-sized: the whole canvas on the GPU, a 0.6 draft of it on the CPU (#330)         let canvas = h.app.canvas_rect.expect("the loupe was drawn");@@ -565,7 +565,7 @@ mod in_the_loupe {     fn drag_exposure(h: &mut Headless) -> (usize, usize) {         h.app.session.begin_interaction("Exposure").unwrap();         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after develop.set");         let image = h.app.image_rect.expect("the photo was drawn");         let small = (image.width().max(image.height()) * crate::region::CPU_DRAFT_SCALE) as usize + crate::state::LOUPE_EDGE_STEP;         (rendered_long_edge(h), small)@@ -596,7 +596,7 @@ mod in_the_loupe {         let lut = lightcraft_pipeline::ProofLut::new(2, vec![[0.5, 0.5, 0.5, 0.0]; 8]).expect("a valid table");         h.app.ui.proof.profile = lightcraft_pipeline::register_proof_profile(0x5eed_9f01, lut);         h.app.ui.soft_proof = true;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.soft_proof");         let (main, small) = drag_exposure(&mut h);         assert!(main <= small, "the draft is {main} px, a small one at most {small}");     }@@ -612,7 +612,7 @@ mod in_the_loupe {         let (_fx, mut h, native) = detail();         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         let (after, before) = (h.app.region_view.expect("After window"), h.app.region_before_view.expect("Before window"));         assert_eq!(after.full, before.full, "the same frame");         assert_eq!(after.full.0.max(after.full.1), native);@@ -641,7 +641,7 @@ mod in_the_loupe {         }         assert!(!h.app.session.media.has_source(id, lightcraft_engine::SourceLevel::Full), "the original is still decoding");         assert_eq!(pending(&h), 1, "one window job decodes it");-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");         assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some(), "both windows arrived");     } @@ -651,7 +651,7 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.before_after = crate::state::BeforeAfter::Original;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(h.app.region_before_view.is_some());         assert_eq!(h.app.region_view, None);         assert!(tile(&h, Slot::RegionBefore).is_some() && tile(&h, Slot::Region).is_none());@@ -663,7 +663,7 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.before_after = crate::state::BeforeAfter::Split;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert_eq!(h.app.region_view.map(|v| v.window), h.app.region_before_view.map(|v| v.window));         assert!(tile(&h, Slot::Region).is_some() && tile(&h, Slot::RegionBefore).is_some());     }@@ -674,10 +674,10 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(tile(&h, Slot::RegionBefore).is_some());         h.app.ui.before_after = crate::state::BeforeAfter::Off;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.before_after");         assert_eq!(tile(&h, Slot::RegionBefore), None);         assert_eq!(h.app.region_before_view, None);         assert!(tile(&h, Slot::Region).is_some());@@ -701,7 +701,7 @@ mod in_the_loupe {         for _ in 0..60 {             h.step();         }-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");         let region = h.app.region_view.expect("the window arrives when the pinch is over");         assert_eq!(region.full.0.max(region.full.1), native);     }@@ -712,7 +712,7 @@ mod in_the_loupe {     fn no_window_over_a_picture_that_is_not_the_frame() {         let (_fx, mut h, _) = detail();         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(h.app.region_view.is_some());         let t = h.app.renderer.textures.get_mut(&Slot::Main).unwrap();         t.size = [t.size[0], t.size[1] / 2];@@ -738,7 +738,7 @@ mod in_the_loupe {             let (mut h, native) = demo_detail_in(size);             h.app.ui.soft_proof = proof;             h.app.ui.zoom = if percent == 0.0 { crate::state::Zoom::Fit } else { crate::state::Zoom::Percent(percent) };-            h.settle(SETTLE);+            h.settled(SETTLE, "after setting ui.zoom");             h.app.session.begin_interaction("Exposure").unwrap();             let mut ms = Vec::new();             for tick in 0..20 {@@ -967,7 +967,7 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.session.media.set_budget(16 << 20);         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(h.app.session.memory_report().full_source.bytes > 16 << 20, "the original stayed over budget");         h.request("ui.set", json!({"view": "photoGrid"}), T);         h.step();@@ -996,7 +996,7 @@ mod in_the_loupe {             let (_fx, mut h, _) = detail();             h.app.ui.before_after = mode;             h.request("engine.execute", json!({"command": "view.zoom100"}), T);-            h.settle(SETTLE);+            h.settled(SETTLE, "after view.zoom100");             h.step();             let (stand_in, window) = (drawn(&h, Slot::Before), drawn(&h, Slot::RegionBefore));             assert!(!stand_in.is_empty() && !window.is_empty(), "{mode:?}: the stand-in {stand_in:?} and the window {window:?} are both painted");@@ -1023,7 +1023,7 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.before_after = crate::state::BeforeAfter::Split;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         h.events.push(egui::Event::Key {             key: egui::Key::Backslash,             physical_key: Some(egui::Key::Backslash),@@ -1031,11 +1031,11 @@ mod in_the_loupe {             repeat: false,             modifiers: egui::Modifiers::NONE,         });-        h.settle(SETTLE);+        h.settled(SETTLE, "after the injected input");         // (the key's own shortcut switches to the Before view; the wipe is chosen again with the         // key still down, which is what holding `\` in a wipe is)         h.app.ui.before_after = crate::state::BeforeAfter::Split;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.before_after");         h.step();         let canvas = h.app.canvas_rect.unwrap();         let window = drawn(&h, Slot::RegionBefore);@@ -1050,7 +1050,7 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail_in([1800.0, 900.0]);         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         h.step();         let canvas = h.app.canvas_rect.unwrap();         let (before, after) = (drawn(&h, Slot::RegionBefore), drawn(&h, Slot::Region));@@ -1066,7 +1066,7 @@ mod in_the_loupe {         let (_fx, mut h, _) = detail();         h.app.ui.before_after = crate::state::BeforeAfter::SideBySide;         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert_eq!(h.app.region_tiles.len(), 2, "{:?}", h.app.region_tiles.keys().collect::<Vec<_>>());     } @@ -1076,16 +1076,16 @@ mod in_the_loupe {     fn a_refused_window_falls_back_to_a_full_size_render() {         let (_fx, mut h, native) = detail();         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(h.app.region_view.is_some());         let id = h.app.session.active().unwrap();         let look = h.app.session.develop_of(id).unwrap().hash64();         h.app.window_refused = Some((id, look, native));-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting window_refused");         assert_eq!(h.app.region_view, None);         assert_eq!(rendered_long_edge(&h), native);         h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after develop.set");         assert!(h.app.region_view.is_some(), "a new look tries a window again");     } @@ -1095,7 +1095,7 @@ mod in_the_loupe {     fn pinching_out_asks_for_no_new_windows() {         let (_fx, mut h, _) = detail();         h.app.ui.zoom = crate::state::Zoom::Percent(400.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         let key = h.app.region_view.expect("a window").key;         let at = h.app.canvas_rect.unwrap().center();         h.request("ui.move", json!({"x": at.x, "y": at.y}), T);@@ -1111,7 +1111,7 @@ mod in_the_loupe {     fn a_drag_over_the_stage_budget_does_not_clear_the_open_views() {         let (_fx, mut h, _) = detail();         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         let held = |h: &Headless| {             let m = h.app.renderer.memory();             (m["stageCaches"]["gpuBytes"].as_u64().unwrap() + m["stageCaches"]["cpuBytes"].as_u64().unwrap()) as usize@@ -1123,7 +1123,7 @@ mod in_the_loupe {         h.app.session.begin_interaction("Exposure").unwrap();         for tick in 0..8 {             h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": tick as f64 * 0.1}}), T);-            h.settle(SETTLE);+            h.settled(SETTLE, "after develop.set");         }         assert_eq!(h.app.renderer.memory()["stageCaches"]["trimmed"], 0);         assert!(held(&h) > before / 3, "and they are still there");@@ -1149,7 +1149,7 @@ mod in_the_loupe {         let (_fx, mut h, native) = detail();         use_display_profile(&mut h);         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(rendered_long_edge(&h) < native, "the whole frame is smaller than the photo");         let view = h.app.region_view.expect("a window render");         assert_eq!(view.full.0.max(view.full.1), native);@@ -1167,7 +1167,7 @@ mod in_the_loupe {         let r = h.request("engine.execute", json!({"command": "view.compare"}), T);         assert_eq!(r["ok"], true, "{r}");         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         for i in 0..2u8 {             assert!(h.app.compare_windows[i as usize].is_some(), "pane {i} asked for a window");             let (whole, window) = (drawn(&h, Slot::Compare(i)), drawn(&h, Slot::CompareRegion(i)));@@ -1187,7 +1187,7 @@ mod in_the_loupe {         assert!(h.app.ui.settings.display_profile.is_empty(), "no profile chosen");         assert!(h.app.renderer.display_id().is_some(), "the screen's profile is in use");         h.request("engine.execute", json!({"command": "view.zoom100"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after view.zoom100");         assert!(rendered_long_edge(&h) < native, "the whole frame is smaller than the photo");         let (whole, window) = (drawn(&h, Slot::Main), drawn(&h, Slot::Region));         assert!(!whole.is_empty(), "the whole-frame render is painted");@@ -1211,7 +1211,7 @@ mod in_the_loupe {         assert_eq!(r["ok"], true, "{r}");         assert_eq!(h.app.ui.view, crate::state::ViewMode::Compare);         h.app.ui.zoom = zoom;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         (fixture, h)     } @@ -1254,14 +1254,14 @@ mod in_the_loupe {         let (_fx, mut h) = compare_at(crate::state::Zoom::Percent(100.0));         assert!(h.app.renderer.textures.contains_key(&Slot::CompareRegion(1)));         h.app.ui.zoom = crate::state::Zoom::Fit;-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         assert!(!h.app.renderer.textures.contains_key(&Slot::CompareRegion(1)), "fit: no window");         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         assert!(h.app.renderer.textures.contains_key(&Slot::CompareRegion(0)));         let r = h.request("ui.set", json!({"view": "detail"}), T);         assert_eq!(r["ok"], true, "{r}");-        h.settle(SETTLE);+        h.settled(SETTLE, "after ui.set");         assert_eq!(h.app.compare_windows, [None, None]);         assert!(!h.app.renderer.textures.contains_key(&Slot::CompareRegion(0)) && !h.app.renderer.textures.contains_key(&Slot::CompareRegion(1)));         assert!(h.app.session.media.usage().2.count <= 1, "one original in Detail");@@ -1275,7 +1275,7 @@ mod in_the_loupe {         h.app.renderer.keep_pixels = true;         h.request("engine.execute", json!({"command": "view.compare"}), T);         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         h.step();         for i in 0..2u8 {             let view = h.app.compare_windows[i as usize].expect("a window");@@ -1320,7 +1320,7 @@ mod in_the_loupe {         assert_eq!(r["ok"], true, "{r}");         assert_eq!(h.app.ui.view, crate::state::ViewMode::Reference);         h.app.ui.zoom = crate::state::Zoom::Percent(100.0);-        h.settle(SETTLE);+        h.settled(SETTLE, "after setting ui.zoom");         let before = h.app.compare_windows[1].expect("the active photo's window");         assert!(h.app.compare_windows[0].is_some(), "the reference's window");         h.app.session.begin_interaction("Exposure").unwrap();@@ -1331,7 +1331,7 @@ mod in_the_loupe {         assert_eq!(h.app.compare_windows[1], Some(before), "no window per drag tick");         assert!(!h.app.renderer.is_pending(Slot::CompareRegion(1)));         h.app.session.end_interaction().unwrap();-        h.settle(SETTLE);+        h.settled(SETTLE, "the UI");         let after = h.app.compare_windows[1].expect("a window for the new look");         assert_ne!(after.settings, before.settings);     }diff --git a/crates/ui-egui/src/tests/print.rs b/crates/ui-egui/src/tests/print.rs--- a/crates/ui-egui/src/tests/print.rs+++ b/crates/ui-egui/src/tests/print.rs@@ -16,7 +16,7 @@ const T: Duration = Duration::from_secs(30); fn demo(services: Services) -> Headless {     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     h } @@ -31,19 +31,19 @@ fn has(h: &Headless, id: &str) -> bool { fn click(h: &mut Headless, id: &str) {     let r = h.request("ui.clickWidget", json!({"id": id}), T);     assert_eq!(r["ok"], true, "{id}: {r}");-    h.settle(T);+    h.settled(T, "after ui.clickWidget"); }  fn drag(h: &mut Headless, id: &str, dx: f32, dy: f32) {     let r = h.request("ui.dragWidget", json!({"id": id, "dx": dx, "dy": dy, "steps": 8}), T);     assert_eq!(r["ok"], true, "{id}: {r}");-    h.settle(T);+    h.settled(T, "after ui.dragWidget"); }  fn cmd(h: &mut Headless, id: &str, params: Value) -> Value {     let r = h.request("engine.execute", json!({"command": id, "params": params}), T);     assert_eq!(r["ok"], true, "{id}: {r}");-    h.settle(T);+    h.settled(T, "after engine.execute");     r["result"].clone() } @@ -328,7 +328,7 @@ fn the_print_dialog_fits_the_window_with_its_buttons_on_screen() {         for style in ["singleImage", "customPackage"] {             let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default());             let mut h = Headless::new(app, size, 1.0);-            h.settle(T);+            h.settled(T, "the new session");             cmd(&mut h, "print.set", json!({"layout": {"style": style}}));             if style == "customPackage" {                 cmd(&mut h, "print.cell.add", json!({"width": 4, "height": 6}));@@ -360,7 +360,7 @@ fn print_job_colour_controls_reach_the_job_and_the_pdf() {     assert_eq!(r["ok"], true, "{r}");     for _ in 0..4 {         h.request("ui.scroll", json!({"dy": -600.0}), T);-        h.settle(T);+        h.settled(T, "after ui.scroll");     }     let job = |h: &Headless| h.app.session.print.job.color.clone();     assert_eq!(job(&h).intent, PrintIntent::Perceptual);@@ -407,7 +407,7 @@ fn the_print_preview_does_not_rebuild_thumbnails_every_frame() {     // Keep the demo's twelve-photo contact sheet with real files that decode cheaply.     let library = SyntheticLibrary::with_photos("print-thumbnail-reuse", 12);     let mut h = Headless::new(library.app(), [1400.0, 900.0], 1.0);-    assert!(h.settle(T), "the print thumbnail fixture settles");+    h.settled(T, "the print thumbnail fixture settles");     assert_eq!(h.app.session.catalog.len(), 12, "the contact sheet retains twelve photos");     let before = h.app.renderer.thumb_jobs_built;     assert!(before > 0, "the fixture exercises actual thumbnail jobs");@@ -419,7 +419,7 @@ fn the_print_preview_does_not_rebuild_thumbnails_every_frame() {             cmd(&mut h, "print.cell.add", json!({"width": 2.0, "height": 2.5}));             cmd(&mut h, "print.cell.add", json!({"width": 5.0, "height": 7.0}));         }-        assert!(h.settle(Duration::from_secs(5)), "{style}: the dialog settles");+        h.settled(Duration::from_secs(5), &format!("{style}: the dialog settles"));         let built = h.app.renderer.thumb_jobs_built;         for _ in 0..60 {             h.step();@@ -439,7 +439,7 @@ fn page_options_checkboxes_set_the_page_and_hide_for_packages() {         assert_eq!(r["ok"], true, "{r}");         for _ in 0..4 {             h.request("ui.scroll", json!({"dy": dy}), T);-            h.settle(T);+            h.settled(T, "after ui.scroll");         }     };     // the Page panel is below Page Setup: scroll down to itdiff --git a/crates/ui-egui/src/tests/profile_menu.rs b/crates/ui-egui/src/tests/profile_menu.rs--- a/crates/ui-egui/src/tests/profile_menu.rs+++ b/crates/ui-egui/src/tests/profile_menu.rs@@ -20,7 +20,7 @@ fn widget(h: &Headless, id: &str) -> Option<egui::Rect> { fn go(h: &mut Headless, method: &str, id: &str) {     let r = h.request(method, json!({"id": id}), T);     assert_eq!(r["ok"], true, "{method} {id}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request"); }  #[test]@@ -38,7 +38,7 @@ fn a_folder_of_many_imported_profiles_scrolls_inside_the_window() {     let r = h.request("ui.set", json!({"view": "detail"}), T);     assert_eq!(r["ok"], true, "{r}");     h.app.ui.right = crate::state::RightPanel::Edit;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.right");     go(&mut h, "ui.clickWidget", "dropdown:profile");     let menu = widget(&h, "menu-level:1").expect("the profile menu is open");     assert!(menu.bottom() <= h.size.y + 0.5, "the profile menu {menu:?} runs past the window's bottom {}", h.size.y);diff --git a/crates/ui-egui/src/tests/rename_person.rs b/crates/ui-egui/src/tests/rename_person.rs--- a/crates/ui-egui/src/tests/rename_person.rs+++ b/crates/ui-egui/src/tests/rename_person.rs@@ -15,7 +15,7 @@ fn demo() -> Headless {     let services = Services { png: None, write_shared: Some(std::sync::Arc::new(|_: &str, _: &[u8]| Ok(()))), ..Default::default() };     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);-    h.settle(T);+    h.settled(T, "the new session");     h } @@ -56,7 +56,7 @@ fn right_click(h: &mut Headless, id: &str) { fn open_people(h: &mut Headless) {     let r = h.request("engine.execute", json!({"command": "view.people"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(T);+    h.settled(T, "after view.people");     h.step(); } diff --git a/crates/ui-egui/src/tests/rules_editor.rs b/crates/ui-egui/src/tests/rules_editor.rs--- a/crates/ui-egui/src/tests/rules_editor.rs+++ b/crates/ui-egui/src/tests/rules_editor.rs@@ -20,14 +20,14 @@ fn editor(rules: serde_json::Value) -> Headless {     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);     let r = h.request("engine.execute", json!({"command": "dialog.newSmartAlbum", "params": {}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after dialog.newSmartAlbum");     h.app.ui.dialog = None;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.dialog");     let r = h.request("engine.execute", json!({"command": "dialog.smartAlbum", "params": {"name": "Dates"}}), T);     assert_eq!(r["ok"], true, "{r}");     let Some(crate::state::Dialog::SmartRules { rules: r, .. }) = &mut h.app.ui.dialog else { panic!("no rule editor") };     *r = serde_json::from_value(json!({"rules": rules})).unwrap();-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     h } @@ -42,7 +42,7 @@ fn has(h: &Headless, id: &str) -> bool { fn request(h: &mut Headless, method: &str, id: &str) {     let r = h.request(method, json!({"id": id}), T);     assert_eq!(r["ok"], true, "{method} {id}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request"); }  fn rule_count(h: &Headless) -> usize {diff --git a/crates/ui-egui/src/tests/scroll.rs b/crates/ui-egui/src/tests/scroll.rs--- a/crates/ui-egui/src/tests/scroll.rs+++ b/crates/ui-egui/src/tests/scroll.rs@@ -22,13 +22,13 @@ fn demo(view: &str) -> Headless {     let first = h.app.session.visible_cloned()[0].0;     let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [first]}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after library.select");     h }  /// Run `n` frames (plus a settle: thumbnails and renders finishing must not move anything). fn idle(h: &mut Headless, n: usize) {-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     for _ in 0..n {         h.step();     }diff --git a/crates/ui-egui/src/tests/slider_reset.rs b/crates/ui-egui/src/tests/slider_reset.rs--- a/crates/ui-egui/src/tests/slider_reset.rs+++ b/crates/ui-egui/src/tests/slider_reset.rs@@ -21,7 +21,7 @@ fn edit_panel() -> Headless {     assert_eq!(h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id]}}), T)["ok"], true);     assert_eq!(h.request("ui.set", json!({"view": "detail", "right": "edit"}), T)["ok"], true);     h.app.ui.open_sections = vec!["light".to_string()];-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.open_sections");     h } @@ -41,7 +41,7 @@ fn default(control: &str) -> f64 { fn ask(h: &mut Headless, method: &str, params: serde_json::Value) {     let r = h.request(method, params.clone(), T);     assert_eq!(r["ok"], true, "{method} {params}: {r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after the request"); }  fn set(h: &mut Headless, control: &str, value: f64) {diff --git a/crates/ui-egui/src/tests/slideshow.rs b/crates/ui-egui/src/tests/slideshow.rs--- a/crates/ui-egui/src/tests/slideshow.rs+++ b/crates/ui-egui/src/tests/slideshow.rs@@ -48,7 +48,7 @@ fn fixture_grid(count: usize, open: Rc<Cell<bool>>) -> (RenderFixture, Headless)     // consume the wall-clock budget before the slideshow scheduling assertions begin.     assert!(open.get(), "the native grid setup gate starts open");     h.app.renderer.set_offload(Box::new(RenderGate::new(open)));-    assert!(h.settle(SETTLE), "the grid renders settle before controlling slide renders");+    h.settled(SETTLE, "the grid renders settle before controlling slide renders");     assert_eq!(h.app.session.catalog.len(), count, "the slideshow fixture photos");     let id = h.app.session.active().expect("a photo is open");     let photo = h.app.session.catalog.photo(id).expect("it is in the catalog");@@ -94,7 +94,7 @@ fn slideshow_advances_pauses_and_ends() {     // is exercised separately by next_slides_are_prepared_before_they_are_due.     let library = SyntheticLibrary::with_photos("slideshow-automatic-controls", 3);     let mut h = Headless::new(library.app(), [1000.0, 700.0], 1.0);-    assert!(h.settle(SETTLE), "the automatic playback fixture settles");+    h.settled(SETTLE, "the automatic playback fixture settles");     let first = h.app.session.visible_cloned()[0];     // The fixture selects its first imported photo; newest-first view order differs.     // Establish the same first-in-view starting selection as the original demo test.@@ -123,7 +123,7 @@ fn slideshow_advances_pauses_and_ends() {     h.step();     assert!(!h.app.ui.fullscreen && h.app.ui.slideshow.is_none());     assert_eq!(slides_held(&h), 0, "slide textures freed");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -166,7 +166,7 @@ fn next_slides_are_prepared_before_they_are_due() {     assert!(h.time() - start < 60.0, "before the first is due");     assert_eq!(photo_up(&h), current, "still on the first slide");     assert!(play(&h).remaining(h.time()) > 59.0, "renders are accepted before the first slide is due");-    assert!(h.settle(SETTLE), "enabled-prefetch renders settle");+    h.settled(SETTLE, "enabled-prefetch renders settle");     drop(h);     drop(fx); @@ -186,7 +186,7 @@ fn next_slides_are_prepared_before_they_are_due() {     open.set(true);     assert!(h.step_until(SETTLE, |h| has_slide(h, current)), "the current slide renders once allowed");     assert!(!has_slide(&h, next) && !h.app.renderer.is_pending(Slot::Slide(next)), "still not prepared");-    assert!(h.settle(SETTLE), "disabled-prefetch renders settle");+    h.settled(SETTLE, "disabled-prefetch renders settle"); }  #[test]@@ -206,7 +206,7 @@ fn a_fade_only_runs_between_rendered_slides() {         }     }     assert!(fades >= 20, "fades ran ({fades})");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -228,7 +228,7 @@ fn arrows_cut_and_repeat_off_ends_after_the_last_slide() {     assert_eq!(play(&h).position(), (photos.len(), photos.len()));     run(&mut h, "slideshow.next", json!({}));     assert!(h.app.ui.slideshow.is_none() && !h.app.ui.fullscreen, "no repeat: past the last one it ends");-    h.settle(SETTLE);+    h.settled(SETTLE, "after slideshow.next"); }  #[test]@@ -298,7 +298,7 @@ fn a_manual_slideshow_waits_for_the_arrow_keys() {     diagnose("after-existing-step", &h);     eprintln!("manual-checkbox journal_after_existing_step={:?}", h.app.session.journal.get(journal_before..));     assert!(h.app.session.slideshow.manual, "the checkbox goes through slideshow.playback");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -313,7 +313,7 @@ fn random_order_starts_at_the_selection_and_is_fixed_by_the_seed() {             run(&mut h, "slideshow.next", json!({}));             seen.push(photo_up(&h));         }-        h.settle(SETTLE);+        h.settled(SETTLE, "after slideshow.next");         (photos, seen)     };     let (photos, a) = order(11);@@ -358,7 +358,7 @@ fn playback_controls_show_on_pointer_movement_and_hide() {     let r = h.request("ui.clickWidget", json!({"id": "icon:slideshowEnd"}), T);     assert_eq!(r["ok"], true, "{r}");     assert!(h.app.ui.slideshow.is_none() && !h.app.ui.fullscreen, "the end button ends it");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking icon:slideshowEnd"); }  #[test]@@ -397,7 +397,7 @@ fn playback_settings_drive_the_slideshow_and_bad_ones_are_refused() {     assert_eq!(r["ok"], true, "{r}");     assert_eq!(r["result"]["rect"], rect_json(validated), "the click used the rect reveal validated");     assert!(h.app.session.slideshow.random_order, "the checkbox goes through slideshow.playback");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking check:settings.slideshowRandom"); }  #[test]@@ -406,7 +406,7 @@ fn a_click_on_a_control_scrolled_out_of_the_settings_page_is_refused() {     // scrolled page room, but they are still laid out and registered.     let mut h = grid_sized([1000.0, 560.0]);     run(&mut h, "view.slideshowOptions", json!({}));-    assert!(h.settle(SETTLE), "the settings page settles");+    h.settled(SETTLE, "the settings page settles");     let target = latest_widget_rect(&h, "check:settings.slideshowRandom").expect("the checkbox is registered");     let hit = crate::control::widget_hit(&h.app, "check:settings.slideshowRandom").expect("the checkbox has an egui hit owner");     assert!(@@ -436,7 +436,7 @@ fn a_photo_deleted_while_it_plays_does_not_stall_the_slideshow() {     assert!(h.app.session.catalog.photo(next).is_none());     assert!(h.step_until(SETTLE, |h| h.app.ui.slideshow.as_ref().is_some_and(|r| r.play.position().0 >= 3)), "it plays on");     assert!(photos.len() > 3);-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  /// The latest registry entry for `id`, as [`crate::control::widget_rect`] looks it up (the last@@ -556,7 +556,7 @@ fn a_composed_slide_shows_during_playback() {     h.step();     assert_eq!(play(&h).current(), next, "navigation shows the prepared slide");     assert_eq!(painted(&h.paint(), 5, 5), [255, 0, 0]);-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -584,7 +584,7 @@ fn the_default_layout_plays_the_photo_as_before() {     if r.top() > 2.0 {         assert_eq!(painted(&img, 500, 1), [0, 0, 0], "black above the photo");     }-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -634,7 +634,7 @@ fn settings_shows_the_slide_layout_and_a_live_preview() {         h.step();     };     assert!(c[1] > 200 && c[0] < 60, "the green backdrop in the preview margin: {c:?}");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -660,7 +660,7 @@ fn overlays_show_during_playback() {         c[1] > 200 && c[0] < 80 && c[2] < 80     });     assert!(green.count() > 200, "the identity plate is on the slide");-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  #[test]@@ -705,7 +705,7 @@ fn settings_shows_the_overlays_and_the_preview_follows_them() {     let before = h.app.renderer.textures.get(&Slot::SlidePreview).map(|t| t.key);     run(&mut h, "slideshow.overlays", json!({"identityPlate": {"text": "Studio", "color": "#ff00ff"}}));     assert!(h.step_until(SETTLE, |h| h.app.renderer.textures.get(&Slot::SlidePreview).map(|t| t.key) != before));-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  /// Shows the playback controls (the pointer moves over the canvas).@@ -782,7 +782,7 @@ fn the_intro_plays_before_the_first_photo_and_the_ending_closes_the_show() {     run(&mut h, "slideshow.next", json!({}));     run(&mut h, "slideshow.next", json!({}));     assert!(h.app.ui.slideshow.is_none() && !h.app.ui.fullscreen, "ended after the ending");-    h.settle(SETTLE);+    h.settled(SETTLE, "after slideshow.next"); }  #[test]@@ -815,7 +815,7 @@ fn titles_survive_deleting_the_only_photo_and_need_photos_to_start() {     assert_ne!(r["ok"], true, "{r}");     assert!(r.to_string().contains("no photos to show"), "{r}");     assert!(h.app.ui.slideshow.is_none());-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.slideshow"); }  #[test]@@ -868,7 +868,7 @@ fn settings_shows_the_titles_and_the_preview_shows_a_title_screen() {     let r = h.request("ui.clickWidget", json!({"id": "button:settingsSlideshowPreviewShows-0"}), T);     assert_eq!(r["ok"], true, "{r}");     assert!(h.step_until(SETTLE, |h| h.app.renderer.textures.get(&Slot::SlidePreview).is_some_and(|t| t.photo == first)));-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI"); }  /// Settings ▸ Slideshow ▸ Templates: built-ins listed, hover previews on the selected photo@@ -988,7 +988,7 @@ fn template_browser_previews_on_hover_applies_and_manages_user_templates() {     assert_eq!(r["ok"], true, "{r}");     h.step();     assert_eq!(SlideLook::of(&h.app.session).overlays, before.overlays);-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     let _ = std::fs::remove_dir_all(dir); } diff --git a/crates/ui-egui/src/tests/sync.rs b/crates/ui-egui/src/tests/sync.rs--- a/crates/ui-egui/src/tests/sync.rs+++ b/crates/ui-egui/src/tests/sync.rs@@ -81,7 +81,7 @@ fn imported_folder(dir: &Scratch, then: impl FnOnce()) -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "photoGrid", "leftPanel": true}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } @@ -365,7 +365,7 @@ fn the_folders_scan_shows_a_row_and_its_cross_closes_the_dialog() {     assert!(h.app.ui.dialog.is_none(), "and closes its dialog: there is nothing to show");     gate.store(true, std::sync::atomic::Ordering::Relaxed);     assert!(h.app.session.activity.list().is_empty());-    h.settle(SETTLE);+    h.settled(SETTLE, "the UI");     assert!(h.app.ui.dialog.is_none(), "a stopped scan opens nothing later"); } diff --git a/crates/ui-egui/src/tests/temp_slider.rs b/crates/ui-egui/src/tests/temp_slider.rs--- a/crates/ui-egui/src/tests/temp_slider.rs+++ b/crates/ui-egui/src/tests/temp_slider.rs@@ -33,7 +33,7 @@ fn edit_panel(raw: bool) -> Headless {     assert_eq!(h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id]}}), T)["ok"], true);     assert_eq!(h.request("ui.set", json!({"view": "detail", "right": "edit"}), T)["ok"], true);     h.app.ui.open_sections = vec!["light".to_string(), "color".to_string()];-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.open_sections");     h } @@ -66,7 +66,7 @@ fn develop(h: &Headless) -> lightcraft_develop::DevelopSettings { fn set(h: &mut Headless, control: &str, value: f64) {     let r = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": control, "value": value}}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after develop.set"); }  /// The thumb sits where Lightroom puts it (within 1 % of the model fitted to Lightroom's positions), clicking or@@ -82,7 +82,7 @@ fn the_kelvin_temp_slider_has_lightrooms_scale() {     }     set(&mut h, "wb.temp", 4850.0);     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:wb.temp", "fx": 0.5}), T)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking slider:wb.temp");     assert_eq!(develop(&h).wb.temp, 7000.0, "a click in the middle of the track");     // drag from As Shot on a Z 6II NEF (about 4850 K, 28 % along the track) to the middle     set(&mut h, "wb.temp", 4850.0);@@ -90,11 +90,11 @@ fn the_kelvin_temp_slider_has_lightrooms_scale() {     let from = track.left() + thumb(&h, "wb.temp") * track.width();     let r = h.request("ui.drag", json!({"x": from, "y": track.center().y, "toX": track.center().x, "toY": track.center().y, "steps": 12}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.drag");     let k = develop(&h).wb.temp;     assert!((k - 7000.0).abs() <= 50.0, "dragged to the middle: {k} K");     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:wb.temp", "fx": 1.0}), T)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking slider:wb.temp");     assert_eq!(develop(&h).wb.temp, 50000.0); } @@ -105,7 +105,7 @@ fn other_sliders_stay_linear() {     set(&mut h, "light.exposure", 1.0);     assert!((thumb(&h, "light.exposure") - 0.6).abs() < 0.005, "+1.00 of −5…+5 at 60 %");     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:light.exposure", "fx": 0.75}), T)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking slider:light.exposure");     assert!((develop(&h).light.exposure - 2.5).abs() < 0.011, "{}", develop(&h).light.exposure); } @@ -116,7 +116,7 @@ fn the_relative_temp_slider_stays_linear() {     assert!(h.app.widgets.iter().all(|(w, _)| w != "slider:wb.temp"), "a JPEG has the relative scale");     assert!((thumb(&h, "wb.tempRel") - 0.5).abs() < 0.005, "as shot in the middle");     assert_eq!(h.request("ui.clickWidget", json!({"id": "slider:wb.tempRel", "fx": 0.75}), T)["ok"], true);-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking slider:wb.tempRel");     assert!((thumb(&h, "wb.tempRel") - 0.75).abs() < 0.005, "the thumb where it was clicked");     // +50 on the relative scale: 40 mired warmer than 6500 K     let k = develop(&h).wb.temp;diff --git a/crates/ui-egui/src/tests/titlebar.rs b/crates/ui-egui/src/tests/titlebar.rs--- a/crates/ui-egui/src/tests/titlebar.rs+++ b/crates/ui-egui/src/tests/titlebar.rs@@ -17,7 +17,7 @@ fn integrated() -> Headless {     let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);     app.integrated_titlebar = true;     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);-    h.settle(Duration::from_secs(120));+    h.settled(Duration::from_secs(120), "the new session");     h } @@ -124,7 +124,7 @@ fn without_the_integrated_title_bar_the_bar_is_not_a_window_handle() {     let services = Services { png: None, ..Default::default() };     let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);     let mut h = Headless::new(app, [1200.0, 800.0], 1.0);-    h.settle(Duration::from_secs(120));+    h.settled(Duration::from_secs(120), "the new session");     assert!(!h.app.integrated_titlebar);     let bar = h.app.widgets.iter().find(|(w, _)| w == "region:titlebar");     assert!(bar.is_none());diff --git a/crates/ui-egui/src/tests/view_cost.rs b/crates/ui-egui/src/tests/view_cost.rs--- a/crates/ui-egui/src/tests/view_cost.rs+++ b/crates/ui-egui/src/tests/view_cost.rs@@ -105,7 +105,7 @@ fn what_one_keyword_costs_in_a_library_of_67_000_photos() {     h.app.ui.left_panel = true;     h.request("engine.execute", json!({"command": "view.squareGrid", "params": {}}), Duration::from_secs(60));     h.request("engine.execute", json!({"command": "panel.keywords", "params": {}}), Duration::from_secs(60));-    h.settle(Duration::from_secs(120));+    h.settled(Duration::from_secs(120), "after panel.keywords");     let frame = |h: &mut Headless| {         let t = Instant::now();         h.step();@@ -132,7 +132,7 @@ fn what_one_keyword_costs_in_a_library_of_67_000_photos() {         if right && h.app.ui.right != crate::state::RightPanel::Keywords {             h.request("engine.execute", json!({"command": "panel.keywords", "params": {}}), Duration::from_secs(60));         }-        h.settle(Duration::from_secs(120));+        h.settled(Duration::from_secs(120), "after panel.keywords");         let id: PhotoId = h.app.session.visible()[500];         h.app.session.execute("library.select", &json!({"ids": [id.0]})).unwrap();         h.step();@@ -144,7 +144,7 @@ fn what_one_keyword_costs_in_a_library_of_67_000_photos() {     h.app.ui.left_panel = true;     let view = "view.photoGrid";     h.request("engine.execute", json!({"command": view, "params": {}}), Duration::from_secs(60));-    h.settle(Duration::from_secs(120));+    h.settled(Duration::from_secs(120), "after engine.execute");     let id: PhotoId = h.app.session.visible()[400];     h.app.session.execute("library.select", &json!({"ids": [id.0]})).unwrap();     h.step();diff --git a/crates/ui-egui/src/tests/wb_selector.rs b/crates/ui-egui/src/tests/wb_selector.rs--- a/crates/ui-egui/src/tests/wb_selector.rs+++ b/crates/ui-egui/src/tests/wb_selector.rs@@ -23,22 +23,22 @@ fn wb_selector_button_in_compare_opens_detail_like_w() {     let r = h.request("engine.execute", json!({"command": "view.compare"}), T);     assert_eq!(r["ok"], true, "{r}");     h.app.ui.right = crate::state::RightPanel::Edit;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.right");     // open the Color section, where the selector sits     if !h.app.widgets.iter().any(|(w, _)| w == "icon:wbPicker") {         h.request("ui.clickWidget", json!({"id": "section:color"}), T);-        h.settle(SETTLE);+        h.settled(SETTLE, "after clicking section:color");     }     assert_eq!(h.app.ui.view, ViewMode::Compare);     let active = h.app.session.selection.active;     let r = h.request("ui.clickWidget", json!({"id": "icon:wbPicker"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking icon:wbPicker");     assert_eq!(h.app.ui.view, ViewMode::Detail, "the button leaves Compare like W");     assert_eq!(h.app.ui.tool, "wbPicker");     assert_eq!(h.app.session.selection.active, active, "on the photo that was active in Compare");     let r = h.request("ui.clickWidget", json!({"id": "icon:wbPicker"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after clicking icon:wbPicker");     assert_eq!(h.app.ui.tool, "", "a second click disarms it"); }diff --git a/crates/ui-egui/src/tests/zoom.rs b/crates/ui-egui/src/tests/zoom.rs--- a/crates/ui-egui/src/tests/zoom.rs+++ b/crates/ui-egui/src/tests/zoom.rs@@ -17,7 +17,7 @@ fn detail() -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h } @@ -30,7 +30,7 @@ fn edit(h: &mut Headless, f: impl FnOnce(&mut lightcraft_develop::DevelopSetting  fn at_100(h: &mut Headless) -> (f32, f32) {     h.request("engine.execute", json!({"command": "view.zoom100"}), T);-    h.settle(SETTLE);+    h.settled(SETTLE, "after view.zoom100");     let r = h.app.image_rect.expect("the loupe drew the photo");     (r.width(), r.height()) }@@ -151,7 +151,7 @@ fn zoomed_before_after_views_stay_in_their_panes() {             h.app.ui.before_after = mode;             h.app.ui.zoom = zoom;             h.app.ui.pan = (0.3, 0.4);-            h.settle(SETTLE);+            h.settled(SETTLE, "after setting ui.pan");             let img = h.app.image_rect.expect("After view on screen");             let canvas = h.app.canvas_rect.expect("canvas");             shots.push((h.paint(), img, canvas));@@ -190,13 +190,13 @@ fn zoomed_in_the_before_label_stays_in_view() {         egui::Rect::from_min_size(egui::pos2(v[0], v[1]), egui::vec2(v[2], v[3]))     };     h.app.ui.zoom = Zoom::Fit;-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.zoom");     let (img, canvas) = (h.app.image_rect.expect("photo on screen"), h.app.canvas_rect.expect("canvas"));     let fitted = label(&mut h);     assert_eq!(fitted.min, img.min + egui::vec2(8.0, 8.0), "at Fit the label sits on the photo's corner");     h.app.ui.zoom = Zoom::Percent(100.0);     h.app.ui.pan = (0.3, 0.4);-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.pan");     let img = h.app.image_rect.expect("photo on screen");     assert!(img.min.x < canvas.min.x && img.min.y < canvas.min.y, "100 % reaches past the canvas ({img:?} in {canvas:?})");     let zoomed = label(&mut h);@@ -214,7 +214,7 @@ fn zoomed_in_the_before_label_keeps_clear_of_the_info_overlay() {     h.app.ui.info_overlay = InfoOverlay::Exposure;     h.app.ui.zoom = Zoom::Percent(100.0);     h.app.ui.pan = (0.3, 0.4);-    h.settle(SETTLE);+    h.settled(SETTLE, "after setting ui.pan");     let rect = |h: &mut Headless, id: &str| -> egui::Rect {         let w = h.request("ui.widgets", json!({"filter": id}), T);         let r = w["result"][0]["rect"].as_array().unwrap_or_else(|| panic!("{id} is registered: {w}"));@@ -240,7 +240,7 @@ fn a_crop_of_no_size_lays_out_at_a_finite_size() {         for zoom in ["view.zoomFit", "view.zoom100"] {             let r = h.request("engine.execute", json!({"command": zoom}), T);             assert_eq!(r["ok"], true, "{zoom}: {r}");-            h.settle(SETTLE);+            h.settled(SETTLE, "after engine.execute");             let img = h.app.image_rect.expect("the loupe laid the photo out");             assert!(img.min.is_finite() && img.max.is_finite(), "{rect:?} at {zoom}: {img:?}");             let r = h.request(diff --git a/crates/ui-egui/src/tests/zoom_keys.rs b/crates/ui-egui/src/tests/zoom_keys.rs--- a/crates/ui-egui/src/tests/zoom_keys.rs+++ b/crates/ui-egui/src/tests/zoom_keys.rs@@ -22,7 +22,7 @@ fn edit_view() -> Headless {     let mut h = Headless::new(app, [1400.0, 900.0], 1.0);     let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);     assert_eq!(r["ok"], true, "{r}");-    h.settle(SETTLE);+    h.settled(SETTLE, "after ui.set");     h }