Stepper.kt

/*
 * Copyright 2023 The Android Open Source Project
 *
 * 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 androidx.wear.compose.material3

import androidx.compose.foundation.layout.BoxScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import kotlin.math.roundToInt

/**
 * [Stepper] allows users to make a selection from a range of values.
 * It's a full-screen control with increase button on the top, decrease button on the bottom and
 * a slot (expected to have either [Text] or [Button]) in the middle.
 * Value can be increased and decreased by clicking on the increase and decrease buttons.
 * Buttons can have custom icons - [decreaseIcon] and [increaseIcon].
 * Step value is calculated as the difference between min and max values divided by [steps]+1.
 * Stepper itself doesn't show the current value but can be displayed via the content slot or
 * [PositionIndicator] if required.
 * If [value] is not equal to any step value, then it will be coerced to the closest step value.
 * However, the [value] itself will not be changed and [onValueChange] in this case will
 * not be triggered.
 * To add range semantics on Stepper, use [Modifier.rangeSemantics].
 *
 * Example of a simple [Stepper]:
 * @sample androidx.wear.compose.material3.samples.StepperSample
 *
 * Example of a [Stepper] with range semantics:
 * @sample androidx.wear.compose.material3.samples.StepperWithRangeSemanticsSample
 *
 * @param value Current value of the Stepper. If outside of [valueRange] provided, value will be
 * coerced to this range.
 * @param onValueChange Lambda in which value should be updated
 * @param steps Specifies the number of discrete values, excluding min and max values, evenly
 * distributed across the whole value range. Must not be negative. If 0, stepper will have only
 * min and max values and no steps in between
 * @param decreaseIcon A slot for an icon which is placed on the decrease (bottom) button
 * @param increaseIcon A slot for an icon which is placed on the increase (top) button
 * @param modifier Modifiers for the Stepper layout
 * @param valueRange Range of values that Stepper value can take. Passed [value] will be coerced to
 * this range
 * @param backgroundColor [Color] representing the background color for the stepper.
 * @param contentColor [Color] representing the color for [content] in the middle.
 * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
 * that defaults to [contentColor], unless specifically overridden.
 * @param content Content body for the Stepper.
 */
@ExperimentalWearMaterial3Api
@Composable
fun Stepper(
    value: Float,
    onValueChange: (Float) -> Unit,
    steps: Int,
    decreaseIcon: @Composable () -> Unit,
    increaseIcon: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    valueRange: ClosedFloatingPointRange<Float> = 0f..(steps + 1).toFloat(),
    backgroundColor: Color = MaterialTheme.colorScheme.background,
    contentColor: Color = MaterialTheme.colorScheme.onSurface,
    iconColor: Color = contentColor,
    content: @Composable BoxScope.() -> Unit
) {
    androidx.wear.compose.materialcore.Stepper(
        value = value,
        onValueChange = onValueChange,
        steps = steps,
        decreaseIcon = decreaseIcon,
        increaseIcon = increaseIcon,
        valueRange = valueRange,
        modifier = modifier,
        backgroundColor = backgroundColor,
        enabledButtonProviderValues = arrayOf(
            LocalContentColor provides iconColor,
            LocalContentAlpha provides iconColor.alpha
        ),
        disabledButtonProviderValues = arrayOf(
            LocalContentColor provides iconColor.copy(alpha = ContentAlpha.disabled),
            LocalContentAlpha provides iconColor.copy(alpha = ContentAlpha.disabled).alpha
        )
    ) {
        CompositionLocalProvider(
            LocalContentColor provides contentColor
        ) {
            content()
        }
    }
}

/**
 * [Stepper] allows users to make a selection from a range of values.
 * It's a full-screen control with increase button on the top, decrease button on the bottom and
 * a slot (expected to have either [Text] or [Button]) in the middle.
 * Value can be increased and decreased by clicking on the increase and decrease buttons.
 * Buttons can have custom icons - [decreaseIcon] and [increaseIcon].
 * Stepper itself doesn't show the current value but can be displayed via the content slot or
 * [PositionIndicator] if required.
 * To add range semantics on Stepper, use [Modifier.rangeSemantics].
 *
 * Example of a [Stepper] with integer values:
 * @sample androidx.wear.compose.material3.samples.StepperWithIntegerSample
 *
 * A number of steps is calculated as the difference between max and min values of
 * [valueProgression] divided by [valueProgression].step - 1.
 * For example, with a range of 100..120 and a step 5,
 * number of steps will be (120-100)/ 5 - 1 = 3. Steps are 100(first), 105, 110, 115, 120(last)
 *
 * If [valueProgression] range is not equally divisible by [valueProgression].step,
 * then [valueProgression].last will be adjusted to the closest divisible value in the range.
 * For example, 1..13 range and a step = 5, steps will be 1(first) , 6 , 11(last)
 *
 * If [value] is not equal to any step value, then it will be coerced to the closest step value.
 * However, the [value] itself will not be changed and [onValueChange] in this case will
 * not be triggered.
 *
 * @param value Current value of the Stepper. If outside of [valueProgression] provided, value will be
 * coerced to this range.
 * @param onValueChange Lambda in which value should be updated
 * @param valueProgression Progression of values that Stepper value can take. Consists of
 * rangeStart, rangeEnd and step. Range will be equally divided by step size
 * @param decreaseIcon A slot for an icon which is placed on the decrease (bottom) button
 * @param increaseIcon A slot for an icon which is placed on the increase (top) button
 * @param modifier Modifiers for the Stepper layout
 * @param backgroundColor [Color] representing the background color for the stepper.
 * @param contentColor [Color] representing the color for [content] in the middle.
 * @param iconColor Icon tint [Color] which used by [increaseIcon] and [decreaseIcon]
 * that defaults to [contentColor], unless specifically overridden.
 * @param content Content body for the Stepper.
 */
@ExperimentalWearMaterial3Api
@Composable
fun Stepper(
    value: Int,
    onValueChange: (Int) -> Unit,
    valueProgression: IntProgression,
    decreaseIcon: @Composable () -> Unit,
    increaseIcon: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colorScheme.background,
    contentColor: Color = MaterialTheme.colorScheme.onSurface,
    iconColor: Color = contentColor,
    content: @Composable BoxScope.() -> Unit
) {
    Stepper(
        value = value.toFloat(),
        onValueChange = { onValueChange(it.roundToInt()) },
        steps = valueProgression.stepsNumber(),
        modifier = modifier,
        valueRange = valueProgression.first.toFloat()..valueProgression.last.toFloat(),
        decreaseIcon = decreaseIcon,
        increaseIcon = increaseIcon,
        backgroundColor = backgroundColor,
        contentColor = contentColor,
        iconColor = iconColor,
        content = content
    )
}

/**
 * Defaults used by stepper.
 */
@ExperimentalWearMaterial3Api
public object StepperDefaults {
    /**
     * Decrease [ImageVector].
     */
    public val Decrease = androidx.wear.compose.materialcore.RangeIcons.Minus

    /**
     * Increase [ImageVector].
     */
    public val Increase = Icons.Filled.Add
}