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) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 20:34:11 +02:00
co-authored by Claude Opus 5
parent eed27eb36d
commit 53dc2d171e
2 changed files with 37 additions and 8 deletions
+33 -5
View File
@@ -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<f32>,
/// 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<Option<i64>>,
/// What the rating filter bar is narrowed to.
///
@@ -4182,6 +4186,29 @@ fn start_thumbnail_sweep(window: &AppWindow, ctl: &Rc<LibraryController>) {
/// 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<LibraryController>) {
// 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<LibraryController>, 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
+4 -3
View File
@@ -93,9 +93,10 @@ export component Timeline inherits Rectangle {
/// happened to fall — never under the pointer. Negative means "not
/// anchored".
in property <float> 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 <bool> anchored: false;
/// Scrub to a fraction along the visible span. Rust turns it into an