Put a scrollbar on the develop column

The column (histogram, compose, every adjustment group, history) runs
several screens past the window. A mouse without a wheel could only drag
the column's contents, and a drag that starts on a slider moves the
slider.

The Flickable now sits in a plain Rectangle with a ScrollBar beside it,
bound to its viewport. Nothing about how the column sizes itself
changes: the Flickable fills the Rectangle, and the Rectangle takes the
stretch the Flickable had in the layout under the group strip. The bar
is drawn over the column's right-hand 10px, so the mandated panel width
is not reduced. When the pointer is on the bar, the lit track covers
the value labels of the compose rows, which already sit flush against
the column's edge.
This commit is contained in:
2026-09-26 07:25:06 -04:00
parent 2ef971bf37
commit 825015a58e
2 changed files with 192 additions and 175 deletions
+183 -166
View File
@@ -9,7 +9,7 @@ import { LaunchScreen } from "launch.slint";
import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip, Library } from "library.slint";
import { GestureRow, GestureSheet } from "gestures.slint";
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow, Scrolling } from "widgets.slint";
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow, Scrolling, ScrollBar } from "widgets.slint";
import { CollectionsPanel, CollectionRow, Collections, OfflinePrompt, CollectionMenu,
MembershipSheet, MembershipRow } from "collections.slint";
import { HistogramPanel, HistogramView, Levels } from "histogram.slint";
@@ -3124,175 +3124,192 @@ in property <bool> panel-visible: true;
// before. `GroupStrip.live` holds the rule.
GroupStrip { }
Flickable {
viewport-height: column.preferred-height;
// **And the width, or the column is drawn centred
// over its own edges.**
//
// Unset, a Flickable's viewport takes its content's
// preferred width, and a viewport wider than the
// Flickable is *centred* in it — the same rule the
// note on the seam's `x: 0` below records. So a
// column narrower than what it held did not clip
// the right-hand side and keep the left, it lost
// half of each: "HISTOGRAM" read "ISTOGRAM",
// "Straighten" read "aighten", and Paste ran off
// the far side. It looked like a rendering fault
// and it was an alignment one.
//
// **`min-width`, not `preferred-width`, and that is
// what makes a mandated column work at all.**
//
// With the preferred width here, the content laid
// itself out at whatever size it would have liked
// and the column clipped the difference: Paste came
// out sliced down the middle and the panel's reset
// was over the window's edge. That is the same
// fault as above wearing a different hat — the
// content was never told how much room it had, so
// it could not adapt to having less.
//
// The floor is the layout's *minimum* instead. A
// preferred width is what a panel would enjoy; a
// minimum is what it cannot go below, and between
// the two the layout does the work it exists to do
// — rows tighten, stretches give way — so the
// column fits because it was asked to rather than
// by luck.
//
// That does less here than it sounds like, and it
// is worth knowing why: this column's minimum and
// its preferred width are within a few pixels of
// each other, because a `Text` that does not elide
// reports the same for both and most of what is in
// here is text. So `panel-width` still has to be a
// number the contents actually fit in — see its
// note in `style.yaml` for the measurement. What
// this binding buys is that the clipping, when it
// comes, is of a panel that genuinely cannot
// shrink rather than of one that simply was not
// asked to, and that panning is then a real
// fallback rather than a permanent condition.
viewport-width: max(self.width, column.min-width);
interactive: !adjust.slider-dragging;
Rectangle {
vertical-stretch: 1;
column-flick := Flickable {
width: parent.width;
height: parent.height;
viewport-height: column.preferred-height;
// **And the width, or the column is drawn centred
// over its own edges.**
//
// Unset, a Flickable's viewport takes its content's
// preferred width, and a viewport wider than the
// Flickable is *centred* in it — the same rule the
// note on the seam's `x: 0` below records. So a
// column narrower than what it held did not clip
// the right-hand side and keep the left, it lost
// half of each: "HISTOGRAM" read "ISTOGRAM",
// "Straighten" read "aighten", and Paste ran off
// the far side. It looked like a rendering fault
// and it was an alignment one.
//
// **`min-width`, not `preferred-width`, and that is
// what makes a mandated column work at all.**
//
// With the preferred width here, the content laid
// itself out at whatever size it would have liked
// and the column clipped the difference: Paste came
// out sliced down the middle and the panel's reset
// was over the window's edge. That is the same
// fault as above wearing a different hat — the
// content was never told how much room it had, so
// it could not adapt to having less.
//
// The floor is the layout's *minimum* instead. A
// preferred width is what a panel would enjoy; a
// minimum is what it cannot go below, and between
// the two the layout does the work it exists to do
// — rows tighten, stretches give way — so the
// column fits because it was asked to rather than
// by luck.
//
// That does less here than it sounds like, and it
// is worth knowing why: this column's minimum and
// its preferred width are within a few pixels of
// each other, because a `Text` that does not elide
// reports the same for both and most of what is in
// here is text. So `panel-width` still has to be a
// number the contents actually fit in — see its
// note in `style.yaml` for the measurement. What
// this binding buys is that the clipping, when it
// comes, is of a panel that genuinely cannot
// shrink rather than of one that simply was not
// asked to, and that panning is then a real
// fallback rather than a permanent condition.
viewport-width: max(self.width, column.min-width);
interactive: !adjust.slider-dragging;
// **What the column holds is the mode's answer.**
//
// In local mode it is the mask stack and then the
// selected layer's adjustments, and nothing else:
// the capture metadata describes the file, copy and
// paste move a whole edit between photographs, and
// framing is a decision about the picture's shape —
// none of the three is a property of a region
// within it, so all three would be controls in
// scope of nothing.
//
// **Each child carries its own condition** rather
// than the three being grouped inside one `if`.
// A nested layout under-reports its height here and
// the panels below it get drawn on top of each
// other — invisible in this file and obvious on
// screen. `masks.slint` carries the same note for
// the same reason.
column := VerticalLayout {
if !Develop.local-mode && !Develop.repairing: InfoPanel { }
// **What the column holds is the mode's answer.**
//
// In local mode it is the mask stack and then the
// selected layer's adjustments, and nothing else:
// the capture metadata describes the file, copy and
// paste move a whole edit between photographs, and
// framing is a decision about the picture's shape —
// none of the three is a property of a region
// within it, so all three would be controls in
// scope of nothing.
//
// **Each child carries its own condition** rather
// than the three being grouped inside one `if`.
// A nested layout under-reports its height here and
// the panels below it get drawn on top of each
// other — invisible in this file and obvious on
// screen. `masks.slint` carries the same note for
// the same reason.
column := VerticalLayout {
if !Develop.local-mode && !Develop.repairing: InfoPanel { }
if !Develop.local-mode && !Develop.repairing: Rectangle {
height: 1px;
background: Theme.rule;
if !Develop.local-mode && !Develop.repairing: Rectangle {
height: 1px;
background: Theme.rule;
}
// Directly under the capture metadata and above every
// control, because it is the thing the controls are
// judged against: exposure, blacks and whites are all
// set by watching this move (FR-DSP-7). An instrument
// below the sliders it reports on would have the
// photographer looking away from it to use it.
//
// Kept in local mode, where it still reports the
// whole frame while the sliders edit a layer. That
// disagreement is real and is N3's to close; taking
// the instrument away instead would be a worse
// answer than an honest one that is not yet scoped.
HistogramPanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// TRACES: FR-CULL-3
// Under the histogram, because the two are the same
// kind of thing: instruments that report on the
// photograph rather than change it. Kept in every
// mode for the same reason the histogram is.
FocusPanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// Framing above the colour work, matching how the edit is
// made rather than how it is applied: the frame is decided
// by eye first and the pipeline runs it last (see
// `dr_pipeline::framing` on the coordinate order).
if !Develop.local-mode && !Develop.repairing: ComposePanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// Above the adjust panel, because it decides what
// those sliders act on. Below it, the photographer
// would set an exposure and only then discover which
// scope it landed in.
//
// Only in local mode: it is no longer a panel among
// peers that silently re-points another panel, it is
// what that mode's column *is*. That also takes one
// panel out of a scrolling column that had six.
// TRACES: FR-DEV-8
// What the column *is* in repair mode, on the same
// terms as the mask panel: not a panel among peers
// that quietly re-points the sliders below it, but
// the whole of what this mode has to say.
if Develop.repairing: SpotPanel { }
if Develop.repairing: Rectangle {
height: 1px;
background: Theme.rule;
}
if Develop.local-mode: MaskPanel { }
if Develop.local-mode: Rectangle {
height: 1px;
background: Theme.rule;
}
adjust := AdjustPanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// Last in the column, and the length is why. The
// stack runs sixty-four deep, so anywhere else it
// would push the controls it is a record of below
// the fold. Its own rows run newest-first, which
// puts the steps worth reaching immediately under
// the heading rather than at the far end of them.
//
// Present in local mode as well. The mask work
// *is* history — adding a layer, moving a
// gradient and feathering an edge are all steps —
// and a list that emptied when the photographer
// entered the mode that produces the most steps
// would be a list they stopped trusting.
HistoryPanel { }
}
}
// Directly under the capture metadata and above every
// control, because it is the thing the controls are
// judged against: exposure, blacks and whites are all
// set by watching this move (FR-DSP-7). An instrument
// below the sliders it reports on would have the
// photographer looking away from it to use it.
//
// Kept in local mode, where it still reports the
// whole frame while the sliders edit a layer. That
// disagreement is real and is N3's to close; taking
// the instrument away instead would be a worse
// answer than an honest one that is not yet scoped.
HistogramPanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// TRACES: FR-CULL-3
// Under the histogram, because the two are the same
// kind of thing: instruments that report on the
// photograph rather than change it. Kept in every
// mode for the same reason the histogram is.
FocusPanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// Framing above the colour work, matching how the edit is
// made rather than how it is applied: the frame is decided
// by eye first and the pipeline runs it last (see
// `dr_pipeline::framing` on the coordinate order).
if !Develop.local-mode && !Develop.repairing: ComposePanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// Above the adjust panel, because it decides what
// those sliders act on. Below it, the photographer
// would set an exposure and only then discover which
// scope it landed in.
//
// Only in local mode: it is no longer a panel among
// peers that silently re-points another panel, it is
// what that mode's column *is*. That also takes one
// panel out of a scrolling column that had six.
// TRACES: FR-DEV-8
// What the column *is* in repair mode, on the same
// terms as the mask panel: not a panel among peers
// that quietly re-points the sliders below it, but
// the whole of what this mode has to say.
if Develop.repairing: SpotPanel { }
if Develop.repairing: Rectangle {
height: 1px;
background: Theme.rule;
}
if Develop.local-mode: MaskPanel { }
if Develop.local-mode: Rectangle {
height: 1px;
background: Theme.rule;
}
adjust := AdjustPanel { }
Rectangle {
height: 1px;
background: Theme.rule;
}
// Last in the column, and the length is why. The
// stack runs sixty-four deep, so anywhere else it
// would push the controls it is a record of below
// the fold. Its own rows run newest-first, which
// puts the steps worth reaching immediately under
// the heading rather than at the far end of them.
//
// Present in local mode as well. The mask work
// *is* history — adding a layer, moving a
// gradient and feathering an edge are all steps —
// and a list that emptied when the photographer
// entered the mode that produces the most steps
// would be a list they stopped trusting.
HistoryPanel { }
// The column's bar, on a desktop: the sliders and the history run far
// past the window, and without it a mouse with no wheel had no way to
// see how far, or to get there. Over the column's right-hand edge
// rather than beside it, so the mandated width is not paid for twice.
ScrollBar {
x: parent.width - self.width;
y: 0px;
viewport-y <=> column-flick.viewport-y;
viewport-height: column-flick.viewport-height;
visible-height: column-flick.height;
}
}
}