Fluid 1.3.0
Material Design for QtQuick and Qml
Loading...
Searching...
No Matches
SliderHandle.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
8import "../internal/MotionAnimation.js" as MotionAnimation
9
18Item {
19 id: root
20
21 // Opt in only for RangeSlider, whose template exposes one incomplete root
22 // accessibility node for two independently focusable handles. A Slider keeps
23 // this visual helper silent and relies on its complete template-provided node.
24 property bool accessibilityEnabled: false
25
26 // RangeSlider supplies a distinct localized name for each handle so screen
27 // readers can identify which end of the selected interval is being changed.
28 property string accessibleName: ""
29
30 // These conventional property names are intentionally kept on the accessible
31 // item: Qt discovers them when constructing its QAccessibleValueInterface.
32 property real value: 0
33 property real minimumValue: 0
34 property real maximumValue: 1
35 property real stepSize: 0
36
37 // The owner provides the template handle operations so accessibility actions
38 // use the same stepping, snapping, and range-order constraints as the keyboard.
39 property var accessibilityIncreaseAction: null
40 property var accessibilityDecreaseAction: null
41
42 required property string valueIndicatorText
43 required property int labelBehavior
44 required property bool handleHasFocus
45 required property bool handlePressed
46 required property bool handleHovered
47 required property bool controlEnabled
48 required property bool horizontal
49 required property bool mirrored
50 required property real controlWidth
51 required property real controlHeight
52 required property real activeHandleWidth
53 required property real handleHeight
54 required property real handleContainerSize
55 required property color handleColor
56 required property real handleOpacity
57 required property color valueIndicatorContainerColor
58 required property color valueIndicatorLabelColor
59
60 readonly property bool valueIndicatorActive: {
61 if (root.labelBehavior === MD.Slider.LabelBehavior.Gone)
62 return false;
63 if (root.labelBehavior === MD.Slider.LabelBehavior.Visible)
64 return root.controlEnabled;
65 return root.controlEnabled && (root.handlePressed || root.handleHasFocus || root.handleHovered);
66 }
67 readonly property real valueIndicatorWidth: valueIndicator.implicitWidth
68 readonly property real valueIndicatorHeight: valueIndicator.implicitHeight
69
70 implicitWidth: horizontal ? handleContainerSize : handleHeight
71 implicitHeight: horizontal ? handleHeight : handleContainerSize
72
73 // Publish the visual helper as an independent slider node only when opted in.
74 // Focus mirrors the real template handle rather than creating another focus
75 // target, while ignored prevents a duplicate node for ordinary Slider usage.
76 Accessible.role: Accessible.Slider
77 Accessible.name: accessibleName
78 Accessible.focusable: accessibilityEnabled
79 Accessible.focused: handleHasFocus
80 Accessible.ignored: !accessibilityEnabled
81
82 // Bridge assistive-technology value actions back to the owning template node.
83 Accessible.onIncreaseAction: {
84 if (root.accessibilityIncreaseAction)
85 root.accessibilityIncreaseAction();
86 }
87 Accessible.onDecreaseAction: {
88 if (root.accessibilityDecreaseAction)
89 root.accessibilityDecreaseAction();
90 }
91
92 function __valueIndicatorOffset(preferred, alternative, handleOffset, indicatorSize, controlSize) {
93 if (root.labelBehavior !== MD.Slider.LabelBehavior.WithinBounds)
94 return preferred;
95
96 const firstAllowed = -handleOffset;
97 const lastAllowed = controlSize - handleOffset - indicatorSize;
98 if (lastAllowed < firstAllowed)
99 return firstAllowed;
100 if (preferred >= firstAllowed && preferred <= lastAllowed)
101 return preferred;
102 if (alternative >= firstAllowed && alternative <= lastAllowed)
103 return alternative;
104 return Math.max(firstAllowed, Math.min(preferred, lastAllowed));
105 }
106
107 MD.Control {
108 id: valueIndicator
109 objectName: "valueIndicator"
110
111 readonly property real indicatorWidth: Math.max(MD.Tokens.slider.valueIndicatorMinWidth, contentItem.implicitWidth + leftPadding + rightPadding)
112 readonly property real indicatorHeight: Math.max(MD.Tokens.slider.valueIndicatorMinHeight, contentItem.implicitHeight + topPadding + bottomPadding)
113
114 x: {
115 if (root.horizontal) {
116 const centered = (root.width - width) / 2;
117 return root.__valueIndicatorOffset(centered, centered, root.x, width, root.controlWidth);
118 }
119
120 const gap = MD.Tokens.slider.valueIndicatorActiveBottomSpace;
121 const beforeHandle = -width - gap;
122 const afterHandle = root.width + gap;
123 const preferred = root.mirrored ? afterHandle : beforeHandle;
124 const alternative = root.mirrored ? beforeHandle : afterHandle;
125 return root.__valueIndicatorOffset(preferred, alternative, root.x, width, root.controlWidth);
126 }
127 y: {
128 if (!root.horizontal) {
129 const centered = (root.height - height) / 2;
130 return root.__valueIndicatorOffset(centered, centered, root.y, height, root.controlHeight);
131 }
132
133 const gap = MD.Tokens.slider.valueIndicatorActiveBottomSpace;
134 const aboveHandle = -height - gap;
135 const belowHandle = root.height + gap;
136 return root.__valueIndicatorOffset(aboveHandle, belowHandle, root.y, height, root.controlHeight);
137 }
138
139 horizontalPadding: MD.Tokens.slider.valueIndicatorHorizontalPadding
140 verticalPadding: MD.Tokens.slider.valueIndicatorVerticalPadding
141
142 implicitWidth: indicatorWidth
143 implicitHeight: indicatorHeight
144
145 opacity: visible ? 1 : 0
146 scale: visible ? 1 : 0
147 visible: root.valueIndicatorActive
148
149 background: Rectangle {
150 implicitWidth: valueIndicator.indicatorWidth
151 implicitHeight: valueIndicator.indicatorHeight
152 radius: height / 2
153 color: root.valueIndicatorContainerColor
154 }
155
156 contentItem: Item {
157 implicitWidth: valueIndicatorLabel.implicitWidth
158 implicitHeight: valueIndicatorLabel.implicitHeight
159
160 MD.Label {
161 id: valueIndicatorLabel
162 objectName: "valueIndicatorLabel"
163
164 anchors.centerIn: parent
165 text: root.valueIndicatorText
166 typescale: MD.Tokens.typescale.labelLarge
167 font.weight: MD.Tokens.typescale.bodyLarge.fontWeight
168 font.letterSpacing: MD.Tokens.typescale.bodyLarge.tracking
169 color: root.valueIndicatorLabelColor
170 }
171 }
172
173 Behavior on scale {
174 NumberAnimation {
175 // qmllint disable unresolved-type
176 duration: MotionAnimation.expressiveFastSpatialDuration
177 easing.type: Easing.BezierSpline
178 easing.bezierCurve: MotionAnimation.expressiveFastSpatialCurve
179 // qmllint enable unresolved-type
180 }
181 }
182
183 Behavior on opacity {
184 NumberAnimation {
185 duration: MD.Tokens.motion.duration.short2
186 }
187 }
188 }
189
190 Rectangle {
191 id: handle
192 objectName: "sliderHandle"
193
194 anchors.centerIn: parent
195 width: root.horizontal ? root.activeHandleWidth : root.handleHeight
196 height: root.horizontal ? root.handleHeight : root.activeHandleWidth
197 radius: Math.min(width, height) / 2
198 color: root.handleColor
199 opacity: root.handleOpacity
200
201 Behavior on width {
202 NumberAnimation {
203 // qmllint disable unresolved-type
204 duration: MotionAnimation.expressiveFastSpatialDuration
205 easing.type: Easing.BezierSpline
206 easing.bezierCurve: MotionAnimation.expressiveFastSpatialCurve
207 // qmllint enable unresolved-type
208 }
209 }
210
211 Behavior on height {
212 NumberAnimation {
213 // qmllint disable unresolved-type
214 duration: MotionAnimation.expressiveFastSpatialDuration
215 easing.type: Easing.BezierSpline
216 easing.bezierCurve: MotionAnimation.expressiveFastSpatialCurve
217 // qmllint enable unresolved-type
218 }
219 }
220
221 Behavior on color {
222 ColorAnimation {
223 duration: MD.Tokens.motion.duration.short2
224 }
225 }
226
227 Behavior on opacity {
228 NumberAnimation {
229 duration: MD.Tokens.motion.duration.short2
230 }
231 }
232 }
233}