Move the people tray's ticks in place instead of rebuilding it per press
Benchmarks / CPU and I/O (per commit) (push) Successful in 1m52s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 45m4s
Build and test / Layer separation (push) Successful in 41s
Traceability / Requirement traces (push) Successful in 29s
🐳 Android image / Build and push (push) Successful in 1s
Build and test / android-image (push) Successful in 2s
🐳 Windows image / Build and push (push) Successful in 1s
Build and test / windows-image (push) Successful in 1s
Build and test / Android (aarch64) (push) Successful in 29m25s
Build and test / Windows (x86_64, cross) (push) Successful in 34m4s
Build and test / Publish the release (push) Skipped

Filtering the grid by a face crawled on the reference library. The SQL
is not it — the person predicate counts in ~20 ms, the eyes-open term in
~60 — but every press on the tray ran `push_people_chips`, which read the
whole people table (26,362 rows, nearly all empty groups a regrouping
pass left behind) and then called `push_people_roster`, which read it
again and replaced the roster model. The roster is every person holding
a face, 1,581 chips, in a row Slint does not virtualise: a new model
tore down and re-created all of them and laid the row out again, to
move one tick.

A press now walks the roster model and sets `picked` on the rows whose
tick changed; the roster is built only when the tray opens. Both reads
use `people_in_use` (2,140 rows) rather than `people`. A picked person
the in-use query leaves out — emptied by a split while the filter held
them — still gets a chip, since a term with no chip cannot be removed,
and without one the in-place update would fall back to a rebuild on
every press.
This commit is contained in:
2026-09-24 20:21:50 -04:00
parent 031315bdb6
commit a3f3e188e1
2 changed files with 77 additions and 11 deletions
+75 -9
View File
@@ -9,7 +9,7 @@
use std::rc::Rc;
use slint::ComponentHandle;
use slint::{ComponentHandle, Model};
use crate::library;
use crate::{AppWindow, Library, PersonChip, View};
@@ -585,15 +585,20 @@ pub(super) fn push_people_chips(window: &AppWindow, ctl: &Rc<LibraryController>)
)));
// The tray, if it is open, has to lose its ticks with the chips: the
// roster carries `picked` and is the same fact drawn a second time.
push_people_roster(window, ctl);
retick_roster(window, ctl);
return;
}
let borrow = ctl.catalog.borrow();
let people = borrow
.as_ref()
.and_then(|cat| dr_catalog::faces::people(cat.connection()).ok())
.unwrap_or_default();
// `people_in_use`, not `people`: the chips name at most a handful, and the
// full table is every empty group a regrouping pass left behind — 26,000
// rows on the reference library, read to name two.
let people = {
let borrow = ctl.catalog.borrow();
borrow
.as_ref()
.and_then(|cat| dr_catalog::faces::people_in_use(cat.connection()).ok())
.unwrap_or_default()
};
let chips: Vec<PersonChip> = ids
.iter()
@@ -633,7 +638,48 @@ pub(super) fn push_people_chips(window: &AppWindow, ctl: &Rc<LibraryController>)
ctl.filter.borrow().people_mode,
library::PeopleMode::All
));
push_people_roster(window, ctl);
retick_roster(window, ctl);
}
/// Move the tray's ticks to match the filter, touching only the chips that
/// changed.
///
/// # Why not rebuild it
///
/// Rebuilding was what every toggle did, and it made filtering by a face
/// crawl. The roster is every person holding a face — 1,581 chips on the
/// reference library — in a row Slint does not virtualise, so a new model
/// tore down and re-created all of them and laid the row out again, to move
/// one tick. It also read the whole people table twice to do it. The people
/// have not changed between two presses on the tray; only `picked` has.
///
/// Falls back to a rebuild when a picked person has no chip — someone sent
/// from the People screen who was set aside, say — because a term with no
/// chip is a term the user cannot take back out. An empty roster is left
/// alone: the tray is closed, and opening it builds it.
fn retick_roster(window: &AppWindow, ctl: &Rc<LibraryController>) {
let picked = ctl.filter.borrow().people.clone();
let model = window.global::<Library>().get_library_people();
if model.row_count() == 0 {
return;
}
let mut shown = 0;
for row in 0..model.row_count() {
let Some(mut chip) = model.row_data(row) else {
continue;
};
let on = picked.contains(&(chip.id as u64));
shown += usize::from(on);
if chip.picked != on {
chip.picked = on;
model.set_row_data(row, chip);
}
}
if shown < picked.len() {
push_people_roster(window, ctl);
}
}
/// Fill the filter bar's people tray.
@@ -650,10 +696,14 @@ pub(super) fn push_people_chips(window: &AppWindow, ctl: &Rc<LibraryController>)
/// says it back.
fn push_people_roster(window: &AppWindow, ctl: &Rc<LibraryController>) {
let picked = ctl.filter.borrow().people.clone();
// In use rather than the whole table: the tray shows people holding a
// face, and the rest are the empty groups a regrouping pass leaves
// behind. A picked person can be one of those — emptied by a split while
// the filter held them — and is given a chip below regardless.
let borrow = ctl.catalog.borrow();
let people = borrow
.as_ref()
.and_then(|cat| dr_catalog::faces::people(cat.connection()).ok())
.and_then(|cat| dr_catalog::faces::people_in_use(cat.connection()).ok())
.unwrap_or_default();
// Sorted as (unnamed, -faces) pairs beside the chip rather than by
@@ -694,6 +744,22 @@ fn push_people_roster(window: &AppWindow, ctl: &Rc<LibraryController>) {
.collect();
// Stable, so the catalog's own tiebreak by name survives inside each of the
// two blocks.
// A term with no chip is a term the user cannot remove, and `retick_roster`
// would rebuild on every press looking for it.
for id in &picked {
if !people.iter().any(|p| p.id.0 == *id) {
rows.push((
true,
0,
PersonChip {
id: *id as i32,
name: "Unnamed (0 faces)".into(),
faces: 0,
picked: true,
},
));
}
}
rows.sort_by_key(|(unnamed, faces, _)| (*unnamed, -faces));
let rows: Vec<PersonChip> = rows.into_iter().map(|(_, _, chip)| chip).collect();