Show and set colour labels in the grid and develop, and filter by them
Colour labels could be read from a Lightroom sidecar and queried by the selector, but nothing drew one or set one, so the only labels a library held were ones another program had written. Every mark carries its label's initial on its colour — R, Y, G, B, P — so a label is read without telling red from green, which is what NFR-A11Y-3 asks of colour labels by name. A grid cell shows the mark before its filename. In the grid, 6, 7, 8 and 9 set red, yellow, green and blue as Lightroom's keys do, on the photograph under the pointer or on the selection by the rule the star keys follow; the same key again takes the label off, and over a mixed selection it sets it on all. The selection bar gains Label, which opens the six choices — each a mark and a name — and purple, which has no key, is there. In develop the top bar says "Label: Green" beside the mark, opens the same choices, and 6-9 label the open photograph. Each gesture is one catalog transaction, then the grid, the counts and both sidecars are written as a rating's are. The filter bar gains a chip per label, its mark and its name with a count, one at a time; the filter is one SQL term, travels in the place record, and "All" clears it.
This commit is contained in:
@@ -113,6 +113,13 @@ pub const GESTURES: &[Gesture] = &[
|
||||
pointer: "Double-click it, or press the zoom readout floating over the canvas",
|
||||
keys: "Z",
|
||||
},
|
||||
Gesture {
|
||||
title: "Give this photograph a colour label",
|
||||
section: "Develop",
|
||||
touch: "Tap Label in the top bar, then a colour",
|
||||
pointer: "Click Label in the top bar, then a colour",
|
||||
keys: "6 red, 7 yellow, 8 green, 9 blue; the same key again takes it off",
|
||||
},
|
||||
Gesture {
|
||||
title: "Move to the next or previous photograph",
|
||||
section: "Develop",
|
||||
@@ -281,6 +288,13 @@ pub const GESTURES: &[Gesture] = &[
|
||||
pointer: "Open the People chip on the filter bar, click each name, then switch the chip beside them to \"all of them\"",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Show only photographs with one colour label",
|
||||
section: "Library grid",
|
||||
touch: "Tap its chip in the filter bar",
|
||||
pointer: "Click its chip in the filter bar",
|
||||
keys: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Export the selection as the last export was",
|
||||
section: "Library grid",
|
||||
@@ -302,6 +316,13 @@ pub const GESTURES: &[Gesture] = &[
|
||||
pointer: "Click a star chip in the filter bar",
|
||||
keys: "Hold F and tap a digit for exactly that many stars, or two digits for everything between them; tap F alone to show every rating again",
|
||||
},
|
||||
Gesture {
|
||||
title: "Give photographs a colour label",
|
||||
section: "Library grid",
|
||||
touch: "Select them, then Label in the selection bar and tap a colour",
|
||||
pointer: "Select them, then Label in the selection bar and click a colour",
|
||||
keys: "6 red, 7 yellow, 8 green, 9 blue, with the pointer over it or on the selection; the same key again takes the label off",
|
||||
},
|
||||
Gesture {
|
||||
title: "Resize the thumbnails",
|
||||
section: "Library grid",
|
||||
|
||||
@@ -153,6 +153,10 @@ pub struct RatingFilter {
|
||||
/// a star count does, and nothing here rejects or rates a frame — the
|
||||
/// line §3.9.1 draws.
|
||||
pub eyes_open: bool,
|
||||
/// TRACES: FR-CAT-6
|
||||
/// Only photographs carrying this colour label. One at a time, as the
|
||||
/// flag is: the chips are a choice of one pile, not a set of them.
|
||||
pub label: Option<dr_types::ColourLabel>,
|
||||
}
|
||||
|
||||
/// How several people combine when the grid is narrowed by identity.
|
||||
@@ -188,6 +192,7 @@ impl RatingFilter {
|
||||
&& self.captured_to.is_none()
|
||||
&& self.people.is_empty()
|
||||
&& !self.eyes_open
|
||||
&& self.label.is_none()
|
||||
}
|
||||
|
||||
/// Whether a date range is narrowing the grid.
|
||||
@@ -349,6 +354,19 @@ impl RatingFilter {
|
||||
));
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-6
|
||||
// A code this module owns, formatted in like the flag above. No
|
||||
// coalesce: an unlabelled image's NULL never equals a code, which is
|
||||
// the answer.
|
||||
if let Some(label) = self.label {
|
||||
terms.push(format!(
|
||||
"(SELECT dv.label FROM versions dv
|
||||
WHERE dv.image_id = i.id AND dv.is_default = 1
|
||||
LIMIT 1) = {}",
|
||||
dr_catalog::rating::label_code(label)
|
||||
));
|
||||
}
|
||||
|
||||
if self.local_only {
|
||||
// `tier_actual`, not `tier_desired`: the question is what is
|
||||
// *here*, not what a pin has promised will be. An image queued for
|
||||
@@ -419,6 +437,31 @@ mod tests {
|
||||
assert_eq!(local_original_count(&catalog).unwrap(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_label_chip_shows_that_label_and_nothing_else() {
|
||||
// TRACES: FR-CAT-6
|
||||
let catalog = with_images(4);
|
||||
let ids = image_ids(&catalog);
|
||||
let c = catalog.connection();
|
||||
dr_catalog::rating::set_label(c, ids[0], Some(dr_types::ColourLabel::Red)).unwrap();
|
||||
dr_catalog::rating::set_label(c, ids[1], Some(dr_types::ColourLabel::Red)).unwrap();
|
||||
dr_catalog::rating::set_label(c, ids[2], Some(dr_types::ColourLabel::Green)).unwrap();
|
||||
|
||||
let red = RatingFilter {
|
||||
label: Some(dr_types::ColourLabel::Red),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!red.is_unfiltered());
|
||||
assert_eq!(read_cells_all(&catalog, &red, 0, 120).unwrap().len(), 2);
|
||||
assert_eq!(total_images_filtered(&catalog, &red).unwrap(), 2);
|
||||
|
||||
let purple = RatingFilter {
|
||||
label: Some(dr_types::ColourLabel::Purple),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(total_images_filtered(&catalog, &purple).unwrap(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cached_preview_is_not_a_local_original() {
|
||||
// The filter answers "can I open this in develop right now", and a
|
||||
|
||||
@@ -914,15 +914,24 @@ mod tests {
|
||||
fn a_label_from_another_device_reaches_the_catalog_and_none_clears_nothing() {
|
||||
// TRACES: FR-CAT-5
|
||||
let cat = library(&["2026/a.CR2"]);
|
||||
assert_eq!(apply_judgement(cat.connection(), 1, "2026/a.drsc", 0, 0, 4).unwrap(), 1);
|
||||
assert_eq!(
|
||||
apply_judgement(cat.connection(), 1, "2026/a.drsc", 0, 0, 4).unwrap(),
|
||||
1
|
||||
);
|
||||
let label = |cat: &Catalog| -> Option<i64> {
|
||||
cat.connection()
|
||||
.query_row("SELECT label FROM versions WHERE is_default = 1", [], |r| r.get(0))
|
||||
.query_row("SELECT label FROM versions WHERE is_default = 1", [], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(label(&cat), Some(4));
|
||||
apply_judgement(cat.connection(), 1, "2026/a.drsc", 3, 0, 0).unwrap();
|
||||
assert_eq!(label(&cat), Some(4), "a sidecar with no label left it alone");
|
||||
assert_eq!(
|
||||
label(&cat),
|
||||
Some(4),
|
||||
"a sidecar with no label left it alone"
|
||||
);
|
||||
}
|
||||
|
||||
/// The regression, in one line: a rating in a sidecar reaches the grid.
|
||||
|
||||
@@ -252,6 +252,24 @@ pub(super) fn wire_filter_ratings_and_people(window: &AppWindow, ctl: &Rc<Librar
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-6
|
||||
// A colour label chip. Independent of the stars and the flag: "the red
|
||||
// ones I have not starred yet" is a question worth being able to ask.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_filter_label_changed(move |code| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let label = dr_catalog::rating::label_from_code(Some(code as i64));
|
||||
ctl.filter.borrow_mut().label = label;
|
||||
w.global::<Library>()
|
||||
.set_library_filter_label(label.map_or(0, |_| code));
|
||||
refilter(&w, &ctl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The date-range filter: turned on from the timeline's own visible span,
|
||||
|
||||
@@ -45,11 +45,15 @@ pub fn sync_ratings(window: &AppWindow, catalog: &Catalog, ids: &[dr_types::Imag
|
||||
// Absent means unrated, which is a real state rather than missing data.
|
||||
let j = found.get(id).copied().unwrap_or_default();
|
||||
let (rating, flag) = (j.rating as i32, flag_code(j.flag));
|
||||
let label = j
|
||||
.label
|
||||
.map_or(0, |l| dr_catalog::rating::label_code(l) as i32);
|
||||
|
||||
if let Some(mut cell) = model.row_data(row) {
|
||||
if cell.rating != rating || cell.flag != flag {
|
||||
if cell.rating != rating || cell.flag != flag || cell.label != label {
|
||||
cell.rating = rating;
|
||||
cell.flag = flag;
|
||||
cell.label = label;
|
||||
model.set_row_data(row, cell);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +82,123 @@ pub(super) fn refresh_rating_counts(window: &AppWindow, catalog: &Catalog) {
|
||||
.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-6
|
||||
/// Refresh the label chips' counts. Separate from the star counts because
|
||||
/// the two change on different events: a star key cannot move a label count,
|
||||
/// so a cull does not pay for this read on every keystroke.
|
||||
pub(super) fn refresh_label_counts(window: &AppWindow, catalog: &Catalog) {
|
||||
let counts = dr_catalog::rating::label_histogram(catalog.connection()).unwrap_or_default();
|
||||
let as_i32: Vec<i32> = counts.iter().map(|n| *n as i32).collect();
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_label_counts(slint::ModelRc::new(slint::VecModel::from(as_i32)));
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-5
|
||||
/// What a label gesture asks for: a key toggles, the picker names the answer.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LabelGesture {
|
||||
Toggle(dr_types::ColourLabel),
|
||||
Set(Option<dr_types::ColourLabel>),
|
||||
}
|
||||
|
||||
impl LabelGesture {
|
||||
/// From Slint's code and whether it came from a key. A code with no
|
||||
/// colour is "none" for the picker, and nothing at all for a key.
|
||||
fn from_code(code: i32, toggle: bool) -> Option<Self> {
|
||||
let label = dr_catalog::rating::label_from_code(Some(code as i64));
|
||||
match (toggle, label) {
|
||||
(true, Some(l)) => Some(Self::Toggle(l)),
|
||||
(true, None) => None,
|
||||
(false, l) => Some(Self::Set(l)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-5 | FR-CAT-13
|
||||
/// Label a set of images: catalog first, in one transaction, then the grid,
|
||||
/// the chips and both sidecars — the order and the reasons of
|
||||
/// [`apply_judgement`].
|
||||
fn apply_label(
|
||||
window: &AppWindow,
|
||||
ctl: &Rc<LibraryController>,
|
||||
images: &[dr_types::ImageId],
|
||||
gesture: LabelGesture,
|
||||
) {
|
||||
if images.is_empty() {
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_status("Select an image first".into());
|
||||
return;
|
||||
}
|
||||
|
||||
let writes = {
|
||||
let borrow = ctl.catalog.borrow();
|
||||
let Some(catalog) = borrow.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let conn = catalog.connection();
|
||||
let label = match gesture {
|
||||
LabelGesture::Set(l) => l,
|
||||
// One read of what they carry now, for the whole set.
|
||||
LabelGesture::Toggle(pressed) => {
|
||||
let now = dr_catalog::rating::judgements(conn, images).unwrap_or_default();
|
||||
dr_catalog::rating::toggled_label(
|
||||
images.iter().map(|id| now.get(id).and_then(|j| j.label)),
|
||||
pressed,
|
||||
)
|
||||
}
|
||||
};
|
||||
if let Err(e) = dr_catalog::rating::set_label_many(conn, images, label) {
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_error(format!("recording label: {e}").into());
|
||||
return;
|
||||
}
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_status(label_summary(images.len(), label).into());
|
||||
|
||||
let visible = ctl.visible_ids();
|
||||
sync_ratings(window, catalog, &visible);
|
||||
refresh_label_counts(window, catalog);
|
||||
collect_sidecar_writes(catalog, images)
|
||||
};
|
||||
|
||||
// A grid narrowed to red no longer holds a frame just made green.
|
||||
if !ctl.filter.borrow().is_unfiltered() {
|
||||
load_window(window, ctl);
|
||||
}
|
||||
|
||||
start_sidecar_writes(window, ctl, writes);
|
||||
start_xmp_writes(window, ctl, images);
|
||||
}
|
||||
|
||||
/// What the status line says about a label that just landed — in words, as
|
||||
/// every other judgement is reported.
|
||||
fn label_summary(n: usize, label: Option<dr_types::ColourLabel>) -> String {
|
||||
let what = match label {
|
||||
Some(l) => format!("{} label", label_name(l).to_lowercase()),
|
||||
None => "no label".to_string(),
|
||||
};
|
||||
if n == 1 {
|
||||
what[..1].to_uppercase() + &what[1..]
|
||||
} else {
|
||||
format!("{n} images · {what}")
|
||||
}
|
||||
}
|
||||
|
||||
/// A label's name, as the chips and the picker spell it.
|
||||
fn label_name(l: dr_types::ColourLabel) -> &'static str {
|
||||
match l {
|
||||
dr_types::ColourLabel::Red => "Red",
|
||||
dr_types::ColourLabel::Yellow => "Yellow",
|
||||
dr_types::ColourLabel::Green => "Green",
|
||||
dr_types::ColourLabel::Blue => "Blue",
|
||||
dr_types::ColourLabel::Purple => "Purple",
|
||||
}
|
||||
}
|
||||
|
||||
// --- keywords (FR-CAT-5, FR-CAT-6) ---------------------------------------
|
||||
//
|
||||
// `dr_catalog::keywords` owns the data rules — the vocabulary, the many-to-many
|
||||
@@ -672,6 +793,7 @@ fn drain_xmp(
|
||||
if let Some(catalog) = ctl.catalog.borrow().as_ref() {
|
||||
sync_ratings(&w, catalog, &visible);
|
||||
refresh_rating_counts(&w, catalog);
|
||||
refresh_label_counts(&w, catalog);
|
||||
}
|
||||
refresh_xmp_conflicts(&w, ctl);
|
||||
}
|
||||
@@ -959,6 +1081,69 @@ pub(super) fn wire_ratings_and_flags(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5 | FR-UI-5
|
||||
// A label key in the grid: the photographs a rating key would reach,
|
||||
// toggled as one set.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let coll_for_keys = coll_ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_labelled(move |code, hovered| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(gesture) = LabelGesture::from_code(code, true) else {
|
||||
return;
|
||||
};
|
||||
let offset = w.global::<Library>().get_library_offset().max(0) as usize;
|
||||
let under_pointer = usize::try_from(hovered)
|
||||
.ok()
|
||||
.and_then(|ordinal| ordinal.checked_sub(offset))
|
||||
.and_then(|row| ctl.visible_ids().get(row).copied());
|
||||
let chosen = judged_images(coll_for_keys.selected(), under_pointer);
|
||||
apply_label(&w, &ctl, &chosen, gesture);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5 | FR-UI-4
|
||||
// The selection bar's picker: the selection, set outright.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let coll = coll_ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_label_chosen(move |code| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(gesture) = LabelGesture::from_code(code, false) else {
|
||||
return;
|
||||
};
|
||||
apply_label(&w, &ctl, &coll.selected(), gesture);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5
|
||||
// One photograph by row — the one open in develop.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_cell_labelled(move |row, code, toggle| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(gesture) = LabelGesture::from_code(code, toggle) else {
|
||||
return;
|
||||
};
|
||||
let id = ctl
|
||||
.image_ids
|
||||
.borrow()
|
||||
.get(row as usize)
|
||||
.map(|id| dr_types::ImageId(*id as u64));
|
||||
let Some(id) = id else { return };
|
||||
apply_label(&w, &ctl, &[id], gesture);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-5
|
||||
@@ -1054,6 +1239,37 @@ mod tests {
|
||||
assert_eq!(judged_images(vec![], None), vec![]);
|
||||
}
|
||||
|
||||
// --- colour labels (FR-CAT-5) -----------------------------------------
|
||||
|
||||
#[test]
|
||||
fn a_label_key_toggles_and_the_picker_sets() {
|
||||
use dr_types::ColourLabel::*;
|
||||
assert_eq!(
|
||||
LabelGesture::from_code(1, true),
|
||||
Some(LabelGesture::Toggle(Red))
|
||||
);
|
||||
// A key with no colour behind it does nothing rather than clearing.
|
||||
assert_eq!(LabelGesture::from_code(0, true), None);
|
||||
assert_eq!(
|
||||
LabelGesture::from_code(0, false),
|
||||
Some(LabelGesture::Set(None))
|
||||
);
|
||||
assert_eq!(
|
||||
LabelGesture::from_code(5, false),
|
||||
Some(LabelGesture::Set(Some(Purple)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_label_is_reported_by_name() {
|
||||
// The status line names the label in words, never only by its mark.
|
||||
assert_eq!(
|
||||
label_summary(1, Some(dr_types::ColourLabel::Green)),
|
||||
"Green label"
|
||||
);
|
||||
assert_eq!(label_summary(3, None), "3 images · no label");
|
||||
}
|
||||
|
||||
// --- what the status line says about a keyword (FR-CAT-5) -------------
|
||||
//
|
||||
// Split out from the callback for the same reason `decide_drop` is: the
|
||||
|
||||
@@ -927,6 +927,11 @@ fn push_filter(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
window
|
||||
.global::<Library>()
|
||||
.set_library_filter_eyes_open(filter.eyes_open);
|
||||
window.global::<Library>().set_library_filter_label(
|
||||
filter
|
||||
.label
|
||||
.map_or(0, |l| dr_catalog::rating::label_code(l) as i32),
|
||||
);
|
||||
push_people_chips(window, ctl);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{AppWindow, Library, LibraryCell};
|
||||
use super::controller::{stop, LibraryController, LibraryFacts};
|
||||
use super::filter_bar::period_headings;
|
||||
use super::offline::{collection_images, refresh_offline, scope_is_pinned};
|
||||
use super::ratings_keywords::{refresh_rating_counts, sync_ratings};
|
||||
use super::ratings_keywords::{refresh_label_counts, refresh_rating_counts, sync_ratings};
|
||||
use super::sync::start_sweep;
|
||||
use super::timeline::{civil_from_unix, format_date, refresh_timeline, restore_position};
|
||||
|
||||
@@ -367,6 +367,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
// window, not one per cell.
|
||||
rating: 0,
|
||||
flag: 0,
|
||||
label: 0,
|
||||
// And by `bursts::sync_badges`, in one more query for the
|
||||
// window. Zero is "not in a burst", which is what almost every
|
||||
// photograph in a library is.
|
||||
@@ -424,6 +425,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||
// so a cull still watches its own chips move.
|
||||
if describes_something_new {
|
||||
refresh_rating_counts(window, catalog);
|
||||
refresh_label_counts(window, catalog);
|
||||
}
|
||||
|
||||
// The library got shorter while the view was looking at it — a delete.
|
||||
|
||||
@@ -33,6 +33,7 @@ impl From<&RatingFilter> for StoredFilter {
|
||||
people: f.people.clone(),
|
||||
people_all: matches!(f.people_mode, PeopleMode::All),
|
||||
eyes_open: f.eyes_open,
|
||||
label: f.label,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +55,7 @@ impl From<&StoredFilter> for RatingFilter {
|
||||
PeopleMode::Any
|
||||
},
|
||||
eyes_open: s.eyes_open,
|
||||
label: s.label,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +191,7 @@ mod tests {
|
||||
people: vec![7, 9],
|
||||
people_all: true,
|
||||
eyes_open: true,
|
||||
label: Some(dr_types::ColourLabel::Green),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -312,6 +315,7 @@ mod tests {
|
||||
people: vec![3, 5, 8],
|
||||
people_mode: PeopleMode::All,
|
||||
eyes_open: true,
|
||||
label: Some(dr_types::ColourLabel::Purple),
|
||||
};
|
||||
let back: RatingFilter = (&StoredFilter::from(&original)).into();
|
||||
assert_eq!(back, original);
|
||||
|
||||
@@ -142,6 +142,21 @@ colors:
|
||||
and a hue would imply it were one series among four rather than the one
|
||||
the other three are decomposing.
|
||||
|
||||
_labels:
|
||||
section: colour labels
|
||||
note: |
|
||||
The five colour labels (FR-CAT-5). The same exception the plot series
|
||||
take: a label's name *is* a colour, so its mark is filled with it. Held
|
||||
to mid-tones so one dark ink reads on every fill — the mark carries the
|
||||
label's initial in `ground`, which is what keeps it legible without hue
|
||||
(NFR-A11Y-3).
|
||||
|
||||
label-red: "#D0666E"
|
||||
label-yellow: "#D6B84E"
|
||||
label-green: "#72B06E"
|
||||
label-blue: "#6A96D4"
|
||||
label-purple: "#A988D0"
|
||||
|
||||
lengths:
|
||||
_gaps: { break: true }
|
||||
gap-sm: 6
|
||||
|
||||
@@ -1461,6 +1461,13 @@ in property <bool> panel-visible: true;
|
||||
// for one fact is how they come to disagree.
|
||||
viewing-trash: root.collection-selected == -1;
|
||||
judged(rating, flag, at) => { Library.library-judged(rating, flag, at); }
|
||||
labelled(code, at) => { Library.library-labelled(code, at); }
|
||||
label-chosen(code) => { Library.library-label-chosen(code); }
|
||||
filter-label: Library.library-filter-label;
|
||||
label-counts: Library.library-label-counts;
|
||||
filter-label-changed(code) => {
|
||||
Library.library-filter-label-changed(code);
|
||||
}
|
||||
// `F2` renames whatever the grid is scoped to. Answered from
|
||||
// the sidebar's selection rather than a second property, for
|
||||
// the same reason `viewing-trash` above is: one fact, one
|
||||
@@ -1497,6 +1504,14 @@ in property <bool> panel-visible: true;
|
||||
fps: root.fps;
|
||||
expanded: root.expanded;
|
||||
filename: root.filename;
|
||||
// TRACES: FR-CAT-5
|
||||
// The open photograph's label, read from its grid cell so the
|
||||
// bar and the grid cannot disagree; -1 with no library.
|
||||
colour-label: Library.library-total > 0 && Library.library-roll-current >= 0
|
||||
? Library.library-cells[Library.library-roll-current].label : -1;
|
||||
label-picked(code) => {
|
||||
Library.library-cell-labelled(Library.library-roll-current, code, false);
|
||||
}
|
||||
position: root.total > 0 ? root.index + 1 + " / " + root.total : "";
|
||||
// Only where a library was opened; command-line files have no
|
||||
// grid to return to.
|
||||
@@ -2402,6 +2417,31 @@ in property <bool> panel-visible: true;
|
||||
Masking.selected-toggled();
|
||||
return accept;
|
||||
}
|
||||
// GESTURE: Give this photograph a colour label
|
||||
// where: Develop
|
||||
// touch: Tap Label in the top bar, then a
|
||||
// colour
|
||||
// pointer: Click Label in the top bar, then a
|
||||
// colour
|
||||
// keys: 6 red, 7 yellow, 8 green, 9 blue; the
|
||||
// same key again takes it off
|
||||
// why: The grid's keys, on the photograph
|
||||
// that is open, so labelling while
|
||||
// stepping through a folder is one
|
||||
// hand's work. The bar names the label
|
||||
// in words beside its mark.
|
||||
//
|
||||
// TRACES: FR-CAT-5 | FR-UI-5 | NFR-A11Y-3
|
||||
// Only with a library: a file named on the command
|
||||
// line has no catalog to hold a label.
|
||||
if (Library.library-total > 0
|
||||
&& Library.library-roll-current >= 0
|
||||
&& !event.modifiers.control) {
|
||||
if (event.text == "6") { Library.library-cell-labelled(Library.library-roll-current, 1, true); return accept; }
|
||||
if (event.text == "7") { Library.library-cell-labelled(Library.library-roll-current, 2, true); return accept; }
|
||||
if (event.text == "8") { Library.library-cell-labelled(Library.library-roll-current, 3, true); return accept; }
|
||||
if (event.text == "9") { Library.library-cell-labelled(Library.library-roll-current, 4, true); return accept; }
|
||||
}
|
||||
// GESTURE: Move to the next or previous photograph
|
||||
// where: Develop
|
||||
// touch: Tap a frame in the roll along the
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Theme } from "theme.slint";
|
||||
import { Develop } from "session.slint";
|
||||
import { Transfer } from "adjust.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel } from "widgets.slint";
|
||||
import { Labels, LabelMark, LabelPicker } from "labels.slint";
|
||||
|
||||
// Status strip — surfaces the GPU backend and adapter, which matters during
|
||||
// v0.1 because assumption A1 is exactly "does this compositing path work on
|
||||
@@ -43,6 +44,13 @@ export component StatusBar inherits Rectangle {
|
||||
/// Whether the edit can be stepped either way (FR-DEV-5).
|
||||
in property <bool> can-undo: false;
|
||||
in property <bool> can-redo: false;
|
||||
/// TRACES: FR-CAT-5
|
||||
/// The open photograph's colour label code, 0 for none, -1 where there
|
||||
/// is no library to hold one (files named on the command line).
|
||||
in property <int> colour-label: -1;
|
||||
/// Whether the label choices are open in the strip.
|
||||
property <bool> picking-label: false;
|
||||
callback label-picked(int);
|
||||
|
||||
callback back-to-library();
|
||||
callback open-people();
|
||||
@@ -115,6 +123,32 @@ export component StatusBar inherits Rectangle {
|
||||
|
||||
Caption { text: root.position; }
|
||||
|
||||
// TRACES: FR-CAT-5 | FR-UI-4 | NFR-A11Y-3
|
||||
// The label, shown and set here. The button says the label's name
|
||||
// beside its mark, so the strip reads "Label: Green" in words; with
|
||||
// none it offers "Label". Pressing it opens the six choices in the
|
||||
// strip, which close on the one chosen.
|
||||
Button {
|
||||
text: root.colour-label > 0 ? "Label: " + Labels.names[root.colour-label] : "Label";
|
||||
active: root.picking-label;
|
||||
visible: root.colour-label >= 0;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.picking-label = !root.picking-label; }
|
||||
}
|
||||
|
||||
if root.colour-label > 0: LabelMark {
|
||||
code: root.colour-label;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
if root.picking-label && root.colour-label >= 0: LabelPicker {
|
||||
current: root.colour-label;
|
||||
chosen(code) => {
|
||||
root.label-picked(code);
|
||||
root.picking-label = false;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
// --- instrumentation lives in Settings now ------------------------
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// TRACES: FR-CAT-5 | NFR-A11Y-3
|
||||
// Colour labels: the mark that shows one, and the row that chooses one.
|
||||
//
|
||||
// **A label is named, not only tinted.** NFR-A11Y-3 names colour labels as
|
||||
// the first thing that must not be told apart by hue alone, and a colour
|
||||
// label is the one status here whose *name* is a colour — so the temptation
|
||||
// is exactly a swatch. Every mark therefore carries the label's initial
|
||||
// (R, Y, G, B, P) in dark ink on the fill, and every place a label can be
|
||||
// chosen or filtered on says its name in words. Red and green are the pair
|
||||
// the commonest colour-vision deficiency merges, and they are "R" and "G"
|
||||
// here before they are anything else.
|
||||
//
|
||||
// The integers are `versions.label`'s own codes (`dr_catalog::rating::
|
||||
// label_code`): 0 for none, 1 red, 2 yellow, 3 green, 4 blue, 5 purple. One
|
||||
// numbering from the column to the pixel, so nothing translates between them.
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
|
||||
/// TRACES: FR-CAT-5 | NFR-A11Y-3
|
||||
/// What each label code is called, and the letter its mark carries.
|
||||
export global Labels {
|
||||
/// Indexed by code. "None" at 0 is what the picker's clearing choice says.
|
||||
out property <[string]> names: ["None", "Red", "Yellow", "Green", "Blue", "Purple"];
|
||||
/// The glyph on the mark, indexed by code. Distinct for every label: this
|
||||
/// is what carries the label for an eye that does not separate the fills.
|
||||
out property <[string]> letters: ["", "R", "Y", "G", "B", "P"];
|
||||
|
||||
/// The fill for a code. Data rather than chrome, the same exception the
|
||||
/// histogram's traces take (see `style.yaml`): the label *is* a colour.
|
||||
public pure function fill(code: int) -> color {
|
||||
if (code == 1) { return Theme.label-red; }
|
||||
if (code == 2) { return Theme.label-yellow; }
|
||||
if (code == 3) { return Theme.label-green; }
|
||||
if (code == 4) { return Theme.label-blue; }
|
||||
if (code == 5) { return Theme.label-purple; }
|
||||
return transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-5 | NFR-A11Y-3
|
||||
/// One label, drawn: a rounded tag in the label's colour with its initial
|
||||
/// on it. Invisible for code 0, so a caller can place it unconditionally.
|
||||
export component LabelMark inherits Rectangle {
|
||||
in property <int> code: 0;
|
||||
in property <length> size: 16px;
|
||||
|
||||
accessible-role: text;
|
||||
accessible-label: Labels.names[root.code] + " label";
|
||||
|
||||
visible: root.code > 0 && root.code <= 5;
|
||||
width: root.size;
|
||||
height: root.size;
|
||||
border-radius: root.size / 4;
|
||||
background: Labels.fill(root.code);
|
||||
// A rim in the ground colour, so a yellow tag on a pale sky still has an
|
||||
// edge.
|
||||
border-width: 1px;
|
||||
border-color: Theme.ground;
|
||||
|
||||
Text {
|
||||
// The letter is the label. Dark ink on every fill: the fills are held
|
||||
// to mid-tones in `style.yaml` so one ink reads on all five.
|
||||
text: Labels.letters[root.code];
|
||||
color: Theme.ground;
|
||||
font-size: root.size * 0.62;
|
||||
font-weight: 800;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-CAT-5 | FR-UI-4 | NFR-A11Y-3
|
||||
/// The six choices — none, then the five labels — each a mark and a word.
|
||||
///
|
||||
/// Inline rather than a popup: it opens in the bar that asked for it, where
|
||||
/// a finger already is, and there is no second surface to dismiss.
|
||||
export component LabelPicker inherits HorizontalLayout {
|
||||
/// The code currently carried, drawn as pressed. -1 where the photographs
|
||||
/// being labelled disagree, so nothing is shown as chosen.
|
||||
in property <int> current: -1;
|
||||
|
||||
callback chosen(int);
|
||||
|
||||
spacing: 4px;
|
||||
|
||||
for code in [0, 1, 2, 3, 4, 5]: Rectangle {
|
||||
accessible-role: button;
|
||||
accessible-label: code == 0 ? "No label" : Labels.names[code] + " label";
|
||||
accessible-checkable: true;
|
||||
accessible-checked: root.current == code;
|
||||
accessible-action-default => { root.chosen(code); }
|
||||
|
||||
height: Theme.control-height - 4px;
|
||||
width: row.preferred-width + 2 * Theme.gap-sm;
|
||||
y: (parent.height - self.height) / 2;
|
||||
border-radius: Theme.radius;
|
||||
border-width: 1px;
|
||||
border-color: root.current == code ? Theme.active : Theme.rule;
|
||||
background: touch.pressed ? Theme.pressed
|
||||
: (touch.has-hover ? Theme.hover : Theme.surface-raised);
|
||||
|
||||
touch := TouchArea {
|
||||
width: 100%;
|
||||
height: max(parent.height, Theme.touch-target);
|
||||
y: (parent.height - self.height) / 2;
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.chosen(code); }
|
||||
}
|
||||
|
||||
row := HorizontalLayout {
|
||||
padding-left: Theme.gap-sm;
|
||||
padding-right: Theme.gap-sm;
|
||||
spacing: 4px;
|
||||
|
||||
if code > 0: LabelMark {
|
||||
code: code;
|
||||
size: 14px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Labels.names[code];
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text-sm;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
-7
@@ -15,6 +15,7 @@ import { Button, IconButton, Label, Value, Caption, EmptyState, FilterChip, Prog
|
||||
// too many.
|
||||
import { CollectionRow } from "collections.slint";
|
||||
import { GestureSheet, GestureRow } from "gestures.slint";
|
||||
import { Labels, LabelMark, LabelPicker } from "labels.slint";
|
||||
|
||||
// TRACES: FR-CAT-5
|
||||
// One keyword in the keywording sheet, already answered against the selection.
|
||||
@@ -462,6 +463,10 @@ export struct LibraryCell {
|
||||
// 0 unflagged, 1 pick, 2 reject. Independent of the stars: rejecting a
|
||||
// four-star frame is a normal thing to do mid-cull.
|
||||
flag: int,
|
||||
// TRACES: FR-CAT-5
|
||||
// The colour label: 0 none, 1..5 red, yellow, green, blue, purple —
|
||||
// `versions.label`'s own codes (see labels.slint).
|
||||
label: int,
|
||||
// Frames in the burst this cell belongs to (FR-CULL-5), itself included; 0 where it
|
||||
// belongs to none, which is most of a library. A burst that is collapsed
|
||||
// draws only its representative, so on that cell this is the count of what
|
||||
@@ -640,6 +645,26 @@ export global Library {
|
||||
/// cell under the pointer, or -1: Rust applies the key to that photograph
|
||||
/// alone unless it is part of the selection (darktable's rule).
|
||||
callback library-judged(int, int, int);
|
||||
/// TRACES: FR-CAT-5 | FR-UI-5
|
||||
/// A label key (6-9) was pressed in the grid: the label code, then the
|
||||
/// ordinal under the pointer or -1, which reaches the same photographs a
|
||||
/// rating key would. The key toggles — see `dr_catalog::rating::
|
||||
/// toggled_label`.
|
||||
callback library-labelled(int, int);
|
||||
/// TRACES: FR-CAT-5 | FR-UI-4
|
||||
/// A label was chosen from the selection bar's picker, 0 for none. Sets
|
||||
/// rather than toggles: the picker names the answer outright.
|
||||
callback library-label-chosen(int);
|
||||
/// TRACES: FR-CAT-5
|
||||
/// One photograph by row, from develop: the code, and whether it toggles
|
||||
/// (a key) or sets (the picker).
|
||||
callback library-cell-labelled(int, int, bool);
|
||||
/// TRACES: FR-CAT-6
|
||||
/// The label the grid is narrowed to, 0 for none; and the images behind
|
||||
/// each label, index 0 unlabelled, for the chips.
|
||||
in property <int> library-filter-label: 0;
|
||||
in-out property <[int]> library-label-counts;
|
||||
callback library-filter-label-changed(int);
|
||||
in property <int> library-filter-min-rating: 0;
|
||||
/// TRACES: FR-UI-5
|
||||
/// The top of a star range, 5 where there is none, and the F chord that
|
||||
@@ -1519,6 +1544,15 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// star, so neither axis is disturbed by a press on the other. The third
|
||||
/// is `hover-ordinal`: which photograph the pointer is over, if any.
|
||||
callback judged(int, int, int);
|
||||
/// TRACES: FR-CAT-5 | FR-UI-5
|
||||
/// A label key, with `hover-ordinal` as for `judged`.
|
||||
callback labelled(int, int);
|
||||
/// TRACES: FR-CAT-5 | FR-UI-4
|
||||
/// A label chosen from the selection bar, 0 for none.
|
||||
callback label-chosen(int);
|
||||
/// Whether the selection bar's label picker is open. Local, like
|
||||
/// `keywording`.
|
||||
property <bool> labelling: false;
|
||||
|
||||
/// TRACES: FR-UI-5
|
||||
/// The library ordinal of the cell under the pointer, -1 when none.
|
||||
@@ -1810,6 +1844,11 @@ export component LibraryGrid inherits Rectangle {
|
||||
callback filter-min-rating-changed(int);
|
||||
callback filter-unjudged-toggled(bool);
|
||||
callback filter-flag-changed(int);
|
||||
/// TRACES: FR-CAT-6
|
||||
/// The colour label narrowed to, 0 for none, and each label's count.
|
||||
in property <int> filter-label: 0;
|
||||
in property <[int]> label-counts;
|
||||
callback filter-label-changed(int);
|
||||
|
||||
// --- offline (FR-CAT-9) -------------------------------------------------
|
||||
//
|
||||
@@ -2342,7 +2381,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// Hidden while there is nothing to filter: an empty library offering
|
||||
// six rating buttons is chrome describing data that does not exist.
|
||||
if root.total > 0 || root.filter-min-rating > 0 || root.filter-unjudged
|
||||
|| root.filter-flag > 0 || root.filter-people.length > 0
|
||||
|| root.filter-flag > 0 || root.filter-label > 0 || root.filter-people.length > 0
|
||||
|| root.filter-eyes-open: Rectangle {
|
||||
height: 34px;
|
||||
background: Theme.surface;
|
||||
@@ -2481,12 +2520,14 @@ export component LibraryGrid inherits Rectangle {
|
||||
FilterChip {
|
||||
label: "All";
|
||||
active: root.filter-min-rating == 0 && root.filter-max-rating >= 5
|
||||
&& !root.filter-unjudged && root.filter-flag == 0;
|
||||
&& !root.filter-unjudged && root.filter-flag == 0
|
||||
&& root.filter-label == 0;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
root.filter-rating-range(0, 5);
|
||||
root.filter-unjudged-toggled(false);
|
||||
root.filter-flag-changed(0);
|
||||
root.filter-label-changed(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2558,6 +2599,32 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
Rectangle { width: Theme.gap; }
|
||||
|
||||
// GESTURE: Show only photographs with one colour label
|
||||
// where: Library grid
|
||||
// touch: Tap its chip in the filter bar
|
||||
// pointer: Click its chip in the filter bar
|
||||
// why: Each chip is the label's mark and its name, so
|
||||
// the one you want is found by reading it; tap
|
||||
// the lit chip again to show every label.
|
||||
//
|
||||
// TRACES: FR-CAT-6 | NFR-A11Y-3
|
||||
// One chip per colour label, each the cell's own mark and the
|
||||
// label's name — never a bare swatch, which is the control
|
||||
// NFR-A11Y-3 was written against. One label at a time, and
|
||||
// pressing the lit one clears it, as the flag chips do.
|
||||
for n in [1, 2, 3, 4, 5]: FilterChip {
|
||||
colour-label: n;
|
||||
label: Labels.names[n];
|
||||
count: root.label-counts.length > n ? root.label-counts[n] : -1;
|
||||
active: root.filter-label == n;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
root.filter-label-changed(root.filter-label == n ? 0 : n);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { width: Theme.gap; }
|
||||
|
||||
// Locally-stored originals. Always offered, not only when
|
||||
// offline: "what can I actually work on right now" is a fair
|
||||
// question on a slow connection too, and a control that
|
||||
@@ -2597,7 +2664,8 @@ export component LibraryGrid inherits Rectangle {
|
||||
// single most confusing state a filter can leave behind.
|
||||
Caption {
|
||||
text: (root.filter-min-rating > 0 || root.filter-unjudged
|
||||
|| root.filter-flag > 0 || root.local-only
|
||||
|| root.filter-flag > 0 || root.filter-label > 0
|
||||
|| root.local-only
|
||||
|| root.filter-eyes-open)
|
||||
? "filtered" : "";
|
||||
emphasised: true;
|
||||
@@ -2980,6 +3048,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
root.keywording = false;
|
||||
return accept;
|
||||
}
|
||||
if (root.labelling) {
|
||||
root.labelling = false;
|
||||
return accept;
|
||||
}
|
||||
if (root.filing) {
|
||||
root.filing = false;
|
||||
return accept;
|
||||
@@ -3085,6 +3157,29 @@ export component LibraryGrid inherits Rectangle {
|
||||
if (event.text == "3") { root.judged(3, -1, root.hover-ordinal); return accept; }
|
||||
if (event.text == "4") { root.judged(4, -1, root.hover-ordinal); return accept; }
|
||||
if (event.text == "5") { root.judged(5, -1, root.hover-ordinal); return accept; }
|
||||
// GESTURE: Give photographs a colour label
|
||||
// where: Library grid
|
||||
// touch: Select them, then Label in the selection bar
|
||||
// and tap a colour
|
||||
// pointer: Select them, then Label in the selection bar
|
||||
// and click a colour
|
||||
// keys: 6 red, 7 yellow, 8 green, 9 blue, with the
|
||||
// pointer over it or on the selection; the
|
||||
// same key again takes the label off
|
||||
// why: Lightroom's keys, so hands that learned them
|
||||
// there need not learn them again. Purple has
|
||||
// no key there either, and is on the bar. Every
|
||||
// mark carries its label's initial, so the
|
||||
// label is read without telling the colours
|
||||
// apart.
|
||||
//
|
||||
// TRACES: FR-CAT-5 | FR-UI-5 | NFR-A11Y-3
|
||||
// After the F chord above, which has returned for its own
|
||||
// digits (0-5 only), so these never filter.
|
||||
if (event.text == "6") { root.labelled(1, root.hover-ordinal); return accept; }
|
||||
if (event.text == "7") { root.labelled(2, root.hover-ordinal); return accept; }
|
||||
if (event.text == "8") { root.labelled(3, root.hover-ordinal); return accept; }
|
||||
if (event.text == "9") { root.labelled(4, root.hover-ordinal); return accept; }
|
||||
// Case-insensitive: caps lock during a long cull must not
|
||||
// silently stop the keys working.
|
||||
if (event.text == "p" || event.text == "P") {
|
||||
@@ -3629,10 +3724,25 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
}
|
||||
|
||||
Label {
|
||||
text: cell.name;
|
||||
emphasised: cell.selected;
|
||||
overflow: elide;
|
||||
// TRACES: FR-CAT-5 | NFR-A11Y-3
|
||||
// The colour label leads the caption: on the
|
||||
// photograph it would compete with the flag and the
|
||||
// badges for four corners, and here it is read with
|
||||
// the name, in the row every cell already has.
|
||||
HorizontalLayout {
|
||||
spacing: 4px;
|
||||
|
||||
if cell.label > 0: LabelMark {
|
||||
code: cell.label;
|
||||
size: 14px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
Label {
|
||||
text: cell.name;
|
||||
emphasised: cell.selected;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4734,6 +4844,23 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5 | FR-UI-4
|
||||
// Label the selection. Opens the six choices in the bar itself —
|
||||
// a mark and a name each — and closes on the one chosen.
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Label";
|
||||
active: root.labelling;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.labelling = !root.labelling; }
|
||||
}
|
||||
|
||||
if !root.ranging && root.selected-count > 0 && root.labelling: LabelPicker {
|
||||
chosen(code) => {
|
||||
root.label-chosen(code);
|
||||
root.labelling = false;
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// Paste the copied edit onto the selection. One of the things a
|
||||
// selection can have done to it, like filing it in a collection,
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Icon } from "icons.slint";
|
||||
import { LabelMark } from "labels.slint";
|
||||
|
||||
export { Icon }
|
||||
|
||||
@@ -267,6 +268,12 @@ export component FilterChip inherits Rectangle {
|
||||
/// Drawn rather than prefixed to `label`, because a symbol inside a string
|
||||
/// is exactly the thing [`Icon`] exists to stop (see icons.slint).
|
||||
in property <string> icon;
|
||||
/// TRACES: FR-CAT-6 | NFR-A11Y-3
|
||||
/// A colour label's mark before the word, for the label chips — the
|
||||
/// same mark the grid cell carries, so the chip and the photographs it
|
||||
/// narrows to are visibly one thing. 0 for none. The word stays: the
|
||||
/// mark's letter is the fallback for the eye, the name is the chip.
|
||||
in property <int> colour-label: 0;
|
||||
|
||||
callback clicked();
|
||||
|
||||
@@ -323,6 +330,12 @@ export component FilterChip inherits Rectangle {
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
if root.colour-label > 0: LabelMark {
|
||||
code: root.colour-label;
|
||||
size: 14px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.label;
|
||||
// Dark on the active fill, which is near-white — the same
|
||||
|
||||
Reference in New Issue
Block a user