Mark a burst in the grid, and let it be folded away
The counterpart to the grouping: where the signatures come from, and how a group reaches a cell. Signatures are computed from the 256px thumbnails dr-thumbs already holds -- vastly more resolution than a 9x8 reduction can use -- so a library that has been browsed, or that has synced somebody else's shards, has already paid for them and no RAW is decoded for this. The consequence is stated rather than hidden: an image with no thumbnail gets no signature and never joins a burst. That is self-correcting, and it is why the pass runs when the thumbnail sweep finishes rather than on a timer. Nothing happens at import and nothing happens at query time. The mark is drawn as a child of the cell's TouchArea, for the same reason the star strip is: a click on it must not also reach `cell-clicked` and throw the user into develop, and children are hit-tested before the element they sit in. It is never hidden on hover the way the stars are -- a collapsed burst stands in for frames that are not on screen, and something has to say so whether or not a pointer is nearby. Folding changes what the grid's *query* returns rather than what its cells draw, because the grid is a window over an ordered query and the frames a fold hides are mostly not loaded. So the predicate joins VISIBLE in every query that lists or counts cells -- the window, the header's count, the run a shift-click resolves, and the ordinal a scrub lands on -- under the discipline VISIBLE's own comment sets out: present in four places of five is worse than absent, because the counts disagree with the cells and neither looks wrong on its own. There is a test for exactly that. `the_window_read_walks_the_ordering_index` now includes the burst clause. It asserts on the query plan while holding its own copy of the query, so left alone it would have gone on reporting green against a query the grid no longer runs. If the clause costs `images_grid_order` and puts the sort back, that fails here rather than becoming jitter someone measures in six months. The pass keeps its own drain timer in a thread-local instead of taking fields on the library controller, so everything the feature needs to run lives in one file and the screen that starts it holds nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+91
-6
@@ -186,6 +186,26 @@ const VISIBLE_UNALIASED: &str = "shadowed_by IS NULL AND trashed_at IS NULL";
|
||||
/// restore the same frame twice.
|
||||
const TRASHED: &str = "i.shadowed_by IS NULL AND i.trashed_at IS NOT NULL";
|
||||
|
||||
/// TRACES: FR-CULL-5
|
||||
/// The clause that hides the frames a collapsed burst is standing in for.
|
||||
///
|
||||
/// Subject to exactly the discipline [`VISIBLE`] is under, and for the same
|
||||
/// reason: the header's count, the scrollbar's size, the run a shift-click
|
||||
/// resolves and the ordinal a scrub lands on are four answers about one list.
|
||||
/// A burst folded away in the cells but still counted in the total would leave
|
||||
/// the grid ending in rows that draw nothing, with no clue why.
|
||||
///
|
||||
/// The predicate itself is `dr_catalog::bursts`'s, not this file's, so the
|
||||
/// interface and the pass that writes the table cannot come to disagree about
|
||||
/// what collapsed means.
|
||||
///
|
||||
/// A function rather than a constant because it has to name the image table,
|
||||
/// and the grid aliases it as `i` where the timeline's queries do not. `image`
|
||||
/// is a table name from this file and never anything a user supplied.
|
||||
fn uncollapsed(image: &str) -> String {
|
||||
format!(" AND {}", dr_catalog::bursts::not_collapsed_away(image))
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-4
|
||||
/// The order the grid lists photographs in: when they were taken.
|
||||
///
|
||||
@@ -3814,10 +3834,11 @@ pub fn read_cells_scoped(
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let rated = filter.sql();
|
||||
let folded = uncollapsed("i");
|
||||
let sql = format!(
|
||||
"SELECT {CELL_COLUMNS}
|
||||
FROM images i
|
||||
WHERE {VISIBLE}{rated}
|
||||
WHERE {VISIBLE}{rated}{folded}
|
||||
AND i.id IN (SELECT image_id FROM collection_members
|
||||
WHERE collection_id IN ({placeholders}))
|
||||
{GRID_ORDER}
|
||||
@@ -3849,11 +3870,12 @@ fn read_cells_all(
|
||||
limit: usize,
|
||||
) -> Result<Vec<LibraryCell>, dr_catalog::CatalogError> {
|
||||
let rated = filter.sql();
|
||||
let folded = uncollapsed("i");
|
||||
let mut rows = {
|
||||
let mut stmt = catalog.connection().prepare(&format!(
|
||||
"SELECT {CELL_COLUMNS}
|
||||
FROM images i
|
||||
WHERE {VISIBLE}{rated}
|
||||
WHERE {VISIBLE}{rated}{folded}
|
||||
{GRID_ORDER}
|
||||
LIMIT ?1 OFFSET ?2"
|
||||
))?;
|
||||
@@ -3954,10 +3976,11 @@ pub fn read_ids_span(
|
||||
} else {
|
||||
let (clause, params) = scope_clause(catalog, scope)?;
|
||||
let rated = filter.sql();
|
||||
let folded = uncollapsed("i");
|
||||
(
|
||||
format!(
|
||||
"SELECT i.id FROM images i
|
||||
WHERE {VISIBLE}{rated}{clause}
|
||||
WHERE {VISIBLE}{rated}{folded}{clause}
|
||||
{GRID_ORDER}
|
||||
LIMIT ? OFFSET ?"
|
||||
),
|
||||
@@ -4086,9 +4109,10 @@ pub fn total_images_scoped(
|
||||
// Counted through `images` rather than over `collection_members` alone, so
|
||||
// `VISIBLE` applies — a trashed photograph is still a member row, and
|
||||
// counting it made the header claim images the grid would not draw.
|
||||
let folded = uncollapsed("i");
|
||||
let sql = format!(
|
||||
"SELECT count(DISTINCT i.id) FROM images i
|
||||
WHERE {VISIBLE}{rated}
|
||||
WHERE {VISIBLE}{rated}{folded}
|
||||
AND i.id IN (SELECT image_id FROM collection_members
|
||||
WHERE collection_id IN ({placeholders}))"
|
||||
);
|
||||
@@ -4259,8 +4283,9 @@ fn total_images_filtered(
|
||||
filter: &RatingFilter,
|
||||
) -> Result<usize, dr_catalog::CatalogError> {
|
||||
let rated = filter.sql();
|
||||
let folded = uncollapsed("i");
|
||||
let n: i64 = catalog.connection().query_row(
|
||||
&format!("SELECT count(*) FROM images i WHERE {VISIBLE}{rated}"),
|
||||
&format!("SELECT count(*) FROM images i WHERE {VISIBLE}{rated}{folded}"),
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)?;
|
||||
@@ -4806,12 +4831,16 @@ mod tests {
|
||||
#[test]
|
||||
fn the_window_read_walks_the_ordering_index() {
|
||||
let catalog = with_images(20);
|
||||
// Including the burst clause, because the grid includes it: a
|
||||
// predicate that quietly cost the ordering index would put the sort
|
||||
// back and this is the only place that would notice.
|
||||
let folded = uncollapsed("i");
|
||||
let plan: Vec<String> = catalog
|
||||
.connection()
|
||||
.prepare(&format!(
|
||||
"EXPLAIN QUERY PLAN
|
||||
SELECT {CELL_COLUMNS} FROM images i
|
||||
WHERE {VISIBLE}
|
||||
WHERE {VISIBLE}{folded}
|
||||
{GRID_ORDER}
|
||||
LIMIT 10 OFFSET 5"
|
||||
))
|
||||
@@ -4864,6 +4893,62 @@ mod tests {
|
||||
catalog
|
||||
}
|
||||
|
||||
/// TRACES: FR-CULL-5
|
||||
/// A folded burst takes rows out of the cells, the count and the range a
|
||||
/// shift-click resolves — all three, together.
|
||||
///
|
||||
/// This is the test that would fail if the clause were added to four of
|
||||
/// the five queries that need it. That failure has no other symptom: the
|
||||
/// header claims images the grid will not draw, the scrollbar sizes itself
|
||||
/// for rows that are not there, and neither number looks wrong on its own.
|
||||
#[test]
|
||||
fn folding_a_burst_takes_the_same_rows_out_of_every_answer() {
|
||||
use dr_catalog::bursts::{self, Rules, Signature};
|
||||
|
||||
let catalog = with_images(4);
|
||||
// Three of the four are one burst: a second apart, one signature.
|
||||
let ids = image_ids(&catalog);
|
||||
for (n, id) in ids.iter().enumerate() {
|
||||
let hash = if n < 3 { 0xFF00 } else { 0x00FF };
|
||||
catalog
|
||||
.connection()
|
||||
.execute(
|
||||
"UPDATE images SET captured_at = ?2, camera = 'Canon EOS R5',
|
||||
perceptual_hash = ?3
|
||||
WHERE id = ?1",
|
||||
rusqlite::params![
|
||||
id.0 as i64,
|
||||
1_000 + n as i64,
|
||||
Signature(hash).to_stored()
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
bursts::regroup(catalog.connection(), Rules::default()).unwrap();
|
||||
|
||||
let filter = RatingFilter::default();
|
||||
// Open, as a new burst is: nothing has been taken away yet.
|
||||
assert_eq!(read_cells(&catalog, 0, 50).unwrap().len(), 4);
|
||||
assert_eq!(total_images_filtered(&catalog, &filter).unwrap(), 4);
|
||||
|
||||
bursts::set_expanded(catalog.connection(), ids[0], false).unwrap();
|
||||
|
||||
let cells = read_cells(&catalog, 0, 50).unwrap();
|
||||
assert_eq!(cells.len(), 2, "the folded frames are still in the cells");
|
||||
assert_eq!(
|
||||
total_images_filtered(&catalog, &filter).unwrap(),
|
||||
cells.len(),
|
||||
"the header's count and the cells disagree"
|
||||
);
|
||||
assert_eq!(
|
||||
read_ids_span(&catalog, None, &filter, false, 0, 49)
|
||||
.unwrap()
|
||||
.len(),
|
||||
cells.len(),
|
||||
"a shift-click over the whole grid would select frames it cannot show"
|
||||
);
|
||||
}
|
||||
|
||||
fn image_ids(catalog: &Catalog) -> Vec<dr_types::ImageId> {
|
||||
let mut stmt = catalog
|
||||
.connection()
|
||||
|
||||
Reference in New Issue
Block a user