1 /*
<lambda>null2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.material3
18 
19 import androidx.annotation.IntRange
20 import androidx.compose.foundation.Canvas
21 import androidx.compose.foundation.MutatePriority
22 import androidx.compose.foundation.MutatorMutex
23 import androidx.compose.foundation.background
24 import androidx.compose.foundation.focusable
25 import androidx.compose.foundation.gestures.DragScope
26 import androidx.compose.foundation.gestures.DraggableState
27 import androidx.compose.foundation.gestures.Orientation
28 import androidx.compose.foundation.gestures.Orientation.Horizontal
29 import androidx.compose.foundation.gestures.Orientation.Vertical
30 import androidx.compose.foundation.gestures.awaitEachGesture
31 import androidx.compose.foundation.gestures.awaitFirstDown
32 import androidx.compose.foundation.gestures.detectTapGestures
33 import androidx.compose.foundation.gestures.draggable
34 import androidx.compose.foundation.gestures.horizontalDrag
35 import androidx.compose.foundation.hoverable
36 import androidx.compose.foundation.interaction.DragInteraction
37 import androidx.compose.foundation.interaction.Interaction
38 import androidx.compose.foundation.interaction.MutableInteractionSource
39 import androidx.compose.foundation.interaction.PressInteraction
40 import androidx.compose.foundation.layout.Box
41 import androidx.compose.foundation.layout.Spacer
42 import androidx.compose.foundation.layout.fillMaxHeight
43 import androidx.compose.foundation.layout.fillMaxWidth
44 import androidx.compose.foundation.layout.height
45 import androidx.compose.foundation.layout.requiredSizeIn
46 import androidx.compose.foundation.layout.size
47 import androidx.compose.foundation.layout.width
48 import androidx.compose.foundation.layout.wrapContentHeight
49 import androidx.compose.foundation.layout.wrapContentWidth
50 import androidx.compose.foundation.progressSemantics
51 import androidx.compose.material3.RangeSliderState.Companion.Saver
52 import androidx.compose.material3.SliderState.Companion.Saver
53 import androidx.compose.material3.internal.IncreaseHorizontalSemanticsBounds
54 import androidx.compose.material3.internal.Strings
55 import androidx.compose.material3.internal.awaitHorizontalPointerSlopOrCancellation
56 import androidx.compose.material3.internal.getString
57 import androidx.compose.material3.internal.pointerSlop
58 import androidx.compose.material3.tokens.SliderTokens
59 import androidx.compose.runtime.Composable
60 import androidx.compose.runtime.Immutable
61 import androidx.compose.runtime.LaunchedEffect
62 import androidx.compose.runtime.Stable
63 import androidx.compose.runtime.getValue
64 import androidx.compose.runtime.mutableFloatStateOf
65 import androidx.compose.runtime.mutableIntStateOf
66 import androidx.compose.runtime.mutableStateListOf
67 import androidx.compose.runtime.mutableStateOf
68 import androidx.compose.runtime.remember
69 import androidx.compose.runtime.saveable.Saver
70 import androidx.compose.runtime.saveable.listSaver
71 import androidx.compose.runtime.saveable.rememberSaveable
72 import androidx.compose.runtime.setValue
73 import androidx.compose.ui.Modifier
74 import androidx.compose.ui.draw.rotate
75 import androidx.compose.ui.draw.scale
76 import androidx.compose.ui.geometry.CornerRadius
77 import androidx.compose.ui.geometry.Offset
78 import androidx.compose.ui.geometry.Rect
79 import androidx.compose.ui.geometry.RoundRect
80 import androidx.compose.ui.geometry.Size
81 import androidx.compose.ui.geometry.lerp
82 import androidx.compose.ui.graphics.Color
83 import androidx.compose.ui.graphics.Path
84 import androidx.compose.ui.graphics.PointMode
85 import androidx.compose.ui.graphics.StrokeCap
86 import androidx.compose.ui.graphics.compositeOver
87 import androidx.compose.ui.graphics.drawscope.DrawScope
88 import androidx.compose.ui.graphics.takeOrElse
89 import androidx.compose.ui.input.key.Key
90 import androidx.compose.ui.input.key.KeyEventType
91 import androidx.compose.ui.input.key.key
92 import androidx.compose.ui.input.key.onKeyEvent
93 import androidx.compose.ui.input.key.type
94 import androidx.compose.ui.input.pointer.AwaitPointerEventScope
95 import androidx.compose.ui.input.pointer.PointerId
96 import androidx.compose.ui.input.pointer.PointerInputChange
97 import androidx.compose.ui.input.pointer.PointerType
98 import androidx.compose.ui.input.pointer.pointerInput
99 import androidx.compose.ui.input.pointer.positionChange
100 import androidx.compose.ui.layout.AlignmentLine
101 import androidx.compose.ui.layout.Layout
102 import androidx.compose.ui.layout.VerticalAlignmentLine
103 import androidx.compose.ui.layout.layout
104 import androidx.compose.ui.layout.layoutId
105 import androidx.compose.ui.layout.onSizeChanged
106 import androidx.compose.ui.platform.LocalLayoutDirection
107 import androidx.compose.ui.semantics.contentDescription
108 import androidx.compose.ui.semantics.disabled
109 import androidx.compose.ui.semantics.semantics
110 import androidx.compose.ui.semantics.setProgress
111 import androidx.compose.ui.unit.Dp
112 import androidx.compose.ui.unit.DpSize
113 import androidx.compose.ui.unit.LayoutDirection
114 import androidx.compose.ui.unit.dp
115 import androidx.compose.ui.unit.offset
116 import androidx.compose.ui.util.fastFirst
117 import androidx.compose.ui.util.fastMap
118 import androidx.compose.ui.util.lerp
119 import androidx.compose.ui.util.packFloats
120 import androidx.compose.ui.util.unpackFloat1
121 import androidx.compose.ui.util.unpackFloat2
122 import kotlin.jvm.JvmInline
123 import kotlin.jvm.JvmName
124 import kotlin.math.abs
125 import kotlin.math.floor
126 import kotlin.math.max
127 import kotlin.math.min
128 import kotlin.math.roundToInt
129 import kotlinx.coroutines.CancellationException
130 import kotlinx.coroutines.CoroutineScope
131 import kotlinx.coroutines.coroutineScope
132 import kotlinx.coroutines.launch
133 
134 /**
135  * [Material Design slider](https://m3.material.io/components/sliders/overview)
136  *
137  * Sliders allow users to make selections from a range of values.
138  *
139  * It uses [SliderDefaults.Thumb] and [SliderDefaults.Track] as the thumb and track.
140  *
141  * Sliders reflect a range of values along a horizontal bar, from which users may select a single
142  * value. They are ideal for adjusting settings such as volume, brightness, or applying image
143  * filters.
144  *
145  * ![Sliders
146  * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqe2zb2b-1.png?alt=media)
147  *
148  * Use continuous sliders to allow users to make meaningful selections that don’t require a specific
149  * value:
150  *
151  * @sample androidx.compose.material3.samples.SliderSample
152  *
153  * You can allow the user to choose only between predefined set of values by specifying the amount
154  * of steps between min and max values:
155  *
156  * @sample androidx.compose.material3.samples.StepsSliderSample
157  * @param value current value of the slider. If outside of [valueRange] provided, value will be
158  *   coerced to this range.
159  * @param onValueChange callback in which value should be updated
160  * @param modifier the [Modifier] to be applied to this slider
161  * @param enabled controls the enabled state of this slider. When `false`, this component will not
162  *   respond to user input, and it will appear visually disabled and disabled to accessibility
163  *   services.
164  * @param valueRange range of values that this slider can take. The passed [value] will be coerced
165  *   to this range.
166  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
167  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
168  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
169  *   continuously and allow any value from the range. Must not be negative.
170  * @param onValueChangeFinished called when value change has ended. This should not be used to
171  *   update the slider value (use [onValueChange] instead), but rather to know when the user has
172  *   completed selecting a new value by ending a drag or a click.
173  * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
174  *   different states. See [SliderDefaults.colors].
175  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
176  *   for this slider. You can create and pass in your own `remember`ed instance to observe
177  *   [Interaction]s and customize the appearance / behavior of this slider in different states.
178  */
179 @OptIn(ExperimentalMaterial3Api::class)
180 @Composable
181 fun Slider(
182     value: Float,
183     onValueChange: (Float) -> Unit,
184     modifier: Modifier = Modifier,
185     enabled: Boolean = true,
186     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
187     @IntRange(from = 0) steps: Int = 0,
188     onValueChangeFinished: (() -> Unit)? = null,
189     colors: SliderColors = SliderDefaults.colors(),
190     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
191 ) {
192     Slider(
193         value = value,
194         onValueChange = onValueChange,
195         modifier = modifier,
196         enabled = enabled,
197         onValueChangeFinished = onValueChangeFinished,
198         colors = colors,
199         interactionSource = interactionSource,
200         steps = steps,
<lambda>null201         thumb = {
202             SliderDefaults.Thumb(
203                 interactionSource = interactionSource,
204                 colors = colors,
205                 enabled = enabled
206             )
207         },
sliderStatenull208         track = { sliderState ->
209             SliderDefaults.Track(colors = colors, enabled = enabled, sliderState = sliderState)
210         },
211         valueRange = valueRange
212     )
213 }
214 
215 /**
216  * [Material Design slider](https://m3.material.io/components/sliders/overview)
217  *
218  * Sliders allow users to make selections from a range of values.
219  *
220  * Sliders reflect a range of values along a horizontal bar, from which users may select a single
221  * value. They are ideal for adjusting settings such as volume, brightness, or applying image
222  * filters.
223  *
224  * ![Sliders
225  * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqe2zb2b-1.png?alt=media)
226  *
227  * Use continuous sliders to allow users to make meaningful selections that don’t require a specific
228  * value:
229  *
230  * @sample androidx.compose.material3.samples.SliderSample
231  *
232  * You can allow the user to choose only between predefined set of values by specifying the amount
233  * of steps between min and max values:
234  *
235  * @sample androidx.compose.material3.samples.StepsSliderSample
236  *
237  * Slider using a custom thumb:
238  *
239  * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
240  *
241  * Slider using custom track and thumb:
242  *
243  * @sample androidx.compose.material3.samples.SliderWithCustomTrackAndThumbSample
244  *
245  * Slider using track icons:
246  *
247  * @sample androidx.compose.material3.samples.SliderWithTrackIconsSample
248  * @param value current value of the slider. If outside of [valueRange] provided, value will be
249  *   coerced to this range.
250  * @param onValueChange callback in which value should be updated
251  * @param modifier the [Modifier] to be applied to this slider
252  * @param enabled controls the enabled state of this slider. When `false`, this component will not
253  *   respond to user input, and it will appear visually disabled and disabled to accessibility
254  *   services.
255  * @param onValueChangeFinished called when value change has ended. This should not be used to
256  *   update the slider value (use [onValueChange] instead), but rather to know when the user has
257  *   completed selecting a new value by ending a drag or a click.
258  * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
259  *   different states. See [SliderDefaults.colors].
260  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
261  *   for this slider. You can create and pass in your own `remember`ed instance to observe
262  *   [Interaction]s and customize the appearance / behavior of this slider in different states.
263  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
264  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
265  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
266  *   continuously and allow any value from the range. Must not be negative.
267  * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
268  *   lambda receives a [SliderState] which is used to obtain the current active track.
269  * @param track the track to be displayed on the slider, it is placed underneath the thumb. The
270  *   lambda receives a [SliderState] which is used to obtain the current active track.
271  * @param valueRange range of values that this slider can take. The passed [value] will be coerced
272  *   to this range.
273  */
274 @Composable
275 @ExperimentalMaterial3Api
Slidernull276 fun Slider(
277     value: Float,
278     onValueChange: (Float) -> Unit,
279     modifier: Modifier = Modifier,
280     enabled: Boolean = true,
281     onValueChangeFinished: (() -> Unit)? = null,
282     colors: SliderColors = SliderDefaults.colors(),
283     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
284     @IntRange(from = 0) steps: Int = 0,
285     thumb: @Composable (SliderState) -> Unit = {
286         SliderDefaults.Thumb(
287             interactionSource = interactionSource,
288             colors = colors,
289             enabled = enabled
290         )
291     },
292     track: @Composable (SliderState) -> Unit = { sliderState ->
293         SliderDefaults.Track(colors = colors, enabled = enabled, sliderState = sliderState)
294     },
295     valueRange: ClosedFloatingPointRange<Float> = 0f..1f
296 ) {
297     val state =
<lambda>null298         remember(steps, valueRange) { SliderState(value, steps, onValueChangeFinished, valueRange) }
299     state.onValueChangeFinished = onValueChangeFinished
300     state.onValueChange = onValueChange
301     state.value = value
302 
303     Slider(
304         state = state,
305         modifier = modifier,
306         enabled = enabled,
307         interactionSource = interactionSource,
308         thumb = thumb,
309         track = track
310     )
311 }
312 
313 /**
314  * [Material Design slider](https://m3.material.io/components/sliders/overview)
315  *
316  * Sliders allow users to make selections from a range of values.
317  *
318  * Sliders reflect a range of values along a horizontal bar, from which users may select a single
319  * value. They are ideal for adjusting settings such as volume, brightness, or applying image
320  * filters.
321  *
322  * ![Sliders
323  * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqe2zb2b-1.png?alt=media)
324  *
325  * Use continuous sliders to allow users to make meaningful selections that don’t require a specific
326  * value:
327  *
328  * @sample androidx.compose.material3.samples.SliderSample
329  *
330  * You can allow the user to choose only between predefined set of values by specifying the amount
331  * of steps between min and max values:
332  *
333  * @sample androidx.compose.material3.samples.StepsSliderSample
334  *
335  * Slider using a custom thumb:
336  *
337  * @sample androidx.compose.material3.samples.SliderWithCustomThumbSample
338  *
339  * Slider using custom track and thumb:
340  *
341  * @sample androidx.compose.material3.samples.SliderWithCustomTrackAndThumbSample
342  *
343  * Slider using track icons:
344  *
345  * @sample androidx.compose.material3.samples.SliderWithTrackIconsSample
346  * @param state [SliderState] which contains the slider's current value.
347  * @param modifier the [Modifier] to be applied to this slider
348  * @param enabled controls the enabled state of this slider. When `false`, this component will not
349  *   respond to user input, and it will appear visually disabled and disabled to accessibility
350  *   services.
351  * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
352  *   different states. See [SliderDefaults.colors].
353  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
354  *   for this slider. You can create and pass in your own `remember`ed instance to observe
355  *   [Interaction]s and customize the appearance / behavior of this slider in different states.
356  * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
357  *   lambda receives a [SliderState] which is used to obtain the current active track.
358  * @param track the track to be displayed on the slider, it is placed underneath the thumb. The
359  *   lambda receives a [SliderState] which is used to obtain the current active track.
360  */
361 @Composable
362 @ExperimentalMaterial3Api
Slidernull363 fun Slider(
364     state: SliderState,
365     modifier: Modifier = Modifier,
366     enabled: Boolean = true,
367     colors: SliderColors = SliderDefaults.colors(),
368     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
369     thumb: @Composable (SliderState) -> Unit = {
370         SliderDefaults.Thumb(
371             interactionSource = interactionSource,
372             colors = colors,
373             enabled = enabled
374         )
375     },
376     track: @Composable (SliderState) -> Unit = { sliderState ->
377         SliderDefaults.Track(colors = colors, enabled = enabled, sliderState = sliderState)
378     }
379 ) {
<lambda>null380     require(state.steps >= 0) { "steps should be >= 0" }
381 
382     SliderImpl(
383         state = state,
384         modifier = modifier,
385         enabled = enabled,
386         interactionSource = interactionSource,
387         thumb = thumb,
388         track = track
389     )
390 }
391 
392 /**
393  * [Material Design slider](https://m3.material.io/components/sliders/overview)
394  *
395  * Vertical Sliders allow users to make selections from a range of values.
396  *
397  * Vertical Sliders reflect a range of values along a vertical bar, from which users may select a
398  * single value. They are ideal for adjusting settings such as volume, brightness, or applying image
399  * filters.
400  *
401  * ![Sliders
402  * image](https://firebasestorage.googleapis.com/v0/b/design-spec/o/projects%2Fgoogle-material-3%2Fimages%2Flqe2zb2b-1.png?alt=media)
403  *
404  * Vertical Slider:
405  *
406  * @sample androidx.compose.material3.samples.VerticalSliderSample
407  * @param state [SliderState] which contains the slider's current value.
408  * @param modifier the [Modifier] to be applied to this slider
409  * @param enabled controls the enabled state of this slider. When `false`, this component will not
410  *   respond to user input, and it will appear visually disabled and disabled to accessibility
411  *   services.
412  * @param reverseDirection controls the direction of this slider. Default is top to bottom.
413  * @param colors [SliderColors] that will be used to resolve the colors used for this slider in
414  *   different states. See [SliderDefaults.colors].
415  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
416  *   for this slider. You can create and pass in your own `remember`ed instance to observe
417  *   [Interaction]s and customize the appearance / behavior of this slider in different states.
418  * @param thumb the thumb to be displayed on the slider, it is placed on top of the track. The
419  *   lambda receives a [SliderState] which is used to obtain the current active track.
420  * @param track the track to be displayed on the slider, it is placed underneath the thumb. The
421  *   lambda receives a [SliderState] which is used to obtain the current active track.
422  */
423 @OptIn(ExperimentalMaterial3Api::class)
424 @ExperimentalMaterial3ExpressiveApi
425 @Composable
VerticalSlidernull426 fun VerticalSlider(
427     state: SliderState,
428     modifier: Modifier = Modifier,
429     enabled: Boolean = true,
430     reverseDirection: Boolean = false,
431     colors: SliderColors = SliderDefaults.colors(),
432     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
433     thumb: @Composable (SliderState) -> Unit = { sliderState ->
434         SliderDefaults.Thumb(
435             interactionSource = interactionSource,
436             sliderState = sliderState,
437             colors = colors,
438             enabled = enabled,
439             thumbSize = VerticalThumbSize
440         )
441     },
442     track: @Composable (SliderState) -> Unit = { sliderState ->
443         SliderDefaults.Track(
444             colors = colors,
445             enabled = enabled,
446             sliderState = sliderState,
447             trackCornerSize = Dp.Unspecified
448         )
449     }
450 ) {
<lambda>null451     require(state.steps >= 0) { "steps should be >= 0" }
452 
453     state.orientation = Vertical
454     state.reverseVerticalDirection = reverseDirection
455 
456     SliderImpl(
457         state = state,
458         modifier = modifier,
459         enabled = enabled,
460         interactionSource = interactionSource,
461         thumb = thumb,
462         track = track
463     )
464 }
465 
466 /**
467  * [Material Design range slider](https://m3.material.io/components/sliders/overview)
468  *
469  * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
470  *
471  * The two values are still bounded by the value range but they also cannot cross each other.
472  *
473  * Use continuous Range Sliders to allow users to make meaningful selections that don’t require a
474  * specific values:
475  *
476  * @sample androidx.compose.material3.samples.RangeSliderSample
477  *
478  * You can allow the user to choose only between predefined set of values by specifying the amount
479  * of steps between min and max values:
480  *
481  * @sample androidx.compose.material3.samples.StepRangeSliderSample
482  * @param value current values of the RangeSlider. If either value is outside of [valueRange]
483  *   provided, it will be coerced to this range.
484  * @param onValueChange lambda in which values should be updated
485  * @param modifier modifiers for the Range Slider layout
486  * @param enabled whether or not component is enabled and can we interacted with or not
487  * @param valueRange range of values that Range Slider values can take. Passed [value] will be
488  *   coerced to this range
489  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
490  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
491  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
492  *   continuously and allow any value from the range. Must not be negative.
493  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
494  *   shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
495  *   to know when the user has completed selecting a new value by ending a drag or a click.
496  * @param colors [SliderColors] that will be used to determine the color of the Range Slider parts
497  *   in different state. See [SliderDefaults.colors] to customize.
498  */
499 @OptIn(ExperimentalMaterial3Api::class)
500 @Composable
RangeSlidernull501 fun RangeSlider(
502     value: ClosedFloatingPointRange<Float>,
503     onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
504     modifier: Modifier = Modifier,
505     enabled: Boolean = true,
506     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
507     @IntRange(from = 0) steps: Int = 0,
508     onValueChangeFinished: (() -> Unit)? = null,
509     colors: SliderColors = SliderDefaults.colors()
510 ) {
511     val startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
512     val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
513 
514     RangeSlider(
515         value = value,
516         onValueChange = onValueChange,
517         modifier = modifier,
518         enabled = enabled,
519         valueRange = valueRange,
520         steps = steps,
521         onValueChangeFinished = onValueChangeFinished,
522         startInteractionSource = startInteractionSource,
523         endInteractionSource = endInteractionSource,
524         startThumb = {
525             SliderDefaults.Thumb(
526                 interactionSource = startInteractionSource,
527                 colors = colors,
528                 enabled = enabled
529             )
530         },
531         endThumb = {
532             SliderDefaults.Thumb(
533                 interactionSource = endInteractionSource,
534                 colors = colors,
535                 enabled = enabled
536             )
537         },
538         track = { rangeSliderState ->
539             SliderDefaults.Track(
540                 colors = colors,
541                 enabled = enabled,
542                 rangeSliderState = rangeSliderState
543             )
544         }
545     )
546 }
547 
548 /**
549  * [Material Design range slider](https://m3.material.io/components/sliders/overview)
550  *
551  * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
552  *
553  * The two values are still bounded by the value range but they also cannot cross each other.
554  *
555  * It uses the provided startThumb for the slider's start thumb and endThumb for the slider's end
556  * thumb. It also uses the provided track for the slider's track. If nothing is passed for these
557  * parameters, it will use [SliderDefaults.Thumb] and [SliderDefaults.Track] for the thumbs and
558  * track.
559  *
560  * Use continuous Range Sliders to allow users to make meaningful selections that don’t require a
561  * specific values:
562  *
563  * @sample androidx.compose.material3.samples.RangeSliderSample
564  *
565  * You can allow the user to choose only between predefined set of values by specifying the amount
566  * of steps between min and max values:
567  *
568  * @sample androidx.compose.material3.samples.StepRangeSliderSample
569  *
570  * A custom start/end thumb and track can be provided:
571  *
572  * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
573  * @param value current values of the RangeSlider. If either value is outside of [valueRange]
574  *   provided, it will be coerced to this range.
575  * @param onValueChange lambda in which values should be updated
576  * @param modifier modifiers for the Range Slider layout
577  * @param enabled whether or not component is enabled and can we interacted with or not
578  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
579  *   shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
580  *   to know when the user has completed selecting a new value by ending a drag or a click.
581  * @param colors [SliderColors] that will be used to determine the color of the Range Slider parts
582  *   in different state. See [SliderDefaults.colors] to customize.
583  * @param startInteractionSource the [MutableInteractionSource] representing the stream of
584  *   [Interaction]s for the start thumb. You can create and pass in your own `remember`ed instance
585  *   to observe.
586  * @param endInteractionSource the [MutableInteractionSource] representing the stream of
587  *   [Interaction]s for the end thumb. You can create and pass in your own `remember`ed instance to
588  *   observe.
589  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
590  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
591  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
592  *   continuously and allow any value from the range. Must not be negative.
593  * @param startThumb the start thumb to be displayed on the Range Slider. The lambda receives a
594  *   [RangeSliderState] which is used to obtain the current active track.
595  * @param endThumb the end thumb to be displayed on the Range Slider. The lambda receives a
596  *   [RangeSliderState] which is used to obtain the current active track.
597  * @param track the track to be displayed on the range slider, it is placed underneath the thumb.
598  *   The lambda receives a [RangeSliderState] which is used to obtain the current active track.
599  * @param valueRange range of values that Range Slider values can take. Passed [value] will be
600  *   coerced to this range.
601  */
602 @Composable
603 @ExperimentalMaterial3Api
RangeSlidernull604 fun RangeSlider(
605     value: ClosedFloatingPointRange<Float>,
606     onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
607     modifier: Modifier = Modifier,
608     enabled: Boolean = true,
609     valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
610     onValueChangeFinished: (() -> Unit)? = null,
611     colors: SliderColors = SliderDefaults.colors(),
612     startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
613     endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
614     startThumb: @Composable (RangeSliderState) -> Unit = {
615         SliderDefaults.Thumb(
616             interactionSource = startInteractionSource,
617             colors = colors,
618             enabled = enabled
619         )
620     },
621     endThumb: @Composable (RangeSliderState) -> Unit = {
622         SliderDefaults.Thumb(
623             interactionSource = endInteractionSource,
624             colors = colors,
625             enabled = enabled
626         )
627     },
628     track: @Composable (RangeSliderState) -> Unit = { rangeSliderState ->
629         SliderDefaults.Track(
630             colors = colors,
631             enabled = enabled,
632             rangeSliderState = rangeSliderState
633         )
634     },
635     @IntRange(from = 0) steps: Int = 0
636 ) {
637     val state =
<lambda>null638         remember(steps, valueRange) {
639             RangeSliderState(
640                 value.start,
641                 value.endInclusive,
642                 steps,
643                 onValueChangeFinished,
644                 valueRange
645             )
646         }
647 
648     state.onValueChangeFinished = onValueChangeFinished
<lambda>null649     state.onValueChange = { onValueChange(it.start..it.endInclusive) }
650     state.activeRangeStart = value.start
651     state.activeRangeEnd = value.endInclusive
652 
653     RangeSlider(
654         modifier = modifier,
655         state = state,
656         enabled = enabled,
657         startInteractionSource = startInteractionSource,
658         endInteractionSource = endInteractionSource,
659         startThumb = startThumb,
660         endThumb = endThumb,
661         track = track
662     )
663 }
664 
665 /**
666  * [Material Design range slider](https://m3.material.io/components/sliders/overview)
667  *
668  * Range Sliders expand upon [Slider] using the same concepts but allow the user to select 2 values.
669  *
670  * The two values are still bounded by the value range but they also cannot cross each other.
671  *
672  * It uses the provided startThumb for the slider's start thumb and endThumb for the slider's end
673  * thumb. It also uses the provided track for the slider's track. If nothing is passed for these
674  * parameters, it will use [SliderDefaults.Thumb] and [SliderDefaults.Track] for the thumbs and
675  * track.
676  *
677  * Use continuous Range Sliders to allow users to make meaningful selections that don’t require a
678  * specific values:
679  *
680  * @sample androidx.compose.material3.samples.RangeSliderSample
681  *
682  * You can allow the user to choose only between predefined set of values by specifying the amount
683  * of steps between min and max values:
684  *
685  * @sample androidx.compose.material3.samples.StepRangeSliderSample
686  *
687  * A custom start/end thumb and track can be provided:
688  *
689  * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
690  * @param state [RangeSliderState] which contains the current values of the RangeSlider.
691  * @param modifier modifiers for the Range Slider layout
692  * @param enabled whether or not component is enabled and can we interacted with or not
693  * @param colors [SliderColors] that will be used to determine the color of the Range Slider parts
694  *   in different state. See [SliderDefaults.colors] to customize.
695  * @param startInteractionSource the [MutableInteractionSource] representing the stream of
696  *   [Interaction]s for the start thumb. You can create and pass in your own `remember`ed instance
697  *   to observe.
698  * @param endInteractionSource the [MutableInteractionSource] representing the stream of
699  *   [Interaction]s for the end thumb. You can create and pass in your own `remember`ed instance to
700  *   observe.
701  * @param startThumb the start thumb to be displayed on the Range Slider. The lambda receives a
702  *   [RangeSliderState] which is used to obtain the current active track.
703  * @param endThumb the end thumb to be displayed on the Range Slider. The lambda receives a
704  *   [RangeSliderState] which is used to obtain the current active track.
705  * @param track the track to be displayed on the range slider, it is placed underneath the thumb.
706  *   The lambda receives a [RangeSliderState] which is used to obtain the current active track.
707  */
708 @Composable
709 @ExperimentalMaterial3Api
RangeSlidernull710 fun RangeSlider(
711     state: RangeSliderState,
712     modifier: Modifier = Modifier,
713     enabled: Boolean = true,
714     colors: SliderColors = SliderDefaults.colors(),
715     startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
716     endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
717     startThumb: @Composable (RangeSliderState) -> Unit = {
718         SliderDefaults.Thumb(
719             interactionSource = startInteractionSource,
720             colors = colors,
721             enabled = enabled
722         )
723     },
724     endThumb: @Composable (RangeSliderState) -> Unit = {
725         SliderDefaults.Thumb(
726             interactionSource = endInteractionSource,
727             colors = colors,
728             enabled = enabled
729         )
730     },
731     track: @Composable (RangeSliderState) -> Unit = { rangeSliderState ->
732         SliderDefaults.Track(
733             colors = colors,
734             enabled = enabled,
735             rangeSliderState = rangeSliderState
736         )
737     }
738 ) {
<lambda>null739     require(state.steps >= 0) { "steps should be >= 0" }
740 
741     RangeSliderImpl(
742         modifier = modifier,
743         state = state,
744         enabled = enabled,
745         startInteractionSource = startInteractionSource,
746         endInteractionSource = endInteractionSource,
747         startThumb = startThumb,
748         endThumb = endThumb,
749         track = track
750     )
751 }
752 
753 @OptIn(ExperimentalMaterial3Api::class)
754 @Composable
SliderImplnull755 private fun SliderImpl(
756     modifier: Modifier,
757     state: SliderState,
758     enabled: Boolean,
759     interactionSource: MutableInteractionSource,
760     thumb: @Composable (SliderState) -> Unit,
761     track: @Composable (SliderState) -> Unit
762 ) {
763     state.isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
764     val reverseDirection =
765         (state.orientation == Horizontal && state.isRtl) ||
766             (state.orientation == Vertical && state.reverseVerticalDirection)
767     val press = Modifier.sliderTapModifier(state, interactionSource, enabled)
768     val drag =
769         Modifier.draggable(
770             orientation = state.orientation,
771             reverseDirection = reverseDirection,
772             enabled = enabled,
773             interactionSource = interactionSource,
774             onDragStopped = { state.gestureEndAction() },
775             startDragImmediately = state.isDragging,
776             state = state
777         )
778     val thumbModifier =
779         if (state.orientation == Vertical) {
780             Modifier.layoutId(SliderComponents.THUMB).wrapContentHeight()
781         } else {
782             Modifier.layoutId(SliderComponents.THUMB).wrapContentWidth()
783         }
784 
785     Layout(
786         {
787             Box(
788                 modifier =
789                     thumbModifier.onSizeChanged {
790                         state.thumbWidth = it.width
791                         state.thumbHeight = it.height
792                     }
793             ) {
794                 thumb(state)
795             }
796             Box(modifier = Modifier.layoutId(SliderComponents.TRACK)) { track(state) }
797         },
798         modifier =
799             modifier
800                 .minimumInteractiveComponentSize()
801                 .requiredSizeIn(
802                     minWidth = if (state.orientation == Vertical) TrackHeight else ThumbWidth,
803                     minHeight = if (state.orientation == Vertical) ThumbWidth else TrackHeight,
804                 )
805                 .sliderSemantics(state, enabled)
806                 .focusable(enabled, interactionSource)
807                 .slideOnKeyEvents(
808                     enabled,
809                     state.steps,
810                     state.valueRange,
811                     state.value,
812                     reverseDirection,
813                     state.onValueChange,
814                     state.onValueChangeFinished
815                 )
816                 .then(press)
817                 .then(drag)
818     ) { measurables, constraints ->
819         val thumbPlaceable =
820             measurables.fastFirst { it.layoutId == SliderComponents.THUMB }.measure(constraints)
821 
822         val trackMeasurable = measurables.fastFirst { it.layoutId == SliderComponents.TRACK }
823         val trackPlaceable =
824             if (state.orientation == Vertical) {
825                 trackMeasurable.measure(
826                     constraints.offset(vertical = -thumbPlaceable.height).copy(minWidth = 0)
827                 )
828             } else {
829                 trackMeasurable.measure(
830                     constraints.offset(horizontal = -thumbPlaceable.width).copy(minHeight = 0)
831                 )
832             }
833 
834         val sliderWidth: Int
835         val sliderHeight: Int
836         val trackOffsetX: Int
837         val trackOffsetY: Int
838         val thumbOffsetX: Int
839         var thumbOffsetY: Int
840         val valueAsFraction = state.coercedValueAsFraction
841         val isOnFirstOrLastStep =
842             valueAsFraction == state.tickFractions.firstOrNull() ||
843                 valueAsFraction == state.tickFractions.lastOrNull()
844         var trackCornerSize =
845             trackPlaceable[CornerSizeAlignmentLine].let {
846                 if (it != AlignmentLine.Unspecified) it else 0
847             }
848 
849         if (layoutDirection == LayoutDirection.Rtl && trackCornerSize != 0) {
850             trackCornerSize = trackPlaceable.width - trackCornerSize
851         }
852         if (state.orientation == Vertical) {
853             sliderWidth = max(trackPlaceable.width, thumbPlaceable.width)
854             sliderHeight = thumbPlaceable.height + trackPlaceable.height
855             trackOffsetX = (sliderWidth - trackPlaceable.width) / 2
856             trackOffsetY = thumbPlaceable.height / 2
857             thumbOffsetX = (sliderWidth - thumbPlaceable.width) / 2
858             thumbOffsetY =
859                 if (state.steps > 0 && !isOnFirstOrLastStep) {
860                     ((trackPlaceable.height - trackCornerSize * 2) * valueAsFraction).roundToInt() +
861                         trackCornerSize
862                 } else {
863                     (trackPlaceable.height * valueAsFraction).roundToInt()
864                 }
865             if (state.reverseVerticalDirection) {
866                 thumbOffsetY = trackPlaceable.height - thumbOffsetY
867             }
868         } else {
869             sliderWidth = thumbPlaceable.width + trackPlaceable.width
870             sliderHeight = max(trackPlaceable.height, thumbPlaceable.height)
871             trackOffsetX = thumbPlaceable.width / 2
872             trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
873             thumbOffsetX =
874                 if (state.steps > 0 && !isOnFirstOrLastStep) {
875                     ((trackPlaceable.width - trackCornerSize * 2) * valueAsFraction).roundToInt() +
876                         trackCornerSize
877                 } else {
878                     (trackPlaceable.width * valueAsFraction).roundToInt()
879                 }
880             thumbOffsetY = (sliderHeight - thumbPlaceable.height) / 2
881         }
882 
883         state.updateDimensions(newTotalWidth = sliderWidth, newTotalHeight = sliderHeight)
884 
885         layout(sliderWidth, sliderHeight) {
886             trackPlaceable.placeRelative(trackOffsetX, trackOffsetY)
887             thumbPlaceable.placeRelative(thumbOffsetX, thumbOffsetY)
888         }
889     }
890 }
891 
Modifiernull892 private fun Modifier.slideOnKeyEvents(
893     enabled: Boolean,
894     steps: Int,
895     valueRange: ClosedFloatingPointRange<Float>,
896     value: Float,
897     reverseDirection: Boolean,
898     onValueChangeState: ((Float) -> Unit)?,
899     onValueChangeFinishedState: (() -> Unit)?
900 ): Modifier {
901     require(steps >= 0) { "steps should be >= 0" }
902     return this.onKeyEvent {
903         if (!enabled) return@onKeyEvent false
904         if (onValueChangeState == null) return@onKeyEvent false
905         when (it.type) {
906             KeyEventType.KeyDown -> {
907                 val rangeLength = abs(valueRange.endInclusive - valueRange.start)
908                 // When steps == 0, it means that a user is not limited by a step length (delta)
909                 // when using touch or mouse. But it is not possible to adjust the value
910                 // continuously when using keyboard buttons - the delta has to be discrete.
911                 // In this case, 1% of the valueRange seems to make sense.
912                 val actualSteps = if (steps > 0) steps + 1 else 100
913                 val delta = rangeLength / actualSteps
914                 val sign = if (reverseDirection) -1 else 1
915                 when (it.key) {
916                     Key.DirectionUp -> {
917                         onValueChangeState((value + sign * delta).coerceIn(valueRange))
918                         true
919                     }
920                     Key.DirectionDown -> {
921                         onValueChangeState((value - sign * delta).coerceIn(valueRange))
922                         true
923                     }
924                     Key.DirectionRight -> {
925                         onValueChangeState((value + sign * delta).coerceIn(valueRange))
926                         true
927                     }
928                     Key.DirectionLeft -> {
929                         onValueChangeState((value - sign * delta).coerceIn(valueRange))
930                         true
931                     }
932                     Key.MoveHome -> {
933                         onValueChangeState(valueRange.start)
934                         true
935                     }
936                     Key.MoveEnd -> {
937                         onValueChangeState(valueRange.endInclusive)
938                         true
939                     }
940                     Key.PageUp -> {
941                         val page = (actualSteps / 10).coerceIn(1, 10)
942                         onValueChangeState((value - page * delta).coerceIn(valueRange))
943                         true
944                     }
945                     Key.PageDown -> {
946                         val page = (actualSteps / 10).coerceIn(1, 10)
947                         onValueChangeState((value + page * delta).coerceIn(valueRange))
948                         true
949                     }
950                     else -> false
951                 }
952             }
953             KeyEventType.KeyUp -> {
954                 when (it.key) {
955                     Key.DirectionUp,
956                     Key.DirectionDown,
957                     Key.DirectionRight,
958                     Key.DirectionLeft,
959                     Key.MoveHome,
960                     Key.MoveEnd,
961                     Key.PageUp,
962                     Key.PageDown -> {
963                         onValueChangeFinishedState?.invoke()
964                         true
965                     }
966                     else -> false
967                 }
968             }
969             else -> false
970         }
971     }
972 }
973 
974 @OptIn(ExperimentalMaterial3Api::class)
975 @Composable
RangeSliderImplnull976 private fun RangeSliderImpl(
977     modifier: Modifier,
978     state: RangeSliderState,
979     enabled: Boolean,
980     startInteractionSource: MutableInteractionSource,
981     endInteractionSource: MutableInteractionSource,
982     startThumb: @Composable ((RangeSliderState) -> Unit),
983     endThumb: @Composable ((RangeSliderState) -> Unit),
984     track: @Composable ((RangeSliderState) -> Unit)
985 ) {
986     state.isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
987 
988     val pressDrag =
989         Modifier.rangeSliderPressDragModifier(
990             state,
991             startInteractionSource,
992             endInteractionSource,
993             enabled
994         )
995 
996     val startContentDescription = getString(Strings.SliderRangeStart)
997     val endContentDescription = getString(Strings.SliderRangeEnd)
998 
999     Layout(
1000         {
1001             Box(
1002                 modifier =
1003                     Modifier.layoutId(RangeSliderComponents.STARTTHUMB)
1004                         .wrapContentWidth()
1005                         .onSizeChanged {
1006                             state.startThumbWidth = it.width.toFloat()
1007                             state.startThumbHeight = it.height.toFloat()
1008                         }
1009                         .rangeSliderStartThumbSemantics(state, enabled)
1010                         .semantics(mergeDescendants = true) {
1011                             contentDescription = startContentDescription
1012                         }
1013                         .focusable(enabled, startInteractionSource)
1014             ) {
1015                 startThumb(state)
1016             }
1017             Box(
1018                 modifier =
1019                     Modifier.layoutId(RangeSliderComponents.ENDTHUMB)
1020                         .wrapContentWidth()
1021                         .onSizeChanged {
1022                             state.endThumbWidth = it.width.toFloat()
1023                             state.endThumbHeight = it.height.toFloat()
1024                         }
1025                         .rangeSliderEndThumbSemantics(state, enabled)
1026                         .semantics(mergeDescendants = true) {
1027                             contentDescription = endContentDescription
1028                         }
1029                         .focusable(enabled, endInteractionSource)
1030             ) {
1031                 endThumb(state)
1032             }
1033             Box(modifier = Modifier.layoutId(RangeSliderComponents.TRACK)) { track(state) }
1034         },
1035         modifier =
1036             modifier
1037                 .minimumInteractiveComponentSize()
1038                 .requiredSizeIn(minWidth = ThumbWidth, minHeight = TrackHeight)
1039                 .then(pressDrag)
1040     ) { measurables, constraints ->
1041         val startThumbPlaceable =
1042             measurables
1043                 .fastFirst { it.layoutId == RangeSliderComponents.STARTTHUMB }
1044                 .measure(constraints)
1045 
1046         val endThumbPlaceable =
1047             measurables
1048                 .fastFirst { it.layoutId == RangeSliderComponents.ENDTHUMB }
1049                 .measure(constraints)
1050 
1051         val trackPlaceable =
1052             measurables
1053                 .fastFirst { it.layoutId == RangeSliderComponents.TRACK }
1054                 .measure(
1055                     constraints
1056                         .offset(
1057                             horizontal = -(startThumbPlaceable.width + endThumbPlaceable.width) / 2
1058                         )
1059                         .copy(minHeight = 0)
1060                 )
1061 
1062         val sliderWidth =
1063             trackPlaceable.width + (startThumbPlaceable.width + endThumbPlaceable.width) / 2
1064         val sliderHeight =
1065             maxOf(trackPlaceable.height, startThumbPlaceable.height, endThumbPlaceable.height)
1066 
1067         state.totalWidth = sliderWidth
1068 
1069         state.updateMinMaxPx()
1070 
1071         val startValueAsFraction = state.coercedActiveRangeStartAsFraction
1072         val isStartOnFirstOrLastStep =
1073             startValueAsFraction == state.tickFractions.firstOrNull() ||
1074                 startValueAsFraction == state.tickFractions.lastOrNull()
1075         val endValueAsFraction = state.coercedActiveRangeEndAsFraction
1076         val isEndOnFirstOrLastStep =
1077             endValueAsFraction == state.tickFractions.firstOrNull() ||
1078                 endValueAsFraction == state.tickFractions.lastOrNull()
1079         val trackOffsetX = startThumbPlaceable.width / 2
1080         var trackCornerSize =
1081             trackPlaceable[CornerSizeAlignmentLine].let {
1082                 if (it != AlignmentLine.Unspecified) it else 0
1083             }
1084 
1085         if (layoutDirection == LayoutDirection.Rtl && trackCornerSize != 0) {
1086             trackCornerSize = trackPlaceable.width - trackCornerSize
1087         }
1088 
1089         val startThumbOffsetX =
1090             if (state.steps > 0 && !isStartOnFirstOrLastStep) {
1091                 ((trackPlaceable.width - trackCornerSize * 2) * startValueAsFraction).roundToInt() +
1092                     trackCornerSize
1093             } else {
1094                 (trackPlaceable.width * startValueAsFraction).roundToInt()
1095             }
1096         // When start thumb and end thumb have different widths,
1097         // we need to add a correction for the centering of the slider.
1098         val endCorrection = (startThumbPlaceable.width - endThumbPlaceable.width) / 2
1099         val endThumbOffsetX =
1100             if (state.steps > 0 && !isEndOnFirstOrLastStep) {
1101                 ((trackPlaceable.width - trackCornerSize * 2) * endValueAsFraction + endCorrection)
1102                     .roundToInt() + trackCornerSize
1103             } else {
1104                 (trackPlaceable.width * endValueAsFraction + endCorrection).roundToInt()
1105             }
1106         val trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
1107         val startThumbOffsetY = (sliderHeight - startThumbPlaceable.height) / 2
1108         val endThumbOffsetY = (sliderHeight - endThumbPlaceable.height) / 2
1109 
1110         layout(sliderWidth, sliderHeight) {
1111             trackPlaceable.placeRelative(trackOffsetX, trackOffsetY)
1112             startThumbPlaceable.placeRelative(startThumbOffsetX, startThumbOffsetY)
1113             endThumbPlaceable.placeRelative(endThumbOffsetX, endThumbOffsetY)
1114         }
1115     }
1116 }
1117 
1118 /** Object to hold defaults used by [Slider] */
1119 @Stable
1120 object SliderDefaults {
1121 
1122     /**
1123      * Creates a [SliderColors] that represents the different colors used in parts of the [Slider]
1124      * in different states.
1125      */
colorsnull1126     @Composable fun colors() = MaterialTheme.colorScheme.defaultSliderColors
1127 
1128     /**
1129      * Creates a [SliderColors] that represents the different colors used in parts of the [Slider]
1130      * in different states.
1131      *
1132      * For the name references below the words "active" and "inactive" are used. Active part of the
1133      * slider is filled with progress, so if slider's progress is 30% out of 100%, left (or right in
1134      * RTL) 30% of the track will be active, while the rest is inactive.
1135      *
1136      * @param thumbColor thumb color when enabled
1137      * @param activeTrackColor color of the track in the part that is "active", meaning that the
1138      *   thumb is ahead of it
1139      * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps`
1140      *   is specified
1141      * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
1142      *   thumb is before it
1143      * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if
1144      *   `steps` are specified on the Slider is specified
1145      * @param disabledThumbColor thumb colors when disabled
1146      * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
1147      *   disabled
1148      * @param disabledActiveTickColor colors to be used to draw tick marks on the active track when
1149      *   Slider is disabled and when `steps` are specified on it
1150      * @param disabledInactiveTrackColor color of the track in the "inactive" part when the Slider
1151      *   is disabled
1152      * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part of
1153      *   the track when Slider is disabled and when `steps` are specified on it
1154      */
1155     @Composable
1156     fun colors(
1157         thumbColor: Color = Color.Unspecified,
1158         activeTrackColor: Color = Color.Unspecified,
1159         activeTickColor: Color = Color.Unspecified,
1160         inactiveTrackColor: Color = Color.Unspecified,
1161         inactiveTickColor: Color = Color.Unspecified,
1162         disabledThumbColor: Color = Color.Unspecified,
1163         disabledActiveTrackColor: Color = Color.Unspecified,
1164         disabledActiveTickColor: Color = Color.Unspecified,
1165         disabledInactiveTrackColor: Color = Color.Unspecified,
1166         disabledInactiveTickColor: Color = Color.Unspecified
1167     ): SliderColors =
1168         MaterialTheme.colorScheme.defaultSliderColors.copy(
1169             thumbColor = thumbColor,
1170             activeTrackColor = activeTrackColor,
1171             activeTickColor = activeTickColor,
1172             inactiveTrackColor = inactiveTrackColor,
1173             inactiveTickColor = inactiveTickColor,
1174             disabledThumbColor = disabledThumbColor,
1175             disabledActiveTrackColor = disabledActiveTrackColor,
1176             disabledActiveTickColor = disabledActiveTickColor,
1177             disabledInactiveTrackColor = disabledInactiveTrackColor,
1178             disabledInactiveTickColor = disabledInactiveTickColor
1179         )
1180 
1181     internal val ColorScheme.defaultSliderColors: SliderColors
1182         get() {
1183             return defaultSliderColorsCached
1184                 ?: SliderColors(
1185                         thumbColor = fromToken(SliderTokens.HandleColor),
1186                         activeTrackColor = fromToken(SliderTokens.ActiveTrackColor),
1187                         activeTickColor = fromToken(SliderTokens.InactiveTrackColor),
1188                         inactiveTrackColor = fromToken(SliderTokens.InactiveTrackColor),
1189                         inactiveTickColor = fromToken(SliderTokens.ActiveTrackColor),
1190                         disabledThumbColor =
1191                             fromToken(SliderTokens.DisabledHandleColor)
1192                                 .copy(alpha = SliderTokens.DisabledHandleOpacity)
1193                                 .compositeOver(surface),
1194                         disabledActiveTrackColor =
1195                             fromToken(SliderTokens.DisabledActiveTrackColor)
1196                                 .copy(alpha = SliderTokens.DisabledActiveTrackOpacity),
1197                         disabledActiveTickColor =
1198                             fromToken(SliderTokens.DisabledInactiveTrackColor)
1199                                 .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
1200                         disabledInactiveTrackColor =
1201                             fromToken(SliderTokens.DisabledInactiveTrackColor)
1202                                 .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
1203                         disabledInactiveTickColor =
1204                             fromToken(SliderTokens.DisabledActiveTrackColor)
1205                                 .copy(alpha = SliderTokens.DisabledActiveTrackOpacity)
1206                     )
1207                     .also { defaultSliderColorsCached = it }
1208         }
1209 
1210     /**
1211      * The Default thumb for [Slider] and [RangeSlider]
1212      *
1213      * @param interactionSource the [MutableInteractionSource] representing the stream of
1214      *   [Interaction]s for this thumb. You can create and pass in your own `remember`ed instance to
1215      *   observe
1216      * @param modifier the [Modifier] to be applied to the thumb.
1217      * @param colors [SliderColors] that will be used to resolve the colors used for this thumb in
1218      *   different states. See [SliderDefaults.colors].
1219      * @param enabled controls the enabled state of this slider. When `false`, this component will
1220      *   not respond to user input, and it will appear visually disabled and disabled to
1221      *   accessibility services.
1222      * @param thumbSize the size of the thumb.
1223      */
1224     @Composable
Thumbnull1225     fun Thumb(
1226         interactionSource: MutableInteractionSource,
1227         modifier: Modifier = Modifier,
1228         colors: SliderColors = colors(),
1229         enabled: Boolean = true,
1230         thumbSize: DpSize = ThumbSize
1231     ) {
1232         val interactions = remember { mutableStateListOf<Interaction>() }
1233         LaunchedEffect(interactionSource) {
1234             interactionSource.interactions.collect { interaction ->
1235                 when (interaction) {
1236                     is PressInteraction.Press -> interactions.add(interaction)
1237                     is PressInteraction.Release -> interactions.remove(interaction.press)
1238                     is PressInteraction.Cancel -> interactions.remove(interaction.press)
1239                     is DragInteraction.Start -> interactions.add(interaction)
1240                     is DragInteraction.Stop -> interactions.remove(interaction.start)
1241                     is DragInteraction.Cancel -> interactions.remove(interaction.start)
1242                 }
1243             }
1244         }
1245 
1246         val size =
1247             if (interactions.isNotEmpty()) {
1248                 thumbSize.copy(width = thumbSize.width / 2)
1249             } else {
1250                 thumbSize
1251             }
1252         Spacer(
1253             modifier
1254                 .size(size)
1255                 .hoverable(interactionSource = interactionSource)
1256                 .background(colors.thumbColor(enabled), SliderTokens.HandleShape.value)
1257         )
1258     }
1259 
1260     /**
1261      * The Default thumb for [Slider], [VerticalSlider] and [RangeSlider]
1262      *
1263      * @param interactionSource the [MutableInteractionSource] representing the stream of
1264      *   [Interaction]s for this thumb. You can create and pass in your own `remember`ed instance to
1265      *   observe
1266      * @param sliderState [SliderState] which is used to obtain the current active track.
1267      * @param modifier the [Modifier] to be applied to the thumb.
1268      * @param colors [SliderColors] that will be used to resolve the colors used for this thumb in
1269      *   different states. See [SliderDefaults.colors].
1270      * @param enabled controls the enabled state of this slider. When `false`, this component will
1271      *   not respond to user input, and it will appear visually disabled and disabled to
1272      *   accessibility services.
1273      * @param thumbSize the size of the thumb.
1274      */
1275     @OptIn(ExperimentalMaterial3Api::class)
1276     @ExperimentalMaterial3ExpressiveApi
1277     @Composable
Thumbnull1278     fun Thumb(
1279         interactionSource: MutableInteractionSource,
1280         sliderState: SliderState,
1281         modifier: Modifier = Modifier,
1282         colors: SliderColors = colors(),
1283         enabled: Boolean = true,
1284         thumbSize: DpSize = ThumbSize
1285     ) {
1286         val interactions = remember { mutableStateListOf<Interaction>() }
1287         LaunchedEffect(interactionSource) {
1288             interactionSource.interactions.collect { interaction ->
1289                 when (interaction) {
1290                     is PressInteraction.Press -> interactions.add(interaction)
1291                     is PressInteraction.Release -> interactions.remove(interaction.press)
1292                     is PressInteraction.Cancel -> interactions.remove(interaction.press)
1293                     is DragInteraction.Start -> interactions.add(interaction)
1294                     is DragInteraction.Stop -> interactions.remove(interaction.start)
1295                     is DragInteraction.Cancel -> interactions.remove(interaction.start)
1296                 }
1297             }
1298         }
1299 
1300         val size =
1301             if (interactions.isNotEmpty()) {
1302                 if (sliderState.orientation == Vertical) {
1303                     thumbSize.copy(height = thumbSize.height / 2)
1304                 } else {
1305                     thumbSize.copy(width = thumbSize.width / 2)
1306                 }
1307             } else {
1308                 thumbSize
1309             }
1310         Spacer(
1311             modifier
1312                 .size(size)
1313                 .hoverable(interactionSource = interactionSource)
1314                 .background(colors.thumbColor(enabled), SliderTokens.HandleShape.value)
1315         )
1316     }
1317 
1318     /**
1319      * The Default track for [Slider] and [RangeSlider]
1320      *
1321      * @param sliderPositions [SliderPositions] which is used to obtain the current active track and
1322      *   the tick positions if the slider is discrete.
1323      * @param modifier the [Modifier] to be applied to the track.
1324      * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1325      *   different states. See [SliderDefaults.colors].
1326      * @param enabled controls the enabled state of this slider. When `false`, this component will
1327      *   not respond to user input, and it will appear visually disabled and disabled to
1328      *   accessibility services.
1329      */
1330     @Suppress("DEPRECATION")
1331     @Composable
1332     @Deprecated("Use version that supports slider state")
Tracknull1333     fun Track(
1334         sliderPositions: SliderPositions,
1335         modifier: Modifier = Modifier,
1336         colors: SliderColors = colors(),
1337         enabled: Boolean = true,
1338     ) {
1339         val inactiveTrackColor = colors.trackColor(enabled, active = false)
1340         val activeTrackColor = colors.trackColor(enabled, active = true)
1341         val inactiveTickColor = colors.tickColor(enabled, active = false)
1342         val activeTickColor = colors.tickColor(enabled, active = true)
1343         Canvas(modifier.fillMaxWidth().height(TrackHeight)) {
1344             val isRtl = layoutDirection == LayoutDirection.Rtl
1345             val sliderLeft = Offset(0f, center.y)
1346             val sliderRight = Offset(size.width, center.y)
1347             val sliderStart = if (isRtl) sliderRight else sliderLeft
1348             val sliderEnd = if (isRtl) sliderLeft else sliderRight
1349             val tickSize = TickSize.toPx()
1350             val trackStrokeWidth = TrackHeight.toPx()
1351             drawLine(inactiveTrackColor, sliderStart, sliderEnd, trackStrokeWidth, StrokeCap.Round)
1352             val sliderValueEnd =
1353                 Offset(
1354                     sliderStart.x +
1355                         (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.endInclusive,
1356                     center.y
1357                 )
1358 
1359             val sliderValueStart =
1360                 Offset(
1361                     sliderStart.x +
1362                         (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.start,
1363                     center.y
1364                 )
1365 
1366             drawLine(
1367                 activeTrackColor,
1368                 sliderValueStart,
1369                 sliderValueEnd,
1370                 trackStrokeWidth,
1371                 StrokeCap.Round
1372             )
1373             sliderPositions.tickFractions
1374                 .groupBy {
1375                     it > sliderPositions.activeRange.endInclusive ||
1376                         it < sliderPositions.activeRange.start
1377                 }
1378                 .forEach { (outsideFraction, list) ->
1379                     drawPoints(
1380                         list.fastMap { Offset(lerp(sliderStart, sliderEnd, it).x, center.y) },
1381                         PointMode.Points,
1382                         (if (outsideFraction) inactiveTickColor else activeTickColor),
1383                         tickSize,
1384                         StrokeCap.Round
1385                     )
1386                 }
1387         }
1388     }
1389 
1390     /**
1391      * The Default track for [Slider]
1392      *
1393      * @param sliderState [SliderState] which is used to obtain the current active track.
1394      * @param modifier the [Modifier] to be applied to the track.
1395      * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1396      *   different states. See [SliderDefaults.colors].
1397      * @param enabled controls the enabled state of this slider. When `false`, this component will
1398      *   not respond to user input, and it will appear visually disabled and disabled to
1399      *   accessibility services.
1400      */
1401     @Deprecated(
1402         message =
1403             "Use the overload that takes `drawStopIndicator`, `drawTick`, " +
1404                 "`thumbTrackGapSize` and `trackInsideCornerSize`, see `LegacySliderSample` " +
1405                 "on how to restore the previous behavior",
1406         replaceWith =
1407             ReplaceWith(
1408                 "Track(sliderState, modifier, enabled, colors, drawStopIndicator, " +
1409                     "drawTick, thumbTrackGapSize, trackInsideCornerSize)"
1410             ),
1411         level = DeprecationLevel.HIDDEN
1412     )
1413     @Composable
1414     @ExperimentalMaterial3Api
Tracknull1415     fun Track(
1416         sliderState: SliderState,
1417         modifier: Modifier = Modifier,
1418         colors: SliderColors = colors(),
1419         enabled: Boolean = true
1420     ) {
1421         Track(
1422             sliderState,
1423             modifier,
1424             enabled,
1425             colors,
1426             thumbTrackGapSize = ThumbTrackGapSize,
1427             trackInsideCornerSize = TrackInsideCornerSize
1428         )
1429     }
1430 
1431     /**
1432      * The Default track for [Slider]
1433      *
1434      * @param sliderState [SliderState] which is used to obtain the current active track.
1435      * @param modifier the [Modifier] to be applied to the track.
1436      * @param enabled controls the enabled state of this slider. When `false`, this component will
1437      *   not respond to user input, and it will appear visually disabled and disabled to
1438      *   accessibility services.
1439      * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1440      *   different states. See [SliderDefaults.colors].
1441      * @param drawStopIndicator lambda that will be called to draw the stop indicator at the end of
1442      *   the track.
1443      * @param drawTick lambda that will be called to draw the ticks if steps are greater than 0.
1444      * @param thumbTrackGapSize size of the gap between the thumb and the track.
1445      * @param trackInsideCornerSize size of the corners towards the thumb when a gap is set.
1446      */
1447     @ExperimentalMaterial3Api
1448     @Composable
Tracknull1449     fun Track(
1450         sliderState: SliderState,
1451         modifier: Modifier = Modifier,
1452         enabled: Boolean = true,
1453         colors: SliderColors = colors(),
1454         drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
1455             drawStopIndicator(
1456                 offset = it,
1457                 color = colors.trackColor(enabled, active = true),
1458                 size = TrackStopIndicatorSize
1459             )
1460         },
offsetnull1461         drawTick: DrawScope.(Offset, Color) -> Unit = { offset, color ->
1462             drawStopIndicator(offset = offset, color = color, size = TickSize)
1463         },
1464         thumbTrackGapSize: Dp = ThumbTrackGapSize,
1465         trackInsideCornerSize: Dp = TrackInsideCornerSize
1466     ) {
1467         TrackImpl(
1468             sliderState = sliderState,
1469             trackCornerSize = Dp.Unspecified,
1470             modifier = modifier,
1471             enabled = enabled,
1472             colors = colors,
1473             drawStopIndicator = drawStopIndicator,
1474             drawTick = drawTick,
1475             thumbTrackGapSize = thumbTrackGapSize,
1476             trackInsideCornerSize = trackInsideCornerSize,
1477             enableCornerShrinking = false
1478         )
1479     }
1480 
1481     /**
1482      * The Default track for [Slider] and [VerticalSlider]
1483      *
1484      * This track has a different corner treatment where the corner size decreases as the thumb gets
1485      * closer.
1486      *
1487      * @param sliderState [SliderState] which is used to obtain the current active track.
1488      * @param trackCornerSize size of the external corners.
1489      * @param modifier the [Modifier] to be applied to the track.
1490      * @param enabled controls the enabled state of this slider. When `false`, this component will
1491      *   not respond to user input, and it will appear visually disabled and disabled to
1492      *   accessibility services.
1493      * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1494      *   different states. See [SliderDefaults.colors].
1495      * @param drawStopIndicator lambda that will be called to draw the stop indicator at the end of
1496      *   the track.
1497      * @param drawTick lambda that will be called to draw the ticks if steps are greater than 0.
1498      * @param thumbTrackGapSize size of the gap between the thumb and the track.
1499      * @param trackInsideCornerSize size of the corners towards the thumb when a gap is set.
1500      */
1501     @OptIn(ExperimentalMaterial3Api::class)
1502     @ExperimentalMaterial3ExpressiveApi
1503     @Composable
Tracknull1504     fun Track(
1505         sliderState: SliderState,
1506         trackCornerSize: Dp,
1507         modifier: Modifier = Modifier,
1508         enabled: Boolean = true,
1509         colors: SliderColors = colors(),
1510         drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
1511             drawStopIndicator(
1512                 offset = it,
1513                 color = colors.trackColor(enabled, active = true),
1514                 size = TrackStopIndicatorSize
1515             )
1516         },
offsetnull1517         drawTick: DrawScope.(Offset, Color) -> Unit = { offset, color ->
1518             drawStopIndicator(offset = offset, color = color, size = TickSize)
1519         },
1520         thumbTrackGapSize: Dp = ThumbTrackGapSize,
1521         trackInsideCornerSize: Dp = TrackInsideCornerSize
1522     ) {
1523         TrackImpl(
1524             sliderState = sliderState,
1525             trackCornerSize = trackCornerSize,
1526             modifier = modifier,
1527             enabled = enabled,
1528             colors = colors,
1529             drawStopIndicator = drawStopIndicator,
1530             drawTick = drawTick,
1531             thumbTrackGapSize = thumbTrackGapSize,
1532             trackInsideCornerSize = trackInsideCornerSize,
1533             enableCornerShrinking = true
1534         )
1535     }
1536 
1537     @OptIn(ExperimentalMaterial3Api::class)
1538     @Composable
TrackImplnull1539     private fun TrackImpl(
1540         sliderState: SliderState,
1541         trackCornerSize: Dp,
1542         modifier: Modifier,
1543         enabled: Boolean,
1544         colors: SliderColors,
1545         drawStopIndicator: (DrawScope.(Offset) -> Unit)?,
1546         drawTick: DrawScope.(Offset, Color) -> Unit,
1547         thumbTrackGapSize: Dp,
1548         trackInsideCornerSize: Dp,
1549         enableCornerShrinking: Boolean
1550     ) {
1551         val inactiveTrackColor = colors.trackColor(enabled = enabled, active = false)
1552         val activeTrackColor = colors.trackColor(enabled = enabled, active = true)
1553         val inactiveTickColor = colors.tickColor(enabled = enabled, active = false)
1554         val activeTickColor = colors.tickColor(enabled = enabled, active = true)
1555         var cornerSize by remember { mutableIntStateOf(0) }
1556         Canvas(
1557             if (sliderState.orientation == Vertical) {
1558                     modifier.width(TrackHeight).fillMaxHeight().let {
1559                         if (sliderState.reverseVerticalDirection) it.scale(1f, -1f) else it
1560                     }
1561                 } else {
1562                     modifier.fillMaxWidth().height(TrackHeight).let {
1563                         if (sliderState.isRtl) it.scale(-1f, 1f) else it
1564                     }
1565                 }
1566                 .then(
1567                     Modifier.layout { measurable, constraints ->
1568                         val placeable = measurable.measure(constraints)
1569                         cornerSize =
1570                             if (trackCornerSize == Dp.Unspecified) {
1571                                 if (sliderState.orientation == Vertical) {
1572                                     placeable.width / 2
1573                                 } else {
1574                                     placeable.height / 2
1575                                 }
1576                             } else {
1577                                 trackCornerSize.roundToPx()
1578                             }
1579                         layout(
1580                             width = placeable.width,
1581                             height = placeable.height,
1582                             alignmentLines = mapOf(CornerSizeAlignmentLine to cornerSize)
1583                         ) {
1584                             placeable.place(0, 0)
1585                         }
1586                     }
1587                 )
1588         ) {
1589             drawTrack(
1590                 tickFractions = sliderState.tickFractions,
1591                 activeRangeStart = 0f,
1592                 activeRangeEnd = sliderState.coercedValueAsFraction,
1593                 inactiveTrackColor = inactiveTrackColor,
1594                 activeTrackColor = activeTrackColor,
1595                 inactiveTickColor = inactiveTickColor,
1596                 activeTickColor = activeTickColor,
1597                 startThumbWidth = 0.toDp(),
1598                 startThumbHeight = 0.toDp(),
1599                 endThumbWidth = sliderState.thumbWidth.toDp(),
1600                 endThumbHeight = sliderState.thumbHeight.toDp(),
1601                 thumbTrackGapSize = thumbTrackGapSize,
1602                 trackInsideCornerSize = trackInsideCornerSize,
1603                 trackCornerSize = cornerSize.toDp(),
1604                 drawStopIndicator = drawStopIndicator,
1605                 drawTick = drawTick,
1606                 isRangeSlider = false,
1607                 enableCornerShrinking = enableCornerShrinking,
1608                 orientation = sliderState.orientation
1609             )
1610         }
1611     }
1612 
1613     /**
1614      * The Default track for [RangeSlider]
1615      *
1616      * @param rangeSliderState [RangeSliderState] which is used to obtain the current active track.
1617      * @param modifier the [Modifier] to be applied to the track.
1618      * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1619      *   different states. See [SliderDefaults.colors].
1620      * @param enabled controls the enabled state of this slider. When `false`, this component will
1621      *   not respond to user input, and it will appear visually disabled and disabled to
1622      *   accessibility services.
1623      */
1624     @Deprecated(
1625         message =
1626             "Use the overload that takes `drawStopIndicator`, `drawTick`, " +
1627                 "`thumbTrackGapSize` and `trackInsideCornerSize`, see `LegacyRangeSliderSample` " +
1628                 "on how to restore the previous behavior",
1629         replaceWith =
1630             ReplaceWith(
1631                 "Track(rangeSliderState, modifier, colors, enabled, drawStopIndicator, " +
1632                     "drawTick, thumbTrackGapSize, trackInsideCornerSize)"
1633             ),
1634         level = DeprecationLevel.HIDDEN
1635     )
1636     @OptIn(ExperimentalMaterial3Api::class)
1637     @Composable
Tracknull1638     fun Track(
1639         rangeSliderState: RangeSliderState,
1640         modifier: Modifier = Modifier,
1641         colors: SliderColors = colors(),
1642         enabled: Boolean = true
1643     ) {
1644         Track(
1645             rangeSliderState,
1646             modifier,
1647             enabled,
1648             colors,
1649             thumbTrackGapSize = ThumbTrackGapSize,
1650             trackInsideCornerSize = TrackInsideCornerSize
1651         )
1652     }
1653 
1654     /**
1655      * The Default track for [RangeSlider]
1656      *
1657      * @param rangeSliderState [RangeSliderState] which is used to obtain the current active track.
1658      * @param modifier the [Modifier] to be applied to the track.
1659      * @param enabled controls the enabled state of this slider. When `false`, this component will
1660      *   not respond to user input, and it will appear visually disabled and disabled to
1661      *   accessibility services.
1662      * @param colors [SliderColors] that will be used to resolve the colors used for this track in
1663      *   different states. See [SliderDefaults.colors].
1664      * @param drawStopIndicator lambda that will be called to draw the stop indicator at the
1665      *   start/end of the track.
1666      * @param drawTick lambda that will be called to draw the ticks if steps are greater than 0.
1667      * @param thumbTrackGapSize size of the gap between the thumbs and the track.
1668      * @param trackInsideCornerSize size of the corners towards the thumbs when a gap is set.
1669      */
1670     @OptIn(ExperimentalMaterial3Api::class)
1671     @Composable
Tracknull1672     fun Track(
1673         rangeSliderState: RangeSliderState,
1674         modifier: Modifier = Modifier,
1675         enabled: Boolean = true,
1676         colors: SliderColors = colors(),
1677         drawStopIndicator: (DrawScope.(Offset) -> Unit)? = {
1678             drawStopIndicator(
1679                 offset = it,
1680                 color = colors.trackColor(enabled, active = true),
1681                 size = TrackStopIndicatorSize
1682             )
1683         },
offsetnull1684         drawTick: DrawScope.(Offset, Color) -> Unit = { offset, color ->
1685             drawStopIndicator(offset = offset, color = color, size = TickSize)
1686         },
1687         thumbTrackGapSize: Dp = ThumbTrackGapSize,
1688         trackInsideCornerSize: Dp = TrackInsideCornerSize
1689     ) {
1690         val inactiveTrackColor = colors.trackColor(enabled, active = false)
1691         val activeTrackColor = colors.trackColor(enabled, active = true)
1692         val inactiveTickColor = colors.tickColor(enabled, active = false)
1693         val activeTickColor = colors.tickColor(enabled, active = true)
<lambda>null1694         var trackCornerSize by remember { mutableIntStateOf(0) }
1695         Canvas(
1696             modifier
1697                 .fillMaxWidth()
1698                 .height(TrackHeight)
1699                 .rotate(if (rangeSliderState.isRtl) 180f else 0f)
constraintsnull1700                 .layout { measurable, constraints ->
1701                     val placeable = measurable.measure(constraints)
1702                     trackCornerSize = placeable.height / 2
1703                     layout(
1704                         width = placeable.width,
1705                         height = placeable.height,
1706                         alignmentLines = mapOf(CornerSizeAlignmentLine to trackCornerSize)
1707                     ) {
1708                         placeable.place(0, 0)
1709                     }
1710                 }
<lambda>null1711         ) {
1712             drawTrack(
1713                 tickFractions = rangeSliderState.tickFractions,
1714                 activeRangeStart = rangeSliderState.coercedActiveRangeStartAsFraction,
1715                 activeRangeEnd = rangeSliderState.coercedActiveRangeEndAsFraction,
1716                 inactiveTrackColor = inactiveTrackColor,
1717                 activeTrackColor = activeTrackColor,
1718                 inactiveTickColor = inactiveTickColor,
1719                 activeTickColor = activeTickColor,
1720                 startThumbWidth = rangeSliderState.startThumbWidth.toDp(),
1721                 startThumbHeight = rangeSliderState.startThumbHeight.toDp(),
1722                 endThumbWidth = rangeSliderState.endThumbWidth.toDp(),
1723                 endThumbHeight = rangeSliderState.endThumbHeight.toDp(),
1724                 thumbTrackGapSize = thumbTrackGapSize,
1725                 trackInsideCornerSize = trackInsideCornerSize,
1726                 trackCornerSize = trackCornerSize.toDp(),
1727                 drawStopIndicator = drawStopIndicator,
1728                 drawTick = drawTick,
1729                 isRangeSlider = true,
1730             )
1731         }
1732     }
1733 
DrawScopenull1734     private fun DrawScope.drawTrack(
1735         tickFractions: FloatArray,
1736         activeRangeStart: Float,
1737         activeRangeEnd: Float,
1738         inactiveTrackColor: Color,
1739         activeTrackColor: Color,
1740         inactiveTickColor: Color,
1741         activeTickColor: Color,
1742         startThumbWidth: Dp,
1743         startThumbHeight: Dp,
1744         endThumbWidth: Dp,
1745         endThumbHeight: Dp,
1746         thumbTrackGapSize: Dp,
1747         trackInsideCornerSize: Dp,
1748         trackCornerSize: Dp,
1749         drawStopIndicator: (DrawScope.(Offset) -> Unit)?,
1750         drawTick: DrawScope.(Offset, Color) -> Unit,
1751         isRangeSlider: Boolean,
1752         enableCornerShrinking: Boolean = false,
1753         orientation: Orientation = Horizontal
1754     ) {
1755         val isVertical = orientation == Vertical
1756         val cornerSize = trackCornerSize.toPx()
1757         val sliderStart = 0f
1758         val sliderEnd = if (isVertical) size.height else size.width
1759 
1760         val isStartOnFirstOrLastStep =
1761             activeRangeStart == tickFractions.firstOrNull() ||
1762                 activeRangeStart == tickFractions.lastOrNull()
1763         val isEndOnFirstOrLastStep =
1764             activeRangeEnd == tickFractions.firstOrNull() ||
1765                 activeRangeEnd == tickFractions.lastOrNull()
1766         val sliderValueEnd =
1767             if (tickFractions.isNotEmpty() && !isEndOnFirstOrLastStep) {
1768                 sliderStart +
1769                     (sliderEnd - sliderStart - cornerSize * 2) * activeRangeEnd +
1770                     cornerSize
1771             } else {
1772                 sliderStart + (sliderEnd - sliderStart) * activeRangeEnd
1773             }
1774         val sliderValueStart =
1775             if (tickFractions.isNotEmpty() && !isStartOnFirstOrLastStep) {
1776                 sliderStart +
1777                     (sliderEnd - sliderStart - cornerSize * 2) * activeRangeStart +
1778                     cornerSize
1779             } else {
1780                 sliderStart + (sliderEnd - sliderStart) * activeRangeStart
1781             }
1782 
1783         val insideCornerSize = trackInsideCornerSize.toPx()
1784         var startGap = 0f
1785         var endGap = 0f
1786         if (thumbTrackGapSize > 0.dp) {
1787             if (isVertical) {
1788                 startGap = startThumbHeight.toPx() / 2 + thumbTrackGapSize.toPx()
1789                 endGap = endThumbHeight.toPx() / 2 + thumbTrackGapSize.toPx()
1790             } else {
1791                 startGap = startThumbWidth.toPx() / 2 + thumbTrackGapSize.toPx()
1792                 endGap = endThumbWidth.toPx() / 2 + thumbTrackGapSize.toPx()
1793             }
1794         }
1795 
1796         // inactive track (range slider)
1797         var rangeInactiveTrackThreshold = sliderStart + startGap
1798         if (!enableCornerShrinking || tickFractions.isNotEmpty()) {
1799             rangeInactiveTrackThreshold += cornerSize
1800         }
1801         if (isRangeSlider && sliderValueStart > rangeInactiveTrackThreshold) {
1802             val start = sliderStart
1803             val end = sliderValueStart - startGap
1804             val size =
1805                 if (isVertical) Size(size.width, end - start) else Size(end - start, size.height)
1806             drawTrackPath(
1807                 orientation,
1808                 Offset.Zero,
1809                 size,
1810                 inactiveTrackColor,
1811                 cornerSize,
1812                 insideCornerSize
1813             )
1814             val stopIndicatorOffset =
1815                 if (isVertical) Offset(center.x, start + cornerSize)
1816                 else Offset(start + cornerSize, center.y)
1817             drawStopIndicator?.invoke(this, stopIndicatorOffset)
1818         }
1819         // inactive track
1820         var inactiveTrackThreshold = sliderEnd - endGap
1821         if (!enableCornerShrinking || tickFractions.isNotEmpty()) {
1822             inactiveTrackThreshold -= cornerSize
1823         }
1824         if (sliderValueEnd < inactiveTrackThreshold) {
1825             val start = sliderValueEnd + endGap
1826             val end = sliderEnd
1827             val inactiveTrackWidth = end - start
1828             val trackOffset = if (isVertical) Offset(0f, start) else Offset(start, 0f)
1829             val size =
1830                 if (isVertical) Size(size.width, inactiveTrackWidth)
1831                 else Size(inactiveTrackWidth, size.height)
1832             drawTrackPath(
1833                 orientation,
1834                 trackOffset,
1835                 size,
1836                 inactiveTrackColor,
1837                 insideCornerSize,
1838                 cornerSize
1839             )
1840             val stopIndicatorOffset =
1841                 if (isVertical) Offset(center.x, end - cornerSize)
1842                 else Offset(end - cornerSize, center.y)
1843             drawStopIndicator?.invoke(this, stopIndicatorOffset)
1844         }
1845         // active track
1846         val activeTrackStart = if (isRangeSlider) sliderValueStart + startGap else 0f
1847         val activeTrackEnd = sliderValueEnd - endGap
1848         val startCornerRadius = if (isRangeSlider) insideCornerSize else cornerSize
1849         val activeTrackWidth = activeTrackEnd - activeTrackStart
1850         val activeTrackThreshold =
1851             if (!enableCornerShrinking || tickFractions.isNotEmpty()) startCornerRadius else 0f
1852         if (activeTrackWidth > activeTrackThreshold) {
1853             val trackOffset =
1854                 if (isVertical) Offset(0f, activeTrackStart) else Offset(activeTrackStart, 0f)
1855             val size =
1856                 if (isVertical) Size(size.width, activeTrackWidth)
1857                 else Size(activeTrackWidth, size.height)
1858             drawTrackPath(
1859                 orientation,
1860                 trackOffset,
1861                 size,
1862                 activeTrackColor,
1863                 startCornerRadius,
1864                 insideCornerSize
1865             )
1866         }
1867 
1868         val start = sliderStart + cornerSize
1869         val end = sliderEnd - cornerSize
1870         val tickStartGap = sliderValueStart - startGap..sliderValueStart + startGap
1871         val tickEndGap = sliderValueEnd - endGap..sliderValueEnd + endGap
1872         tickFractions.forEachIndexed { index, tick ->
1873             // skip ticks that fall on the stop indicator
1874             if (drawStopIndicator != null) {
1875                 if ((isRangeSlider && index == 0) || index == tickFractions.size - 1) {
1876                     return@forEachIndexed
1877                 }
1878             }
1879 
1880             val outsideFraction = tick > activeRangeEnd || tick < activeRangeStart
1881             val centerTick = lerp(start, end, tick)
1882             // skip ticks that fall on a gap
1883             if ((isRangeSlider && centerTick in tickStartGap) || centerTick in tickEndGap) {
1884                 return@forEachIndexed
1885             }
1886             val offset =
1887                 if (isVertical) Offset(center.x, centerTick) else Offset(centerTick, center.y)
1888             drawTick(
1889                 this,
1890                 offset, // offset
1891                 if (outsideFraction) inactiveTickColor else activeTickColor // color
1892             )
1893         }
1894     }
1895 
DrawScopenull1896     private fun DrawScope.drawTrackPath(
1897         orientation: Orientation,
1898         offset: Offset,
1899         size: Size,
1900         color: Color,
1901         startCornerRadius: Float,
1902         endCornerRadius: Float
1903     ) {
1904         val startCorner = CornerRadius(startCornerRadius, startCornerRadius)
1905         val endCorner = CornerRadius(endCornerRadius, endCornerRadius)
1906         val track =
1907             if (orientation == Vertical) {
1908                 RoundRect(
1909                     rect = Rect(offset, size = Size(size.width, size.height)),
1910                     topLeft = startCorner,
1911                     topRight = startCorner,
1912                     bottomRight = endCorner,
1913                     bottomLeft = endCorner
1914                 )
1915             } else {
1916                 RoundRect(
1917                     rect = Rect(offset, size = Size(size.width, size.height)),
1918                     topLeft = startCorner,
1919                     topRight = endCorner,
1920                     bottomRight = endCorner,
1921                     bottomLeft = startCorner
1922                 )
1923             }
1924         trackPath.addRoundRect(track)
1925         drawPath(trackPath, color)
1926         trackPath.rewind()
1927     }
1928 
1929     /**
1930      * The Default stop indicator.
1931      *
1932      * @param offset the coordinate where the indicator is to be drawn.
1933      * @param size the size of the indicator.
1934      * @param color the color of the indicator.
1935      */
DrawScopenull1936     fun DrawScope.drawStopIndicator(offset: Offset, size: Dp, color: Color) {
1937         drawCircle(color = color, center = offset, radius = size.toPx() / 2f)
1938     }
1939 
1940     /** The default size for the stop indicator at the end of the track. */
1941     val TrackStopIndicatorSize: Dp = SliderTokens.StopIndicatorSize
1942 
1943     /** The default size for the ticks if steps are greater than 0. */
1944     val TickSize: Dp = SliderTokens.StopIndicatorSize
1945 
1946     private val trackPath = Path()
1947 }
1948 
snapValueToTicknull1949 private fun snapValueToTick(
1950     current: Float,
1951     tickFractions: FloatArray,
1952     minPx: Float,
1953     maxPx: Float
1954 ): Float {
1955     // target is a closest anchor to the `current`, if exists
1956     return tickFractions
1957         .minByOrNull { abs(lerp(minPx, maxPx, it) - current) }
1958         ?.run { lerp(minPx, maxPx, this) } ?: current
1959 }
1960 
awaitSlopnull1961 private suspend fun AwaitPointerEventScope.awaitSlop(
1962     id: PointerId,
1963     type: PointerType
1964 ): Pair<PointerInputChange, Float>? {
1965     var initialDelta = 0f
1966     val postPointerSlop = { pointerInput: PointerInputChange, offset: Float ->
1967         pointerInput.consume()
1968         initialDelta = offset
1969     }
1970     val afterSlopResult = awaitHorizontalPointerSlopOrCancellation(id, type, postPointerSlop)
1971     return if (afterSlopResult != null) afterSlopResult to initialDelta else null
1972 }
1973 
stepsToTickFractionsnull1974 private fun stepsToTickFractions(steps: Int): FloatArray {
1975     return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }
1976 }
1977 
1978 // Scale x1 from a1..b1 range to a2..b2 range
scalenull1979 private fun scale(a1: Float, b1: Float, x1: Float, a2: Float, b2: Float) =
1980     lerp(a2, b2, calcFraction(a1, b1, x1))
1981 
1982 // Scale x.start, x.endInclusive from a1..b1 range to a2..b2 range
1983 private fun scale(a1: Float, b1: Float, x: SliderRange, a2: Float, b2: Float) =
1984     SliderRange(scale(a1, b1, x.start, a2, b2), scale(a1, b1, x.endInclusive, a2, b2))
1985 
1986 // Calculate the 0..1 fraction that `pos` value represents between `a` and `b`
1987 private fun calcFraction(a: Float, b: Float, pos: Float) =
1988     (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)
1989 
1990 @OptIn(ExperimentalMaterial3Api::class)
1991 private fun Modifier.sliderSemantics(state: SliderState, enabled: Boolean): Modifier {
1992     return semantics {
1993             if (!enabled) disabled()
1994             setProgress(
1995                 action = { targetValue ->
1996                     var newValue =
1997                         targetValue.coerceIn(state.valueRange.start, state.valueRange.endInclusive)
1998                     val originalVal = newValue
1999                     val resolvedValue =
2000                         if (state.steps > 0) {
2001                             var distance: Float = newValue
2002                             for (i in 0..state.steps + 1) {
2003                                 val stepValue =
2004                                     lerp(
2005                                         state.valueRange.start,
2006                                         state.valueRange.endInclusive,
2007                                         i.toFloat() / (state.steps + 1)
2008                                     )
2009                                 if (abs(stepValue - originalVal) <= distance) {
2010                                     distance = abs(stepValue - originalVal)
2011                                     newValue = stepValue
2012                                 }
2013                             }
2014                             newValue
2015                         } else {
2016                             newValue
2017                         }
2018 
2019                     // This is to keep it consistent with AbsSeekbar.java: return false if no
2020                     // change from current.
2021                     if (resolvedValue == state.value) {
2022                         false
2023                     } else {
2024                         if (resolvedValue != state.value) {
2025                             if (state.onValueChange != null) {
2026                                 state.onValueChange?.let { it(resolvedValue) }
2027                             } else {
2028                                 state.value = resolvedValue
2029                             }
2030                         }
2031                         state.onValueChangeFinished?.invoke()
2032                         true
2033                     }
2034                 }
2035             )
2036         }
2037         .then(IncreaseHorizontalSemanticsBounds)
2038         .progressSemantics(
2039             state.value,
2040             state.valueRange.start..state.valueRange.endInclusive,
2041             state.steps
2042         )
2043 }
2044 
2045 @OptIn(ExperimentalMaterial3Api::class)
Modifiernull2046 private fun Modifier.rangeSliderStartThumbSemantics(
2047     state: RangeSliderState,
2048     enabled: Boolean
2049 ): Modifier {
2050     val valueRange = state.valueRange.start..state.activeRangeEnd
2051 
2052     return semantics {
2053             if (!enabled) disabled()
2054             setProgress(
2055                 action = { targetValue ->
2056                     var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
2057                     val originalVal = newValue
2058                     val resolvedValue =
2059                         if (state.startSteps > 0) {
2060                             var distance: Float = newValue
2061                             for (i in 0..state.startSteps + 1) {
2062                                 val stepValue =
2063                                     lerp(
2064                                         valueRange.start,
2065                                         valueRange.endInclusive,
2066                                         i.toFloat() / (state.startSteps + 1)
2067                                     )
2068                                 if (abs(stepValue - originalVal) <= distance) {
2069                                     distance = abs(stepValue - originalVal)
2070                                     newValue = stepValue
2071                                 }
2072                             }
2073                             newValue
2074                         } else {
2075                             newValue
2076                         }
2077 
2078                     // This is to keep it consistent with AbsSeekbar.java: return false if no
2079                     // change from current.
2080                     if (resolvedValue == state.activeRangeStart) {
2081                         false
2082                     } else {
2083                         val resolvedRange = SliderRange(resolvedValue, state.activeRangeEnd)
2084                         val activeRange = SliderRange(state.activeRangeStart, state.activeRangeEnd)
2085                         if (resolvedRange != activeRange) {
2086                             if (state.onValueChange != null) {
2087                                 state.onValueChange?.let { it(resolvedRange) }
2088                             } else {
2089                                 state.activeRangeStart = resolvedRange.start
2090                                 state.activeRangeEnd = resolvedRange.endInclusive
2091                             }
2092                         }
2093                         state.onValueChangeFinished?.invoke()
2094                         true
2095                     }
2096                 }
2097             )
2098         }
2099         .then(IncreaseHorizontalSemanticsBounds)
2100         .progressSemantics(state.activeRangeStart, valueRange, state.startSteps)
2101 }
2102 
2103 @OptIn(ExperimentalMaterial3Api::class)
rangeSliderEndThumbSemanticsnull2104 private fun Modifier.rangeSliderEndThumbSemantics(
2105     state: RangeSliderState,
2106     enabled: Boolean
2107 ): Modifier {
2108     val valueRange = state.activeRangeStart..state.valueRange.endInclusive
2109 
2110     return semantics {
2111             if (!enabled) disabled()
2112 
2113             setProgress(
2114                 action = { targetValue ->
2115                     var newValue = targetValue.coerceIn(valueRange.start, valueRange.endInclusive)
2116                     val originalVal = newValue
2117                     val resolvedValue =
2118                         if (state.endSteps > 0) {
2119                             var distance: Float = newValue
2120                             for (i in 0..state.endSteps + 1) {
2121                                 val stepValue =
2122                                     lerp(
2123                                         valueRange.start,
2124                                         valueRange.endInclusive,
2125                                         i.toFloat() / (state.endSteps + 1)
2126                                     )
2127                                 if (abs(stepValue - originalVal) <= distance) {
2128                                     distance = abs(stepValue - originalVal)
2129                                     newValue = stepValue
2130                                 }
2131                             }
2132                             newValue
2133                         } else {
2134                             newValue
2135                         }
2136 
2137                     // This is to keep it consistent with AbsSeekbar.java: return false if no
2138                     // change from current.
2139                     if (resolvedValue == state.activeRangeEnd) {
2140                         false
2141                     } else {
2142                         val resolvedRange = SliderRange(state.activeRangeStart, resolvedValue)
2143                         val activeRange = SliderRange(state.activeRangeStart, state.activeRangeEnd)
2144                         if (resolvedRange != activeRange) {
2145                             if (state.onValueChange != null) {
2146                                 state.onValueChange?.let { it(resolvedRange) }
2147                             } else {
2148                                 state.activeRangeStart = resolvedRange.start
2149                                 state.activeRangeEnd = resolvedRange.endInclusive
2150                             }
2151                         }
2152                         state.onValueChangeFinished?.invoke()
2153                         true
2154                     }
2155                 }
2156             )
2157         }
2158         .then(IncreaseHorizontalSemanticsBounds)
2159         .progressSemantics(state.activeRangeEnd, valueRange, state.endSteps)
2160 }
2161 
2162 @OptIn(ExperimentalMaterial3Api::class)
2163 @Stable
sliderTapModifiernull2164 private fun Modifier.sliderTapModifier(
2165     state: SliderState,
2166     interactionSource: MutableInteractionSource,
2167     enabled: Boolean
2168 ) =
2169     if (enabled) {
2170         pointerInput(state, interactionSource) {
2171             detectTapGestures(
2172                 onPress = { state.onPress(it) },
2173                 onTap = {
2174                     state.dispatchRawDelta(0f)
2175                     state.gestureEndAction()
2176                 }
2177             )
2178         }
2179     } else {
2180         this
2181     }
2182 
2183 @OptIn(ExperimentalMaterial3Api::class)
2184 @Stable
rangeSliderPressDragModifiernull2185 private fun Modifier.rangeSliderPressDragModifier(
2186     state: RangeSliderState,
2187     startInteractionSource: MutableInteractionSource,
2188     endInteractionSource: MutableInteractionSource,
2189     enabled: Boolean
2190 ): Modifier =
2191     if (enabled) {
2192         pointerInput(startInteractionSource, endInteractionSource, state) {
2193             val rangeSliderLogic =
2194                 RangeSliderLogic(state, startInteractionSource, endInteractionSource)
2195             coroutineScope {
2196                 awaitEachGesture {
2197                     val event = awaitFirstDown(requireUnconsumed = false)
2198                     val interaction = DragInteraction.Start()
2199                     var posX =
2200                         if (state.isRtl) state.totalWidth - event.position.x else event.position.x
2201                     val compare = rangeSliderLogic.compareOffsets(posX)
2202                     var draggingStart =
2203                         if (compare != 0) {
2204                             compare < 0
2205                         } else {
2206                             state.rawOffsetStart > posX
2207                         }
2208 
2209                     awaitSlop(event.id, event.type)?.let {
2210                         val slop = viewConfiguration.pointerSlop(event.type)
2211                         val shouldUpdateCapturedThumb =
2212                             abs(state.rawOffsetEnd - posX) < slop &&
2213                                 abs(state.rawOffsetStart - posX) < slop
2214                         if (shouldUpdateCapturedThumb) {
2215                             val dir = it.second
2216                             draggingStart = if (state.isRtl) dir >= 0f else dir < 0f
2217                             posX += it.first.positionChange().x
2218                         }
2219                     }
2220 
2221                     rangeSliderLogic.captureThumb(
2222                         draggingStart,
2223                         posX,
2224                         interaction,
2225                         this@coroutineScope
2226                     )
2227 
2228                     val finishInteraction =
2229                         try {
2230                             val success =
2231                                 horizontalDrag(pointerId = event.id) {
2232                                     val deltaX = it.positionChange().x
2233                                     state.onDrag(
2234                                         draggingStart,
2235                                         if (state.isRtl) -deltaX else deltaX
2236                                     )
2237                                 }
2238                             if (success) {
2239                                 DragInteraction.Stop(interaction)
2240                             } else {
2241                                 DragInteraction.Cancel(interaction)
2242                             }
2243                         } catch (e: CancellationException) {
2244                             DragInteraction.Cancel(interaction)
2245                         }
2246 
2247                     state.gestureEndAction(draggingStart)
2248                     launch {
2249                         rangeSliderLogic.activeInteraction(draggingStart).emit(finishInteraction)
2250                     }
2251                 }
2252             }
2253         }
2254     } else {
2255         this
2256     }
2257 
2258 @OptIn(ExperimentalMaterial3Api::class)
2259 private class RangeSliderLogic(
2260     val state: RangeSliderState,
2261     val startInteractionSource: MutableInteractionSource,
2262     val endInteractionSource: MutableInteractionSource
2263 ) {
activeInteractionnull2264     fun activeInteraction(draggingStart: Boolean): MutableInteractionSource =
2265         if (draggingStart) startInteractionSource else endInteractionSource
2266 
2267     fun compareOffsets(eventX: Float): Int {
2268         val diffStart = abs(state.rawOffsetStart - eventX)
2269         val diffEnd = abs(state.rawOffsetEnd - eventX)
2270         return diffStart.compareTo(diffEnd)
2271     }
2272 
captureThumbnull2273     fun captureThumb(
2274         draggingStart: Boolean,
2275         posX: Float,
2276         interaction: Interaction,
2277         scope: CoroutineScope
2278     ) {
2279         state.onDrag(
2280             draggingStart,
2281             posX - if (draggingStart) state.rawOffsetStart else state.rawOffsetEnd
2282         )
2283         scope.launch { activeInteraction(draggingStart).emit(interaction) }
2284     }
2285 }
2286 
2287 /**
2288  * Represents the color used by a [Slider] in different states.
2289  *
2290  * @param thumbColor thumb color when enabled
2291  * @param activeTrackColor color of the track in the part that is "active", meaning that the thumb
2292  *   is ahead of it
2293  * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps` is
2294  *   specified
2295  * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
2296  *   thumb is before it
2297  * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if `steps`
2298  *   are specified on the Slider is specified
2299  * @param disabledThumbColor thumb colors when disabled
2300  * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
2301  *   disabled
2302  * @param disabledActiveTickColor colors to be used to draw tick marks on the active track when
2303  *   Slider is disabled and when `steps` are specified on it
2304  * @param disabledInactiveTrackColor color of the track in the "inactive" part when the Slider is
2305  *   disabled
2306  * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part of the
2307  *   track when Slider is disabled and when `steps` are specified on it
2308  * @constructor create an instance with arbitrary colors. See [SliderDefaults.colors] for the
2309  *   default implementation that follows Material specifications.
2310  */
2311 @Immutable
2312 class SliderColors(
2313     val thumbColor: Color,
2314     val activeTrackColor: Color,
2315     val activeTickColor: Color,
2316     val inactiveTrackColor: Color,
2317     val inactiveTickColor: Color,
2318     val disabledThumbColor: Color,
2319     val disabledActiveTrackColor: Color,
2320     val disabledActiveTickColor: Color,
2321     val disabledInactiveTrackColor: Color,
2322     val disabledInactiveTickColor: Color
2323 ) {
2324 
2325     /**
2326      * Returns a copy of this SelectableChipColors, optionally overriding some of the values. This
2327      * uses the Color.Unspecified to mean “use the value from the source”
2328      */
copynull2329     fun copy(
2330         thumbColor: Color = this.thumbColor,
2331         activeTrackColor: Color = this.activeTrackColor,
2332         activeTickColor: Color = this.activeTickColor,
2333         inactiveTrackColor: Color = this.inactiveTrackColor,
2334         inactiveTickColor: Color = this.inactiveTickColor,
2335         disabledThumbColor: Color = this.disabledThumbColor,
2336         disabledActiveTrackColor: Color = this.disabledActiveTrackColor,
2337         disabledActiveTickColor: Color = this.disabledActiveTickColor,
2338         disabledInactiveTrackColor: Color = this.disabledInactiveTrackColor,
2339         disabledInactiveTickColor: Color = this.disabledInactiveTickColor,
2340     ) =
2341         SliderColors(
2342             thumbColor.takeOrElse { this.thumbColor },
<lambda>null2343             activeTrackColor.takeOrElse { this.activeTrackColor },
<lambda>null2344             activeTickColor.takeOrElse { this.activeTickColor },
<lambda>null2345             inactiveTrackColor.takeOrElse { this.inactiveTrackColor },
<lambda>null2346             inactiveTickColor.takeOrElse { this.inactiveTickColor },
<lambda>null2347             disabledThumbColor.takeOrElse { this.disabledThumbColor },
<lambda>null2348             disabledActiveTrackColor.takeOrElse { this.disabledActiveTrackColor },
<lambda>null2349             disabledActiveTickColor.takeOrElse { this.disabledActiveTickColor },
<lambda>null2350             disabledInactiveTrackColor.takeOrElse { this.disabledInactiveTrackColor },
<lambda>null2351             disabledInactiveTickColor.takeOrElse { this.disabledInactiveTickColor },
2352         )
2353 
2354     @Stable
thumbColornull2355     internal fun thumbColor(enabled: Boolean): Color =
2356         if (enabled) thumbColor else disabledThumbColor
2357 
2358     @Stable
2359     internal fun trackColor(enabled: Boolean, active: Boolean): Color =
2360         if (enabled) {
2361             if (active) activeTrackColor else inactiveTrackColor
2362         } else {
2363             if (active) disabledActiveTrackColor else disabledInactiveTrackColor
2364         }
2365 
2366     @Stable
tickColornull2367     internal fun tickColor(enabled: Boolean, active: Boolean): Color =
2368         if (enabled) {
2369             if (active) activeTickColor else inactiveTickColor
2370         } else {
2371             if (active) disabledActiveTickColor else disabledInactiveTickColor
2372         }
2373 
equalsnull2374     override fun equals(other: Any?): Boolean {
2375         if (this === other) return true
2376         if (other == null || other !is SliderColors) return false
2377 
2378         if (thumbColor != other.thumbColor) return false
2379         if (activeTrackColor != other.activeTrackColor) return false
2380         if (activeTickColor != other.activeTickColor) return false
2381         if (inactiveTrackColor != other.inactiveTrackColor) return false
2382         if (inactiveTickColor != other.inactiveTickColor) return false
2383         if (disabledThumbColor != other.disabledThumbColor) return false
2384         if (disabledActiveTrackColor != other.disabledActiveTrackColor) return false
2385         if (disabledActiveTickColor != other.disabledActiveTickColor) return false
2386         if (disabledInactiveTrackColor != other.disabledInactiveTrackColor) return false
2387         if (disabledInactiveTickColor != other.disabledInactiveTickColor) return false
2388 
2389         return true
2390     }
2391 
hashCodenull2392     override fun hashCode(): Int {
2393         var result = thumbColor.hashCode()
2394         result = 31 * result + activeTrackColor.hashCode()
2395         result = 31 * result + activeTickColor.hashCode()
2396         result = 31 * result + inactiveTrackColor.hashCode()
2397         result = 31 * result + inactiveTickColor.hashCode()
2398         result = 31 * result + disabledThumbColor.hashCode()
2399         result = 31 * result + disabledActiveTrackColor.hashCode()
2400         result = 31 * result + disabledActiveTickColor.hashCode()
2401         result = 31 * result + disabledInactiveTrackColor.hashCode()
2402         result = 31 * result + disabledInactiveTickColor.hashCode()
2403         return result
2404     }
2405 }
2406 
2407 // Internal to be referred to in tests
2408 internal val TrackHeight = SliderTokens.InactiveTrackHeight
2409 internal val ThumbWidth = SliderTokens.HandleWidth
2410 private val ThumbHeight = SliderTokens.HandleHeight
2411 private val ThumbSize = DpSize(ThumbWidth, ThumbHeight)
2412 private val VerticalThumbSize = DpSize(ThumbHeight, ThumbWidth)
2413 private val ThumbTrackGapSize: Dp = SliderTokens.ActiveHandleLeadingSpace
2414 private val TrackInsideCornerSize: Dp = 2.dp
2415 private const val SliderRangeTolerance = 0.0001
2416 
2417 private enum class SliderComponents {
2418     THUMB,
2419     TRACK
2420 }
2421 
2422 private enum class RangeSliderComponents {
2423     ENDTHUMB,
2424     STARTTHUMB,
2425     TRACK
2426 }
2427 
2428 /**
2429  * Class that holds information about [Slider]'s and [RangeSlider]'s active track and fractional
2430  * positions where the discrete ticks should be drawn on the track.
2431  */
2432 @Suppress("DEPRECATION")
2433 @Deprecated("Not necessary with the introduction of Slider state")
2434 @Stable
2435 class SliderPositions(
2436     initialActiveRange: ClosedFloatingPointRange<Float> = 0f..1f,
2437     initialTickFractions: FloatArray = floatArrayOf()
2438 ) {
2439     /**
2440      * [ClosedFloatingPointRange] that indicates the current active range for the start to thumb for
2441      * a [Slider] and start thumb to end thumb for a [RangeSlider].
2442      */
2443     var activeRange: ClosedFloatingPointRange<Float> by mutableStateOf(initialActiveRange)
2444         internal set
2445 
2446     /**
2447      * The discrete points where a tick should be drawn on the track. Each value of tickFractions
2448      * should be within the range [0f, 1f]. If the track is continuous, then tickFractions will be
2449      * an empty [FloatArray].
2450      */
2451     var tickFractions: FloatArray by mutableStateOf(initialTickFractions)
2452         internal set
2453 
equalsnull2454     override fun equals(other: Any?): Boolean {
2455         if (this === other) return true
2456         if (other !is SliderPositions) return false
2457 
2458         if (activeRange != other.activeRange) return false
2459         if (!tickFractions.contentEquals(other.tickFractions)) return false
2460 
2461         return true
2462     }
2463 
hashCodenull2464     override fun hashCode(): Int {
2465         var result = activeRange.hashCode()
2466         result = 31 * result + tickFractions.contentHashCode()
2467         return result
2468     }
2469 }
2470 
2471 /**
2472  * Class that holds information about [Slider]'s active range.
2473  *
2474  * @param value [Float] that indicates the initial position of the thumb. If outside of [valueRange]
2475  *   provided, value will be coerced to this range.
2476  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2477  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2478  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2479  *   continuously and allow any value from the range. Must not be negative.
2480  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2481  *   shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
2482  *   to know when the user has completed selecting a new value by ending a drag or a click.
2483  * @param valueRange range of values that Slider values can take. [value] will be coerced to this
2484  *   range.
2485  */
2486 @ExperimentalMaterial3Api
2487 class SliderState(
2488     value: Float = 0f,
2489     @IntRange(from = 0) val steps: Int = 0,
2490     var onValueChangeFinished: (() -> Unit)? = null,
2491     val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2492 ) : DraggableState {
2493 
2494     private var valueState by mutableFloatStateOf(value)
2495 
2496     /** [Float] that indicates the value that the thumb currently is in respect to the track. */
2497     var value: Float
2498         set(newVal) {
2499             valueState =
2500                 if (shouldAutoSnap) {
2501                     calculateSnappedValue(newVal)
2502                 } else {
2503                     newVal
2504                 }
2505         }
2506         get() = valueState
2507 
calculateSnappedValuenull2508     private fun calculateSnappedValue(newVal: Float): Float {
2509         val coercedValue = newVal.coerceIn(valueRange.start, valueRange.endInclusive)
2510         return snapValueToTick(
2511             coercedValue,
2512             tickFractions,
2513             valueRange.start,
2514             valueRange.endInclusive
2515         )
2516     }
2517 
dragnull2518     override suspend fun drag(
2519         dragPriority: MutatePriority,
2520         block: suspend DragScope.() -> Unit
2521     ): Unit = coroutineScope {
2522         isDragging = true
2523         scrollMutex.mutateWith(dragScope, dragPriority, block)
2524         isDragging = false
2525     }
2526 
dispatchRawDeltanull2527     override fun dispatchRawDelta(delta: Float) {
2528         val maxPx: Float
2529         val minPx: Float
2530         if (orientation == Vertical) {
2531             maxPx = max(totalHeight - thumbHeight / 2f, 0f)
2532             minPx = min(thumbHeight / 2f, maxPx)
2533         } else {
2534             maxPx = max(totalWidth - thumbWidth / 2f, 0f)
2535             minPx = min(thumbWidth / 2f, maxPx)
2536         }
2537         rawOffset = (rawOffset + delta + pressOffset)
2538         pressOffset = 0f
2539         val offsetInTrack = snapValueToTick(rawOffset, tickFractions, minPx, maxPx)
2540         val scaledUserValue = scaleToUserValue(minPx, maxPx, offsetInTrack)
2541         if (scaledUserValue != this.value) {
2542             if (onValueChange != null) {
2543                 onValueChange?.let { it(scaledUserValue) }
2544             } else {
2545                 this.value = scaledUserValue
2546             }
2547         }
2548     }
2549 
2550     /** Callback in which value should be updated. */
2551     var onValueChange: ((Float) -> Unit)? = null
2552 
2553     /** Controls the auto-snapping mechanism, disabling it may be useful for custom animations. */
2554     @get:JvmName("shouldAutoSnap") var shouldAutoSnap: Boolean = true
2555 
2556     internal val tickFractions = stepsToTickFractions(steps)
2557     private var totalWidth by mutableIntStateOf(0)
2558     private var totalHeight by mutableIntStateOf(0)
2559     internal var isRtl = false
2560     internal var thumbWidth by mutableIntStateOf(0)
2561     internal var thumbHeight by mutableIntStateOf(0)
2562     internal var orientation = Horizontal
2563     internal var reverseVerticalDirection = false
2564 
2565     /** The fraction of the track that the thumb currently is in. */
2566     val coercedValueAsFraction: Float
2567         get() =
2568             calcFraction(
2569                 valueRange.start,
2570                 valueRange.endInclusive,
2571                 value.coerceIn(valueRange.start, valueRange.endInclusive)
2572             )
2573 
2574     var isDragging by mutableStateOf(false)
2575         private set
2576 
updateDimensionsnull2577     internal fun updateDimensions(newTotalWidth: Int, newTotalHeight: Int) {
2578         totalWidth = newTotalWidth
2579         totalHeight = newTotalHeight
2580     }
2581 
<lambda>null2582     internal val gestureEndAction = {
2583         if (!isDragging) {
2584             // check isDragging in case the change is still in progress (touch -> drag case)
2585             onValueChangeFinished?.invoke()
2586         }
2587     }
2588 
onPressnull2589     internal fun onPress(pos: Offset) {
2590         val to =
2591             if (orientation == Vertical) {
2592                 if (reverseVerticalDirection) totalHeight - pos.y else pos.y
2593             } else {
2594                 if (isRtl) totalWidth - pos.x else pos.x
2595             }
2596         pressOffset = to - rawOffset
2597     }
2598 
2599     private var rawOffset by mutableFloatStateOf(scaleToOffset(0f, 0f, value))
2600     private var pressOffset by mutableFloatStateOf(0f)
2601     private val dragScope: DragScope =
2602         object : DragScope {
dragBynull2603             override fun dragBy(pixels: Float): Unit = dispatchRawDelta(pixels)
2604         }
2605 
2606     private val scrollMutex = MutatorMutex()
2607 
2608     private fun scaleToUserValue(minPx: Float, maxPx: Float, offset: Float) =
2609         scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
2610 
2611     private fun scaleToOffset(minPx: Float, maxPx: Float, userValue: Float) =
2612         scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
2613 
2614     companion object {
2615         /**
2616          * The default [Saver] implementation for [SliderState].
2617          *
2618          * @param onValueChangeFinished lambda to be invoked when value change has ended. This
2619          *   callback shouldn't be used to update the range slider values (use [onValueChange] for
2620          *   that), but rather to know when the user has completed selecting a new value by ending a
2621          *   drag or a click.
2622          * @param valueRange range of values that Slider values can take. [value] will be coerced to
2623          *   this range.
2624          */
2625         fun Saver(
2626             onValueChangeFinished: (() -> Unit)?,
2627             valueRange: ClosedFloatingPointRange<Float>
2628         ): Saver<SliderState, *> =
2629             listSaver(
2630                 save = { listOf(it.value, it.steps) },
2631                 restore = {
2632                     SliderState(
2633                         value = it[0] as Float,
2634                         steps = it[1] as Int,
2635                         onValueChangeFinished = onValueChangeFinished,
2636                         valueRange = valueRange
2637                     )
2638                 }
2639             )
2640     }
2641 }
2642 
2643 /**
2644  * Creates a [SliderState] that is remembered across compositions.
2645  *
2646  * Changes to the provided initial values will **not** result in the state being recreated or
2647  * changed in any way if it has already been created.
2648  *
2649  * @param value [Float] that indicates the initial position of the thumb. If outside of [valueRange]
2650  *   provided, value will be coerced to this range.
2651  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2652  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2653  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2654  *   continuously and allow any value from the range. Must not be negative.
2655  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2656  *   shouldn't be used to update the range slider values (use [SliderState.onValueChange] for that),
2657  *   but rather to know when the user has completed selecting a new value by ending a drag or a
2658  *   click.
2659  * @param valueRange range of values that Slider values can take. [value] will be coerced to this
2660  *   range.
2661  */
2662 @ExperimentalMaterial3Api
2663 @Composable
rememberSliderStatenull2664 fun rememberSliderState(
2665     value: Float = 0f,
2666     @IntRange(from = 0) steps: Int = 0,
2667     onValueChangeFinished: (() -> Unit)? = null,
2668     valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2669 ): SliderState {
2670     return rememberSaveable(saver = SliderState.Saver(onValueChangeFinished, valueRange)) {
2671         SliderState(
2672             value = value,
2673             steps = steps,
2674             onValueChangeFinished = onValueChangeFinished,
2675             valueRange = valueRange
2676         )
2677     }
2678 }
2679 
2680 /**
2681  * Class that holds information about [RangeSlider]'s active range.
2682  *
2683  * @param activeRangeStart [Float] that indicates the initial start of the active range of the
2684  *   slider. If outside of [valueRange] provided, value will be coerced to this range.
2685  * @param activeRangeEnd [Float] that indicates the initial end of the active range of the slider.
2686  *   If outside of [valueRange] provided, value will be coerced to this range.
2687  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2688  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2689  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2690  *   continuously and allow any value from the range. Must not be negative.
2691  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2692  *   shouldn't be used to update the range slider values (use [onValueChange] for that), but rather
2693  *   to know when the user has completed selecting a new value by ending a drag or a click.
2694  * @param valueRange range of values that Range Slider values can take. [activeRangeStart] and
2695  *   [activeRangeEnd] will be coerced to this range.
2696  */
2697 @ExperimentalMaterial3Api
2698 class RangeSliderState(
2699     activeRangeStart: Float = 0f,
2700     activeRangeEnd: Float = 1f,
2701     @IntRange(from = 0) val steps: Int = 0,
2702     var onValueChangeFinished: (() -> Unit)? = null,
2703     val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2704 ) {
2705     private var activeRangeStartState by mutableFloatStateOf(activeRangeStart)
2706     private var activeRangeEndState by mutableFloatStateOf(activeRangeEnd)
2707 
2708     /** [Float] that indicates the start of the current active range for the [RangeSlider]. */
2709     var activeRangeStart: Float
2710         set(newVal) {
2711             val coercedValue = newVal.coerceIn(valueRange.start, activeRangeEnd)
2712             val snappedValue =
2713                 snapValueToTick(
2714                     coercedValue,
2715                     tickFractions,
2716                     valueRange.start,
2717                     valueRange.endInclusive
2718                 )
2719             activeRangeStartState = snappedValue
2720         }
2721         get() = activeRangeStartState
2722 
2723     /** [Float] that indicates the end of the current active range for the [RangeSlider]. */
2724     var activeRangeEnd: Float
2725         set(newVal) {
2726             val coercedValue = newVal.coerceIn(activeRangeStart, valueRange.endInclusive)
2727             val snappedValue =
2728                 snapValueToTick(
2729                     coercedValue,
2730                     tickFractions,
2731                     valueRange.start,
2732                     valueRange.endInclusive
2733                 )
2734             activeRangeEndState = snappedValue
2735         }
2736         get() = activeRangeEndState
2737 
2738     internal var onValueChange: ((SliderRange) -> Unit)? = null
2739 
2740     internal val tickFractions = stepsToTickFractions(steps)
2741 
2742     internal var startThumbWidth by mutableFloatStateOf(0f)
2743     internal var startThumbHeight by mutableFloatStateOf(0f)
2744     internal var endThumbWidth by mutableFloatStateOf(0f)
2745     internal var endThumbHeight by mutableFloatStateOf(0f)
2746     internal var totalWidth by mutableIntStateOf(0)
2747     internal var rawOffsetStart by mutableFloatStateOf(0f)
2748     internal var rawOffsetEnd by mutableFloatStateOf(0f)
2749 
2750     internal var isRtl by mutableStateOf(false)
2751 
<lambda>null2752     internal val gestureEndAction: (Boolean) -> Unit = { onValueChangeFinished?.invoke() }
2753 
2754     private var maxPx by mutableFloatStateOf(0f)
2755     private var minPx by mutableFloatStateOf(0f)
2756 
onDragnull2757     internal fun onDrag(isStart: Boolean, offset: Float) {
2758         val offsetRange =
2759             if (isStart) {
2760                 rawOffsetStart = (rawOffsetStart + offset)
2761                 rawOffsetEnd = scaleToOffset(minPx, maxPx, activeRangeEnd)
2762                 val offsetEnd = rawOffsetEnd
2763                 var offsetStart = rawOffsetStart.coerceIn(minPx, offsetEnd)
2764                 offsetStart = snapValueToTick(offsetStart, tickFractions, minPx, maxPx)
2765                 SliderRange(offsetStart, offsetEnd)
2766             } else {
2767                 rawOffsetEnd = (rawOffsetEnd + offset)
2768                 rawOffsetStart = scaleToOffset(minPx, maxPx, activeRangeStart)
2769                 val offsetStart = rawOffsetStart
2770                 var offsetEnd = rawOffsetEnd.coerceIn(offsetStart, maxPx)
2771                 offsetEnd = snapValueToTick(offsetEnd, tickFractions, minPx, maxPx)
2772                 SliderRange(offsetStart, offsetEnd)
2773             }
2774         val scaledUserValue = scaleToUserValue(minPx, maxPx, offsetRange)
2775         if (scaledUserValue != SliderRange(activeRangeStart, activeRangeEnd)) {
2776             if (onValueChange != null) {
2777                 onValueChange?.let { it(scaledUserValue) }
2778             } else {
2779                 this.activeRangeStart = scaledUserValue.start
2780                 this.activeRangeEnd = scaledUserValue.endInclusive
2781             }
2782         }
2783     }
2784 
2785     internal val coercedActiveRangeStartAsFraction
2786         get() = calcFraction(valueRange.start, valueRange.endInclusive, activeRangeStart)
2787 
2788     internal val coercedActiveRangeEndAsFraction
2789         get() = calcFraction(valueRange.start, valueRange.endInclusive, activeRangeEnd)
2790 
2791     internal val startSteps
2792         get() = floor(steps * coercedActiveRangeEndAsFraction).toInt()
2793 
2794     internal val endSteps
2795         get() = floor(steps * (1f - coercedActiveRangeStartAsFraction)).toInt()
2796 
2797     // scales range offset from within minPx..maxPx to within valueRange.start..valueRange.end
scaleToUserValuenull2798     private fun scaleToUserValue(minPx: Float, maxPx: Float, offset: SliderRange) =
2799         scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
2800 
2801     // scales float userValue within valueRange.start..valueRange.end to within minPx..maxPx
2802     private fun scaleToOffset(minPx: Float, maxPx: Float, userValue: Float) =
2803         scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
2804 
2805     internal fun updateMinMaxPx() {
2806         val newMaxPx = max(totalWidth - endThumbWidth / 2, 0f)
2807         val newMinPx = min(startThumbWidth / 2, newMaxPx)
2808         if (minPx != newMinPx || maxPx != newMaxPx) {
2809             minPx = newMinPx
2810             maxPx = newMaxPx
2811             rawOffsetStart = scaleToOffset(minPx, maxPx, activeRangeStart)
2812             rawOffsetEnd = scaleToOffset(minPx, maxPx, activeRangeEnd)
2813         }
2814     }
2815 
2816     companion object {
2817         /**
2818          * The default [Saver] implementation for [RangeSliderState].
2819          *
2820          * @param onValueChangeFinished lambda to be invoked when value change has ended. This
2821          *   callback shouldn't be used to update the range slider values (use [onValueChange] for
2822          *   that), but rather to know when the user has completed selecting a new value by ending a
2823          *   drag or a click.
2824          * @param valueRange range of values that Range Slider values can take. [activeRangeStart]
2825          *   and [activeRangeEnd] will be coerced to this range.
2826          */
Savernull2827         fun Saver(
2828             onValueChangeFinished: (() -> Unit)?,
2829             valueRange: ClosedFloatingPointRange<Float>
2830         ): Saver<RangeSliderState, *> =
2831             listSaver(
2832                 save = { listOf(it.activeRangeStart, it.activeRangeEnd, it.steps) },
<lambda>null2833                 restore = {
2834                     RangeSliderState(
2835                         activeRangeStart = it[0] as Float,
2836                         activeRangeEnd = it[1] as Float,
2837                         steps = it[2] as Int,
2838                         onValueChangeFinished = onValueChangeFinished,
2839                         valueRange = valueRange
2840                     )
2841                 }
2842             )
2843     }
2844 }
2845 
2846 /**
2847  * Creates a [SliderState] that is remembered across compositions.
2848  *
2849  * Changes to the provided initial values will **not** result in the state being recreated or
2850  * changed in any way if it has already been created.
2851  *
2852  * @param activeRangeStart [Float] that indicates the initial start of the active range of the
2853  *   slider. If outside of [valueRange] provided, value will be coerced to this range.
2854  * @param activeRangeEnd [Float] that indicates the initial end of the active range of the slider.
2855  *   If outside of [valueRange] provided, value will be coerced to this range.
2856  * @param steps if positive, specifies the amount of discrete allowable values between the endpoints
2857  *   of [valueRange]. For example, a range from 0 to 10 with 4 [steps] allows 4 values evenly
2858  *   distributed between 0 and 10 (i.e., 2, 4, 6, 8). If [steps] is 0, the slider will behave
2859  *   continuously and allow any value from the range. Must not be negative.
2860  * @param onValueChangeFinished lambda to be invoked when value change has ended. This callback
2861  *   shouldn't be used to update the range slider values (use [RangeSliderState.onValueChange] for
2862  *   that), but rather to know when the user has completed selecting a new value by ending a drag or
2863  *   a click.
2864  * @param valueRange range of values that Range Slider values can take. [activeRangeStart] and
2865  *   [activeRangeEnd] will be coerced to this range.
2866  */
2867 @ExperimentalMaterial3Api
2868 @Composable
rememberRangeSliderStatenull2869 fun rememberRangeSliderState(
2870     activeRangeStart: Float = 0f,
2871     activeRangeEnd: Float = 1f,
2872     @IntRange(from = 0) steps: Int = 0,
2873     onValueChangeFinished: (() -> Unit)? = null,
2874     valueRange: ClosedFloatingPointRange<Float> = 0f..1f
2875 ): RangeSliderState {
2876     return rememberSaveable(saver = RangeSliderState.Saver(onValueChangeFinished, valueRange)) {
2877         RangeSliderState(
2878             activeRangeStart = activeRangeStart,
2879             activeRangeEnd = activeRangeEnd,
2880             steps = steps,
2881             onValueChangeFinished = onValueChangeFinished,
2882             valueRange = valueRange
2883         )
2884     }
2885 }
2886 
2887 /**
2888  * Immutable float range for [RangeSlider]
2889  *
2890  * Used in [RangeSlider] to determine the active track range for the component. The range is as
2891  * follows: SliderRange.start..SliderRange.endInclusive.
2892  */
2893 @Immutable
2894 @JvmInline
2895 internal value class SliderRange(val packedValue: Long) {
2896     /** start of the [SliderRange] */
2897     @Stable
2898     val start: Float
2899         get() {
2900             // Explicitly compare against packed values to avoid auto-boxing of Size.Unspecified
<lambda>null2901             check(this.packedValue != Unspecified.packedValue) { "SliderRange is unspecified" }
2902             return unpackFloat1(packedValue)
2903         }
2904 
2905     /** End (inclusive) of the [SliderRange] */
2906     @Stable
2907     val endInclusive: Float
2908         get() {
2909             // Explicitly compare against packed values to avoid auto-boxing of Size.Unspecified
<lambda>null2910             check(this.packedValue != Unspecified.packedValue) { "SliderRange is unspecified" }
2911             return unpackFloat2(packedValue)
2912         }
2913 
2914     companion object {
2915         /**
2916          * Represents an unspecified [SliderRange] value, usually a replacement for `null` when a
2917          * primitive value is desired.
2918          */
2919         @Stable val Unspecified = SliderRange(Float.NaN, Float.NaN)
2920     }
2921 
2922     /** String representation of the [SliderRange] */
toStringnull2923     override fun toString() =
2924         if (isSpecified) {
2925             "$start..$endInclusive"
2926         } else {
2927             "FloatRange.Unspecified"
2928         }
2929 }
2930 
2931 /**
2932  * Creates a [SliderRange] from a given start and endInclusive float. It requires endInclusive to
2933  * be >= start.
2934  *
2935  * @param start float that indicates the start of the range
2936  * @param endInclusive float that indicates the end of the range
2937  */
2938 @Stable
SliderRangenull2939 internal fun SliderRange(start: Float, endInclusive: Float): SliderRange {
2940     val isUnspecified = start.isNaN() && endInclusive.isNaN()
2941 
2942     require(isUnspecified || start <= endInclusive + SliderRangeTolerance) {
2943         "start($start) must be <= endInclusive($endInclusive)"
2944     }
2945     return SliderRange(packFloats(start, endInclusive))
2946 }
2947 
2948 /**
2949  * Creates a [SliderRange] from a given [ClosedFloatingPointRange]. It requires
2950  * range.endInclusive >= range.start.
2951  *
2952  * @param range the ClosedFloatingPointRange<Float> for the range.
2953  */
2954 @Stable
SliderRangenull2955 internal fun SliderRange(range: ClosedFloatingPointRange<Float>): SliderRange {
2956     val start = range.start
2957     val endInclusive = range.endInclusive
2958     val isUnspecified = start.isNaN() && endInclusive.isNaN()
2959     require(isUnspecified || start <= endInclusive + SliderRangeTolerance) {
2960         "ClosedFloatingPointRange<Float>.start($start) must be <= " +
2961             "ClosedFloatingPoint.endInclusive($endInclusive)"
2962     }
2963     return SliderRange(packFloats(start, endInclusive))
2964 }
2965 
2966 /** Check for if a given [SliderRange] is not [SliderRange.Unspecified]. */
2967 @Stable
2968 internal val SliderRange.isSpecified: Boolean
2969     get() = packedValue != SliderRange.Unspecified.packedValue
2970 
2971 internal val CornerSizeAlignmentLine = VerticalAlignmentLine(::min)
2972