Files
Panama/config/dot/quickshell/widgets/ValueSlider.qml
T
Gabriel Brown e6b4d3c1a1 Stop lists and sliders from losing input under the user
WifiList and the notification toasts built their model from a plain
computed array, so any background property tick (a scan result, an
unrelated notification arriving) reassigned the whole array and the
Repeater destroyed and recreated every delegate -- including one with
an open, focused password field or an in-progress reply. Switched
both to a ScriptModel, which diffs by identity instead of resetting.

ValueSlider had its pointer-to-value mapping offset by 16px (the
hit-area margin was applied with the wrong sign), so 0% was
unreachable and every click landed to the right of where it was
placed -- affects every slider in the shell. SliderRow used -1 as a
sentinel for "nothing pending," which collides with legitimate
negative preference values like pointer sensitivity.

Dock intellihide read the globally focused workspace instead of each
monitor's own, so an empty workspace on one screen could hide the
dock on another; ActivityPanel rebuilt every row once a second during
a recording because the elapsed-time read lived in the model
construction instead of each row's own binding.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-18 21:22:58 -04:00

127 lines
4.2 KiB
QML

// A GNOME-style slider: a thick rounded track with an inline leading icon,
// used for volume and brightness in the quick settings panel.
//
// Deliberately not QtQuick.Controls.Slider — that pulls in a style plugin and
// fights the theme. This is ~50 lines and does exactly what's needed.
import QtQuick
import Quickshell
import Quickshell.Widgets
import qs.config
Item {
id: root
property real value: 0 // 0.0 .. 1.0
property string icon: ""
property bool enabled: true
// Emitted continuously while dragging.
signal moved(real value)
implicitWidth: 240
implicitHeight: 32
opacity: enabled ? 1.0 : 0.45
// ThemedIcon, not a bare IconImage — see the note in Toggle.qml: Adwaita
// symbolic icons have a hardcoded near-black fill that Qt will not recolour.
ThemedIcon {
id: leadingIcon
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
size: 18
icon: root.icon
tint: Theme.fg
visible: root.icon !== ""
}
Rectangle {
id: track
anchors.left: leadingIcon.visible ? leadingIcon.right : parent.left
anchors.leftMargin: leadingIcon.visible ? 10 : 0
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
height: 10
radius: height / 2
color: Theme.alpha(Theme.fg, 0.12)
border.width: 0
Rectangle {
id: fill
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.max(0, Math.min(1, root.value))
radius: parent.radius
border.width: 0
// The gradient is anchored to the full track, not to the fill, so
// the colour at a given position never moves as the value changes —
// dragging reveals the prism rather than squashing it.
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: Theme.accent
}
GradientStop {
position: 1.0
color: Theme.mix(Theme.accent, Theme.accentSecondary, Math.max(0.001, Math.min(1, root.value)))
}
}
// Only animates when the value changes from outside (e.g. a media
// key); dragging sets it directly and reads as instant.
Behavior on width {
enabled: !drag.pressed
NumberAnimation {
duration: Theme.durFast
easing.type: Easing.OutQuad
}
}
}
Rectangle {
id: knob
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, fill.width - width / 2))
visible: drag.containsMouse || drag.pressed
}
MouseArea {
id: drag
anchors.fill: parent
anchors.margins: -8 // easier to grab than a 10px track
enabled: root.enabled
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function apply(mouseX) {
// mouseX is local to this MouseArea, whose anchors.margins: -8
// pushes its own origin 8px before the track's left edge. So
// MouseArea-local x=8 is track-local x=0 -- subtract the
// margin to land back in the track's own coordinate space
// before turning it into a fraction.
const ratio = Math.max(0, Math.min(1, (mouseX - 8) / track.width));
root.moved(ratio);
}
onPressed: event => apply(event.x)
onPositionChanged: event => {
if (pressed)
apply(event.x);
}
onWheel: event => {
const step = event.angleDelta.y > 0 ? 0.05 : -0.05;
root.moved(Math.max(0, Math.min(1, root.value + step)));
}
}
}
}