Give the controls a vocabulary, and let a node ask for one
widgets.slint set the rule — screens consume components, and a bare `Theme.*` at a call site means a component is missing — and it set it for chrome only. The controls never got the same treatment, so they were written wherever they were first needed and copied from there. **The slider was private to the develop panel.** `SliderTrack`, with the fifty-line preamble explaining how it wrests a drag away from a Flickable, lived inside adjust.slint and no other screen could reach it. It shows: export quality is a 1-to-100 value, and the settings page offered a free-text box for it, with the range written in a hint and enforced nowhere. `to-float()` answers 0 for anything it cannot parse, so a typo saved a quality of 0 and the page displayed the 0 back as though it had been asked for. The tick-box was written twice, in launch.slint and settings.slint, from the same 18px box and the same handler; the second carried a comment deferring the lift until a third caller appeared. The label-and-hint header was written three times inside settings.slint alone. controls.slint is the input layer beside widgets.slint's chrome layer, and the constraint that makes it reusable is that **nothing in it knows about `ParamRow`** — that struct is the develop panel's flattening of the capability model, and a control that imported it could only ever be used by the develop panel. The primitives take plain numbers; the ParamRow-shaped wrappers stay in the panel that owns the model. 658 lines came out of the three screens. `SliderRow` is the slider-plus-number-box ARCH §4.3 names as the pointer presentation of a bounded scalar, and quality is its first adopter. It commits on gesture end rather than on every movement, because the settings page saves to disk on change and a two-second drag is a couple of hundred writes where a text field committed once. The develop panel keeps the live stream — that is what its pipeline is for — so `SliderTrack` now reports both. **The other half is the descriptor.** FR-DEV-3a and ARCH §4.3a already specify more than was built: an ordered preference list of widgets rather than one, the demands a widget makes, and kinds beyond scalar and bool. - `Presentation.widgets` is now a list, walked by `choose`, falling back to plain sliders. Falling off the end is not an error, and there is a test asserting an operation asking only for an unimplemented widget still yields one control per parameter. - `WidgetDemand` carries what a widget inherently needs — two-dimensional dragging, precise pointing — and no pixels, breakpoints or platform names. - `WidgetKind` grows to the specified set. There is deliberately no `Colour` *kind*: a colour is three numbers, and a value type that is not an `f32` would reach through the graph, the uniform block and the sidecar format to buy what `ColourWheel` over three scalars already describes. Every widget here is a hint over ordinary scalars, which is what keeps the fallback honest. - `ParamKind::Enum` is the one new shape, and it fits because a variant index is exact in binary32. `kind: enum` with a `variants:` list works in `ops/*.yaml`, so a node declaring one gets a segmented control with no UI file edited — which is the promise ops/mod.rs already makes. The panel's dispatch was duplicated: a lone parameter and a grouped one each wrote out their own list of kinds, so `enum` would have had to be added twice and a kind added to one would appear or vanish depending on how many parameters its operation happened to declare. `ParamControl` is now the only such chain. `rows_from` is free-standing rather than a method, which is what lets the FR-DEV-3c acceptance test requirements.md asks for actually be written: an operation the frontend has never heard of, appearing in a generated panel, with no GPU in sight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+134
-426
@@ -9,6 +9,7 @@
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { PanelHeading, Label, Value, Caption, Button, IconButton, Swatch } from "widgets.slint";
|
||||
import { SliderTrack, ControlRow, CurveEditor, Segmented } from "controls.slint";
|
||||
|
||||
// One parameter, flattened for Slint's model system.
|
||||
//
|
||||
@@ -43,7 +44,7 @@ export struct ParamRow {
|
||||
|
||||
// Which control to build. Mirrors ParamKind, plus the widget kinds an
|
||||
// operation can request through its presentation.
|
||||
kind: string, // "scalar" | "bool" | "curve"
|
||||
kind: string, // "scalar" | "bool" | "enum" | "curve"
|
||||
|
||||
// A run of rows inside a group, for an operation whose parameters form a
|
||||
// grid rather than a list.
|
||||
@@ -83,6 +84,14 @@ export struct ParamRow {
|
||||
// `param-index` on a curve row is the index of the *first* point
|
||||
// parameter, so a drag routes back by offsetting from it.
|
||||
points: [float],
|
||||
|
||||
// Enum rows only: the variant names, in index order.
|
||||
//
|
||||
// Resolved in Rust against the UI's catalogue, like every other label
|
||||
// here — the core publishes localisation keys and never a display string.
|
||||
// `value` on such a row is the chosen index, which is why an enum needs no
|
||||
// separate selection field.
|
||||
choices: [string],
|
||||
}
|
||||
|
||||
// The name of a group of controls, and what can be done to the group.
|
||||
@@ -151,165 +160,6 @@ component GroupHeading inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// **The** slider. One track, one hit area, one set of gesture rules.
|
||||
//
|
||||
// This exists because there used to be two of these, written out separately —
|
||||
// one reading its geometry from a `ParamRow` for the generated panel, one
|
||||
// taking plain numbers for the straighten angle — with a comment claiming the
|
||||
// duplication was safe because "the track behaviour is the same and
|
||||
// deliberately so". It was not safe and did not stay the same: the moment the
|
||||
// touch arbitration below was fixed in one copy, the two sliders in the same
|
||||
// sidebar started behaving differently, and which one you got depended on which
|
||||
// panel you happened to be dragging in. The wrappers below now differ only in
|
||||
// where their numbers come from.
|
||||
component SliderTrack inherits Rectangle {
|
||||
in property <float> value;
|
||||
in property <float> default-value;
|
||||
in property <float> minimum;
|
||||
in property <float> maximum;
|
||||
|
||||
callback changed(float);
|
||||
callback reset();
|
||||
/// The pointer is on this control. A scrolling ancestor listens so it can
|
||||
/// stand down — see the note on `engaged` below.
|
||||
callback engaged-changed(bool);
|
||||
|
||||
height: Theme.touch-target / 2;
|
||||
|
||||
// Guarded, because a descriptor with a zero range would otherwise divide
|
||||
// by nothing and put every position at infinity.
|
||||
property <float> span: max(0.000001, root.maximum - root.minimum);
|
||||
|
||||
// **Why hover, and not the drag itself.**
|
||||
//
|
||||
// A Flickable does not merely compete for a gesture, it *withholds* the
|
||||
// press: `DelayForwarding` holds it back for 100ms and only delivers it if
|
||||
// nothing has claimed the gesture by then. A finger that starts moving
|
||||
// inside that window therefore leaves this TouchArea never pressed at all —
|
||||
// so `moved` never fires, and any handler keyed on the drag having started
|
||||
// can never run. That is why the panel kept taking sliders away from a
|
||||
// finger while a tap worked perfectly: a tap's release arrives before the
|
||||
// 100ms is up, so press and release are delivered together, and only a
|
||||
// *drag* falls in the hole.
|
||||
//
|
||||
// Hover is the one signal that does get through. Move events are dispatched
|
||||
// to children even while the press is withheld, so the moment a finger
|
||||
// lands on a track and travels a pixel this goes true, the panel sets
|
||||
// `interactive: false`, and the Flickable stops arbitrating before it can
|
||||
// capture anything.
|
||||
//
|
||||
// The cost is that a drag *starting* on a track no longer scrolls the
|
||||
// panel. The label above each track and the padding around it still do, and
|
||||
// the wheel is unaffected — a Flickable handles wheel events whether or not
|
||||
// it is interactive.
|
||||
property <bool> engaged: area.has-hover || area.claimed;
|
||||
changed engaged => { root.engaged-changed(root.engaged); }
|
||||
|
||||
// The rail.
|
||||
Rectangle {
|
||||
y: (parent.height - 3px) / 2;
|
||||
height: 3px;
|
||||
background: Theme.surface-raised;
|
||||
border-radius: 1.5px;
|
||||
}
|
||||
|
||||
// The default position, drawn only when it is not at an end. Hand-built
|
||||
// rather than using the standard Slint slider so this can be marked at
|
||||
// all — a symmetric control needs to show where zero is.
|
||||
if root.minimum < root.default-value && root.default-value < root.maximum: Rectangle {
|
||||
x: (root.default-value - root.minimum) / root.span * parent.width - 1px;
|
||||
y: (parent.height - 9px) / 2;
|
||||
width: 2px;
|
||||
height: 9px;
|
||||
background: Theme.rule;
|
||||
}
|
||||
|
||||
// Fill from the default to the current value, so the control shows the
|
||||
// size and direction of the adjustment rather than an absolute magnitude.
|
||||
Rectangle {
|
||||
property <length> default-x:
|
||||
(root.default-value - root.minimum) / root.span * parent.width;
|
||||
property <length> value-x:
|
||||
(root.value - root.minimum) / root.span * parent.width;
|
||||
|
||||
x: min(self.default-x, self.value-x);
|
||||
width: abs(self.value-x / 1px - self.default-x / 1px) * 1px;
|
||||
y: (parent.height - 3px) / 2;
|
||||
height: 3px;
|
||||
// The fill is the engaged part of the control — the span the
|
||||
// photographer has actually moved — so it takes `active` rather than
|
||||
// the ink the rest of the track is drawn in.
|
||||
background: Theme.active;
|
||||
border-radius: 1.5px;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
x: (root.value - root.minimum) / root.span * parent.width - 6px;
|
||||
y: (parent.height - 12px) / 2;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: area.has-hover || area.pressed ? Theme.ink : Theme.ink-dim;
|
||||
}
|
||||
|
||||
area := TouchArea {
|
||||
// Explicitly fill the track. A TouchArea with no geometry collapses to
|
||||
// zero and only reports the events that happen to land on it, which
|
||||
// shows up as a slider that clicks but does not drag.
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Whether this gesture has been claimed as a slider drag.
|
||||
//
|
||||
// A scrolling panel acts vertically and this control acts
|
||||
// horizontally, so the axis of the movement says which was meant.
|
||||
// Committing on press-down instead — the obvious approach — makes
|
||||
// every attempt to scroll from a slider jump its value first, which is
|
||||
// destructive and happens constantly given how much of a panel is
|
||||
// sliders.
|
||||
property <bool> claimed: false;
|
||||
|
||||
function value-at(px: length) -> float {
|
||||
return clamp(
|
||||
root.minimum + (px / self.width) * root.span,
|
||||
root.minimum,
|
||||
root.maximum);
|
||||
}
|
||||
|
||||
moved => {
|
||||
// `moved` fires only while pressed, so this is a drag.
|
||||
if (!self.claimed
|
||||
&& abs(self.mouse-x - self.pressed-x)
|
||||
> abs(self.mouse-y - self.pressed-y)) {
|
||||
self.claimed = true;
|
||||
}
|
||||
if (self.claimed) {
|
||||
root.changed(self.value-at(self.mouse-x));
|
||||
}
|
||||
}
|
||||
pointer-event(ev) => {
|
||||
if (ev.kind == PointerEventKind.up
|
||||
|| ev.kind == PointerEventKind.cancel) {
|
||||
self.claimed = false;
|
||||
}
|
||||
// Right-click resets, alongside double-click.
|
||||
if (ev.kind == PointerEventKind.down
|
||||
&& ev.button == PointerEventButton.right) {
|
||||
root.reset();
|
||||
}
|
||||
}
|
||||
clicked => {
|
||||
// A press with no meaningful drag: jump to it. Handled on release
|
||||
// rather than on press so it cannot fire during a scroll that
|
||||
// merely started here.
|
||||
if (!self.claimed) {
|
||||
root.changed(self.value-at(self.mouse-x));
|
||||
}
|
||||
}
|
||||
double-clicked => { root.reset(); }
|
||||
}
|
||||
}
|
||||
|
||||
// A parameter's value, at the precision its descriptor declares.
|
||||
//
|
||||
// A global rather than the same ternary written into every control that shows
|
||||
@@ -327,6 +177,13 @@ global Readout {
|
||||
}
|
||||
|
||||
// One generated parameter: a label, a readout, and the track above.
|
||||
//
|
||||
// The readout is *not* editable, where the settings page's `SliderRow` pairs
|
||||
// the same track with a number box. That is a considered difference rather than
|
||||
// an inconsistency: this column is 280px wide and the colour mixer alone puts
|
||||
// thirty-six of these in it, so a text box per row would be most of the width
|
||||
// and a keyboard target nobody is aiming for. The number is still reachable —
|
||||
// the track resets on double-click and right-click.
|
||||
component ParamSlider inherits Rectangle {
|
||||
in property <ParamRow> data;
|
||||
callback changed(float);
|
||||
@@ -336,27 +193,13 @@ component ParamSlider inherits Rectangle {
|
||||
|
||||
height: 46px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 2px;
|
||||
|
||||
HorizontalLayout {
|
||||
Label {
|
||||
text: root.data.param-label;
|
||||
emphasised: root.data.value != root.data.default-value;
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
Value {
|
||||
text: Readout.of(root.data);
|
||||
// The one readout in the panel that has moved off its default
|
||||
// is what the eye is hunting for, and `modified` is the only
|
||||
// thing left to say it with once hue is gone.
|
||||
modified: root.data.value != root.data.default-value;
|
||||
placeholder: root.data.value == root.data.default-value;
|
||||
compact: true;
|
||||
}
|
||||
}
|
||||
ControlRow {
|
||||
label: root.data.param-label;
|
||||
readout: Readout.of(root.data);
|
||||
// The one readout in the panel that has moved off its default is what
|
||||
// the eye is hunting for, and `modified` is the only thing left to say
|
||||
// it with once hue is gone.
|
||||
modified: root.data.value != root.data.default-value;
|
||||
|
||||
SliderTrack {
|
||||
value: root.data.value;
|
||||
@@ -486,24 +329,10 @@ component PlainSlider inherits Rectangle {
|
||||
|
||||
height: 46px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 2px;
|
||||
|
||||
HorizontalLayout {
|
||||
Label {
|
||||
text: root.label;
|
||||
emphasised: root.value != root.default-value;
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
Value {
|
||||
text: (Math.round(root.value * 10) / 10) + root.unit;
|
||||
modified: root.value != root.default-value;
|
||||
placeholder: root.value == root.default-value;
|
||||
compact: true;
|
||||
}
|
||||
}
|
||||
ControlRow {
|
||||
label: root.label;
|
||||
readout: (Math.round(root.value * 10) / 10) + root.unit;
|
||||
modified: root.value != root.default-value;
|
||||
|
||||
SliderTrack {
|
||||
value: root.value;
|
||||
@@ -518,6 +347,97 @@ component PlainSlider inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// **The control registry: one row in, one control out.**
|
||||
//
|
||||
// Slint cannot instantiate a component from a runtime string, so mapping a
|
||||
// declared kind to a control is necessarily a chain of `if`s. The thing worth
|
||||
// insisting on is that there is exactly *one* such chain. There were two — the
|
||||
// panel draws a lone parameter bare and a group under a heading, and each
|
||||
// branch wrote out its own list of kinds — so `enum` would have had to be added
|
||||
// in both, and a kind added to only one would appear or vanish depending on how
|
||||
// many parameters its operation happened to declare.
|
||||
//
|
||||
// Everything below routes back through `param-changed` by index. This component
|
||||
// knows a curve point spans two parameters and a swatch names a hue band; it
|
||||
// knows nothing about which operation it is drawing, which is the property that
|
||||
// makes it a registry rather than a panel.
|
||||
component ParamControl inherits Rectangle {
|
||||
in property <ParamRow> data;
|
||||
/// Curve rows only; ignored by every other kind.
|
||||
in property <[float]> curve-samples;
|
||||
|
||||
callback param-changed(int, int, float);
|
||||
callback param-reset(int, int);
|
||||
callback curve-reset(int);
|
||||
callback drag-changed(bool);
|
||||
|
||||
height: layout.preferred-height;
|
||||
|
||||
layout := VerticalLayout {
|
||||
spacing: 0px;
|
||||
alignment: start;
|
||||
|
||||
// A row whose subject is a colour is identified by that colour; every
|
||||
// other scalar keeps its name. The two differ only in what stands in
|
||||
// for the label — the track, the gestures and the routing are the same
|
||||
// underneath.
|
||||
if root.data.kind == "scalar" && root.data.swatch-hue >= 0: SwatchSlider {
|
||||
data: root.data;
|
||||
drag-changed(on) => { root.drag-changed(on); }
|
||||
changed(v) => {
|
||||
root.param-changed(root.data.op-index, root.data.param-index, v);
|
||||
}
|
||||
reset => {
|
||||
root.param-reset(root.data.op-index, root.data.param-index);
|
||||
}
|
||||
}
|
||||
|
||||
if root.data.kind == "scalar" && root.data.swatch-hue < 0: ParamSlider {
|
||||
data: root.data;
|
||||
drag-changed(on) => { root.drag-changed(on); }
|
||||
changed(v) => {
|
||||
root.param-changed(root.data.op-index, root.data.param-index, v);
|
||||
}
|
||||
reset => {
|
||||
root.param-reset(root.data.op-index, root.data.param-index);
|
||||
}
|
||||
}
|
||||
|
||||
// A fixed list of alternatives. The value *is* the index, so picking
|
||||
// one is an ordinary parameter change and needs no separate route.
|
||||
//
|
||||
// Chips rather than a dropdown for the same reason the settings page
|
||||
// uses them: these lists are short, and a collapsed menu hides the
|
||||
// alternatives behind a click. ARCH §4.3 names the dropdown as the
|
||||
// pointer presentation of the same kind, so this is where that choice
|
||||
// will be made when the modality switch lands.
|
||||
if root.data.kind == "enum": Segmented {
|
||||
label: root.data.param-label;
|
||||
options: root.data.choices;
|
||||
selected: Math.round(root.data.value);
|
||||
picked(i) => {
|
||||
root.param-changed(root.data.op-index, root.data.param-index, i);
|
||||
}
|
||||
}
|
||||
|
||||
if root.data.kind == "curve": CurveEditor {
|
||||
points: root.data.points;
|
||||
samples: root.curve-samples;
|
||||
drag-changed(on) => { root.drag-changed(on); }
|
||||
// A point carries two parameters, so the parameter index is the
|
||||
// row's base plus the point's offset. This component still knows
|
||||
// nothing about which operation it belongs to.
|
||||
point-moved(point, x, y) => {
|
||||
root.param-changed(
|
||||
root.data.op-index, root.data.param-index + point * 2, x);
|
||||
root.param-changed(
|
||||
root.data.op-index, root.data.param-index + point * 2 + 1, y);
|
||||
}
|
||||
reset => { root.curve-reset(root.data.op-index); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Crop, rotation, flips and straightening — the framing controls.
|
||||
//
|
||||
// **Why this is hand-built when the rest of the panel is generated.** The
|
||||
@@ -632,165 +552,6 @@ export component GeometryPanel inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// A tone curve editor: a square grid with draggable control points.
|
||||
//
|
||||
// The curve *line* is drawn from `samples`, which Rust evaluates with the
|
||||
// same spline the shader uses. Reimplementing the interpolation here would
|
||||
// mean two curves that could disagree — the drawn one and the applied one —
|
||||
// which is the worst possible failure for a control whose whole job is to
|
||||
// show you what it is doing.
|
||||
component CurveEditor inherits Rectangle {
|
||||
in property <ParamRow> data;
|
||||
// Polyline of the curve, y values sampled at even x. 0..1, y up.
|
||||
in property <[float]> samples;
|
||||
// Which point is being dragged, or -1.
|
||||
in-out property <int> active-point: -1;
|
||||
|
||||
callback point-moved(int, float, float);
|
||||
callback reset();
|
||||
/// The pointer is on a control point. The panel stands its Flickable down
|
||||
/// while it is, for the reason spelled out on `ParamSlider`'s `engaged` —
|
||||
/// and more acutely here, because a curve point is dragged *vertically*,
|
||||
/// which is the Flickable's own axis and so is contested every time.
|
||||
callback drag-changed(bool);
|
||||
|
||||
property <int> point-count: root.data.points.length / 2;
|
||||
|
||||
// Hover, not the drag: `active-point` is set on press, and the press is
|
||||
// exactly what a Flickable withholds. Only the plot's grab targets count,
|
||||
// so the rest of the plot still scrolls the panel.
|
||||
property <bool> engaged: root.active-point >= 0 || root.hovered-point >= 0;
|
||||
changed engaged => { root.drag-changed(root.engaged); }
|
||||
|
||||
// The point the pointer is over, or -1. Set by the grab targets below,
|
||||
// and used only to highlight the marker.
|
||||
in-out property <int> hovered-point: -1;
|
||||
|
||||
// Square: a tone curve is read as a deviation from the 45° diagonal, and
|
||||
// that reading only works if the axes share a scale.
|
||||
height: self.width;
|
||||
|
||||
plot := Rectangle {
|
||||
background: Theme.ground;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
|
||||
// Quarter gridlines and the identity diagonal, so the shape of the
|
||||
// edit is legible at a glance.
|
||||
for i in [1, 2, 3]: Rectangle {
|
||||
x: parent.width * i / 4;
|
||||
width: 1px;
|
||||
background: Theme.rule;
|
||||
opacity: 0.5;
|
||||
}
|
||||
for i in [1, 2, 3]: Rectangle {
|
||||
y: parent.height * i / 4;
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// The curve. One thin rectangle per sample: Slint has no polyline
|
||||
// primitive, and at this size the segments are sub-pixel anyway.
|
||||
for s[i] in root.samples: Rectangle {
|
||||
property <float> next: i + 1 < root.samples.length
|
||||
? root.samples[i + 1] : s;
|
||||
x: parent.width * i / max(root.samples.length - 1, 1);
|
||||
width: parent.width / max(root.samples.length - 1, 1) + 1px;
|
||||
// Span the segment vertically, so a steep section stays joined.
|
||||
y: parent.height * (1.0 - max(s, self.next));
|
||||
height: max(parent.height * abs(self.next - s), 1.5px);
|
||||
background: Theme.active;
|
||||
}
|
||||
|
||||
// Control points.
|
||||
for idx in [0, 1, 2, 3, 4]: Rectangle {
|
||||
property <bool> exists: idx < root.point-count;
|
||||
property <float> px: root.data.points[idx * 2];
|
||||
property <float> py: root.data.points[idx * 2 + 1];
|
||||
|
||||
visible: self.exists;
|
||||
x: parent.width * self.px - 5px;
|
||||
y: parent.height * (1.0 - self.py) - 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
// Grown and tinted when grabbable, so it is obvious where the
|
||||
// curve takes the gesture and where the panel scrolls instead.
|
||||
property <bool> live: root.active-point == idx
|
||||
|| root.hovered-point == idx;
|
||||
background: self.live ? Theme.active : Theme.ink;
|
||||
border-width: 1px;
|
||||
border-color: Theme.ground;
|
||||
}
|
||||
|
||||
// **One grab target per point, and nothing covering the rest.**
|
||||
//
|
||||
// Three constraints meet here, and only this arrangement satisfies
|
||||
// all of them:
|
||||
//
|
||||
// 1. The panel scrolls, and Slint cannot hand back a press once
|
||||
// taken — so an area spanning the plot would swallow every scroll
|
||||
// gesture beginning over the curve. Small targets leave the rest
|
||||
// of the plot free.
|
||||
// 2. `enabled: false` does not work as a gate: a disabled TouchArea
|
||||
// recognises *no* events at all, hover included, so it cannot
|
||||
// report where the pointer is in order to decide.
|
||||
// 3. A target positioned by its own point would slide out from under
|
||||
// the pointer on the first movement, stalling the drag. So while
|
||||
// a point is being dragged its target **freezes** at the press
|
||||
// position and grows to cover the plot, keeping the pointer
|
||||
// inside it however far the point travels.
|
||||
for idx in [0, 1, 2, 3, 4]: TouchArea {
|
||||
property <bool> exists: idx < root.point-count;
|
||||
property <bool> dragging: root.active-point == idx;
|
||||
|
||||
// Frozen and expanded while dragging; tracking the point
|
||||
// otherwise.
|
||||
x: self.dragging ? 0px
|
||||
: parent.width * root.data.points[idx * 2] - 14px;
|
||||
y: self.dragging ? 0px
|
||||
: parent.height * (1.0 - root.data.points[idx * 2 + 1]) - 14px;
|
||||
width: self.dragging ? parent.width : 28px;
|
||||
height: self.dragging ? parent.height : 28px;
|
||||
visible: self.exists;
|
||||
mouse-cursor: pointer;
|
||||
|
||||
// Highlights the marker, so it is visible where the curve takes
|
||||
// the gesture and where the panel scrolls instead.
|
||||
changed has-hover => {
|
||||
if (self.has-hover) {
|
||||
root.hovered-point = idx;
|
||||
} else if (root.hovered-point == idx) {
|
||||
root.hovered-point = -1;
|
||||
}
|
||||
}
|
||||
|
||||
pointer-event(ev) => {
|
||||
if (ev.kind == PointerEventKind.down
|
||||
&& ev.button == PointerEventButton.left) {
|
||||
root.active-point = idx;
|
||||
}
|
||||
if (ev.kind == PointerEventKind.up
|
||||
|| ev.kind == PointerEventKind.cancel) {
|
||||
root.active-point = -1;
|
||||
}
|
||||
}
|
||||
moved => {
|
||||
if (self.dragging) {
|
||||
// Coordinates are relative to this area, which is the
|
||||
// whole plot while dragging — so no offset is needed.
|
||||
root.point-moved(
|
||||
idx,
|
||||
clamp(self.mouse-x / parent.width, 0.0, 1.0),
|
||||
clamp(1.0 - self.mouse-y / parent.height, 0.0, 1.0));
|
||||
}
|
||||
}
|
||||
double-clicked => { root.reset(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The panel: a heading per multi-parameter operation, a control per parameter.
|
||||
//
|
||||
// **Why the loop is shaped the way it is.** `rows` is flat, and Slint can
|
||||
@@ -906,33 +667,15 @@ export component AdjustPanel inherits Rectangle {
|
||||
//
|
||||
// `row` is the entry here — the group's head is its only
|
||||
// member — so there is nothing to index back into.
|
||||
if row.group-head == i && row.group-len == 1: VerticalLayout {
|
||||
spacing: 0px;
|
||||
|
||||
if row.kind == "scalar": ParamSlider {
|
||||
data: row;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
changed(v) => {
|
||||
root.param-changed(
|
||||
row.op-index, row.param-index, v);
|
||||
}
|
||||
reset => {
|
||||
root.param-reset(row.op-index, row.param-index);
|
||||
}
|
||||
}
|
||||
|
||||
if row.kind == "curve": CurveEditor {
|
||||
data: row;
|
||||
samples: root.curve-samples;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
point-moved(point, x, y) => {
|
||||
root.param-changed(
|
||||
row.op-index, row.param-index + point * 2, x);
|
||||
root.param-changed(
|
||||
row.op-index, row.param-index + point * 2 + 1, y);
|
||||
}
|
||||
reset => { root.curve-reset(row.op-index); }
|
||||
if row.group-head == i && row.group-len == 1: ParamControl {
|
||||
data: row;
|
||||
curve-samples: root.curve-samples;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
param-changed(op, param, v) => {
|
||||
root.param-changed(op, param, v);
|
||||
}
|
||||
param-reset(op, param) => { root.param-reset(op, param); }
|
||||
curve-reset(op) => { root.curve-reset(op); }
|
||||
}
|
||||
|
||||
// `if` rather than a zero height: a hidden-but-present
|
||||
@@ -975,52 +718,17 @@ export component AdjustPanel inherits Rectangle {
|
||||
title: entry.facet-label;
|
||||
}
|
||||
|
||||
// A row whose subject is a colour is identified by
|
||||
// that colour; every other scalar keeps its name.
|
||||
// The two differ only in what stands in for the
|
||||
// label — the track, the gestures and the routing
|
||||
// are the same underneath.
|
||||
if entry.kind == "scalar" && entry.swatch-hue >= 0: SwatchSlider {
|
||||
ParamControl {
|
||||
data: entry;
|
||||
curve-samples: root.curve-samples;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
changed(v) => {
|
||||
root.param-changed(
|
||||
entry.op-index, entry.param-index, v);
|
||||
param-changed(op, param, v) => {
|
||||
root.param-changed(op, param, v);
|
||||
}
|
||||
reset => {
|
||||
root.param-reset(entry.op-index, entry.param-index);
|
||||
}
|
||||
}
|
||||
|
||||
if entry.kind == "scalar" && entry.swatch-hue < 0: ParamSlider {
|
||||
data: entry;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
changed(v) => {
|
||||
root.param-changed(
|
||||
entry.op-index, entry.param-index, v);
|
||||
}
|
||||
reset => {
|
||||
root.param-reset(entry.op-index, entry.param-index);
|
||||
}
|
||||
}
|
||||
|
||||
if entry.kind == "curve": CurveEditor {
|
||||
data: entry;
|
||||
samples: root.curve-samples;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
// A point carries two parameters, so the
|
||||
// parameter index is the row's base plus the
|
||||
// point's offset. This component still knows
|
||||
// nothing about which operation it belongs to.
|
||||
point-moved(point, x, y) => {
|
||||
root.param-changed(
|
||||
entry.op-index, entry.param-index + point * 2, x);
|
||||
root.param-changed(
|
||||
entry.op-index, entry.param-index + point * 2 + 1, y);
|
||||
}
|
||||
reset => {
|
||||
root.curve-reset(entry.op-index);
|
||||
param-reset(op, param) => {
|
||||
root.param-reset(op, param);
|
||||
}
|
||||
curve-reset(op) => { root.curve-reset(op); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user