From 53dc2d171ec2f6bee8a5940937d706abfbdc1422 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 30 Aug 2026 20:34:11 +0200 Subject: [PATCH] Say where the grid is on the capture-time axis from the first frame The sidebar's marker rested greyed at mid-track until the first scroll or scrub. The reasoning was that anchoring it would imply a choice the user had not made -- but that reads the marker as reporting an intention, and it does not. The sidebar's whole claim is to say *when* you are, and that is known from the first frame: the grid is at the top of the library, or wherever it was last left. So a launch opened with the marker halfway down an axis whose visible photographs were all from the wrong end of it. Dimmed rather than absent, which made it look like a reading rather than the absence of one. Seeded in `refresh_timeline` -- the one place that decides what the marker says, and the one that runs on every route which builds the axis -- and only when nothing has claimed it, so a scroll or a scrub still speaks for itself. Co-Authored-By: Claude Opus 5 (1M context) --- ui/dr-ui/src/library_ui.rs | 38 +++++++++++++++++++++++++++++++++----- ui/dr-ui/ui/library.slint | 7 ++++--- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index f84c146..193c384 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -225,8 +225,12 @@ pub struct LibraryController { /// held until it reaches a doubling. Without it a slow spread would either /// do nothing or, if each update were rounded, leap several levels. pinch_accum: RefCell, - /// The instant the grid is showing. `None` until the user has moved the - /// timeline, which is what leaves the marker resting at the middle. + /// The instant the grid is showing. + /// + /// `None` only before the first axis has been built: `refresh_timeline` + /// seeds it from wherever the view sits, so the marker reports a position + /// from the first frame rather than resting greyed at mid-track until the + /// user happens to scroll. A scroll or a scrub then sets it directly. current_bucket: RefCell>, /// What the rating filter bar is narrowed to. /// @@ -4182,6 +4186,29 @@ fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc) { /// from the data rather than fixing it means the histogram is informative at /// both scales instead of one flat bar or ten thousand slivers. fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc) { + // Where the grid sits, if nothing has said so yet. + // + // **The marker used to rest greyed at mid-track until the first scroll or + // scrub**, on the reasoning that anchoring it would imply a choice the user + // had not made. That reads the marker as reporting an intention, and it does + // not: the sidebar's whole claim is to say *when* you are, and the answer is + // known from the first frame — the grid is at the top of the library, or at + // whatever position was restored into it. A launch that opened with the + // marker halfway down an axis whose photographs were all from the wrong end + // was simply wrong, and dimmed rather than absent, which made it look like a + // reading rather than the absence of one. + // + // Seeded here rather than at the end of the scan because this is the one + // place that decides what the marker says, and it runs on every route that + // builds the axis. Only when nothing has claimed it: a scroll or a scrub + // sets it directly, and this must never overwrite them. + let unanchored = ctl.current_bucket.borrow().is_none(); + if unanchored { + if let Some(when) = capture_time_at(ctl, catalog, ctl.resume_at.get()) { + *ctl.current_bucket.borrow_mut() = Some(when); + } + } + // Scoped to whatever the grid is showing. A collection's histogram drawn // over the whole library's span said almost nothing: every bar for a // fortnight in Arosa landed in one column of a fifteen-year axis. @@ -4574,9 +4601,10 @@ fn scrub_to(window: &AppWindow, ctl: &Rc, when: i64) { .unwrap_or(0) as usize }; - // Record where the grid now sits, which anchors the timeline marker. Until - // the first scrub this stays `None` and the marker rests at the middle - // rather than implying a choice the user has not made. + // Record where the grid now sits, which anchors the timeline marker. + // `refresh_timeline` seeds this from the view's own position when nothing + // has claimed it, so by the time a scrub arrives it is an update rather than + // the first word. *ctl.current_bucket.borrow_mut() = Some(when); *ctl.offset.borrow_mut() = position; // Move the viewport as well as the window. Cells are drawn at their diff --git a/ui/dr-ui/ui/library.slint b/ui/dr-ui/ui/library.slint index 09fdf71..6857049 100644 --- a/ui/dr-ui/ui/library.slint +++ b/ui/dr-ui/ui/library.slint @@ -93,9 +93,10 @@ export component Timeline inherits Rectangle { /// happened to fall — never under the pointer. Negative means "not /// anchored". in property current-fraction: -1; - /// True once the user has taken control. Until then the marker rests at the - /// middle rather than pinning to either end, which would imply a selection - /// that has not been made. + /// Whether the marker has a position to report. False only before the first + /// axis has been built — Rust seeds it from wherever the grid sits, so a + /// launch draws the marker where the photographs on screen are rather than + /// leaving it dimmed at mid-track waiting for a scroll. in property anchored: false; /// Scrub to a fraction along the visible span. Rust turns it into an