Put the adjustment groups in the rail where a finger is driving
Reported from the tablet: the tool rail is very useful there, and the same interface under a mouse and keyboard is not. That is `ui-navigation.md` D-N2's central assumption failing in use, and the interesting part is which half of it failed. D-N2 was right that platform is the wrong axis and width is the wrong axis: a tablet in landscape wants what a desktop wants, and a desktop window dragged narrow wants what a small screen wants. `apply_layout_class` still decides the layout class from the window and nothing here changes that. What D-N2 got wrong is the sentence "touch changes hit regions, not layout" — it identified input as the real difference between the targets and then assumed that difference could never reach the layout. Two controls answer one question — which group of adjustments am I looking at — and neither is better in general. A horizontal strip above the column is one gesture to a target the eye has already found, and it pans when the operation set is rich, so a group can sit off the end with nothing saying so: a pointer user tolerates that, a finger user never discovers it. The same list down the rail is every entry visible at once, each finger-sized, on the edge of the screen the hand is already holding, and it costs no width because the rail is already there. So `ToolRail` grows a second section, and `GroupStrip` stands down when it does. The two are never both on screen, which is why they can share `adjust-tab-picked`: Rust is not told which was pressed and has no reason to want to. Mode and group stay independent axes as N1 requires — one entry lit in each section, and choosing a group while a tool is held still filters without putting the tool down. They stay drawn differently, which N1 also required. The tools fill with `active-dim` and invert their ink; the groups take a bar down the leading edge — the strip's underline turned ninety degrees — so a lit entry says which kind of state it is without the reader having to remember which section it was in. The rule between the sections is the second signal. The rail scrolls now. Its own note argued against a Flickable because "this list is four entries written in this file"; with the groups in it the list comes from the operation set, which is exactly the "something the user's data decides" that note excluded this control from. The axis is input, and it is a preference because the automatic answer is a guess that cannot be made reliable. Neither platform can be asked what the user is holding: an Android tablet in a keyboard case is being driven like a desktop, and a touchscreen laptop is whichever its owner says. `dr_plat::is_touch_first` reports the usual case per platform, and `GroupNavigation` lets it be overridden. Settings names what Automatic resolves to on this device rather than leaving it to be found by pressing. D-N6 records the reversal beside the decision it reverses, including the half that still stands and the question it opens: whether Local is a mode at all, or a scope that would collapse the two sections into one list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+189
-16
@@ -1,12 +1,24 @@
|
||||
// The develop view's tool rail: which tool the photographer is holding.
|
||||
// The develop view's tool rail: which tool the photographer is holding, and —
|
||||
// where the interface is driven by a finger — which group of adjustments they
|
||||
// are looking at.
|
||||
//
|
||||
// **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.
|
||||
// **Two sections, two sources, one rule between them.**
|
||||
//
|
||||
// The tools are 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 and there is no second list that could fall out of
|
||||
// step with it.
|
||||
//
|
||||
// The groups are not written here at all, and must not be: they are whatever
|
||||
// the operation set declares itself to be about, resolved in Rust and handed
|
||||
// over as `tabs` (FR-DEV-3a). This file names no group, exactly as
|
||||
// `GroupStrip` names none — it takes the same model, because the two controls
|
||||
// answer the same question and only one of them is on screen at a time.
|
||||
//
|
||||
// **Why a rail is the right shape for a finger and the wrong one for a mouse.**
|
||||
// See `groups-in-rail` below, and D-N6 in `docs/ui-navigation.md` for the
|
||||
// decision it reverses and the half of that decision that still stands.
|
||||
//
|
||||
// **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
|
||||
@@ -65,7 +77,55 @@ export component ToolRail inherits Rectangle {
|
||||
/// dead icons beside a blank canvas suggest otherwise.
|
||||
in property <bool> enabled: true;
|
||||
|
||||
/// The adjustment groups, already resolved — the same model `GroupStrip`
|
||||
/// takes, because it is the same list answering the same question.
|
||||
///
|
||||
/// Empty unless this rail is carrying them. Nothing here names a group:
|
||||
/// the strings arrive from whatever the operations declared themselves to
|
||||
/// be about (FR-DEV-3a).
|
||||
in property <[string]> tabs;
|
||||
/// Index into `tabs`, or -1 for "everything".
|
||||
in property <int> active-tab: -1;
|
||||
/// TRACES: FR-UI-1 | FR-UI-7
|
||||
/// Whether the groups live in this rail or in the strip above the develop
|
||||
/// column.
|
||||
///
|
||||
/// **The one property that makes this rail two different controls**, and
|
||||
/// it is set from how the interface is being *driven* rather than from
|
||||
/// which binary is running — see `input_class` in `lib.rs`.
|
||||
///
|
||||
/// With a mouse, a horizontal run of words above the column is a tab bar,
|
||||
/// which is what a pointer is good at: it is one gesture to a target the
|
||||
/// eye has already found, and the strip costs a row of a column that has
|
||||
/// plenty of height. With a finger it is the wrong control twice over. The
|
||||
/// strip pans when the operation set is rich, so a group can be off the
|
||||
/// end of a row with nothing saying so; and it sits at the top of a
|
||||
/// column, which on a tablet held in two hands is the furthest point from
|
||||
/// either thumb.
|
||||
///
|
||||
/// Down the rail the same list is a column of finger-sized targets, all of
|
||||
/// them visible at once, on the edge of the screen a hand is already at.
|
||||
in property <bool> groups-in-rail: false;
|
||||
|
||||
callback picked(ViewMode);
|
||||
/// A group was chosen: an index into `tabs`, or -1 for "everything".
|
||||
///
|
||||
/// Deliberately the same signature `GroupStrip` emits, and routed to the
|
||||
/// same callback in `app.slint`. The two controls are alternatives, not
|
||||
/// peers — only one is on screen at a time — and giving them one contract
|
||||
/// means Rust cannot tell which of them the user pressed, and has no
|
||||
/// reason to want to.
|
||||
callback group-picked(int);
|
||||
|
||||
/// The groups this rail actually draws.
|
||||
///
|
||||
/// A conditional model rather than an `if` wrapped around the repeater:
|
||||
/// Slint has no way to nest one inside the other, and putting the
|
||||
/// condition on the model keeps the entries as direct children of the
|
||||
/// layout below — which is the shape that matters here. A nested layout
|
||||
/// under-reports its height and its siblings get drawn on top of each
|
||||
/// other; `app.slint`'s develop column carries the same note.
|
||||
private property <[string]> rail-tabs: root.groups-in-rail ? root.tabs : [];
|
||||
|
||||
// **The table.** Add a row to get a tool.
|
||||
//
|
||||
@@ -95,14 +155,27 @@ export component ToolRail inherits Rectangle {
|
||||
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 {
|
||||
// **A Flickable now, and this comment used to argue the opposite.** It
|
||||
// said the rail held four entries written in this file, on an axis with
|
||||
// room for fourteen, and that a rail which scrolls is the answer only if
|
||||
// that stops being true. It has stopped being true: with `groups-in-rail`
|
||||
// the list is four tools plus one entry per group the operation set
|
||||
// declares, and an operation set is exactly the "something the user's data
|
||||
// decides" the old note excluded this control from.
|
||||
//
|
||||
// Four tools, "All" and today's five groups is ten entries — comfortable
|
||||
// on any supported screen. The point is not today's count but that the
|
||||
// count is no longer written here, and a rail that overflows loses its
|
||||
// last entries silently, on the one control the develop view is navigated
|
||||
// by.
|
||||
Flickable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
viewport-width: self.width;
|
||||
viewport-height: max(self.height, layout.preferred-height);
|
||||
|
||||
layout := VerticalLayout {
|
||||
height: parent.viewport-height;
|
||||
padding-top: Theme.gap-sm;
|
||||
spacing: 0px;
|
||||
alignment: start;
|
||||
@@ -202,6 +275,106 @@ export component ToolRail inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **The seam between the two kinds of entry.**
|
||||
//
|
||||
// A rule and a gap, because above it are things that change what a
|
||||
// click on the photograph *does* and below it are things that change
|
||||
// which sliders are on screen. `ui-navigation.md` §N1 made that
|
||||
// distinction by drawing a mode and a group differently in one strip;
|
||||
// it holds here by separating them, which is the cheaper signal when
|
||||
// the axis is vertical and there is a whole rail's width to draw a
|
||||
// line across.
|
||||
if root.groups-in-rail: Rectangle {
|
||||
height: Theme.gap;
|
||||
background: transparent;
|
||||
|
||||
Rectangle {
|
||||
x: Theme.gap-sm;
|
||||
width: parent.width - 2 * Theme.gap-sm;
|
||||
height: 1px;
|
||||
y: (parent.height - 1px) / 2;
|
||||
background: Theme.rule;
|
||||
}
|
||||
}
|
||||
|
||||
// "All", and it is not decoration. A group filter that cannot be
|
||||
// cleared is a way to make controls unreachable, and this is the only
|
||||
// entry in the run below that is not generated.
|
||||
if root.groups-in-rail: all := TouchArea {
|
||||
height: Theme.touch-target;
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.group-picked(-1); }
|
||||
|
||||
accessible-role: button;
|
||||
accessible-label: "All adjustments";
|
||||
accessible-checkable: true;
|
||||
accessible-checked: root.active-tab == -1;
|
||||
accessible-action-default => { root.group-picked(-1); }
|
||||
|
||||
Rectangle {
|
||||
x: 0;
|
||||
width: 2px;
|
||||
height: parent.height;
|
||||
background: root.active-tab == -1 ? Theme.ink : transparent;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "All";
|
||||
font-size: Theme.text-sm;
|
||||
color: root.active-tab == -1
|
||||
? Theme.ink
|
||||
: (all.has-hover ? Theme.ink : Theme.ink-faint);
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// **A bar down the leading edge, not a filled tile.**
|
||||
//
|
||||
// The tools above fill with `active-dim` and invert their ink; these
|
||||
// do not, and the difference is the one §N1 insisted on — a mode and a
|
||||
// filter are not the same kind of state, and a reader should not have
|
||||
// to remember which section a lit entry was in to know which they are
|
||||
// looking at. An underline is what said so when the groups were a
|
||||
// horizontal strip; turned ninety degrees, that is a bar down the
|
||||
// edge.
|
||||
for tab[i] in root.rail-tabs: group := TouchArea {
|
||||
height: Theme.touch-target;
|
||||
mouse-cursor: pointer;
|
||||
clicked => { root.group-picked(i); }
|
||||
|
||||
accessible-role: button;
|
||||
accessible-label: tab;
|
||||
accessible-checkable: true;
|
||||
accessible-checked: root.active-tab == i;
|
||||
accessible-action-default => { root.group-picked(i); }
|
||||
|
||||
Rectangle {
|
||||
x: 0;
|
||||
width: 2px;
|
||||
height: parent.height;
|
||||
background: root.active-tab == i ? Theme.ink : transparent;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: tab;
|
||||
font-size: Theme.text-sm;
|
||||
color: root.active-tab == i
|
||||
? Theme.ink
|
||||
: (group.has-hover ? Theme.ink : Theme.ink-faint);
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// A group's name comes from the operation set and this rail is
|
||||
// a mandated width, so a long one has to give somewhere.
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The rail's own edge. Drawn here rather than by whatever contains it, so
|
||||
|
||||
Reference in New Issue
Block a user