diff --git a/ui/dr-ui/src/library_ui.rs b/ui/dr-ui/src/library_ui.rs index 0e5fc37..13d0f33 100644 --- a/ui/dr-ui/src/library_ui.rs +++ b/ui/dr-ui/src/library_ui.rs @@ -2008,7 +2008,7 @@ fn load_window(window: &AppWindow, ctl: &Rc) { period_heading: heading.into(), // A freshly loaded window has no drag in flight. lifted: false, - name: c.name.as_str().into(), + name: without_extension(&c.name).into(), thumbnail: carried.map(|h| h.thumbnail.clone()).unwrap_or_default(), has_thumb: carried.is_some_and(|h| h.has_thumb), unavailable: carried.is_some_and(|h| h.unavailable), @@ -5500,3 +5500,53 @@ mod tests { assert_eq!(paths, vec!["a.CR2", "b.CR2"]); } } + +/// TRACES: FR-UI-2 +/// A filename as a caption: without the part that says how it is stored. +/// +/// A grid cell is about four words wide, and `.CR2` spends one of them saying +/// something the photographer already knows — every frame in a RAW library +/// ends the same way, so the extension distinguishes nothing while taking +/// room from the part that does. The name is elided under pressure, and it is +/// the *end* that goes, so an extension can push the digits that identify a +/// frame off the visible part of its own label. +/// +/// Display only. `LibraryCell::name` keeps the true filename and +/// `remote_path` the full path, because both are used to find the file again +/// and a stem is not a filename. +/// +/// **The case this is wrong for**, worth knowing before it is reported: a +/// library holding `IMG_1234.CR2` beside `IMG_1234.JPG` shows two cells +/// captioned `IMG_1234`. They are still two rows with two thumbnails and two +/// entries in the info panel, and RAW+JPEG pairs are usually shot to be one +/// photograph anyway — but the caption alone no longer separates them. +fn without_extension(name: &str) -> &str { + match name.rsplit_once('.') { + // The guard is for a leading dot: `.hidden` splits to an empty stem, + // and a dotfile's name starts with that dot rather than ending with an + // extension. A *trailing* dot needs no guard — `odd.` splits to `odd`, + // which is the better caption anyway. + Some((stem, _)) if !stem.is_empty() => stem, + _ => name, + } +} + +#[cfg(test)] +mod display_name_tests { + use super::without_extension; + + #[test] + fn an_extension_is_dropped_and_nothing_else_is() { + assert_eq!(without_extension("IMG_1234.CR2"), "IMG_1234"); + assert_eq!(without_extension("IMG_1234.jpeg"), "IMG_1234"); + // Only the last one: a name can contain dots and they are part of it. + assert_eq!(without_extension("2026.08.23-a.dng"), "2026.08.23-a"); + // Nothing to drop. + assert_eq!(without_extension("IMG_1234"), "IMG_1234"); + // A dotfile is not an extensionless name with an extension. + assert_eq!(without_extension(".hidden"), ".hidden"); + // A trailing dot is an empty extension, and dropping it is right. + assert_eq!(without_extension("odd."), "odd"); + assert_eq!(without_extension(""), ""); + } +}