Announce a month at the next row it opens, not only if it begins one

A heading was drawn only on a cell that both began a month and began a
row, so at seven columns most months were never named, and the one
heading on screen — always on the window's first cell — was wrong about
every row below it. Worse, two headings drawn on the same cell overprinted
each other. Now a row carries a heading whenever its first cell's month is
not the one last announced: a month starting mid-row is named on the next
row it opens, one row late and right about everything under it.
This commit is contained in:
2026-09-20 00:21:14 +02:00
parent 7fba28f7d8
commit eddaa44cd3
+89 -31
View File
@@ -2824,38 +2824,17 @@ fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
// Month headings. The grid is ordered by capture time, so without these a
// wall of thumbnails gives no sense of *when* you are looking — the
// sidebar says it, but only if you consult it.
//
// Marked on the cell that both begins a month and begins a row: a heading
// stranded mid-row would label the cells to its left, which belong to the
// previous month.
let columns = window.get_library_columns().max(1) as usize;
let mut previous_month: Option<(i64, i64)> = None;
let headings: Vec<String> = cells
.iter()
.enumerate()
.map(|(i, c)| {
let Some(t) = c.captured_at else {
return String::new();
};
let (y, m, _, _) = civil_from_unix(t);
let is_new = previous_month != Some((y, m));
previous_month = Some((y, m));
// A heading is drawn above its row, so it can only sit on a cell
// that begins one — a heading stranded mid-row would appear to
// label the cells to its left, which belong to the month before.
//
// The first cell of the window always carries one, whichever
// column it lands in: a scrolled window would otherwise show no
// date at all until the next month began.
let begins_row = (i + offset) % columns == 0;
if i == 0 || (is_new && begins_row) {
format!("{} {y}", month_name(m))
} else {
String::new()
}
})
.collect();
let headings = period_headings(
cells.iter().map(|c| {
c.captured_at.map(|t| {
let (y, m, _, _) = civil_from_unix(t);
(y, m)
})
}),
offset,
columns,
);
// What the outgoing model is still holding — see [`hold_thumbnails`].
let held = {
@@ -7151,6 +7130,46 @@ fn stop(slot: &RefCell<Option<slint::Timer>>) {
}
}
/// The heading each cell of a window carries: a month name on some, nothing
/// on most.
///
/// A heading is drawn above its row, so it can only sit on a cell that
/// begins one — stranded mid-row it would appear to label the cells to its
/// left, which belong to the month before. So a row carries a heading when
/// its first cell's month is not the one last announced. A month that begins
/// mid-row is therefore announced at the *next* row it opens, which is one
/// row late and still right about every cell under it. It used to be
/// announced only if it happened to begin a row, which at seven columns
/// meant most months were never named at all and the one heading on screen
/// was wrong about everything below its first row.
///
/// The first cell of the window always carries one, whichever column it
/// lands in: a scrolled window would otherwise show no date at all until
/// the next month began.
fn period_headings(
months: impl Iterator<Item = Option<(i64, i64)>>,
offset: usize,
columns: usize,
) -> Vec<String> {
let columns = columns.max(1);
let mut announced: Option<(i64, i64)> = None;
months
.enumerate()
.map(|(i, month)| {
let Some((y, m)) = month else {
return String::new();
};
let begins_row = (i + offset).is_multiple_of(columns);
if i == 0 || (begins_row && announced != Some((y, m))) {
announced = Some((y, m));
format!("{} {y}", month_name(m))
} else {
String::new()
}
})
.collect()
}
/// The line in the library header that says what is being catalogued.
///
/// For an account with a user, the user and the chosen subtree, or the
@@ -7180,6 +7199,45 @@ fn library_root_label(account: &Account) -> String {
mod tests {
use super::*;
#[test]
fn a_month_that_begins_mid_row_is_still_announced() {
// Four columns; August starts in the second column of the second
// row and October in the third of the third. Neither begins a row,
// and both must still be named — on the next row each opens.
let mar = Some((2024, 3));
let aug = Some((2024, 8));
let oct = Some((2025, 10));
let months = [
mar, mar, mar, mar, //
mar, aug, aug, aug, //
aug, aug, oct, oct, //
oct,
];
let h = period_headings(months.into_iter(), 0, 4);
assert_eq!(h[0], "March 2024");
assert_eq!(h[4], "", "still March at the top of row two");
assert_eq!(h[5], "", "never mid-row");
assert_eq!(h[8], "August 2024");
assert_eq!(h[12], "October 2025");
assert_eq!(h.iter().filter(|s| !s.is_empty()).count(), 3);
}
#[test]
fn a_scrolled_window_names_its_first_cell_wherever_it_lands() {
let jan = Some((2026, 1));
let feb = Some((2026, 2));
// Offset 2 into a 4-column grid: the window's first cell is in the
// third column, and the row after it begins a new month.
let h = period_headings([jan, jan, feb, feb, feb].into_iter(), 2, 4);
assert_eq!(h[0], "January 2026");
assert_eq!(h[2], "February 2026", "cell 2 begins a row at offset 2");
assert_eq!(h[3], "");
// Undated cells carry nothing and announce nothing.
let h = period_headings([None, jan, None, None, jan].into_iter(), 0, 4);
assert_eq!(h[0], "");
assert_eq!(h[4], "January 2026");
}
#[test]
fn the_header_names_a_folder_library_by_its_folder() {
let mut folder = Account::new("folder", "/var/tmp/dr-demo/library");