@@ -7,51 +7,170 @@ import QtQuick.Controls
77 * Features:
88 * - Expanding thumb (4.5px -> 9px) on hover.
99 * - Auto-hides after 2 seconds of inactivity.
10- * - Reappears on mouse movement over target, scrolling, or focus activity.
10+ * - Optional "pillar" track (showTrack: true).
11+ * - Self-contained geometry logic + optional external flickable binding.
1112 */
1213ScrollBar {
1314 id: control
1415
15- // The Flickable (ListView/GridView) this scrollbar controls
16- property Flickable flickable: parent
16+ // --- CONFIGURATION ---
17+ // Optional: Flickable binding for manual usage (triggers activity)
18+ property Flickable flickable: null
19+ // Optional: Show a subtle background track when active
20+ property bool showTrack: false
1721
22+ // --- PHYSICS CONFIGURATION ---
23+ // Toggle internal physics engine
24+ property bool physicsEnabled: false
25+ // Enable "Turbo" acceleration curve (faster ramp-up)
26+ property bool turboMode: true
27+ // Current acceleration multiplier
28+ property real acceleration: 1.0
29+
30+ // Internal State for Physics
31+ property real lastWheelTime: 0
32+ property int lastDeltaSign: 0
33+
34+ // Physics Timer for "SnapBack" (overshoot recovery)
35+ Timer {
36+ id: snapBackTimer
37+ interval: 150
38+ onTriggered: {
39+ if (! control .flickable ) return
40+ // Dynamic Bounds: Respect margins (paddings)
41+ let minY = - control .flickable .topMargin
42+ let maxY = Math .max (minY, control .flickable .contentHeight - control .flickable .height + control .flickable .bottomMargin )
43+
44+ if (control .flickable .contentY < minY) {
45+ control .flickable .contentY = minY
46+ } else if (control .flickable .contentY > maxY) {
47+ control .flickable .contentY = maxY
48+ }
49+ }
50+ }
51+
52+ // --- PHYSICS ENGINE ---
53+
54+ // Public API: Call this from parent's WheelHandler
55+ function handleWheel (event ) {
56+ if (! control .physicsEnabled || ! control .flickable ) return
57+
58+ // Dynamic Bounds
59+ let minY = - control .flickable .topMargin
60+ let maxY = Math .max (minY, control .flickable .contentHeight - control .flickable .height + control .flickable .bottomMargin )
61+
62+ if (maxY < 20 ) return
63+
64+ // 1. Acceleration Logic
65+ let now = new Date ().getTime ()
66+ let dt = now - control .lastWheelTime
67+ control .lastWheelTime = now
68+
69+ let currentSign = (event .angleDelta .y > 0 ) ? 1 : - 1
70+
71+ if (dt < 100 && currentSign === control .lastDeltaSign && event .angleDelta .y !== 0 ) {
72+ let ramp = control .turboMode ? 1.0 : 0.5
73+ let limit = control .turboMode ? 10.0 : 6.0
74+ control .acceleration = Math .min (control .acceleration + ramp, limit)
75+ } else {
76+ control .acceleration = 1.0
77+ }
78+ control .lastDeltaSign = currentSign
79+
80+ // 2. Calculate Delta
81+ let delta = 0
82+ if (event .angleDelta .y !== 0 ) {
83+ delta = - (event .angleDelta .y / 1.2 )
84+ delta *= control .acceleration
85+ } else if (event .pixelDelta .y !== 0 ) {
86+ delta = - event .pixelDelta .y
87+ }
88+
89+ if (delta === 0 ) return
90+
91+ // 3. Resistance (Rubber Banding)
92+ if (control .flickable .contentY < minY || control .flickable .contentY > maxY) {
93+ delta *= 0.3
94+ }
95+
96+ // 4. Apply with Clamped Overshoot Limits
97+ let newY = control .flickable .contentY + delta
98+ if (newY < minY - 300 ) newY = minY - 300
99+ if (newY > maxY + 300 ) newY = maxY + 300
100+
101+ control .flickable .contentY = newY
102+
103+ snapBackTimer .restart ()
104+ activityTimer .restart ()
105+ }
106+
107+ // fallback: Internal handler (only works if hovering scrollbar directly)
108+ // We keep this for cases where user HOVERS the scrollbar itself
109+ WheelHandler {
110+ target: null // Logic handled via function call
111+ enabled: control .physicsEnabled
112+ acceptedDevices: PointerDevice .Mouse | PointerDevice .TouchPad
113+ onWheel : (event ) => control .handleWheel (event )
114+ }
115+
116+ // --- LOGIC ---
117+ // Active when hovered, pressed, or recently used (timer)
18118 active: hovered || pressed || activityTimer .running
119+
120+ // Only render when content is scrollable (size < 1.0) and valid
121+ visible: control .size < 1.0 && control .size > 0
122+
19123 interactive: true
20124 hoverEnabled: true
21-
22- // Track: Transparent
23- background: null
24125
25126 SystemPalette { id: activePalette; colorGroup: SystemPalette .Active }
26127
27- // Logic: Auto-hide timer
128+ // Auto-hide Timer
28129 Timer {
29130 id: activityTimer
30131 interval: 2000
31132 onTriggered: stop ()
32133 }
33134
34- // Activity Trigger: Scrolling
135+ // --- ACTIVITY TRIGGERS ---
136+ // 1. Internal Geometry Changes (Works universally)
137+ onPositionChanged: activityTimer .restart ()
138+ onSizeChanged: activityTimer .restart ()
139+
140+ // 2. Mouse Interaction
141+ onHoveredChanged: if (hovered) activityTimer .restart ()
142+
143+ // 3. External Flickable Signals (if bound)
35144 Connections {
36145 target: flickable
37146 function onContentYChanged () { activityTimer .restart () }
38147 function onContentXChanged () { activityTimer .restart () }
148+ function onMovementStarted () { activityTimer .restart () }
149+ ignoreUnknownSignals: true
39150 }
40-
41- // Activity Trigger: Mouse Movement over the target area
42- HoverHandler {
43- target: flickable
44- acceptedDevices: PointerDevice .Mouse | PointerDevice .TouchPad
45- onPointChanged: activityTimer .restart ()
151+
152+ // 4. Parent View Interactivity (if parent is Flickable/ListView)
153+ // This handles the "unhide on scroll/hover" feature requested
154+ Connections {
155+ target: control .parent && control .parent .flickableItem ? control .parent .flickableItem : (control .parent instanceof Flickable ? control .parent : null )
156+ function onMovementStarted () { activityTimer .restart () }
157+ ignoreUnknownSignals: true
46158 }
47159
48- // Activity Trigger: Focus/Key events (when attached to a focused item)
49- Connections {
50- target: flickable
51- function onActiveFocusChanged () { if (flickable .activeFocus ) activityTimer .restart () }
160+ // --- VISUALS ---
161+
162+ // Background Track (Pillar) - Optional
163+ background: Rectangle {
164+ visible: control .showTrack && control .active
165+ // Ensure track fills the scrollbar area
166+ anchors .fill : parent
167+ // Slightly more visible color for testing/usage
168+ color: Qt .alpha (activePalette .windowText , 0.1 )
169+ opacity: control .active ? 1.0 : 0.0
170+ Behavior on opacity { NumberAnimation { duration: 200 } }
52171 }
53172
54- // VISUALS
173+ // Thumb
55174 contentItem: Rectangle {
56175 implicitWidth: control .hovered || control .pressed ? 9 : 4.5
57176 radius: width / 2
@@ -61,8 +180,12 @@ ScrollBar {
61180 control .hovered ? Qt .alpha (activePalette .text , 0.7 ) :
62181 Qt .alpha (activePalette .text , 0.4 )
63182
183+ // Fade in/out
184+ opacity: control .active ? 1.0 : 0.0
185+
64186 // Animations: Smooth GTK feel
65187 Behavior on implicitWidth { NumberAnimation { duration: 100 } }
66188 Behavior on color { ColorAnimation { duration: 150 } }
189+ Behavior on opacity { NumberAnimation { duration: 200 } }
67190 }
68191}
0 commit comments