Files
DarkRoom/ui/dr-ui/ui/labels.slint
T
dtourolle 46f5b95828 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.
2026-09-24 21:52:23 -04:00

132 lines
5.0 KiB
Plaintext

// 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;
}
}
}
}