Fluid 1.3.0
Material Design for QtQuick and Qml
Loading...
Searching...
No Matches
SliderTrack.qml
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
2// SPDX-License-Identifier: MPL-2.0
3
4pragma ComponentBehavior: Bound
5
6import QtQuick
7import Fluid as MD
8
17Item {
18 id: root
19 objectName: "sliderTrack"
20 Accessible.ignored: true
21
22 required property bool horizontal
23 required property bool mirrored
24 required property bool centered
25 required property bool rangeMode
26 required property bool controlEnabled
27 required property real visualPosition
28 required property real secondVisualPosition
29 required property real rangeFrom
30 required property real rangeTo
31 required property real stepSize
32 required property int tickVisibilityMode
33 required property int autoLimitMode
34 required property int autoHideMode
35 required property int hiddenMode
36 required property real trackHeight
37 required property real trackOuterCornerRadius
38 required property real handleHeight
39 required property real effectiveHandleWidth
40 required property real labelSpace
41 required property color activeTrackColor
42 required property color inactiveTrackColor
43 required property color activeTickColor
44 required property color inactiveTickColor
45 required property real activeTrackOpacity
46 required property real inactiveTrackOpacity
47 required property string trackIconActiveStart
48 required property string trackIconActiveEnd
49 required property string trackIconInactiveStart
50 required property string trackIconInactiveEnd
51 required property color trackIconActiveColor
52 required property color trackIconInactiveColor
53 required property real trackIconSize
54 required property real trackIconPadding
55
56 readonly property real baseCrossSize: Math.max(MD.Tokens.slider.minimumInteractiveSize, trackHeight, handleHeight)
57 implicitWidth: horizontal ? MD.Tokens.slider.defaultLength : baseCrossSize + labelSpace
58 implicitHeight: horizontal ? baseCrossSize + labelSpace : MD.Tokens.slider.defaultLength
59
60 readonly property real axisLength: horizontal ? width : height
61 readonly property real crossLength: horizontal ? height : width
62 readonly property real crossCenter: {
63 if (root.horizontal)
64 return root.labelSpace + (root.height - root.labelSpace) / 2;
65 if (root.mirrored)
66 return (root.width - root.labelSpace) / 2;
67 return root.labelSpace + (root.width - root.labelSpace) / 2;
68 }
69 readonly property real handleContainerSize: trackOuterCornerRadius * 2
70 readonly property real handleTravel: Math.max(0, axisLength - handleContainerSize)
71 readonly property real handleCenter: trackOuterCornerRadius + visualPosition * handleTravel
72 readonly property real secondHandleCenter: trackOuterCornerRadius + secondVisualPosition * handleTravel
73 readonly property real rangeStartHandleCenter: Math.min(handleCenter, secondHandleCenter)
74 readonly property real rangeEndHandleCenter: Math.max(handleCenter, secondHandleCenter)
75 readonly property real handleGap: effectiveHandleWidth / 2 + MD.Tokens.slider.activeHandleLeadingSpace
76 readonly property real centerPosition: axisLength / 2
77 readonly property real centerGap: MD.Tokens.slider.activeHandleLeadingSpace
78 readonly property bool handleBeforeCenter: handleCenter < centerPosition
79 readonly property bool handleAfterCenter: handleCenter > centerPosition
80 readonly property bool mirroredHorizontal: horizontal && mirrored
81
82 readonly property real activeStart: {
83 if (root.rangeMode)
84 return root.rangeStartHandleCenter + root.handleGap;
85 if (!root.centered)
86 return root.mirroredHorizontal ? root.handleCenter + root.handleGap : 0;
87 if (root.handleBeforeCenter)
88 return root.handleCenter + root.handleGap;
89 if (root.handleAfterCenter)
90 return root.centerPosition + root.centerGap;
91 return root.centerPosition;
92 }
93 readonly property real activeEnd: {
94 if (root.rangeMode)
95 return root.rangeEndHandleCenter - root.handleGap;
96 if (!root.centered)
97 return root.mirroredHorizontal ? root.axisLength : root.handleCenter - root.handleGap;
98 if (root.handleBeforeCenter)
99 return root.centerPosition - root.centerGap;
100 if (root.handleAfterCenter)
101 return root.handleCenter - root.handleGap;
102 return root.centerPosition;
103 }
104 readonly property real leadingInactiveStart: 0
105 readonly property real leadingInactiveEnd: {
106 if (root.rangeMode)
107 return root.rangeStartHandleCenter - root.handleGap;
108 if (!root.centered)
109 return root.mirroredHorizontal ? root.handleCenter - root.handleGap : 0;
110 if (root.handleBeforeCenter)
111 return root.handleCenter - root.handleGap;
112 if (!root.handleAfterCenter)
113 return root.centerPosition - root.handleGap;
114 return root.centerPosition - root.centerGap;
115 }
116 readonly property real trailingInactiveStart: {
117 if (root.rangeMode)
118 return root.rangeEndHandleCenter + root.handleGap;
119 if (!root.centered)
120 return root.mirroredHorizontal ? root.axisLength : root.handleCenter + root.handleGap;
121 if (root.handleAfterCenter)
122 return root.handleCenter + root.handleGap;
123 if (!root.handleBeforeCenter)
124 return root.centerPosition + root.handleGap;
125 return root.centerPosition + root.centerGap;
126 }
127 readonly property real trailingInactiveEnd: root.axisLength
128 readonly property real inactiveStart: mirroredHorizontal ? leadingInactiveStart : trailingInactiveStart
129 readonly property real inactiveEnd: mirroredHorizontal ? leadingInactiveEnd : trailingInactiveEnd
130
131 readonly property int desiredTickCount: {
132 if (root.stepSize <= 0 || root.rangeTo <= root.rangeFrom)
133 return 0;
134 return Math.floor((root.rangeTo - root.rangeFrom) / root.stepSize + Number.EPSILON) + 1;
135 }
136 readonly property real lastTickPosition: {
137 if (root.desiredTickCount <= 1)
138 return 0;
139 return (root.desiredTickCount - 1) * root.stepSize / (root.rangeTo - root.rangeFrom);
140 }
141 readonly property real tickSpanLength: handleTravel * lastTickPosition
142 readonly property int maxTickCount: {
143 if (root.desiredTickCount <= 0 || root.handleTravel <= 0)
144 return 0;
145 if (root.desiredTickCount === 1)
146 return 1;
147 return Math.floor(root.tickSpanLength / MD.Tokens.slider.tickMinSpacing) + 1;
148 }
149 readonly property int tickCount: {
150 if (root.desiredTickCount <= 0 || root.maxTickCount <= 0)
151 return 0;
152 if (root.tickVisibilityMode === root.autoLimitMode)
153 return Math.min(root.desiredTickCount, root.maxTickCount);
154 if (root.tickVisibilityMode === root.autoHideMode)
155 return root.desiredTickCount <= root.maxTickCount ? root.desiredTickCount : 0;
156 if (root.tickVisibilityMode === root.hiddenMode)
157 return 0;
158 return 0;
159 }
160 readonly property bool iconsAllowed: stepSize <= 0 && !centered && !rangeMode && trackIconSize > 0
161 readonly property real iconRequiredLength: trackIconSize + root.trackIconPadding * 2
162
163 function tickStepIndex(displayIndex) {
164 if (root.tickCount <= 1)
165 return 0;
166 return Math.round(displayIndex * (root.desiredTickCount - 1) / (root.tickCount - 1));
167 }
168
169 function tickAxisPosition(displayIndex) {
170 const logicalPosition = root.tickStepIndex(displayIndex) * root.stepSize / (root.rangeTo - root.rangeFrom);
171 const visual = root.mirroredHorizontal ? 1 - logicalPosition : logicalPosition;
172 return root.trackOuterCornerRadius + visual * root.handleTravel;
173 }
174
175 function pointIsActive(point) {
176 return point >= root.activeStart && point <= root.activeEnd;
177 }
178
179 function pointOverlapsGap(point) {
180 if (Math.abs(point - root.handleCenter) < root.handleGap)
181 return true;
182 if (root.rangeMode && Math.abs(point - root.secondHandleCenter) < root.handleGap)
183 return true;
184 return root.centered && Math.abs(point - root.centerPosition) < root.centerGap;
185 }
186
187 component TrackSegment: Rectangle {
188 required property real segmentStart
189 required property real segmentEnd
190 required property real startCornerRadius
191 required property real endCornerRadius
192
193 readonly property real segmentLength: Math.max(0, segmentEnd - segmentStart)
194 readonly property real boundedStartRadius: Math.min(startCornerRadius, segmentLength / 2, root.trackHeight / 2)
195 readonly property real boundedEndRadius: Math.min(endCornerRadius, segmentLength / 2, root.trackHeight / 2)
196
197 visible: segmentLength > 0
198 x: root.horizontal ? segmentStart : root.crossCenter - root.trackHeight / 2
199 y: root.horizontal ? root.crossCenter - root.trackHeight / 2 : segmentStart
200 width: root.horizontal ? segmentLength : root.trackHeight
201 height: root.horizontal ? root.trackHeight : segmentLength
202
203 topLeftRadius: root.horizontal ? boundedStartRadius : boundedStartRadius
204 bottomLeftRadius: root.horizontal ? boundedStartRadius : boundedEndRadius
205 topRightRadius: root.horizontal ? boundedEndRadius : boundedStartRadius
206 bottomRightRadius: root.horizontal ? boundedEndRadius : boundedEndRadius
207
208 Behavior on color {
209 ColorAnimation {
210 duration: MD.Tokens.motion.duration.short2
211 }
212 }
213
214 Behavior on opacity {
215 NumberAnimation {
216 duration: MD.Tokens.motion.duration.short2
217 }
218 }
219 }
220
221 TrackSegment {
222 id: leadingInactiveTrack
223 objectName: "inactiveTrackLeading"
224 segmentStart: root.leadingInactiveStart
225 segmentEnd: root.leadingInactiveEnd
226 startCornerRadius: root.trackOuterCornerRadius
227 endCornerRadius: MD.Tokens.slider.trackInsideCornerRadius
228 color: root.inactiveTrackColor
229 opacity: root.inactiveTrackOpacity
230 }
231
232 TrackSegment {
233 id: activeTrack
234 objectName: "activeTrack"
235 segmentStart: root.activeStart
236 segmentEnd: root.activeEnd
237 startCornerRadius: segmentStart <= 0 ? root.trackOuterCornerRadius : MD.Tokens.slider.trackInsideCornerRadius
238 endCornerRadius: segmentEnd >= root.axisLength ? root.trackOuterCornerRadius : MD.Tokens.slider.trackInsideCornerRadius
239 color: root.activeTrackColor
240 opacity: root.activeTrackOpacity
241 }
242
243 TrackSegment {
244 id: trailingInactiveTrack
245 objectName: "inactiveTrackTrailing"
246 segmentStart: root.trailingInactiveStart
247 segmentEnd: root.trailingInactiveEnd
248 startCornerRadius: MD.Tokens.slider.trackInsideCornerRadius
249 endCornerRadius: root.trackOuterCornerRadius
250 color: root.inactiveTrackColor
251 opacity: root.inactiveTrackOpacity
252 }
253
254 Repeater {
255 id: ticks
256 objectName: "tickRepeater"
257 model: root.tickCount
258
259 delegate: Rectangle {
260 id: tick
261 required property int index
262
263 readonly property real axisPosition: root.tickAxisPosition(index)
264
265 objectName: "sliderTick"
266 visible: !root.pointOverlapsGap(axisPosition)
267 x: root.horizontal ? axisPosition - width / 2 : root.crossCenter - width / 2
268 y: root.horizontal ? root.crossCenter - height / 2 : axisPosition - height / 2
269 width: MD.Tokens.slider.tickSize
270 height: MD.Tokens.slider.tickSize
271 radius: width / 2
272 color: root.pointIsActive(axisPosition) ? root.activeTickColor : root.inactiveTickColor
273 opacity: MD.Tokens.slider.visibleOpacity
274 }
275 }
276
277 component StopIndicator: Rectangle {
278 required property real axisPosition
279
280 width: MD.Tokens.slider.stopIndicatorSize
281 height: MD.Tokens.slider.stopIndicatorSize
282 radius: width / 2
283 x: root.horizontal ? axisPosition - width / 2 : root.crossCenter - width / 2
284 y: root.horizontal ? root.crossCenter - height / 2 : axisPosition - height / 2
285 color: root.inactiveTickColor
286 opacity: MD.Tokens.slider.visibleOpacity
287 }
288
289 StopIndicator {
290 objectName: "startStopIndicator"
291 axisPosition: root.trackOuterCornerRadius
292 visible: (root.centered || root.rangeMode) && !root.pointOverlapsGap(axisPosition)
293 }
294
295 StopIndicator {
296 objectName: "endStopIndicator"
297 axisPosition: root.axisLength - root.trackOuterCornerRadius
298 visible: (root.centered || root.rangeMode || !root.mirroredHorizontal) && !root.pointOverlapsGap(axisPosition)
299 }
300
301 StopIndicator {
302 objectName: "mirroredEndStopIndicator"
303 axisPosition: root.trackOuterCornerRadius
304 visible: !root.centered && !root.rangeMode && root.mirroredHorizontal && !root.pointOverlapsGap(axisPosition)
305 }
306
307 component TrackIcon: MD.Symbol {
308 required property bool activeSegment
309 required property bool logicalStart
310
311 readonly property real segmentStart: activeSegment ? root.activeStart : root.inactiveStart
312 readonly property real segmentEnd: activeSegment ? root.activeEnd : root.inactiveEnd
313 readonly property bool logicalForward: !root.mirroredHorizontal
314 readonly property real axisPosition: {
315 const fromStart = logicalStart === logicalForward;
316 if (fromStart)
317 return segmentStart + root.trackIconPadding + root.trackIconSize / 2;
318 return segmentEnd - root.trackIconPadding - root.trackIconSize / 2;
319 }
320
321 visible: root.iconsAllowed && name.length > 0 && segmentEnd - segmentStart >= root.iconRequiredLength
322 x: root.horizontal ? axisPosition - width / 2 : root.crossCenter - width / 2
323 y: root.horizontal ? root.crossCenter - height / 2 : axisPosition - height / 2
324 iconWidth: root.trackIconSize
325 iconHeight: root.trackIconSize
326 }
327
328 TrackIcon {
329 objectName: "activeStartIcon"
330 activeSegment: true
331 logicalStart: true
332 name: root.trackIconActiveStart
333 color: root.trackIconActiveColor
334 }
335
336 TrackIcon {
337 objectName: "activeEndIcon"
338 activeSegment: true
339 logicalStart: false
340 name: root.trackIconActiveEnd
341 color: root.trackIconActiveColor
342 }
343
344 TrackIcon {
345 objectName: "inactiveStartIcon"
346 activeSegment: false
347 logicalStart: true
348 name: root.trackIconInactiveStart
349 color: root.trackIconInactiveColor
350 }
351
352 TrackIcon {
353 objectName: "inactiveEndIcon"
354 activeSegment: false
355 logicalStart: false
356 name: root.trackIconInactiveEnd
357 color: root.trackIconInactiveColor
358 }
359}