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
127 lines
4.2 KiB
QML
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)));
|
|
}
|
|
}
|
|
}
|
|
}
|