Files
Panama/config/dot/quickshell/modules/settings/GradientSliderRow.qml
T

253 lines
8.9 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// A slider whose track carries a meaning, and whose value is committed once the
// gesture is over rather than on every pixel.
//
// Two rows on the Displays page need something SliderRow cannot give them:
//
// * The night light temperature, whose track should BE the warmth it sets --
// a neutral track with a prism fill says nothing about what 2700 K looks
// like, and this is the one slider whose numbers most people cannot picture.
// * The SDR trims, which write through the display transaction. A transaction
// per pixel of drag would arm a fifteen-second countdown dozens of times;
// the value is applied once, when the button comes up.
//
// On release, not on an idle timer. Holding a slider still for a third of a
// second is not the end of a gesture, and the commit an idle timer made
// mid-hold armed a display transaction that turned this row's `enabled` false
// with the button still down -- the control went dead under the hand using it.
// The wheel has no release, so it keeps the timer, and that is all the timer is
// for now.
//
// Not schema-bound and not a SettingRow: the caller says what the value is and
// what to do with a new one, and the row stacks its control below the label in
// a narrow window exactly as SliderRow does.
import QtQuick
import qs.config
Item {
id: root
property string label: ""
property string detail: ""
property bool divider: true
property bool enabled: true
property real minimum: 0
property real maximum: 100
property real step: 1
// What is in effect. While a drag is in flight the row shows `pending`
// instead, and hands the display back once the commit has been made.
property real value: 0
property var pending: null
readonly property real shown: root.pending !== null ? root.pending : root.value
// The caller formats the number, because "1.15×", "3500 K" and "72%" have
// nothing in common but the digits.
property string readout: ""
// Two or three colors, left to right. Painted across the whole track when
// `fullTrack` is set -- for a value whose range is the point, like color
// temperature -- and as the fill alone otherwise.
property var trackColors: [Theme.accent, Theme.accentSecondary]
property bool fullTrack: false
property int commitDelay: 320
signal committed(real value)
readonly property bool inline: root.width >= 520
readonly property int controlSpan: 300
function stopColor(index: int): color {
const colors = root.trackColors;
if (!colors || colors.length === 0)
return Theme.accent;
return colors[Math.min(index, colors.length - 1)];
}
// Hands the pending value to the caller. Called on release, and by the
// wheel's idle timer, which is the one input that never has one.
function commit(): void {
commitTimer.stop();
if (root.pending === null)
return;
root.committed(root.pending);
releaseTimer.restart();
}
function quantise(ratio: real): real {
const raw = root.minimum + ratio * (root.maximum - root.minimum);
const snapped = Math.round(raw / root.step) * root.step;
const clamped = Math.max(root.minimum, Math.min(root.maximum, snapped));
return root.step < 1 ? Math.round(clamped * 100) / 100 : clamped;
}
width: parent ? parent.width : 620
implicitHeight: root.inline
? Math.max(56, copy.implicitHeight + 20)
: copy.implicitHeight + 32 + 30
opacity: root.enabled ? 1 : 0.45
Column {
id: copy
x: 0
y: root.inline ? (root.height - height) / 2 : 10
width: root.inline ? root.width - root.controlSpan - 20 : root.width
spacing: 3
Text {
width: parent.width
text: root.label
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.Medium
elide: Text.ElideRight
}
Text {
width: parent.width
visible: root.detail !== ""
text: root.detail
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
}
Item {
id: control
width: root.inline ? root.controlSpan : root.width
height: 32
x: root.inline ? root.width - width : 0
y: root.inline ? (root.height - height) / 2 : copy.y + copy.height + 10
Rectangle {
id: track
anchors.left: parent.left
anchors.right: valueLabel.left
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
height: 10
radius: 5
border.width: 0
color: Theme.alpha(Theme.fg, 0.12)
readonly property real ratio: root.maximum > root.minimum
? Math.max(0, Math.min(1, (root.shown - root.minimum) / (root.maximum - root.minimum)))
: 0
Rectangle {
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: root.fullTrack ? parent.width : parent.width * track.ratio
radius: parent.radius
border.width: 0
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: root.stopColor(0) }
GradientStop { position: 0.5; color: root.stopColor(1) }
GradientStop { position: 1.0; color: root.stopColor(2) }
}
}
Rectangle {
width: 16
height: 16
radius: 8
border.width: 0
color: Theme.fg
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(parent.width - width, parent.width * track.ratio - width / 2))
}
MouseArea {
id: drag
anchors.fill: parent
anchors.margins: -8
enabled: root.enabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
// Local x is 8px ahead of the track's own origin because of the
// negative margin above; subtract it before turning the
// position into a fraction of the track.
function move(positionX: real): void {
root.pending = root.quantise(
Math.max(0, Math.min(1, (positionX - 8) / track.width)));
}
onPressed: event => drag.move(event.x)
onPositionChanged: event => {
if (drag.pressed)
drag.move(event.x);
}
onReleased: root.commit()
// A grab taken away mid-drag is not a decision. The row goes
// back to showing whatever is really in effect rather than
// keeping a number nobody asked it to apply.
onCanceled: {
commitTimer.stop();
root.pending = null;
}
onWheel: event => {
const direction = event.angleDelta.y > 0 ? 1 : -1;
root.pending = Math.max(root.minimum,
Math.min(root.maximum, root.shown + direction * root.step));
commitTimer.restart();
}
}
}
Text {
id: valueLabel
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 58
horizontalAlignment: Text.AlignRight
text: root.readout
color: Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSize
elide: Text.ElideRight
}
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
visible: root.divider
color: Theme.alpha(Theme.fg, 0.065)
}
// The wheel's release. A notch is a whole gesture on its own, and a hand
// spinning through a range means one commit at the end of the spin rather
// than one per notch -- there is no button coming up to say when that is.
Timer {
id: commitTimer
interval: root.commitDelay
onTriggered: root.commit()
}
// Hands the readout back to whatever is really in effect. If the change was
// refused -- an unclean value, a busy transaction -- the row snaps back to
// the value it had rather than showing one nothing accepted.
Timer {
id: releaseTimer
interval: 400
onTriggered: root.pending = null
}
}