Draw the date histogram over the collection you are looking at
Build and test / Desktop (Linux) (push) Failing after 57m25s
Build and test / Layer separation (push) Successful in 33s
Traceability / Requirement traces (push) Failing after 27s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 9m45s

The timeline counted the whole library whatever the grid was showing, so
opening a collection left a fortnight in Arosa as one column of a
fifteen-year axis — an axis describing photographs that were not on
screen.

Scope the buckets and the span to the same collection and rating filter
the grid uses. `timeline_range` counts `images` alone and cannot express
the membership join, so the scoped query lives beside the other scoped
readers in the UI and shares their descendants-of-scope rule.

`catalog_span` now delegates to the same scoped reader. Zoom and scrub
measured the full library while the bars were scoped, so a scrub could
land on an instant the collection did not contain and send the view
somewhere the user had not asked to go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 22:15:59 +02:00
co-authored by Claude Opus 5
parent b0206cbc7a
commit 02d629922f
3 changed files with 116 additions and 30 deletions
+18 -29
View File
@@ -2720,7 +2720,12 @@ fn start_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>) {
let span = match catalog_span(catalog) {
// 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.
let scope = *ctl.scope.borrow();
let filter = *ctl.filter.borrow();
let span = match library::span_scoped(catalog, scope, &filter) {
Some(s) => s,
None => {
// No dated images yet. An empty histogram is honest — EXIF is read
@@ -2737,13 +2742,7 @@ fn refresh_timeline(window: &AppWindow, catalog: &Catalog, ctl: &Rc<LibraryContr
let (from, to) = zoomed_span(span, zoom, *ctl.timeline_centre.borrow());
let granularity = dr_catalog::Granularity::for_span(to - from);
let buckets = match catalog.timeline_range(
&dr_catalog::Query::default(),
granularity,
from,
to,
now_secs(),
) {
let buckets = match library::timeline_scoped(catalog, scope, &filter, granularity, from, to) {
Ok(b) => b,
Err(e) => {
log::debug!("timeline: {e}");
@@ -2875,17 +2874,14 @@ fn zoomed_span(full: (i64, i64), zoom: i32, centre: Option<i64>) -> (i64, i64) {
}
/// Earliest and latest capture time in the catalog.
fn catalog_span(catalog: &Catalog) -> Option<(i64, i64)> {
catalog
.connection()
.query_row(
"SELECT min(captured_at), max(captured_at)
FROM images WHERE captured_at IS NOT NULL",
[],
|r| Ok((r.get::<_, Option<i64>>(0)?, r.get::<_, Option<i64>>(1)?)),
)
.ok()
.and_then(|(lo, hi)| Some((lo?, hi?)))
/// The timeline's extent, over exactly the images the grid is showing.
///
/// Scrubbing and zooming must measure the same span the histogram is drawn
/// over. When this read the whole library while the bars were scoped to a
/// collection, a scrub landed at an instant the collection did not contain and
/// the view jumped somewhere the user had not asked for.
fn catalog_span(catalog: &Catalog, ctl: &Rc<LibraryController>) -> Option<(i64, i64)> {
library::span_scoped(catalog, *ctl.scope.borrow(), &ctl.filter.borrow())
}
/// Capture time of the image at row `ordinal` in the grid's own ordering.
@@ -3003,13 +2999,6 @@ fn civil_from_unix(t: i64) -> (i64, i64, i64, i64) {
(if m <= 2 { y + 1 } else { y }, m, d, secs / 3600)
}
fn now_secs() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
/// Copy decoded RGBA into a Slint image.
///
/// This is a CPU copy, which is acceptable here and not in the develop path:
@@ -3318,7 +3307,7 @@ pub fn wire<F>(
let zoom = *ctl.timeline_zoom.borrow();
let centre = *ctl.timeline_centre.borrow();
if let Some(full) = catalog_span(catalog) {
if let Some(full) = catalog_span(catalog, &ctl) {
let (from, to) = zoomed_span(full, zoom, centre);
w.set_library_current_bucket(when as i32);
w.set_library_current_fraction(
@@ -3362,7 +3351,7 @@ pub fn wire<F>(
let Some(catalog) = borrow.as_ref() else {
return;
};
let Some(full) = catalog_span(catalog) else {
let Some(full) = catalog_span(catalog, &ctl) else {
return;
};
@@ -3410,7 +3399,7 @@ pub fn wire<F>(
let Some(catalog) = borrow.as_ref() else {
return;
};
let Some(full) = catalog_span(catalog) else {
let Some(full) = catalog_span(catalog, &ctl) else {
return;
};