Make the thin ring the only mark a selected photograph carries

Two treatments said "selected" and neither said it well.

A selected cell got a 2px border and a lifted fill. The border is drawn
on the outside of a cell whose content sits 6px in, so it ate into the
thumbnail: selecting appeared to nudge the photograph. Inside it, a
second thin ring marked the anchor — the end a shift-click measures from
— and that ring was the clearest thing on the cell, so it read as *the*
selection to everyone who had not written it.

Worse, the ring outlived what it described. An anchor survives a
deselection, so a thin box sat around the last photograph touched with
nothing selected at all, indistinguishable from a cell that had stayed
behind. That is the one thing a selection cue must never be: ambiguous
about whether something is selected.

So the ring is now what it already looked like. One mark, drawn inside
the cell and 4px clear of its edge, so it never touches the thumbnail and
never changes a dimension — selecting adds ink and moves nothing. Two
pixels rather than one, because it is now carrying the whole cue across
forty cells at arm's length against a thumbnail of any brightness. The
outer border is hover alone.

The anchor keeps no mark, and loses nothing it was earning: the bar says
"Tap the last photograph" while a range is armed, which answers the
question the ring existed to answer. The ordinal still lives in the
controller and still decides where a range extends from; what is gone is
the claim that the user needs to see it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-30 09:47:32 +02:00
co-authored by Claude Opus 5
parent b120ce48ec
commit 9e519eb8a6
3 changed files with 50 additions and 28 deletions
+14 -8
View File
@@ -898,19 +898,25 @@ pub fn sync_lifted(window: &AppWindow, lifted: &[ImageId], ids: &[ImageId]) {
pub fn sync_selection(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[ImageId]) {
let selection = ctl.selection.borrow();
let model = window.get_library_cells();
// The anchor is an ordinal and the model is a window, so it marks a row
// only while the photograph it names is on screen. Off screen it marks
// nothing, which is the honest answer: the range still extends from there.
let offset = window.get_library_offset().max(0) as usize;
let anchor_row = ctl.anchor.borrow().and_then(|at| at.checked_sub(offset));
// The anchor is deliberately **not** pushed to the grid.
//
// It used to be, and it was drawn as a thin ring inside the cell. Two
// things were wrong with that. It was the clearest mark on the cell, so it
// read as *the* selection to anyone who had not written it; and an anchor
// outlives a deselection, so the ring sat around the last photograph
// touched while nothing was selected at all — indistinguishable from a cell
// that had stayed behind.
//
// The ordinal still lives in the controller and still decides where a range
// extends from. What is gone is the claim that the user needs to see it:
// the selection bar says "Tap the last photograph" while a range is armed,
// which answers the question the ring was there to answer.
for (row, id) in ids.iter().enumerate() {
let want = selection.contains(id);
let anchored = anchor_row == Some(row);
if let Some(mut cell) = model.row_data(row) {
if cell.selected != want || cell.anchor != anchored {
if cell.selected != want {
cell.selected = want;
cell.anchor = anchored;
model.set_row_data(row, cell);
}
}
-1
View File
@@ -2305,7 +2305,6 @@ fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
// which owns the selection and queries the badge counts for the
// whole window in one statement rather than one per cell.
selected: false,
anchor: false,
collection_count: 0,
// Likewise filled by `sync_ratings` below — one query for the
// window, not one per cell.