All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.peanuuutz.fork.ui.preset.Slider.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 The Android Open Source Project
 * Modifications Copyright 2022 Peanuuutz
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.peanuuutz.fork.ui.preset

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import net.peanuuutz.fork.ui.animation.spec.target.DefaultFloatAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.FiniteAnimationSpec
import net.peanuuutz.fork.ui.animation.spec.target.composite.SnapSpec
import net.peanuuutz.fork.ui.foundation.draw.BorderStroke
import net.peanuuutz.fork.ui.foundation.draw.background
import net.peanuuutz.fork.ui.foundation.draw.border
import net.peanuuutz.fork.ui.foundation.input.AxisAnchorThreshold
import net.peanuuutz.fork.ui.foundation.input.AxisAnchoredDragState
import net.peanuuutz.fork.ui.foundation.input.AxisAnchoredDragState.Companion.MinimumAnchorsSpacing
import net.peanuuutz.fork.ui.foundation.input.AxisAnchors
import net.peanuuutz.fork.ui.foundation.input.AxisDragState
import net.peanuuutz.fork.ui.foundation.input.LocalVisualIndication
import net.peanuuutz.fork.ui.foundation.input.axisAnchoredDraggable
import net.peanuuutz.fork.ui.foundation.input.detectDrag
import net.peanuuutz.fork.ui.foundation.input.detectPressRelease
import net.peanuuutz.fork.ui.foundation.input.focusable
import net.peanuuutz.fork.ui.foundation.input.hoverable
import net.peanuuutz.fork.ui.foundation.input.interaction.DragInteraction
import net.peanuuutz.fork.ui.foundation.input.interaction.MutableInteractionSource
import net.peanuuutz.fork.ui.foundation.input.interaction.PressInteraction
import net.peanuuutz.fork.ui.foundation.input.interaction.collectFocusState
import net.peanuuutz.fork.ui.foundation.input.interaction.collectHoverState
import net.peanuuutz.fork.ui.foundation.input.interaction.detectAndEmitDragInteractions
import net.peanuuutz.fork.ui.foundation.input.interaction.detectAndEmitPressInteractions
import net.peanuuutz.fork.ui.foundation.input.interaction.tryEmitCancelOnDrag
import net.peanuuutz.fork.ui.foundation.input.interaction.tryEmitCancelOnPress
import net.peanuuutz.fork.ui.foundation.input.pointerIconOnHover
import net.peanuuutz.fork.ui.foundation.input.rememberAxisAnchoredDragState
import net.peanuuutz.fork.ui.foundation.input.rememberAxisDragState
import net.peanuuutz.fork.ui.foundation.input.visualIndication
import net.peanuuutz.fork.ui.foundation.layout.Arrangement
import net.peanuuutz.fork.ui.foundation.layout.Box
import net.peanuuutz.fork.ui.foundation.layout.BoxScope
import net.peanuuutz.fork.ui.foundation.layout.NoPadding
import net.peanuuutz.fork.ui.foundation.layout.PaddingValues
import net.peanuuutz.fork.ui.foundation.layout.fillMaxHeight
import net.peanuuutz.fork.ui.foundation.layout.fillMaxWidth
import net.peanuuutz.fork.ui.foundation.layout.list.Row
import net.peanuuutz.fork.ui.foundation.layout.list.RowScope
import net.peanuuutz.fork.ui.foundation.layout.minHeight
import net.peanuuutz.fork.ui.foundation.layout.minWidth
import net.peanuuutz.fork.ui.foundation.layout.padding
import net.peanuuutz.fork.ui.foundation.layout.wrapContentSize
import net.peanuuutz.fork.ui.inspection.InspectInfo
import net.peanuuutz.fork.ui.preset.theme.Theme
import net.peanuuutz.fork.ui.ui.context.key.Key
import net.peanuuutz.fork.ui.ui.context.key.KeyEvent
import net.peanuuutz.fork.ui.ui.context.key.KeyboardModifier
import net.peanuuutz.fork.ui.ui.context.key.isCtrlPressed
import net.peanuuutz.fork.ui.ui.context.key.isReleased
import net.peanuuutz.fork.ui.ui.context.key.isShiftPressed
import net.peanuuutz.fork.ui.ui.context.pointer.PointerEvent
import net.peanuuutz.fork.ui.ui.context.pointer.PointerIcon
import net.peanuuutz.fork.ui.ui.draw.Painter
import net.peanuuutz.fork.ui.ui.layout.Alignment
import net.peanuuutz.fork.ui.ui.layout.Constraints
import net.peanuuutz.fork.ui.ui.layout.LayoutDirection
import net.peanuuutz.fork.ui.ui.layout.LayoutDirection.Down
import net.peanuuutz.fork.ui.ui.layout.LayoutDirection.Left
import net.peanuuutz.fork.ui.ui.layout.LayoutDirection.Right
import net.peanuuutz.fork.ui.ui.layout.LayoutDirection.Up
import net.peanuuutz.fork.ui.ui.layout.Measurable
import net.peanuuutz.fork.ui.ui.layout.MeasureResult
import net.peanuuutz.fork.ui.ui.layout.isHorizontal
import net.peanuuutz.fork.ui.ui.modifier.Modifier
import net.peanuuutz.fork.ui.ui.modifier.ModifierNodeElement
import net.peanuuutz.fork.ui.ui.modifier.conditional
import net.peanuuutz.fork.ui.ui.modifier.input.SuspendingPointerInputModifierNode
import net.peanuuutz.fork.ui.ui.node.BranchingModifierNode
import net.peanuuutz.fork.ui.ui.node.KeyEventPass
import net.peanuuutz.fork.ui.ui.node.KeyEventPass.Out
import net.peanuuutz.fork.ui.ui.node.KeyInputModifierNode
import net.peanuuutz.fork.ui.ui.node.LayoutCallbackModifierNode
import net.peanuuutz.fork.ui.ui.node.LayoutInfo
import net.peanuuutz.fork.ui.ui.node.LayoutModifierNode
import net.peanuuutz.fork.ui.ui.node.ModifierNode
import net.peanuuutz.fork.ui.ui.node.PointerEventPass
import net.peanuuutz.fork.ui.ui.node.PointerInputModifierNode
import net.peanuuutz.fork.ui.ui.unit.FloatOffset
import net.peanuuutz.fork.ui.ui.unit.IntOffset
import net.peanuuutz.fork.ui.ui.unit.IntSize
import net.peanuuutz.fork.util.common.Color
import net.peanuuutz.fork.util.common.ref
import kotlin.math.abs
import kotlin.math.roundToInt

@Composable
fun Slider(
    value: Float,
    onValueChanged: (Float) -> Unit,
    range: ClosedFloatingPointRange,
    modifier: Modifier = Modifier,
    direction: LayoutDirection = Right,
    isEnabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    thumbStyle: SliderThumbStyle = Theme.sliderThumb,
    thumbPadding: PaddingValues = SliderDefaults.ThumbPadding,
    thumbSizer: ThumbSizer = if (direction.isHorizontal) {
        SliderDefaults.Horizontal.DefaultThumbSizer
    } else {
        SliderDefaults.Vertical.DefaultThumbSizer
    },
    trackStyle: SliderTrackStyle = Theme.sliderTrack,
    trackContentPadding: PaddingValues = if (direction.isHorizontal) {
        SliderDefaults.Horizontal.TrackContentPadding
    } else {
        SliderDefaults.Vertical.TrackContentPadding
    },
    trackContentArrangement: Arrangement.Horizontal = Arrangement.Center,
    trackContent: (@Composable RowScope.(value: Float) -> Unit)? = null
) {
    ContinuousSlider(
        value = value,
        onValueChanged = onValueChanged,
        range = range,
        direction = direction,
        isEnabled = isEnabled,
        interactionSource = interactionSource,
        thumbStyle = thumbStyle,
        thumbPadding = thumbPadding,
        thumbSizer = thumbSizer,
        trackStyle = trackStyle,
        trackContentPadding = trackContentPadding,
        trackContentArrangement = trackContentArrangement,
        trackContent = trackContent,
        modifier = modifier
    )
}

@Composable
fun  Slider(
    value: T,
    onValueChanged: (T) -> Unit,
    values: List,
    modifier: Modifier = Modifier,
    direction: LayoutDirection = Right,
    isEnabled: Boolean = true,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    thumbStyle: SliderThumbStyle = Theme.sliderThumb,
    thumbPadding: PaddingValues = SliderDefaults.ThumbPadding,
    thumbSizer: ThumbSizer = if (direction.isHorizontal) {
        SliderDefaults.Horizontal.DefaultThumbSizer
    } else {
        SliderDefaults.Vertical.DefaultThumbSizer
    },
    trackStyle: SliderTrackStyle = Theme.sliderTrack,
    trackContentPadding: PaddingValues = if (direction.isHorizontal) {
        SliderDefaults.Horizontal.TrackContentPadding
    } else {
        SliderDefaults.Vertical.TrackContentPadding
    },
    trackContentArrangement: Arrangement.Horizontal = Arrangement.Center,
    trackContent: (@Composable RowScope.(value: T) -> Unit)? = null
) {
    DiscreteSlider(
        value = value,
        onValueChanged = onValueChanged,
        values = values,
        direction = direction,
        isEnabled = isEnabled,
        interactionSource = interactionSource,
        thumbStyle = thumbStyle,
        thumbPadding = thumbPadding,
        thumbSizer = thumbSizer,
        trackStyle = trackStyle,
        trackContentPadding = trackContentPadding,
        trackContentArrangement = trackContentArrangement,
        trackContent = trackContent,
        modifier = modifier
    )
}

object SliderDefaults {
    val ThumbPadding: PaddingValues = NoPadding

    object Horizontal {
        const val MinHeight: Int = 8

        const val DefaultHeight: Int = 20

        val DefaultThumbSizer: ThumbSizer = object : ThumbSizer {
            override fun calculateThumbSize(availableSpace: IntSize): IntSize {
                return IntSize(
                    width = 8,
                    height = availableSpace.height
                )
            }

            override fun toString(): String {
                return "SliderDefaults.Horizontal.DefaultThumbSizer"
            }
        }

        val TrackContentPadding: PaddingValues = PaddingValues(4, 1)
    }

    object Vertical {
        const val MinWidth: Int = 8

        const val DefaultWidth: Int = 20

        val DefaultThumbSizer: ThumbSizer = object : ThumbSizer {
            override fun calculateThumbSize(availableSpace: IntSize): IntSize {
                return IntSize(
                    width = availableSpace.width,
                    height = 8
                )
            }

            override fun toString(): String {
                return "SliderDefaults.Vertical.DefaultThumbSizer"
            }
        }

        val TrackContentPadding: PaddingValues = PaddingValues(1, 4)
    }
}

@Stable
fun interface ThumbSizer {
    fun calculateThumbSize(availableSpace: IntSize): IntSize

    companion object {
        @Stable
        fun absolute(size: IntSize): ThumbSizer {
            return AbsoluteThumbSizer(size)
        }

        @Stable
        fun relative(
            widthFraction: Float,
            heightFraction: Float
        ): ThumbSizer {
            return RelativeThumbSizer(
                widthFraction = widthFraction,
                heightFraction = heightFraction
            )
        }
    }
}

@Stable
interface SliderTrackStyle {
    @Composable
    fun border(
        isEnabled: Boolean,
        isSelected: Boolean,
        fraction: Float
    ): State

    @Composable
    fun background(
        isEnabled: Boolean,
        isSelected: Boolean,
        fraction: Float
    ): State

    @Composable
    fun content(
        isEnabled: Boolean,
        fraction: Float
    ): State

    @Stable
    abstract class Delegated(
        val delegate: SliderTrackStyle
    ) : SliderTrackStyle by delegate
}

@Stable
interface SliderThumbStyle {
    @Composable
    fun border(
        isEnabled: Boolean,
        isSelected: Boolean,
        fraction: Float
    ): State

    @Composable
    fun background(
        isEnabled: Boolean,
        isSelected: Boolean,
        fraction: Float
    ): State

    @Stable
    abstract class Delegated(
        val delegate: SliderThumbStyle
    ) : SliderThumbStyle by delegate
}

val Theme.sliderTrack: SliderTrackStyle
    @ReadOnlyComposable
    @Composable
    get() = LocalSliderTrack.current

val Theme.sliderThumb: SliderThumbStyle
    @ReadOnlyComposable
    @Composable
    get() = LocalSliderThumb.current

@NonRestartableComposable
@Composable
fun SliderStyleProvider(
    thumbStyle: SliderThumbStyle,
    trackStyle: SliderTrackStyle,
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalSliderThumb provides thumbStyle,
        LocalSliderTrack provides trackStyle,
        content = content
    )
}

@Stable
class DefaultSliderTrackStyle(
    val border: BorderStroke,
    val background: Painter,
    val contentColor: Color,
    val selectedBorder: BorderStroke,
    val selectedBackground: Painter,
    val disabledBorder: BorderStroke,
    val disabledBackground: Painter,
    val disabledContentColor: Color
) : SliderTrackStyle {
    @Composable
    override fun border(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
        val borderStroke = when {
            !isEnabled -> disabledBorder
            !isSelected -> border
            else -> selectedBorder
        }
        return rememberUpdatedState(borderStroke)
    }

    @Composable
    override fun background(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
        val painter = when {
            !isEnabled -> disabledBackground
            !isSelected -> background
            else -> selectedBackground
        }
        return rememberUpdatedState(painter)
    }

    @Composable
    override fun content(isEnabled: Boolean, fraction: Float): State {
        val color = if (isEnabled) contentColor else disabledContentColor
        return rememberUpdatedState(color)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is DefaultSliderTrackStyle) return false
        if (border != other.border) return false
        if (background != other.background) return false
        if (contentColor != other.contentColor) return false
        if (selectedBorder != other.selectedBorder) return false
        if (selectedBackground != other.selectedBackground) return false
        if (disabledBorder != other.disabledBorder) return false
        if (disabledBackground != other.disabledBackground) return false
        if (disabledContentColor != other.disabledContentColor) return false
        return true
    }

    override fun hashCode(): Int {
        var result = border.hashCode()
        result = 31 * result + background.hashCode()
        result = 31 * result + contentColor.hashCode()
        result = 31 * result + selectedBorder.hashCode()
        result = 31 * result + selectedBackground.hashCode()
        result = 31 * result + disabledBorder.hashCode()
        result = 31 * result + disabledBackground.hashCode()
        result = 31 * result + disabledContentColor.hashCode()
        return result
    }

    override fun toString(): String {
        return "DefaultSliderTrackStyle(border=$border, background=$background, " +
                "contentColor=$contentColor, " +
                "selectedBorder=$selectedBorder, selectedBackground=$selectedBackground, " +
                "disabledBorder=$disabledBorder, disabledBackground=$disabledBackground, " +
                "disabledContentColor=$disabledContentColor)"
    }
}

@Stable
class DefaultSliderThumbStyle(
    val border: BorderStroke,
    val background: Painter,
    val selectedBorder: BorderStroke,
    val selectedBackground: Painter,
    val disabledBorder: BorderStroke,
    val disabledBackground: Painter
) : SliderThumbStyle {
    @Composable
    override fun border(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
        val borderStroke = when {
            !isEnabled -> disabledBorder
            !isSelected -> border
            else -> selectedBorder
        }
        return rememberUpdatedState(borderStroke)
    }

    @Composable
    override fun background(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
        val painter = when {
            !isEnabled -> disabledBackground
            !isSelected -> background
            else -> selectedBackground
        }
        return rememberUpdatedState(painter)
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is DefaultSliderThumbStyle) return false
        if (border != other.border) return false
        if (background != other.background) return false
        if (selectedBorder != other.selectedBorder) return false
        if (selectedBackground != other.selectedBackground) return false
        if (disabledBorder != other.disabledBorder) return false
        if (disabledBackground != other.disabledBackground) return false
        return true
    }

    override fun hashCode(): Int {
        var result = border.hashCode()
        result = 31 * result + background.hashCode()
        result = 31 * result + selectedBorder.hashCode()
        result = 31 * result + selectedBackground.hashCode()
        result = 31 * result + disabledBorder.hashCode()
        result = 31 * result + disabledBackground.hashCode()
        return result
    }

    override fun toString(): String {
        return "DefaultSliderThumbStyle(border=$border, background=$background, " +
                "selectedBorder=$selectedBorder, selectedBackground=$selectedBackground, " +
                "disabledBorder=$disabledBorder, disabledBackground=$disabledBackground)"
    }
}

// ======== Internal ========

// -------- Common --------

// ---- Constants ----

private val AxisDirections: Map = mapOf(
    Right to FloatOffset.Right,
    Up to FloatOffset.Up,
    Down to FloatOffset.Down,
    Left to FloatOffset.Left
)

internal val DefaultThresholds: (Any?, Any?) -> AxisAnchorThreshold = { _, _ -> DefaultThreshold }

private val DefaultThreshold: AxisAnchorThreshold = AxisAnchorThreshold.relative(0.3f)

private val WrapContentAlignments: Map = mapOf(
    Right to Alignment.CenterLeft,
    Up to Alignment.BottomCenter,
    Down to Alignment.TopCenter,
    Left to Alignment.CenterRight
)

// ---- Thumb Sizer Presets ----

private data class AbsoluteThumbSizer(val size: IntSize) : ThumbSizer {
    init {
        require(size.minDimension > 0) {
            "Thumb should have a positive size, but found $this"
        }
    }

    override fun calculateThumbSize(availableSpace: IntSize): IntSize {
        return size
    }
}

private data class RelativeThumbSizer(
    val widthFraction: Float,
    val heightFraction: Float
) : ThumbSizer {
    init {
        require(widthFraction > 0.0f && heightFraction > 0.0f) {
            "Thumb should have a positive size, but found $this"
        }
    }

    override fun calculateThumbSize(availableSpace: IntSize): IntSize {
        return IntSize(
            width = (availableSpace.width * widthFraction).roundToInt(),
            height = (availableSpace.height * heightFraction).roundToInt()
        )
    }
}

// ---- Base Slider Track ----

@Composable
private fun  BaseSliderTrack(
    valueProvider: () -> T,
    fractionProvider: () -> Float,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    trackStyle: SliderTrackStyle,
    trackContentPadding: PaddingValues,
    trackContentArrangement: Arrangement.Horizontal,
    trackContent: (@Composable RowScope.(value: T) -> Unit)?,
    modifier: Modifier
) {
    val fraction = fractionProvider()
    val isFocused by interactionSource.collectFocusState(TrackThumbInteractions.Track)
    val isSelected = isFocused
    val border by trackStyle.border(
        isEnabled = isEnabled,
        isSelected = isSelected,
        fraction = fraction
    )
    val background by trackStyle.background(
        isEnabled = isEnabled,
        isSelected = isSelected,
        fraction = fraction
    )

    Row(
        modifier = modifier
            .border(border)
            .background(background)
            .padding(trackContentPadding),
        horizontalArrangement = trackContentArrangement,
        verticalAlignment = Alignment.CenterVertically
    ) {
        if (trackContent != null) {
            val contentColor by trackStyle.content(
                isEnabled = isEnabled,
                fraction = fraction
            )

            CompositionLocalProvider(
                LocalContentColor provides contentColor
            ) {
                trackContent(valueProvider())
            }
        }
    }
}

// ---- Base Slider Thumb ----

@Composable
private fun BaseSliderThumb(
    fractionProvider: () -> Float,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    thumbStyle: SliderThumbStyle,
    modifier: Modifier
) {
    val fraction = fractionProvider()
    val isHovered by interactionSource.collectHoverState(TrackThumbInteractions.Thumb)
    val isFocused by interactionSource.collectFocusState(TrackThumbInteractions.Thumb)
    val isSelected = isHovered || isFocused
    val border by thumbStyle.border(
        isEnabled = isEnabled,
        isSelected = isSelected,
        fraction = fraction
    )
    val background by thumbStyle.background(
        isEnabled = isEnabled,
        isSelected = isSelected,
        fraction = fraction
    )

    Box(
        modifier = modifier
            .border(border)
            .background(background)
    )
}

// ---- Slider Default Size Modifier ----

private fun Modifier.sliderDefaultSize(direction: LayoutDirection): Modifier {
    return if (direction.isHorizontal) {
        this
            .fillMaxWidth()
            .minHeight(SliderDefaults.Horizontal.MinHeight)
    } else {
        this
            .fillMaxHeight()
            .minWidth(SliderDefaults.Vertical.MinWidth)
    }
}

// ---- Slider Track Selector Modifier ----

private sealed class SliderTrackSelectorModifierNode(
    val thumbSizeProvider: () -> IntSize,
    var direction: LayoutDirection,
    interactionSource: MutableInteractionSource,
    var thumbPadding: PaddingValues
) : BranchingModifierNode(), PointerInputModifierNode {
    var interactionSource: MutableInteractionSource = interactionSource
        set(value) {
            if (field == value) {
                return
            }
            field.tryEmitCancelOnDrag(
                stateProvider = this::dragState,
                stateUpdater = this::dragState::set
            )
            field.tryEmitCancelOnPress(
                stateProvider = this::pressState,
                stateUpdater = this::pressState::set
            )
            field = value
        }

    private val pointerInputHandler: SuspendingPointerInputModifierNode = branch {
        SuspendingPointerInputModifierNode {
            coroutineScope {
                launch {
                    detectAndEmitDragInteractions(
                        interactionSourceProvider = this@SliderTrackSelectorModifierNode::interactionSource,
                        stateProvider = this@SliderTrackSelectorModifierNode::dragState,
                        stateUpdater = this@SliderTrackSelectorModifierNode::dragState::set,
                        labelProvider = { TrackThumbInteractions.Thumb }
                    )
                }
                launch {
                    detectDrag(
                        onDrag = { moveEvent ->
                            emitValueFromSliderLocalPosition(
                                sliderSize = scopeInfo.size,
                                sliderLocalOffset = moveEvent.position
                            )
                        }
                    )
                }
                launch {
                    detectAndEmitPressInteractions(
                        interactionSourceProvider = this@SliderTrackSelectorModifierNode::interactionSource,
                        stateProvider = this@SliderTrackSelectorModifierNode::pressState,
                        stateUpdater = this@SliderTrackSelectorModifierNode::pressState::set,
                        labelProvider = { TrackThumbInteractions.Thumb }
                    )
                }
                launch {
                    detectPressRelease(
                        onPress = { tapEvent ->
                            emitValueFromSliderLocalPosition(
                                sliderSize = scopeInfo.size,
                                sliderLocalOffset = tapEvent.position
                            )
                        }
                    )
                }
            }
        }
    }

    private var dragState: DragInteraction.Start? = null

    private var pressState: PressInteraction.Press? = null

    private fun emitValueFromSliderLocalPosition(
        sliderSize: IntSize,
        sliderLocalOffset: FloatOffset
    ) {
        val thumbSize = thumbSizeProvider()
        val direction = direction
        val thumbPadding = thumbPadding
        val maxOffset: Float
        val offset: Float
        if (direction.isHorizontal) {
            val sliderWidth = sliderSize.width.toFloat()
            val sliderLocalX = sliderLocalOffset.x
            val thumbWidth = thumbSize.width.toFloat()
            val left = thumbPadding.calculateLeftPadding()
            val right = thumbPadding.calculateRightPadding()
            val startOffset = sliderLocalX - left - thumbWidth / 2
            maxOffset = sliderWidth - left - thumbWidth - right
            offset = if (direction == Right) startOffset else maxOffset - startOffset
        } else {
            val sliderHeight = sliderSize.height.toFloat()
            val sliderLocalY = sliderLocalOffset.y
            val thumbHeight = thumbSize.height.toFloat()
            val top = thumbPadding.calculateTopPadding()
            val bottom = thumbPadding.calculateBottomPadding()
            val startOffset = sliderLocalY - top - thumbHeight / 2
            maxOffset = sliderHeight - top - thumbHeight - bottom
            offset = if (direction == Up) maxOffset - startOffset else startOffset
        }
        emitValueFromOffset(
            maxOffset = maxOffset,
            offset = offset
        )
    }

    protected abstract fun emitValueFromOffset(
        maxOffset: Float,
        offset: Float
    )

    override fun onPointerEvent(pass: PointerEventPass, pointerEvent: PointerEvent) {
        pointerInputHandler.onPointerEvent(pass, pointerEvent)
    }
}

// ---- Slider Track Pointer Icon Modifier ----

private fun Modifier.sliderTrackPointerIcon(direction: LayoutDirection): Modifier {
    val pointerIcon = if (direction.isHorizontal) {
        PointerIcon.MoveHorizontally
    } else {
        PointerIcon.MoveVertically
    }
    return pointerIconOnHover(pointerIcon)
}

// ---- Slider Thumb Offset Modifier ----

private fun Modifier.sliderThumbOffset(
    offsetProvider: () -> Int,
    direction: LayoutDirection
): Modifier {
    val element = SliderThumbOffsetModifier(
        offsetProvider = offsetProvider,
        direction = direction
    )
    return this then element
}

private data class SliderThumbOffsetModifier(
    val offsetProvider: () -> Int,
    val direction: LayoutDirection
) : ModifierNodeElement() {
    override fun create(): SliderThumbOffsetModifierNode {
        return SliderThumbOffsetModifierNode(
            offsetProvider = offsetProvider,
            direction = direction
        )
    }

    override fun update(node: SliderThumbOffsetModifierNode) {
        node.offsetProvider = offsetProvider
        node.direction = direction
    }

    override fun InspectInfo.inspect() {
        set("direction", direction)
    }
}

private class SliderThumbOffsetModifierNode(
    var offsetProvider: () -> Int,
    var direction: LayoutDirection
) : ModifierNode(), LayoutModifierNode {
    override fun measure(measurable: Measurable, constraints: Constraints): MeasureResult {
        val placeable = measurable.measure(constraints)
        return MeasureResult(placeable.width, placeable.height) {
            val offset = offsetProvider()
            val position = when (direction) {
                Right -> IntOffset(offset, 0)
                Up -> IntOffset(0, -offset)
                Down -> IntOffset(0, offset)
                Left -> IntOffset(-offset, 0)
            }
            placeable.place(position)
        }
    }
}

// ---- Thumb Sizer Modifier ----

internal fun Modifier.thumbSizer(
    thumbSizeUpdater: (IntSize) -> Unit,
    sizer: ThumbSizer
): Modifier {
    val element = ThumbSizerModifier(
        thumbSizeUpdater = thumbSizeUpdater,
        sizer = sizer
    )
    return this then element
}

private data class ThumbSizerModifier(
    val thumbSizeUpdater: (IntSize) -> Unit,
    val sizer: ThumbSizer
) : ModifierNodeElement() {
    override fun create(): ThumbSizerModifierNode {
        return ThumbSizerModifierNode(
            thumbSizeUpdater = thumbSizeUpdater,
            sizer = sizer
        )
    }

    override fun update(node: ThumbSizerModifierNode) {
        node.sizer = sizer
    }

    override fun InspectInfo.inspect() {
        set("sizer", sizer)
    }
}

private class ThumbSizerModifierNode(
    val thumbSizeUpdater: (IntSize) -> Unit,
    var sizer: ThumbSizer
) : ModifierNode(),
    LayoutModifierNode,
    LayoutCallbackModifierNode
{
    override fun measure(measurable: Measurable, constraints: Constraints): MeasureResult {
        val availableSpace = IntSize(constraints.maxWidth, constraints.maxHeight)
        val thumbSize = sizer.calculateThumbSize(availableSpace)
        val contentConstraints = Constraints.fixed(thumbSize)
        val placeable = measurable.measure(contentConstraints)
        return MeasureResult(placeable.width, placeable.height) {
            placeable.place(IntOffset.Zero)
        }
    }

    override fun onSizeChanged(size: IntSize) {
        thumbSizeUpdater(size)
    }
}

// ---- Slider Thumb Key Selector Modifier ----

private sealed class SliderThumbKeySelectorModifierNode(
    var direction: LayoutDirection
) : ModifierNode(), KeyInputModifierNode {
    override fun onKeyEvent(pass: KeyEventPass, keyEvent: KeyEvent): Boolean {
        if (pass != Out || keyEvent.isReleased) {
            return false
        }
        return when (keyEvent.key) {
            Key.Right -> {
                when (direction) {
                    Right -> next(keyEvent.modifier)
                    Left -> previous(keyEvent.modifier)
                    else -> false
                }
            }
            Key.Left -> {
                when (direction) {
                    Right -> previous(keyEvent.modifier)
                    Left -> next(keyEvent.modifier)
                    else -> false
                }
            }
            Key.Up -> {
                when (direction) {
                    Up -> next(keyEvent.modifier)
                    Down -> previous(keyEvent.modifier)
                    else -> false
                }
            }
            Key.Down -> {
                when (direction) {
                    Up -> previous(keyEvent.modifier)
                    Down -> next(keyEvent.modifier)
                    else -> false
                }
            }
            Key.Home -> toStart()
            Key.End -> toEnd()
            else -> false
        }
    }

    protected abstract fun next(modifier: KeyboardModifier): Boolean

    protected abstract fun previous(modifier: KeyboardModifier): Boolean

    protected abstract fun toStart(): Boolean

    protected abstract fun toEnd(): Boolean
}

// ---- Max Drag Offset ----

internal fun calculateMaxDragOffset(
    thumbInfo: LayoutInfo,
    direction: LayoutDirection,
    thumbPadding: PaddingValues
): Int {
    return if (direction.isHorizontal) {
        val left = thumbPadding.calculateLeftPadding()
        val right = thumbPadding.calculateRightPadding()
        require(left >= 0 && right >= 0) {
            "Cannot have negative padding, but found $thumbPadding"
        }
        val sliderWidth = thumbInfo.parentInfo!!.size.width
        val thumbWidth = thumbInfo.size.width
        sliderWidth - left - thumbWidth - right
    } else {
        val top = thumbPadding.calculateTopPadding()
        val bottom = thumbPadding.calculateBottomPadding()
        require(top >= 0 && bottom >= 0) {
            "Cannot have negative padding, but found $thumbPadding"
        }
        val sliderHeight = thumbInfo.parentInfo!!.size.height
        val thumbHeight = thumbInfo.size.height
        sliderHeight - top - thumbHeight - bottom
    }
}

// -------- Continuous Slider --------

@Composable
private fun ContinuousSlider(
    value: Float,
    onValueChanged: (Float) -> Unit,
    range: ClosedFloatingPointRange,
    direction: LayoutDirection,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    thumbStyle: SliderThumbStyle,
    thumbPadding: PaddingValues,
    thumbSizer: ThumbSizer,
    trackStyle: SliderTrackStyle,
    trackContentPadding: PaddingValues,
    trackContentArrangement: Arrangement.Horizontal,
    trackContent: (@Composable RowScope.(value: Float) -> Unit)?,
    modifier: Modifier
) {
    Box(
        modifier = modifier.sliderDefaultSize(direction),
        propagateMinConstraints = true
    ) {
        val dragState = rememberAxisDragState().apply {
            updateAxisDirection(AxisDirections.getValue(direction))
        }
        val thumbSizeRef = remember { ref(IntSize.Zero) }
        val thumbSizeProvider = remember { thumbSizeRef::value }
        val thumbSizeUpdater = remember { thumbSizeRef::value::set }
        val valueState = rememberUpdatedState(value)
        val valueProvider = remember { valueState::value }
        val rangeStart = range.start
        val rangeLength = (range.endInclusive - rangeStart).coerceAtLeast(0.0f)
        val fraction = if (rangeLength != 0.0f) {
            val fraction = (value - rangeStart) / rangeLength
            fraction.coerceIn(0.0f, 1.0f)
        } else {
            0.0f
        }
        val fractionState = rememberUpdatedState(fraction)
        val fractionProvider = remember { fractionState::value }

        LaunchedEffect(Unit) {
            snapshotFlow { fractionProvider() }.collect { fraction ->
                dragState.offset = dragState.maxOffset * fraction
            }
        }

        ContinuousSliderTrack(
            thumbSizeProvider = thumbSizeProvider,
            valueProvider = valueProvider,
            onValueChanged = onValueChanged,
            rangeStart = rangeStart,
            rangeLength = rangeLength,
            fractionProvider = fractionProvider,
            direction = direction,
            isEnabled = isEnabled,
            interactionSource = interactionSource,
            thumbPadding = thumbPadding,
            trackStyle = trackStyle,
            trackContentPadding = trackContentPadding,
            trackContentArrangement = trackContentArrangement,
            trackContent = trackContent,
            modifier = Modifier
        )

        ContinuousSliderThumb(
            dragState = dragState,
            thumbSizeUpdater = thumbSizeUpdater,
            valueProvider = valueProvider,
            onValueChanged = onValueChanged,
            rangeStart = rangeStart,
            rangeLength = rangeLength,
            fractionProvider = fractionProvider,
            direction = direction,
            isEnabled = isEnabled,
            interactionSource = interactionSource,
            thumbStyle = thumbStyle,
            thumbPadding = thumbPadding,
            thumbSizer = thumbSizer,
            modifier = Modifier
        )
    }
}

@Composable
private fun BoxScope.ContinuousSliderTrack(
    thumbSizeProvider: () -> IntSize,
    valueProvider: () -> Float,
    onValueChanged: (Float) -> Unit,
    rangeStart: Float,
    rangeLength: Float,
    fractionProvider: () -> Float,
    direction: LayoutDirection,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    thumbPadding: PaddingValues,
    trackStyle: SliderTrackStyle,
    trackContentPadding: PaddingValues,
    trackContentArrangement: Arrangement.Horizontal,
    trackContent: (@Composable RowScope.(value: Float) -> Unit)?,
    modifier: Modifier
) {
    BaseSliderTrack(
        valueProvider = valueProvider,
        fractionProvider = fractionProvider,
        isEnabled = isEnabled,
        interactionSource = interactionSource,
        trackStyle = trackStyle,
        trackContentPadding = trackContentPadding,
        trackContentArrangement = trackContentArrangement,
        trackContent = trackContent,
        modifier = modifier
            .matchParentSize()
            .conditional(isEnabled) {
                this
                    .continuousSliderTrackSelector(
                        thumbSizeProvider = thumbSizeProvider,
                        onValueChanged = onValueChanged,
                        rangeStart = rangeStart,
                        rangeLength = rangeLength,
                        direction = direction,
                        interactionSource = interactionSource,
                        thumbPadding = thumbPadding
                    )
                    .sliderTrackPointerIcon(direction)
                    .hoverable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Thumb
                    )
                    .focusable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Track
                    )
            }
    )
}

@Composable
private fun BoxScope.ContinuousSliderThumb(
    dragState: AxisDragState,
    thumbSizeUpdater: (IntSize) -> Unit,
    valueProvider: () -> Float,
    onValueChanged: (Float) -> Unit,
    rangeStart: Float,
    rangeLength: Float,
    fractionProvider: () -> Float,
    direction: LayoutDirection,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    thumbStyle: SliderThumbStyle,
    thumbPadding: PaddingValues,
    thumbSizer: ThumbSizer,
    modifier: Modifier
) {
    BaseSliderThumb(
        fractionProvider = fractionProvider,
        isEnabled = isEnabled,
        interactionSource = interactionSource,
        thumbStyle = thumbStyle,
        modifier = modifier
            .matchParentSize()
            .wrapContentSize(WrapContentAlignments.getValue(direction))
            .padding(thumbPadding)
            .sliderThumbOffset(
                offsetProvider = dragState::roundedOffset,
                direction = direction
            )
            .thumbSizer(
                thumbSizeUpdater = thumbSizeUpdater,
                sizer = thumbSizer
            )
            .continuousSliderMaxDragOffsetCalculator(
                dragState = dragState,
                direction = direction,
                thumbPadding = thumbPadding
            )
            .conditional(isEnabled) {
                this
                    .hoverable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Thumb
                    )
                    .continuousSliderThumbSelector(
                        valueProvider = valueProvider,
                        onValueChanged = onValueChanged,
                        rangeStart = rangeStart,
                        rangeLength = rangeLength,
                        direction = direction
                    )
                    .focusable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Thumb
                    )
                    .visualIndication(
                        interactionSource = interactionSource,
                        indication = LocalVisualIndication.current,
                        label = TrackThumbInteractions.Thumb
                    )
            }
    )
}

// ---- Continuous Slider Track Selector Modifier ----

private fun Modifier.continuousSliderTrackSelector(
    thumbSizeProvider: () -> IntSize,
    onValueChanged: (Float) -> Unit,
    rangeStart: Float,
    rangeLength: Float,
    direction: LayoutDirection,
    interactionSource: MutableInteractionSource,
    thumbPadding: PaddingValues
): Modifier {
    val element = ContinuousSliderTrackSelectorModifier(
        thumbSizeProvider = thumbSizeProvider,
        onValueChanged = onValueChanged,
        rangeStart = rangeStart,
        rangeLength = rangeLength,
        direction = direction,
        interactionSource = interactionSource,
        thumbPadding = thumbPadding
    )
    return this then element
}

private data class ContinuousSliderTrackSelectorModifier(
    val thumbSizeProvider: () -> IntSize,
    val onValueChanged: (Float) -> Unit,
    val rangeStart: Float,
    val rangeLength: Float,
    val direction: LayoutDirection,
    val interactionSource: MutableInteractionSource,
    val thumbPadding: PaddingValues
) : ModifierNodeElement() {
    override fun create(): ContinuousSliderTrackSelectorModifierNode {
        return ContinuousSliderTrackSelectorModifierNode(
            thumbSizeProvider = thumbSizeProvider,
            onValueChanged = onValueChanged,
            rangeStart = rangeStart,
            rangeLength = rangeLength,
            direction = direction,
            interactionSource = interactionSource,
            thumbPadding = thumbPadding
        )
    }

    override fun update(node: ContinuousSliderTrackSelectorModifierNode) {
        node.onValueChanged = onValueChanged
        node.rangeStart = rangeStart
        node.rangeLength = rangeLength
        node.direction = direction
        node.interactionSource = interactionSource
        node.thumbPadding = thumbPadding
    }

    override fun InspectInfo.inspect() {
        set("rangeStart", rangeStart)
        set("rangeLength", rangeLength)
        set("direction", direction)
        set("thumbPadding", thumbPadding)
    }
}

private class ContinuousSliderTrackSelectorModifierNode(
    thumbSizeProvider: () -> IntSize,
    var onValueChanged: (Float) -> Unit,
    var rangeStart: Float,
    var rangeLength: Float,
    direction: LayoutDirection,
    interactionSource: MutableInteractionSource,
    thumbPadding: PaddingValues
) : SliderTrackSelectorModifierNode(
    thumbSizeProvider = thumbSizeProvider,
    direction = direction,
    interactionSource = interactionSource,
    thumbPadding = thumbPadding
) {
    override fun emitValueFromOffset(
        maxOffset: Float,
        offset: Float
    ) {
        val fraction = offset / maxOffset
        val coercedFraction = fraction.coerceIn(0.0f, 1.0f)
        val value = rangeStart + rangeLength * coercedFraction
        onValueChanged(value)
    }
}

// ---- Continuous Slider Max Drag Offset Calculator Modifier ----

private fun Modifier.continuousSliderMaxDragOffsetCalculator(
    dragState: AxisDragState,
    direction: LayoutDirection,
    thumbPadding: PaddingValues
): Modifier {
    val element = ContinuousSliderMaxDragOffsetCalculatorModifier(
        dragState = dragState,
        direction = direction,
        thumbPadding = thumbPadding
    )
    return this then element
}

private data class ContinuousSliderMaxDragOffsetCalculatorModifier(
    val dragState: AxisDragState,
    val direction: LayoutDirection,
    val thumbPadding: PaddingValues
) : ModifierNodeElement() {
    override fun create(): ContinuousSliderMaxDragOffsetCalculatorModifierNode {
        return ContinuousSliderMaxDragOffsetCalculatorModifierNode(
            dragState = dragState,
            direction = direction,
            thumbPadding = thumbPadding
        )
    }

    override fun update(node: ContinuousSliderMaxDragOffsetCalculatorModifierNode) {
        node.direction = direction
        node.thumbPadding = thumbPadding
    }

    override fun InspectInfo.inspect() {
        set("direction", direction)
        set("thumbPadding", thumbPadding)
    }
}

private class ContinuousSliderMaxDragOffsetCalculatorModifierNode(
    val dragState: AxisDragState,
    direction: LayoutDirection,
    thumbPadding: PaddingValues
) : ModifierNode(), LayoutCallbackModifierNode {
    var direction: LayoutDirection = direction
        set(value) {
            if (field == value) {
                return
            }
            field = value
            shouldResetDragDistance = true
        }

    var thumbPadding: PaddingValues = thumbPadding
        set(value) {
            if (field == value) {
                return
            }
            field = value
            shouldResetDragDistance = true
        }

    private var shouldResetDragDistance: Boolean = true

    override fun onSizeChanged(size: IntSize) {
        shouldResetDragDistance = true
    }

    override fun onPlaced(info: LayoutInfo) {
        if (shouldResetDragDistance) {
            shouldResetDragDistance = false
            resetDragDistance(info)
        }
    }

    private fun resetDragDistance(info: LayoutInfo) {
        val intMaxOffset = calculateMaxDragOffset(
            thumbInfo = info,
            direction = direction,
            thumbPadding = thumbPadding
        )
        dragState.maxOffset = intMaxOffset.toFloat()
    }
}

// ---- Continuous Slider Thumb Selector Modifier ----

private fun Modifier.continuousSliderThumbSelector(
    valueProvider: () -> Float,
    onValueChanged: (Float) -> Unit,
    rangeStart: Float,
    rangeLength: Float,
    direction: LayoutDirection
): Modifier {
    val element = ContinuousSliderThumbSelectorModifier(
        valueProvider = valueProvider,
        onValueChanged = onValueChanged,
        rangeStart = rangeStart,
        rangeLength = rangeLength,
        direction = direction
    )
    return this then element
}

private data class ContinuousSliderThumbSelectorModifier(
    val valueProvider: () -> Float,
    val onValueChanged: (Float) -> Unit,
    val rangeStart: Float,
    val rangeLength: Float,
    val direction: LayoutDirection
) : ModifierNodeElement() {
    override fun create(): ContinuousSliderThumbSelectorModifierNode {
        return ContinuousSliderThumbSelectorModifierNode(
            valueProvider = valueProvider,
            onValueChanged = onValueChanged,
            rangeStart = rangeStart,
            rangeLength = rangeLength,
            direction = direction
        )
    }

    override fun update(node: ContinuousSliderThumbSelectorModifierNode) {
        node.onValueChanged = onValueChanged
        node.rangeStart = rangeStart
        node.rangeLength = rangeLength
        node.direction = direction
    }

    override fun InspectInfo.inspect() {
        set("rangeStart", rangeStart)
        set("rangeLength", rangeLength)
        set("direction", direction)
    }
}

private class ContinuousSliderThumbSelectorModifierNode(
    val valueProvider: () -> Float,
    var onValueChanged: (Float) -> Unit,
    var rangeStart: Float,
    var rangeLength: Float,
    direction: LayoutDirection
) : SliderThumbKeySelectorModifierNode(direction) {
    override fun next(modifier: KeyboardModifier): Boolean {
        val value = valueProvider()
        val rangeEnd = rangeStart + rangeLength
        if (value >= rangeEnd) {
            return false
        }
        val valueStep = calculatePseudoValueStep(
            rangeLength = rangeLength,
            modifier = modifier
        )
        val next = (value + valueStep).coerceAtMost(rangeEnd)
        onValueChanged(next)
        return true
    }

    override fun previous(modifier: KeyboardModifier): Boolean {
        val value = valueProvider()
        val rangeStart = rangeStart
        if (value <= rangeStart) {
            return false
        }
        val valueStep = calculatePseudoValueStep(
            rangeLength = rangeLength,
            modifier = modifier
        )
        val previous = (value - valueStep).coerceAtLeast(rangeStart)
        onValueChanged(previous)
        return true
    }

    override fun toStart(): Boolean {
        onValueChanged(rangeStart)
        return true
    }

    override fun toEnd(): Boolean {
        onValueChanged(rangeStart + rangeLength)
        return true
    }
}

private fun calculatePseudoValueStep(
    rangeLength: Float,
    modifier: KeyboardModifier
): Float {
    fun Float.getBaseValueStep(): Float {
        return when {
            this <= 0.1f -> 0.001f
            this <= 1.0f -> 0.01f
            this <= 10.0f -> 0.1f
            this <= 100.0f -> 1.0f
            this <= 1000.0f -> 10.0f
            else -> 100.0f
        }
    }
    fun KeyboardModifier.getValueStepModifier(): Float {
        val isShiftPressed = isShiftPressed
        val isCtrlPressed = isCtrlPressed
        return when {
            isShiftPressed && isCtrlPressed -> 1.0f
            isShiftPressed -> 5.0f
            isCtrlPressed -> 0.2f
            else -> 1.0f
        }
    }
    val baseValueStep = rangeLength.getBaseValueStep()
    val valueStepModifier = modifier.getValueStepModifier()
    return baseValueStep * valueStepModifier
}

// -------- Discrete Slider --------

@Composable
private fun  DiscreteSlider(
    value: T,
    onValueChanged: (T) -> Unit,
    values: List,
    direction: LayoutDirection,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    thumbStyle: SliderThumbStyle,
    thumbPadding: PaddingValues,
    thumbSizer: ThumbSizer,
    trackStyle: SliderTrackStyle,
    trackContentPadding: PaddingValues,
    trackContentArrangement: Arrangement.Horizontal,
    trackContent: (@Composable RowScope.(value: T) -> Unit)?,
    modifier: Modifier
) {
    Box(
        modifier = modifier.sliderDefaultSize(direction),
        propagateMinConstraints = true
    ) {
        val dragState = rememberAxisAnchoredDragState(value).apply {
            updateCandidatePredicate { candidate ->
                onValueChanged(candidate)
                true
            }
            updateAxisDirection(AxisDirections.getValue(direction))
            updateThresholds(DefaultThresholds)
        }
        val thumbSizeRef = remember { ref(IntSize.Zero) }
        val thumbSizeProvider = remember { thumbSizeRef::value }
        val thumbSizeUpdater = remember { thumbSizeRef::value::set }
        val valueState = rememberUpdatedState(value)
        val valueProvider = remember { valueState::value }
        val fractionProvider = remember { dragState::fraction }

        LaunchedEffect(value) {
            if (value != dragState.targetValue) {
                dragState.animateTo(value)
            }
        }

        DiscreteSliderTrack(
            dragState = dragState,
            thumbSizeProvider = thumbSizeProvider,
            valueProvider = valueProvider,
            onValueChanged = onValueChanged,
            fractionProvider = fractionProvider,
            direction = direction,
            isEnabled = isEnabled,
            interactionSource = interactionSource,
            thumbPadding = thumbPadding,
            trackStyle = trackStyle,
            trackContentPadding = trackContentPadding,
            trackContentArrangement = trackContentArrangement,
            trackContent = trackContent,
            modifier = Modifier
        )

        DiscreteSliderThumb(
            dragState = dragState,
            thumbSizeUpdater = thumbSizeUpdater,
            valueProvider = valueProvider,
            onValueChanged = onValueChanged,
            values = values,
            fractionProvider = fractionProvider,
            direction = direction,
            isEnabled = isEnabled,
            interactionSource = interactionSource,
            thumbStyle = thumbStyle,
            thumbPadding = thumbPadding,
            thumbSizer = thumbSizer,
            modifier = Modifier
        )
    }
}

@Composable
private fun  BoxScope.DiscreteSliderTrack(
    dragState: AxisAnchoredDragState,
    thumbSizeProvider: () -> IntSize,
    valueProvider: () -> T,
    onValueChanged: (T) -> Unit,
    fractionProvider: () -> Float,
    direction: LayoutDirection,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    thumbPadding: PaddingValues,
    trackStyle: SliderTrackStyle,
    trackContentPadding: PaddingValues,
    trackContentArrangement: Arrangement.Horizontal,
    trackContent: (@Composable RowScope.(value: T) -> Unit)?,
    modifier: Modifier
) {
    BaseSliderTrack(
        valueProvider = valueProvider,
        fractionProvider = fractionProvider,
        isEnabled = isEnabled,
        interactionSource = interactionSource,
        trackStyle = trackStyle,
        trackContentPadding = trackContentPadding,
        trackContentArrangement = trackContentArrangement,
        trackContent = trackContent,
        modifier = modifier
            .matchParentSize()
            .conditional(isEnabled) {
                this
                    .discreteSliderTrackSelector(
                        dragState = dragState,
                        thumbSizeProvider = thumbSizeProvider,
                        onValueChanged = onValueChanged,
                        direction = direction,
                        interactionSource = interactionSource,
                        thumbPadding = thumbPadding
                    )
                    .sliderTrackPointerIcon(direction)
                    .hoverable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Thumb
                    )
                    .focusable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Track
                    )
            }
    )
}

@Composable
private fun  BoxScope.DiscreteSliderThumb(
    dragState: AxisAnchoredDragState,
    thumbSizeUpdater: (IntSize) -> Unit,
    valueProvider: () -> T,
    onValueChanged: (T) -> Unit,
    values: List,
    fractionProvider: () -> Float,
    direction: LayoutDirection,
    isEnabled: Boolean,
    interactionSource: MutableInteractionSource,
    thumbStyle: SliderThumbStyle,
    thumbPadding: PaddingValues,
    thumbSizer: ThumbSizer,
    modifier: Modifier
) {
    BaseSliderThumb(
        fractionProvider = fractionProvider,
        isEnabled = isEnabled,
        interactionSource = interactionSource,
        thumbStyle = thumbStyle,
        modifier = modifier
            .matchParentSize()
            .wrapContentSize(WrapContentAlignments.getValue(direction))
            .padding(thumbPadding)
            .sliderThumbOffset(
                offsetProvider = dragState::roundedOffset,
                direction = direction
            )
            .thumbSizer(
                thumbSizeUpdater = thumbSizeUpdater,
                sizer = thumbSizer
            )
            .discreteSliderAnchorsCalculator(
                dragState = dragState,
                values = values,
                direction = direction,
                thumbPadding = thumbPadding
            )
            .conditional(isEnabled) {
                this
                    .hoverable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Thumb
                    )
                    .axisAnchoredDraggable(
                        state = dragState,
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Thumb
                    )
                    .discreteSliderThumbSelector(
                        valueProvider = valueProvider,
                        onValueChanged = onValueChanged,
                        values = values,
                        direction = direction
                    )
                    .focusable(
                        interactionSource = interactionSource,
                        label = TrackThumbInteractions.Thumb
                    )
                    .visualIndication(
                        interactionSource = interactionSource,
                        indication = LocalVisualIndication.current,
                        label = TrackThumbInteractions.Thumb
                    )
            }
    )
}

// ---- Discrete Slider Track Selector Modifier ----

private fun  Modifier.discreteSliderTrackSelector(
    dragState: AxisAnchoredDragState,
    thumbSizeProvider: () -> IntSize,
    onValueChanged: (T) -> Unit,
    direction: LayoutDirection,
    interactionSource: MutableInteractionSource,
    thumbPadding: PaddingValues
): Modifier {
    val element = DiscreteSliderTrackSelectorModifier(
        dragState = dragState,
        thumbSizeProvider = thumbSizeProvider,
        onValueChanged = onValueChanged,
        direction = direction,
        interactionSource = interactionSource,
        thumbPadding = thumbPadding
    )
    return this then element
}

private data class DiscreteSliderTrackSelectorModifier(
    val dragState: AxisAnchoredDragState,
    val thumbSizeProvider: () -> IntSize,
    val onValueChanged: (T) -> Unit,
    val direction: LayoutDirection,
    val interactionSource: MutableInteractionSource,
    val thumbPadding: PaddingValues
) : ModifierNodeElement>() {
    override fun create(): DiscreteSliderTrackSelectorModifierNode {
        return DiscreteSliderTrackSelectorModifierNode(
            dragState = dragState,
            thumbSizeProvider = thumbSizeProvider,
            onValueChanged = onValueChanged,
            direction = direction,
            interactionSource = interactionSource,
            thumbPadding = thumbPadding
        )
    }

    override fun update(node: DiscreteSliderTrackSelectorModifierNode) {
        node.onValueChanged = onValueChanged
        node.direction = direction
        node.interactionSource = interactionSource
        node.thumbPadding = thumbPadding
    }

    override fun InspectInfo.inspect() {
        set("direction", direction)
        set("thumbPadding", thumbPadding)
    }
}

private class DiscreteSliderTrackSelectorModifierNode(
    val dragState: AxisAnchoredDragState,
    thumbSizeProvider: () -> IntSize,
    var onValueChanged: (T) -> Unit,
    direction: LayoutDirection,
    interactionSource: MutableInteractionSource,
    thumbPadding: PaddingValues
) : SliderTrackSelectorModifierNode(
    thumbSizeProvider = thumbSizeProvider,
    direction = direction,
    interactionSource = interactionSource,
    thumbPadding = thumbPadding
) {
    override fun emitValueFromOffset(
        maxOffset: Float,
        offset: Float
    ) {
        val anchors = dragState.anchors
        val coercedOffset = anchors.keys.minBy { anchorOffset ->
            abs(anchorOffset - offset)
        }
        val value = anchors.getValue(coercedOffset)
        onValueChanged(value)
    }
}

// ---- Discrete Slider Anchors Calculator Modifier ----

private const val AnimatableAnchorsSpacing: Float = 10.0f

private val ThumbSnapSpec: FiniteAnimationSpec = SnapSpec()

private fun  Modifier.discreteSliderAnchorsCalculator(
    dragState: AxisAnchoredDragState,
    values: List,
    direction: LayoutDirection,
    thumbPadding: PaddingValues
): Modifier {
    val element = DiscreteSliderAnchorsCalculatorModifier(
        dragState = dragState,
        values = values,
        direction = direction,
        thumbPadding = thumbPadding
    )
    return this then element
}

private data class DiscreteSliderAnchorsCalculatorModifier(
    val dragState: AxisAnchoredDragState,
    val values: List,
    val direction: LayoutDirection,
    val thumbPadding: PaddingValues
) : ModifierNodeElement>() {
    override fun create(): DiscreteSliderAnchorsCalculatorModifierNode {
        return DiscreteSliderAnchorsCalculatorModifierNode(
            dragState = dragState,
            values = values,
            direction = direction,
            thumbPadding = thumbPadding
        )
    }

    override fun update(node: DiscreteSliderAnchorsCalculatorModifierNode) {
        node.values = values
        node.direction = direction
        node.thumbPadding = thumbPadding
    }

    override fun InspectInfo.inspect() {
        set("values", values)
        set("direction", direction)
        set("thumbPadding", thumbPadding)
    }
}

private class DiscreteSliderAnchorsCalculatorModifierNode(
    val dragState: AxisAnchoredDragState,
    values: List,
    direction: LayoutDirection,
    thumbPadding: PaddingValues,
) : ModifierNode(), LayoutCallbackModifierNode {
    var values: List = values
        set(value) {
            if (field == value) {
                return
            }
            field = value
            shouldResetAnchors = true
        }

    var direction: LayoutDirection = direction
        set(value) {
            if (field == value) {
                return
            }
            field = value
            shouldResetAnchors = true
        }

    var thumbPadding: PaddingValues = thumbPadding
        set(value) {
            if (field == value) {
                return
            }
            field = value
            shouldResetAnchors = true
        }

    private var shouldResetAnchors: Boolean = true

    override fun onSizeChanged(size: IntSize) {
        shouldResetAnchors = true
    }

    override fun onPlaced(info: LayoutInfo) {
        if (shouldResetAnchors) {
            shouldResetAnchors = false
            resetAnchors(info)
        }
    }

    private fun resetAnchors(info: LayoutInfo) {
        // AxisAnchoredDragState requires actual dragging distance on
        // created, but we can't know this before all the following
        // values are determined, so we do a callback to set it properly
        val values = values
        val offsetStep: Float
        val anchors: AxisAnchors
        if (values.size > 1) {
            val spacingCount = values.size - 1
            val intMaxOffset = calculateMaxDragOffset(
                thumbInfo = info,
                direction = direction,
                thumbPadding = thumbPadding
            )
            val maxOffset = intMaxOffset.toFloat()
            val maxSpacingCount = (maxOffset / MinimumAnchorsSpacing).toInt()
            val coercedSpacingCount = spacingCount.coerceAtMost(maxSpacingCount)
            offsetStep = maxOffset / coercedSpacingCount
            anchors = buildMap(coercedSpacingCount + 1) {
                var currentOffset = 0.0f
                repeat(coercedSpacingCount + 1) { index ->
                    put(currentOffset, values[index]) // TODO Skip certain values if too many
                    currentOffset += offsetStep
                }
            }
        } else {
            offsetStep = Float.POSITIVE_INFINITY
            anchors = mapOf(0.0f to values.first())
        }
        nodeScope.launch {
            val animationSpec = if (offsetStep >= AnimatableAnchorsSpacing) {
                DefaultFloatAnimationSpec
            } else {
                // Animation won't work properly if anchors are too close to each other,
                // as it will be frequently interrupted by user's gestures, which then
                // causes the thumb to stand still instead of moving towards the target
                ThumbSnapSpec
            }
            with(dragState) {
                updateAnchors(anchors)
                updateDefaultAnimationSpec(animationSpec)
            }
        }
    }
}

// ---- Discrete Slider Thumb Selector Modifier ----

private fun  Modifier.discreteSliderThumbSelector(
    valueProvider: () -> T,
    onValueChanged: (T) -> Unit,
    values: List,
    direction: LayoutDirection
): Modifier {
    val element = DiscreteSliderThumbSelectorModifier(
        valueProvider = valueProvider,
        onValueChanged = onValueChanged,
        values = values,
        direction = direction
    )
    return this then element
}

private data class DiscreteSliderThumbSelectorModifier(
    val valueProvider: () -> T,
    val onValueChanged: (T) -> Unit,
    val values: List,
    val direction: LayoutDirection
) : ModifierNodeElement>() {
    override fun create(): DiscreteSliderThumbSelectorModifierNode {
        return DiscreteSliderThumbSelectorModifierNode(
            valueProvider = valueProvider,
            onValueChanged = onValueChanged,
            values = values,
            direction = direction
        )
    }

    override fun update(node: DiscreteSliderThumbSelectorModifierNode) {
        node.onValueChanged = onValueChanged
        node.values = values
        node.direction = direction
    }

    override fun InspectInfo.inspect() {
        set("values", values)
        set("direction", direction)
    }
}

private class DiscreteSliderThumbSelectorModifierNode(
    val valueProvider: () -> T,
    var onValueChanged: (T) -> Unit,
    var values: List,
    direction: LayoutDirection
) : SliderThumbKeySelectorModifierNode(direction) {
    override fun next(modifier: KeyboardModifier): Boolean {
        val value = valueProvider()
        val values = values
        val currentIndex = values.indexOf(value)
        if (currentIndex == values.lastIndex) {
            return false
        }
        val next = values[currentIndex + 1]
        onValueChanged(next)
        return true
    }

    override fun previous(modifier: KeyboardModifier): Boolean {
        val value = valueProvider()
        val values = values
        val currentIndex = values.indexOf(value)
        if (currentIndex == 0) {
            return false
        }
        val previous = values[currentIndex - 1]
        onValueChanged(previous)
        return true
    }

    override fun toStart(): Boolean {
        onValueChanged(values.first())
        return true
    }

    override fun toEnd(): Boolean {
        onValueChanged(values.last())
        return true
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy