Flag chip and filter bar show what flag and flags let through together

With both set (only via library.filter) they AND, but the chip and the
bar's buttons showed their union. Filter::flags_shown gives the flags
that pass both; the chip names both constraints when none does.
This commit is contained in:
Marco Arnulfo
2026-10-30 02:26:41 +01:00
parent 1ee9991b41
commit e7fa046d11
7 changed files with 55 additions and 15 deletions
+11
View File
@@ -270,6 +270,17 @@ impl Filter {
self.describe_in(Some(cat))
}
/// The flags that `flag` and `flags` let through together (they AND), in the filter bar's
/// order; `None` when neither is set, empty when they exclude each other. The chip and the
/// filter bar's buttons show this, so they match what is filtered.
pub fn flags_shown(&self) -> Option<Vec<Flag>> {
if self.flag.is_none() && self.flags.is_empty() {
return None;
}
let passes = |f: &Flag| self.flag.is_none_or(|x| x == *f) && (self.flags.is_empty() || self.flags.contains(f));
Some([Flag::Pick, Flag::Reject, Flag::None].into_iter().filter(passes).collect())
}
fn describe_in(&self, cat: Option<&crate::Catalog>) -> String {
let mut v: Vec<String> = Vec::new();
if self.rating > 0 {
+21
View File
@@ -391,6 +391,27 @@ fn filter_any_of_several_flags() {
assert_eq!(s.visible_cloned().len(), picks);
}
/// `flag` and `flags` set together (only through `library.filter`) AND, and the chip says what
/// they let through, not their union.
#[test]
fn filter_flag_and_flags_together_chip() {
use lightcraft_catalog::Flag;
let mut s = crate::Session::with_demo();
let ids: Vec<u64> = s.catalog.photos().take(3).map(|p| p.id.0).collect();
for (id, f) in ids.iter().zip(["pick", "reject", "none"]) {
s.execute("photo.flag", &json!({"ids": [id], "flag": f})).unwrap();
}
let picks = s.catalog.photos().filter(|p| p.in_library() && p.flag == Flag::Pick).count();
let chip = |s: &Session| crate::filter_chips(&s.filter, &s.catalog).into_iter().find(|c| c.label.starts_with("Flag")).expect("a flag chip").label;
s.execute("library.filter", &json!({"flag": "pick", "flags": ["pick", "none"]})).unwrap();
assert_eq!(s.visible_cloned().len(), picks);
assert_eq!(chip(&s), "Flag: pick");
// nothing passes both: the chip names both constraints
s.execute("library.filter", &json!({"flag": "pick", "flags": ["reject", "none"]})).unwrap();
assert_eq!(s.visible_cloned().len(), 0);
assert_eq!(chip(&s), "Flag: pick and reject or none");
}
/// Quick Collection / target album: B toggles the selection in it (created on first use);
/// another album can be the target; clear empties it; all undoable.
#[test]
+6 -5
View File
@@ -158,11 +158,12 @@ pub fn filter_chips(f: &Filter, cat: &Catalog) -> Vec<FilterChip> {
};
add(format!("Rating {op} {}★", f.rating), json!({"rating": 0}));
}
let mut flags = f.flags.clone();
flags.extend(f.flag.filter(|fl| !flags.contains(fl)));
if !flags.is_empty() {
let names: Vec<String> = flags.iter().map(|fl| format!("{fl:?}").to_lowercase()).collect();
add(format!("Flag: {}", names.join(" or ")), json!({"flag": Null, "flags": []}));
if let Some(shown) = f.flags_shown() {
let name = |fl: &lightcraft_catalog::Flag| format!("{fl:?}").to_lowercase();
let or = |fls: &[lightcraft_catalog::Flag]| fls.iter().map(name).collect::<Vec<_>>().join(" or ");
// empty: `flag` and `flags` exclude each other, so name both constraints
let names = if shown.is_empty() { format!("{} and {}", f.flag.as_ref().map(name).unwrap_or_default(), or(&f.flags)) } else { or(&shown) };
add(format!("Flag: {names}"), json!({"flag": Null, "flags": []}));
}
if let Some(shown) = f.labels_shown() {
let or = |ls: &[lightcraft_catalog::ColorLabel]| ls.iter().map(|l| cat.label_name(*l)).collect::<Vec<_>>().join(" or ");
+5
View File
@@ -1611,6 +1611,11 @@ fn filter_bar_flags_combine() {
h.request("ui.clickWidget", json!({"id": "filter:none"}), t);
let f = h.app.session.filter.clone();
assert_eq!((f.flag, f.flags), (None, vec![Flag::Reject, Flag::None]));
// both set through the API: only Picked passes, so only Picked is lit and Unflagged joins it
h.app.session.execute("library.filter", &json!({"flag": "pick", "flags": ["pick", "none"]})).unwrap();
h.request("ui.clickWidget", json!({"id": "filter:none"}), t);
let f = h.app.session.filter.clone();
assert_eq!((f.flag, f.flags), (None, vec![Flag::Pick, Flag::None]));
h.settle(SETTLE);
}
+2
View File
@@ -605,6 +605,8 @@ fn german_switches_via_menu_and_control_and_persists() {
assert!(label.contains("Color") && !label.contains(&red) && label.contains(&blue), "{label}");
let filter = lightcraft_catalog::Filter { flags: vec![lightcraft_catalog::Flag::Pick, lightcraft_catalog::Flag::None], ..Default::default() };
assert_eq!(filter_label(&filter, &app.session.catalog), "Markierung: ausgewählt oder keine");
let filter = lightcraft_catalog::Filter { flag: Some(lightcraft_catalog::Flag::Pick), ..filter };
assert_eq!(filter_label(&filter, &app.session.catalog), "Markierung: ausgewählt");
set_language(Locale::En);
}
+9 -4
View File
@@ -69,10 +69,15 @@ pub(crate) fn display_label(chip: &FilterChip, filter: &lightcraft_catalog::Filt
return format!("{}: {}", tr("Label"), label_names(filter, catalog));
}
if chip.clear.get("flags").is_some() {
let mut flags = filter.flags.clone();
flags.extend(filter.flag.filter(|flag| !flags.contains(flag)));
let names = flags.iter().map(|flag| tr(&format!("{flag:?}").to_lowercase()).to_string()).collect::<Vec<_>>();
return format!("{}: {}", tr("Flag"), names.join(&format!(" {} ", tr("or"))));
let shown = filter.flags_shown().unwrap_or_default();
let name = |flag: &lightcraft_catalog::Flag| tr(&format!("{flag:?}").to_lowercase()).to_string();
let or = |flags: &[lightcraft_catalog::Flag]| flags.iter().map(name).collect::<Vec<_>>().join(&format!(" {} ", tr("or")));
let names = if shown.is_empty() {
format!("{} {} {}", filter.flag.as_ref().map(name).unwrap_or_default(), tr("and"), or(&filter.flags))
} else {
or(&shown)
};
return format!("{}: {names}", tr("Flag"));
}
if chip.clear.get("dateFrom").is_some() || chip.clear.get("dateTo").is_some() {
return match (&filter.date_from, &filter.date_to) {
+1 -6
View File
@@ -137,12 +137,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
// flags: several can be on at once (any of them matches), as in Lightroom Classic
caption(ui, "Flag");
let mut chosen: Vec<Flag> = f.flags.clone();
if let Some(fl) = f.flag
&& !chosen.contains(&fl)
{
chosen.push(fl);
}
let chosen: Vec<Flag> = f.flags_shown().unwrap_or_default();
for (id, icon, flag, tip) in [
("pick", Icon::FlagPick, Flag::Pick, "Picked"),
("reject", Icon::FlagReject, Flag::Reject, "Rejected"),