scrollbars QOL updates
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m10s

This commit is contained in:
2026-06-13 12:56:53 +02:00
parent 4b7ba30272
commit f1dbff1208
5 changed files with 127 additions and 137 deletions
+13
View File
@@ -0,0 +1,13 @@
import Quickshell.Widgets
import QtQuick
ClippingWrapperRectangle {
id: root
color: "transparent"
Behavior on color {
CAnim {
}
}
}
+97 -127
View File
@@ -5,52 +5,26 @@ import qs.Config
ScrollBar { ScrollBar {
id: root id: root
property bool _updatingFromFlickable: false readonly property real effectiveSize: Math.max(nonAnimHeight, root.minimumSize)
property bool _updatingFromUser: false readonly property real effectiveTravel: Math.max(0, 1 - root.effectiveSize)
property bool animating
required property Flickable flickable required property Flickable flickable
property real nonAnimPosition readonly property real nonAnimHeight: flickable.height / flickable.contentHeight
readonly property real nonAnimY: flickable.contentY / flickable.contentHeight
readonly property real rawTravel: Math.max(0, 1 - root.nonAnimHeight)
readonly property bool reversed: flickable instanceof ListView && flickable.verticalLayoutDirection === ListView.BottomToTop
property bool shouldBeActive property bool shouldBeActive
readonly property real travelScale: root.rawTravel > 0 ? root.effectiveTravel / root.rawTravel : 0
implicitWidth: mouse.containsMouse ? Appearance.padding.extraSmall * 2 : Appearance.padding.extraSmall implicitWidth: Appearance.padding.extraSmall * 2
contentItem: CustomRect {
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.palette.m3secondary
opacity: {
if (root.size === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (mouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
return 0;
}
radius: Appearance.rounding.full
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
contentItem: Item {
MouseArea { MouseArea {
id: mouse id: mouse
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
anchors.bottom: parent.bottom anchors.fill: parent
anchors.right: parent.right
anchors.top: parent.top
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
hoverEnabled: true hoverEnabled: true
implicitWidth: Appearance.padding.extraSmall * 2
}
}
Behavior on implicitWidth {
Anim {
} }
} }
Behavior on position { Behavior on position {
@@ -60,54 +34,21 @@ ScrollBar {
} }
} }
Component.onCompleted: {
if (flickable) {
const contentHeight = flickable.contentHeight;
const height = flickable.height;
if (contentHeight > height) {
nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height)));
}
}
}
onHoveredChanged: { onHoveredChanged: {
if (hovered) if (hovered)
shouldBeActive = true; shouldBeActive = hovered;
else else
shouldBeActive = flickable.moving; hideDelay.restart();
} }
// Sync nonAnimPosition with Qt's automatic position binding
onPositionChanged: { onPositionChanged: {
if (_updatingFromUser) { const handleY = handle.y;
_updatingFromUser = false; const pos = position;
return;
} console.log("\n" + "handle y: " + handleY + "\n" + "pos: " + pos);
if (position === nonAnimPosition) {
animating = false;
return;
}
if (!animating && !_updatingFromFlickable && !fullMouse.pressed) {
nonAnimPosition = position;
}
} }
// Sync nonAnimPosition with flickable when not animating
Connections { Connections {
function onContentYChanged() { function onMovingChanged() {
if (!root.animating && !fullMouse.pressed) {
root._updatingFromFlickable = true;
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
root.nonAnimPosition = Math.max(0, Math.min(1, root.flickable.contentY / (contentHeight - height)));
} else {
root.nonAnimPosition = 0;
}
root._updatingFromFlickable = false;
}
}
function onMovingChanged(): void {
if (root.flickable.moving) if (root.flickable.moving)
root.shouldBeActive = true; root.shouldBeActive = true;
else else
@@ -117,6 +58,46 @@ ScrollBar {
target: root.flickable target: root.flickable
} }
CustomClippingRect {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Appearance.rounding.full
CustomRect {
id: handle
anchors.right: parent.right
color: DynamicColors.palette.m3secondary
implicitHeight: root.height * root.effectiveSize
implicitWidth: fullMouse.pressed || fullMouse.containsMouse ? Appearance.padding.extraSmall * 2 : Appearance.padding.extraSmall
opacity: {
if (root.effectiveSize === 1)
return 0;
if (fullMouse.pressed)
return 1;
if (mouse.containsMouse)
return 0.8;
if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive)
return 0.6;
return 0;
}
radius: Appearance.rounding.full
y: root.reversed ? root.height * (1 + root.nonAnimY) * root.travelScale : root.height * root.nonAnimY * root.travelScale
Behavior on implicitWidth {
Anim {
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
Timer { Timer {
id: hideDelay id: hideDelay
@@ -128,66 +109,55 @@ ScrollBar {
CustomMouseArea { CustomMouseArea {
id: fullMouse id: fullMouse
function onWheel(event: WheelEvent): void { property real pressOffset: 0
root.animating = true;
root._updatingFromUser = true; function contentYFromThumbTop(thumbTop) {
let newPos = root.nonAnimPosition; var visualPos = root.effectiveTravel > 0 ? thumbTop / root.travelScale : 0;
if (event.angleDelta.y > 0)
newPos = Math.max(0, root.nonAnimPosition - 0.1); return root.reversed ? (visualPos - 1) * root.flickable.contentHeight : visualPos * root.flickable.contentHeight;
else if (event.angleDelta.y < 0) }
newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1);
root.nonAnimPosition = newPos; function updateFromEvent(event) {
// Update flickable position var posInTrack = event.y / root.height;
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY] var thumbTop = posInTrack - pressOffset;
if (root.flickable) { thumbTop = Math.max(0, Math.min(root.effectiveTravel, thumbTop));
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height; root.flickable.contentY = contentYFromThumbTop(thumbTop);
if (contentHeight > height) { }
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size; function visualThumbTop() {
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; const visualPos = root.reversed ? (1 + root.nonAnimY) : root.nonAnimY;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); return visualPos * root.travelScale;
}
}
} }
anchors.fill: parent anchors.fill: parent
cursorShape: undefined
hoverEnabled: true
preventStealing: true preventStealing: true
onPositionChanged: event => { onPositionChanged: event => {
root._updatingFromUser = true; if (pressed)
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); updateFromEvent(event);
root.nonAnimPosition = newPos;
// Update flickable position
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) {
const contentHeight = root.flickable.contentHeight;
const height = root.flickable.height;
if (contentHeight > height) {
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
}
} }
onPressed: event => { onPressed: event => {
root.animating = true; var currentTop = visualThumbTop();
root._updatingFromUser = true; var currentBottom = currentTop + root.effectiveSize;
const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); var clickPos = event.y / root.height;
root.nonAnimPosition = newPos;
// Update flickable position var clickedInsideThumb = clickPos >= currentTop && clickPos <= currentBottom;
// Map scrollbar position [0, 1-size] to contentY [0, maxContentY]
if (root.flickable) { if (clickedInsideThumb) {
const contentHeight = root.flickable.contentHeight; pressOffset = clickPos - currentTop;
const height = root.flickable.height; } else {
if (contentHeight > height) { pressOffset = root.effectiveSize / 2;
const maxContentY = contentHeight - height;
const maxPos = 1 - root.size;
const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0;
root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY));
}
} }
updateFromEvent(event);
}
onWheel: event => {
var delta = event.angleDelta.y > 0 ? -0.1 : 0.1;
var newPos = Math.max(0, Math.min(1 - root.size, root.position + delta));
root.position = newPos;
} }
} }
} }
+9 -2
View File
@@ -99,7 +99,7 @@ Item {
} }
} }
CustomClippingRect { Item {
id: entries id: entries
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
@@ -107,12 +107,12 @@ Item {
anchors.top: search.bottom anchors.top: search.bottom
anchors.topMargin: Appearance.spacing.normal anchors.topMargin: Appearance.spacing.normal
implicitWidth: Config.clipboard.sizes.width implicitWidth: Config.clipboard.sizes.width
radius: Appearance.rounding.small
CustomListView { CustomListView {
id: view id: view
anchors.fill: parent anchors.fill: parent
cacheBuffer: (root.itemHeight + spacing) * 2
highlightFollowsCurrentItem: false highlightFollowsCurrentItem: false
highlightRangeMode: ListView.ApplyRange highlightRangeMode: ListView.ApplyRange
preferredHighlightBegin: 0 preferredHighlightBegin: 0
@@ -121,6 +121,7 @@ Item {
CustomScrollBar.vertical: CustomScrollBar { CustomScrollBar.vertical: CustomScrollBar {
flickable: view flickable: view
minimumSize: 0.1
} }
delegate: RowLayout { delegate: RowLayout {
id: clipItem id: clipItem
@@ -255,6 +256,12 @@ Item {
ClipHistory.currentEntry = currentItem.modelData; ClipHistory.currentEntry = currentItem.modelData;
ClipHistory.refreshPreview(); ClipHistory.refreshPreview();
} }
CustomClippingWrapperRect {
anchors.fill: parent
child: view.contentItem
radius: Appearance.rounding.small
}
} }
} }
} }
+1 -1
View File
@@ -181,7 +181,7 @@ Item {
} }
} }
CustomClippingRect { CustomRect {
id: categoryContent id: categoryContent
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
+7 -7
View File
@@ -1,25 +1,21 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Controls
import qs.Components import qs.Components
import qs.Config import qs.Config
import qs.Helpers import qs.Helpers
CustomClippingRect { Item {
id: root id: root
default property alias contentData: clayout.data default property alias contentData: clayout.data
// Find and scroll to a section by its sectionId, then highlight a specific setting
function scrollToSectionAndHighlight(sectionId: string, settingName: string): bool { function scrollToSectionAndHighlight(sectionId: string, settingName: string): bool {
// Find the section with matching sectionId
for (let i = 0; i < clayout.children.length; i++) { for (let i = 0; i < clayout.children.length; i++) {
const section = clayout.children[i]; const section = clayout.children[i];
if (section.sectionId === sectionId) { if (section.sectionId === sectionId) {
// Scroll to the section with some padding
const targetY = section.y - Appearance.padding.normal; const targetY = section.y - Appearance.padding.normal;
flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height)); flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height));
// Use the singleton to highlight the setting
SettingsHighlight.highlight(settingName); SettingsHighlight.highlight(settingName);
return true; return true;
} }
@@ -27,7 +23,11 @@ CustomClippingRect {
return false; return false;
} }
radius: Appearance.rounding.normal - Appearance.padding.extraSmall CustomClippingWrapperRect {
anchors.fill: parent
child: flickable.contentItem
radius: Appearance.rounding.normal - Appearance.padding.extraSmall
}
CustomFlickable { CustomFlickable {
id: flickable id: flickable