The rail is the develop view's primary navigation and reached the platform as four pieces of static text. The labels got through — a `Text` announces itself — so a screen reader could read "Photo, Crop, Local, Repair" and had no way to learn that any of them could be pressed, or which one was currently held. The one control that decides what a click on the photograph does was a caption. Each entry now declares itself a checkable button carrying the tool's own word, with the held state reported rather than left to the fill. Checkable is unconditional here, unlike `Button`'s: a rail entry is always a held-or-not state, so an unheld one should say "not pressed" rather than pass for an ordinary button. The action repeats the click handler's expression rather than calling it. A `TouchArea`'s `clicked` is raised by the pointer and cannot be raised from a binding, so the alternative is a function wrapping two lines — and the duplicated ternary sits four lines below the original where the two cannot drift out of sight of each other. Worth recording for the next control like this: `accessible-*` on an element inside a `for` does work, despite the accessibility pass skipping repeated elements. `process_repeater_components` runs first and moves the bindings into a real component whose root is not repeated; what the later pass skips is the empty placeholder left behind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
216 lines
10 KiB
Plaintext
216 lines
10 KiB
Plaintext
// The develop view's tool rail: which tool the photographer is holding.
|
|
//
|
|
// **One table, one file.** Everything that decides what this rail contains is
|
|
// the array literal in `TOOLS` below. A tool is a row in it — an icon name, a
|
|
// word, and the `ViewMode` it arms — and adding one is that row plus a drawing
|
|
// in `icons.slint` plus a variant on the enum. Nothing in `app.slint` is
|
|
// touched, nothing here is per-tool, and there is no second list anywhere that
|
|
// could fall out of step with this one. That is the whole design: the rail is
|
|
// generated, so it cannot be *partly* updated.
|
|
//
|
|
// **Why it left the chip strip.** These four used to be chips at the top of
|
|
// the develop column, sharing a row with the adjustment groups — two kinds of
|
|
// state in one strip, told apart by the shape of their highlight. Three things
|
|
// were wrong with that, and only the third is about tidiness:
|
|
//
|
|
// 1. The column can be put away. The strip went with it, so the way *out* of
|
|
// crop went away with the way in, and the fix was a second "Done
|
|
// Cropping" button floating over the canvas — one control duplicated
|
|
// because the first one was reachable only sometimes.
|
|
// 2. The strip sized the column. Its chips are generated from the operation
|
|
// set, so the widest thing in the develop sidebar was a row nobody had
|
|
// chosen the contents of, and a richer operation set silently took width
|
|
// from the photograph.
|
|
// 3. A mode and a filter are not the same kind of thing. "Crop" changes what
|
|
// a click on the photograph does; "Light" changes which sliders are on
|
|
// screen. Putting them in one row and distinguishing them by underline
|
|
// versus fill asks the eye to carry a distinction the layout could just
|
|
// make.
|
|
//
|
|
// The rail is always up, so (1) is gone; it is a fixed width that no operation
|
|
// set can influence, so (2) is gone; and it is somewhere else entirely, so (3)
|
|
// is gone. What is left in the column is a row of group filters and nothing
|
|
// else — see `GroupStrip` in `adjust.slint`.
|
|
//
|
|
// **Down the left, not the right.** The develop column is on the right and
|
|
// holds the *consequences* of a choice — the sliders the tool exposes. The
|
|
// choice itself goes on the far side, so the eye's path across the window is
|
|
// tool, photograph, adjustment, in that order, and the rail never moves when
|
|
// the column opens and closes beside it.
|
|
|
|
import { Theme } from "theme.slint";
|
|
import { Icon } from "icons.slint";
|
|
import { ViewMode } from "adjust.slint";
|
|
|
|
// One tool. A struct rather than four parallel arrays so a row cannot be
|
|
// half-added — the compiler will not let a new entry omit its icon.
|
|
struct Tool {
|
|
/// A name from `icons.slint`'s vocabulary. A typo here draws nothing,
|
|
/// which is loud: the entry becomes a label with a hole above it.
|
|
icon: string,
|
|
/// What it is called. One word — see `rail-width` in `style.yaml`.
|
|
label: string,
|
|
/// What arming it puts the canvas into.
|
|
mode: ViewMode,
|
|
}
|
|
|
|
export component ToolRail inherits Rectangle {
|
|
/// Which tool is held. Owned by Rust, like every other piece of session
|
|
/// state: the rail asks for a mode and is told what the mode became, so a
|
|
/// change made anywhere else — the keyboard, the back gesture, the button
|
|
/// over the canvas — lights the same entry.
|
|
in property <ViewMode> mode: ViewMode.photo;
|
|
/// Whether there is a photograph to point a tool at. The rail collapses
|
|
/// rather than greying out: an empty develop view has no tools, and four
|
|
/// dead icons beside a blank canvas suggest otherwise.
|
|
in property <bool> enabled: true;
|
|
|
|
callback picked(ViewMode);
|
|
|
|
// **The table.** Add a row to get a tool.
|
|
//
|
|
// Order is the order they appear, and it is not arbitrary: `photo` first
|
|
// because it is the resting state and the way back from everywhere else,
|
|
// then the three that arm a gesture on the canvas, roughly in the order a
|
|
// photograph is worked — frame it, then adjust parts of it, then clean it
|
|
// up.
|
|
//
|
|
// `MaskSource::Brush` is the next one to land here. The core already has
|
|
// the stroke calls; what is missing is the canvas interaction, and when it
|
|
// arrives this file's share of the work is one line.
|
|
private property <[Tool]> tools: [
|
|
{ icon: "photo", label: "Photo", mode: ViewMode.photo },
|
|
{ icon: "crop", label: "Crop", mode: ViewMode.crop },
|
|
{ icon: "mask", label: "Local", mode: ViewMode.local },
|
|
{ icon: "repair", label: "Repair", mode: ViewMode.spots },
|
|
];
|
|
|
|
// TRACES: FR-UI-1
|
|
// Fixed, and the point of the exercise. This rail flanks the photograph,
|
|
// so its width is taken out of the picture — and a rail measured from its
|
|
// contents would hand that decision to whichever tool label happens to be
|
|
// longest. `style.yaml` carries the number and the reasoning.
|
|
width: root.enabled ? Theme.rail-width : 0px;
|
|
visible: root.enabled;
|
|
background: Theme.surface;
|
|
clip: true;
|
|
|
|
// No Flickable. Every other strip in this view has one, because every
|
|
// other strip is generated from something the user's data decides and can
|
|
// therefore outgrow its space. This list is four entries written in this
|
|
// file, running down an axis with a whole window of room — the shortest
|
|
// supported window fits fourteen. If that ever stops being true the answer
|
|
// is a rail that scrolls, not a rail that overflows, and this comment is
|
|
// where to start.
|
|
VerticalLayout {
|
|
padding-top: Theme.gap-sm;
|
|
spacing: 0px;
|
|
alignment: start;
|
|
|
|
for tool in root.tools: entry := TouchArea {
|
|
height: Theme.rail-entry-height;
|
|
mouse-cursor: pointer;
|
|
|
|
property <bool> on: root.mode == tool.mode;
|
|
|
|
// Pressing the tool you are holding puts it down, exactly as the
|
|
// chips did: the same control both directions. `photo` is the
|
|
// exception — it *is* putting the tool down, so pressing it while
|
|
// it is lit is a no-op rather than a toggle into itself.
|
|
clicked => {
|
|
root.picked(entry.on ? ViewMode.photo : tool.mode);
|
|
}
|
|
|
|
// **The word below is drawn; this is what makes it a control.**
|
|
// Without these the rail reaches a screen reader as four pieces of
|
|
// static text — the labels get through, because a `Text` announces
|
|
// itself, and nothing says any of them can be pressed. That is the
|
|
// develop view's primary navigation reduced to a caption.
|
|
//
|
|
// `checkable` unconditionally, unlike `Button`'s: a rail entry is
|
|
// always a held-or-not state, so an unheld one should say "not
|
|
// pressed" rather than pass for an ordinary button. The action
|
|
// repeats the click handler rather than calling it, because a
|
|
// `TouchArea`'s `clicked` is raised by the pointer and cannot be
|
|
// raised from here.
|
|
accessible-role: button;
|
|
accessible-label: tool.label;
|
|
accessible-checkable: true;
|
|
accessible-checked: entry.on;
|
|
accessible-action-default => {
|
|
root.picked(entry.on ? ViewMode.photo : tool.mode);
|
|
}
|
|
|
|
// The lit tile, and the only marker there is. Inset from the
|
|
// rail's edges so the run of four reads as four things rather than
|
|
// as one striped column.
|
|
//
|
|
// A bright bar against the outer edge was tried alongside it, on
|
|
// the theory that a rail is scanned from the side and needs
|
|
// something unambiguous. It is not needed and it is not
|
|
// unambiguous: `active-dim` is near-white and `hover` is a shade
|
|
// above the surface, so the held tool and a tool under the pointer
|
|
// are not two similar greys — they are opposite ends of the
|
|
// palette. The bar sat against the lit tile and merged with it.
|
|
Rectangle {
|
|
x: Theme.gap-sm / 2;
|
|
width: parent.width - Theme.gap-sm;
|
|
height: parent.height - 2px;
|
|
y: 1px;
|
|
border-radius: Theme.radius;
|
|
background: entry.on
|
|
? Theme.active-dim
|
|
: (entry.has-hover ? Theme.hover : transparent);
|
|
}
|
|
|
|
VerticalLayout {
|
|
alignment: center;
|
|
spacing: 3px;
|
|
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
Icon {
|
|
name: tool.icon;
|
|
size: 20px;
|
|
// Dark on the lit tile, which is near-white: the same
|
|
// inversion `Button`'s primary state makes, and the
|
|
// same one the chips made before this.
|
|
ink: entry.on
|
|
? Theme.ground
|
|
: (entry.has-hover ? Theme.ink : Theme.ink-dim);
|
|
}
|
|
}
|
|
|
|
// Named, not just drawn. An icon-only rail is a quiz — these
|
|
// four are conventional enough to guess and not conventional
|
|
// enough to be sure of, and "sure" is what a tool that changes
|
|
// what a click does has to be. The word is `text-sm` and dim,
|
|
// so it reads as the icon's caption rather than as a button in
|
|
// its own right.
|
|
Text {
|
|
text: tool.label;
|
|
font-size: Theme.text-sm;
|
|
color: entry.on
|
|
? Theme.ground
|
|
: (entry.has-hover ? Theme.ink : Theme.ink-faint);
|
|
horizontal-alignment: center;
|
|
// Elided rather than wrapped: a two-line label would make
|
|
// this entry taller than the three beside it, and a rail
|
|
// whose rows are different heights reads as a list of
|
|
// unrelated things.
|
|
overflow: elide;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The rail's own edge. Drawn here rather than by whatever contains it, so
|
|
// the rail is a complete thing wherever it is put — and on the right,
|
|
// where it meets the canvas.
|
|
Rectangle {
|
|
x: parent.width - 1px;
|
|
width: 1px;
|
|
background: Theme.rule;
|
|
}
|
|
}
|