ToggleControls.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.animation.core.AnimationSpec
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import androidx.wear.compose.material3.tokens.MotionTokens
import androidx.wear.compose.materialcore.animateSelectionColor
import androidx.wear.compose.materialcore.directionVector
import androidx.wear.compose.materialcore.toRadians

/**
 * [Checkbox] provides an animated checkbox for use as a toggle control in
 * [ToggleButton] or [SplitToggleButton].
 *
 * Checkbox sample:
 * @sample androidx.wear.compose.material3.samples.ToggleButtonWithCheckbox
 *
 * @param modifier Modifier to be applied to the checkbox. This can be used to provide a
 * content description for accessibility.
 * @param colors [CheckboxColors] from which the box and checkmark colors will be obtained.
 */
@Composable
fun ToggleControlScope.Checkbox(
    modifier: Modifier = Modifier,
    colors: CheckboxColors = CheckboxDefaults.colors(),
) = androidx.wear.compose.materialcore.Checkbox(
    checked = isChecked,
    modifier = modifier,
    boxColor = { isEnabled, isChecked ->
        colors.boxColor(
            enabled = isEnabled,
            checked = isChecked
        )
    },
    checkmarkColor = { isEnabled, isChecked ->
        colors.checkmarkColor(
            enabled = isEnabled,
            checked = isChecked
        )
    },
    enabled = isEnabled,
    onCheckedChange = null,
    interactionSource = null,
    drawBox = { drawScope, color, progress, isRtl ->
        drawScope.drawBox(
            color = color,
            progress = progress,
            isRtl = isRtl
        )
    },
    progressAnimationSpec = PROGRESS_ANIMATION_SPEC,
    width = WIDTH,
    height = HEIGHT,
    ripple = rippleOrFallbackImplementation()
)

/**
 * [Switch] provides an animated switch for use as a toggle control in
 * [ToggleButton] or [SplitToggleButton].
 *
 * Switch samples:
 * @sample androidx.wear.compose.material3.samples.ToggleButtonWithSwitch
 *
 * @param modifier Modifier to be applied to the switch. This can be used to provide a
 * content description for accessibility.
 * @param colors [SwitchColors] from which the colors of the thumb and track will be obtained.
 */
@Composable
fun ToggleControlScope.Switch(
    modifier: Modifier = Modifier,
    colors: SwitchColors = SwitchDefaults.colors(),
) = androidx.wear.compose.materialcore.Switch(
    modifier = modifier,
    checked = isChecked,
    enabled = isEnabled,
    onCheckedChange = null,
    interactionSource = null,
    trackFillColor = { isEnabled, isChecked ->
        colors.trackColor(
            enabled = isEnabled,
            checked = isChecked
        )
    },
    trackStrokeColor = { isEnabled, isChecked ->
        colors.trackStrokeColor(
            enabled = isEnabled,
            checked = isChecked
        )
    },
    thumbColor = { isEnabled, isChecked ->
        colors.thumbColor(
            enabled = isEnabled,
            checked = isChecked
        )
    },
    thumbIconColor = { isEnabled, isChecked ->
        colors.thumbIconColor(
            enabled = isEnabled,
            checked = isChecked
        )
    },
    trackWidth = TRACK_WIDTH,
    trackHeight = TRACK_HEIGHT,
    drawThumb = { drawScope, thumbColor, progress, thumbIconColor, isRtl ->
        drawScope.drawThumb(
            thumbColor,
            progress,
            thumbIconColor,
            isRtl
        )
    },
    progressAnimationSpec = SWITCH_PROGRESS_ANIMATION_SPEC,
    width = WIDTH,
    height = HEIGHT,
    ripple = rippleOrFallbackImplementation()
)

/**
 * Represents the content colors used in [Checkbox] in different states.
 *
 * @param checkedBoxColor The box color of [Checkbox] when enabled and checked.
 * @param checkedCheckmarkColor The check mark color of [Checkbox] when enabled
 * and checked.
 * @param uncheckedBoxColor The box color of [Checkbox] when enabled and unchecked.
 * @param uncheckedCheckmarkColor The check mark color of [Checkbox] when enabled
 * and unchecked.
 * @param disabledCheckedBoxColor The box color of [Checkbox] when disabled and checked.
 * @param disabledCheckedCheckmarkColor The check mark color of [Checkbox] when disabled
 * and checked.
 * @param disabledUncheckedBoxColor The box color of [Checkbox] when disabled and unchecked.
 * @param disabledUncheckedCheckmarkColor The check mark color of [Checkbox] when disabled
 * and unchecked.
 */
@Immutable
class CheckboxColors(
    val checkedBoxColor: Color,
    val checkedCheckmarkColor: Color,
    val uncheckedBoxColor: Color,
    val uncheckedCheckmarkColor: Color,
    val disabledCheckedBoxColor: Color,
    val disabledCheckedCheckmarkColor: Color,
    val disabledUncheckedBoxColor: Color,
    val disabledUncheckedCheckmarkColor: Color,
) {

    /**
     * Represents the box color for this [Checkbox], depending on the [enabled] and [checked]
     * properties.
     *
     * @param enabled Whether the [Checkbox] is enabled
     * @param checked Whether the [Checkbox] is currently checked or unchecked
     */
    @Composable
    internal fun boxColor(enabled: Boolean, checked: Boolean): State<Color> = animateSelectionColor(
        enabled = enabled,
        checked = checked,
        checkedColor = checkedBoxColor,
        uncheckedColor = uncheckedBoxColor,
        disabledCheckedColor = disabledCheckedBoxColor,
        disabledUncheckedColor = disabledUncheckedBoxColor,
        animationSpec = COLOR_ANIMATION_SPEC
    )

    /**
     * Represents the checkmark color for this [Checkbox], depending on the [enabled] and [checked]
     * properties.
     *
     * @param enabled Whether the [Checkbox] is enabled
     * @param checked Whether the [Checkbox] is currently checked or unchecked
     */
    @Composable
    internal fun checkmarkColor(enabled: Boolean, checked: Boolean): State<Color> =
        animateSelectionColor(
            enabled = enabled,
            checked = checked,
            checkedColor = checkedCheckmarkColor,
            uncheckedColor = uncheckedCheckmarkColor,
            disabledCheckedColor = disabledCheckedCheckmarkColor,
            disabledUncheckedColor = disabledUncheckedCheckmarkColor,
            animationSpec = COLOR_ANIMATION_SPEC
        )

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || other !is CheckboxColors) return false

        if (checkedBoxColor != other.checkedBoxColor) return false
        if (checkedCheckmarkColor != other.checkedCheckmarkColor) return false
        if (uncheckedCheckmarkColor != other.uncheckedCheckmarkColor) return false
        if (uncheckedBoxColor != other.uncheckedBoxColor) return false
        if (disabledCheckedBoxColor != other.disabledCheckedBoxColor) return false
        if (disabledCheckedCheckmarkColor != other.disabledCheckedCheckmarkColor) return false
        if (disabledUncheckedBoxColor != other.disabledUncheckedBoxColor) return false
        if (disabledUncheckedCheckmarkColor != other.disabledUncheckedCheckmarkColor) return false

        return true
    }

    override fun hashCode(): Int {
        var result = checkedBoxColor.hashCode()
        result = 31 * result + checkedCheckmarkColor.hashCode()
        result = 31 * result + uncheckedCheckmarkColor.hashCode()
        result = 31 * result + uncheckedBoxColor.hashCode()
        result = 31 * result + disabledCheckedBoxColor.hashCode()
        result = 31 * result + disabledCheckedCheckmarkColor.hashCode()
        result = 31 * result + disabledUncheckedBoxColor.hashCode()
        result = 31 * result + disabledUncheckedCheckmarkColor.hashCode()
        return result
    }
}

/**
 * Represents the content colors used in [Switch] in different states.
 *
 * @param checkedThumbColor The thumb color of [Switch] when enabled and checked.
 * @param checkedThumbIconColor The thumb icon color of [Switch] when enabled and checked.
 * @param checkedTrackColor The track color of [Switch] when enabled and checked.
 * @param checkedTrackBorderColor The track border color of [Switch] when enabled and checked.
 * @param uncheckedThumbColor The thumb color of [Switch] when enabled and unchecked.
 * @param uncheckedThumbIconColor The thumb icon color of [Switch] when enabled and unchecked.
 * @param uncheckedTrackColor The track color of [Switch] when enabled and unchecked.
 * @param uncheckedTrackBorderColor The track border color of [Switch] when enabled and unchecked.
 * @param disabledCheckedThumbColor The thumb color of [Switch] when disabled and checked.
 * @param disabledCheckedThumbIconColor The thumb icon color of [Switch] when disabled and checked.
 * @param disabledCheckedTrackColor The track color of [Switch] when disabled and checked.
 * @param disabledCheckedTrackBorderColor The track border color of [Switch] when disabled
 * and checked.
 * @param disabledUncheckedThumbColor The thumb color of [Switch] when disabled and unchecked.
 * @param disabledUncheckedThumbIconColor The thumb icon color of [Switch] when disabled
 * and unchecked.
 * @param disabledUncheckedTrackColor The track color of [Switch] when disabled and unchecked.
 * @param disabledUncheckedTrackBorderColor The track border color of [Switch] when disabled
 * and unchecked.
 */
@Immutable
class SwitchColors(
    val checkedThumbColor: Color,
    val checkedThumbIconColor: Color,
    val checkedTrackColor: Color,
    val checkedTrackBorderColor: Color,
    val uncheckedThumbColor: Color,
    val uncheckedThumbIconColor: Color,
    val uncheckedTrackColor: Color,
    val uncheckedTrackBorderColor: Color,
    val disabledCheckedThumbColor: Color,
    val disabledCheckedThumbIconColor: Color,
    val disabledCheckedTrackColor: Color,
    val disabledCheckedTrackBorderColor: Color,
    val disabledUncheckedThumbColor: Color,
    val disabledUncheckedThumbIconColor: Color,
    val disabledUncheckedTrackColor: Color,
    val disabledUncheckedTrackBorderColor: Color,
) {
    @Composable
    internal fun thumbColor(enabled: Boolean, checked: Boolean): State<Color> =
        animateSelectionColor(
            enabled = enabled,
            checked = checked,
            checkedColor = checkedThumbColor,
            uncheckedColor = uncheckedThumbColor,
            disabledCheckedColor = disabledCheckedThumbColor,
            disabledUncheckedColor = disabledUncheckedThumbColor,
            animationSpec = COLOR_ANIMATION_SPEC
        )

    @Composable
    internal fun thumbIconColor(enabled: Boolean, checked: Boolean): State<Color> =
        animateSelectionColor(
            enabled = enabled,
            checked = checked,
            checkedColor = checkedThumbIconColor,
            uncheckedColor = uncheckedThumbIconColor,
            disabledCheckedColor = disabledCheckedThumbIconColor,
            disabledUncheckedColor = disabledUncheckedThumbIconColor,
            animationSpec = COLOR_ANIMATION_SPEC
        )

    @Composable
    internal fun trackColor(enabled: Boolean, checked: Boolean): State<Color> =
        animateSelectionColor(
            enabled = enabled,
            checked = checked,
            checkedColor = checkedTrackColor,
            uncheckedColor = uncheckedTrackColor,
            disabledCheckedColor = disabledCheckedTrackBorderColor,
            disabledUncheckedColor = disabledUncheckedTrackColor,
            animationSpec = COLOR_ANIMATION_SPEC
        )

    @Composable
    internal fun trackStrokeColor(enabled: Boolean, checked: Boolean): State<Color> =
        animateSelectionColor(
            enabled = enabled,
            checked = checked,
            checkedColor = checkedTrackBorderColor,
            uncheckedColor = uncheckedTrackBorderColor,
            disabledCheckedColor = disabledCheckedTrackColor,
            disabledUncheckedColor = disabledUncheckedTrackBorderColor,
            animationSpec = COLOR_ANIMATION_SPEC
        )

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other == null || other !is SwitchColors) return false

        if (checkedThumbColor != other.checkedThumbColor) return false
        if (checkedThumbIconColor != other.checkedThumbIconColor) return false
        if (checkedTrackColor != other.checkedTrackColor) return false
        if (checkedTrackBorderColor != other.checkedTrackBorderColor) return false
        if (uncheckedThumbColor != other.uncheckedThumbColor) return false
        if (uncheckedThumbIconColor != other.uncheckedThumbIconColor) return false
        if (uncheckedTrackColor != other.uncheckedTrackColor) return false
        if (uncheckedTrackBorderColor != other.uncheckedTrackBorderColor) return false
        if (disabledCheckedThumbColor != other.disabledCheckedThumbColor) return false
        if (disabledCheckedThumbIconColor != other.disabledCheckedThumbIconColor) return false
        if (disabledCheckedTrackColor != other.disabledCheckedTrackColor) return false
        if (disabledCheckedTrackBorderColor != other.disabledCheckedTrackBorderColor) return false
        if (disabledUncheckedThumbColor != other.disabledUncheckedThumbColor) return false
        if (disabledUncheckedThumbIconColor != other.disabledUncheckedThumbIconColor) return false
        if (disabledUncheckedTrackColor != other.disabledUncheckedTrackColor) return false
        if (disabledUncheckedTrackBorderColor != other.disabledCheckedTrackBorderColor) return false

        return true
    }

    override fun hashCode(): Int {
        var result = checkedThumbColor.hashCode()
        result = 31 * result + checkedThumbIconColor.hashCode()
        result = 31 * result + checkedTrackColor.hashCode()
        result = 31 * result + checkedTrackBorderColor.hashCode()
        result = 31 * result + uncheckedThumbColor.hashCode()
        result = 31 * result + uncheckedThumbIconColor.hashCode()
        result = 31 * result + uncheckedTrackColor.hashCode()
        result = 31 * result + uncheckedTrackBorderColor.hashCode()
        result = 31 * result + disabledCheckedThumbColor.hashCode()
        result = 31 * result + disabledCheckedThumbIconColor.hashCode()
        result = 31 * result + disabledCheckedTrackColor.hashCode()
        result = 31 * result + disabledCheckedTrackBorderColor.hashCode()
        result = 31 * result + disabledUncheckedThumbColor.hashCode()
        result = 31 * result + disabledUncheckedThumbIconColor.hashCode()
        result = 31 * result + disabledUncheckedTrackColor.hashCode()
        result = 31 * result + disabledUncheckedTrackBorderColor.hashCode()
        return result
    }
}

/**
 * Contains the default values used by [Checkbox].
 */
object CheckboxDefaults {
    /**
     * Creates a [CheckboxColors] for use in a [Checkbox].
     *
     * @param checkedBoxColor The box color of this [Checkbox] when enabled and checked.
     * @param checkedCheckmarkColor The check mark color of this [Checkbox] when enabled
     * and checked.
     * @param uncheckedBoxColor The box color of this [Checkbox] when enabled and unchecked.
     * @param uncheckedCheckmarkColor The check mark color of this [Checkbox] when enabled
     * and unchecked.
     */
    @Composable
    fun colors(
        checkedBoxColor: Color = MaterialTheme.colorScheme.primary,
        checkedCheckmarkColor: Color = MaterialTheme.colorScheme.onPrimary,
        uncheckedBoxColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
        uncheckedCheckmarkColor: Color = MaterialTheme.colorScheme.background,
    ): CheckboxColors = CheckboxColors(
        checkedBoxColor = checkedBoxColor,
        checkedCheckmarkColor = checkedCheckmarkColor,
        uncheckedBoxColor = uncheckedBoxColor,
        uncheckedCheckmarkColor = uncheckedCheckmarkColor,
        disabledCheckedBoxColor = checkedBoxColor.toDisabledColor(),
        disabledCheckedCheckmarkColor = checkedCheckmarkColor.toDisabledColor(),
        disabledUncheckedBoxColor = uncheckedBoxColor.toDisabledColor(),
        disabledUncheckedCheckmarkColor = uncheckedCheckmarkColor.toDisabledColor()
    )
}

/**
 * Contains the default values used by [Switch].
 */
object SwitchDefaults {
    /**
     * Creates a [SwitchColors] for use in a [Switch].
     *
     * @param checkedThumbColor The thumb color of this [Switch] when enabled and checked.
     * @param checkedThumbIconColor The thumb icon color of this [Switch] when enabled and checked.
     * @param checkedTrackColor The track color of this [Switch] when enabled and checked.
     * @param checkedTrackBorderColor The track border color of this [Switch] when enabled and checked.
     * @param uncheckedThumbColor The thumb color of this [Switch] when enabled and unchecked.
     * @param uncheckedThumbIconColor The thumb icon color of this [Switch] when enabled and checked.
     * @param uncheckedTrackColor The track color of this [Switch] when enabled and unchecked.
     * @param uncheckedTrackBorderColor The track border color of this [Switch] when enabled and unchecked.
     */
    @Composable
    fun colors(
        checkedThumbColor: Color = MaterialTheme.colorScheme.onPrimary,
        checkedThumbIconColor: Color = MaterialTheme.colorScheme.primary,
        checkedTrackColor: Color = MaterialTheme.colorScheme.primaryDim,
        checkedTrackBorderColor: Color = MaterialTheme.colorScheme.primaryDim,
        uncheckedThumbColor: Color = MaterialTheme.colorScheme.outline,
        uncheckedThumbIconColor: Color = MaterialTheme.colorScheme.background,
        uncheckedTrackColor: Color = MaterialTheme.colorScheme.surface,
        uncheckedTrackBorderColor: Color = MaterialTheme.colorScheme.outline
    ): SwitchColors = SwitchColors(
        checkedThumbColor = checkedThumbColor,
        checkedThumbIconColor = checkedThumbIconColor,
        checkedTrackColor = checkedTrackColor,
        checkedTrackBorderColor = checkedTrackBorderColor,
        uncheckedThumbColor = uncheckedThumbColor,
        uncheckedThumbIconColor = uncheckedThumbIconColor,
        uncheckedTrackColor = uncheckedTrackColor,
        uncheckedTrackBorderColor = uncheckedTrackBorderColor,
        disabledCheckedThumbColor = checkedThumbColor.toDisabledColor(),
        disabledCheckedThumbIconColor = checkedThumbIconColor.toDisabledColor(),
        disabledCheckedTrackColor = checkedTrackColor.toDisabledColor(
            disabledAlpha = DisabledContainerAlpha
        ),
        disabledCheckedTrackBorderColor = checkedTrackBorderColor.toDisabledColor(
            disabledAlpha = DisabledBorderAlpha
        ),
        disabledUncheckedThumbColor = uncheckedThumbColor.toDisabledColor(),
        disabledUncheckedThumbIconColor = uncheckedThumbIconColor.toDisabledColor(),
        disabledUncheckedTrackColor = uncheckedTrackColor.toDisabledColor(
            disabledAlpha = DisabledContainerAlpha
        ),
        disabledUncheckedTrackBorderColor = uncheckedTrackBorderColor.toDisabledColor(
            disabledAlpha = DisabledBorderAlpha
        )
    )
}

private fun DrawScope.drawBox(color: Color, progress: Float, isRtl: Boolean) {
    // Centering vertically.
    val topCornerPx = (HEIGHT - BOX_SIZE).toPx() / 2
    val strokeWidthPx = BOX_STROKE.toPx()
    val halfStrokeWidthPx = strokeWidthPx / 2.0f
    val radiusPx = BOX_RADIUS.toPx()
    val checkboxSizePx = BOX_SIZE.toPx()
    // Aligning the box to the end.
    val startXOffsetPx = if (isRtl) 0f else (WIDTH - HEIGHT).toPx()

    // Draw the outline of the box.
    drawRoundRect(
        color,
        topLeft = Offset(
            topCornerPx + halfStrokeWidthPx + startXOffsetPx,
            topCornerPx + halfStrokeWidthPx
        ),
        size = Size(checkboxSizePx - strokeWidthPx, checkboxSizePx - strokeWidthPx),
        cornerRadius = CornerRadius(radiusPx - halfStrokeWidthPx),
        alpha = 1 - progress,
        style = Stroke(strokeWidthPx)
    )

    // Fills the box.
    drawRoundRect(
        color,
        topLeft = Offset(topCornerPx + startXOffsetPx, topCornerPx),
        size = Size(checkboxSizePx, checkboxSizePx),
        cornerRadius = CornerRadius(radiusPx),
        alpha = progress,
        style = Fill
    )
}

private fun DrawScope.drawThumb(
    thumbColor: Color,
    progress: Float,
    thumbIconColor: Color,
    isRtl: Boolean
) {

    val thumbPaddingUnchecked = TRACK_HEIGHT / 2 - THUMB_RADIUS_UNCHECKED
    val thumbPaddingChecked = TRACK_HEIGHT / 2 - THUMB_RADIUS_CHECKED

    val switchThumbRadiusPx = lerp(
        start = THUMB_RADIUS_UNCHECKED.toPx(),
        stop = THUMB_RADIUS_CHECKED.toPx(),
        fraction = progress
    )

    val switchTrackLengthPx = WIDTH.toPx()

    // For Rtl mode the thumb progress will start from the end of the switch.
    val thumbProgressPx = if (isRtl)
        lerp(
            start = switchTrackLengthPx - switchThumbRadiusPx - thumbPaddingUnchecked.toPx(),
            stop = switchThumbRadiusPx + thumbPaddingChecked.toPx(),
            fraction = progress
        )
    else
        lerp(
            start = switchThumbRadiusPx + thumbPaddingUnchecked.toPx(),
            stop = switchTrackLengthPx - switchThumbRadiusPx - thumbPaddingChecked.toPx(),
            fraction = progress
        )

    drawCircle(
        color = thumbColor,
        radius = switchThumbRadiusPx,
        center = Offset(thumbProgressPx, center.y)
    )

    val totalDist = switchTrackLengthPx - 2 * switchThumbRadiusPx - 4.dp.toPx()

    // Offset value to be added if RTL mode is enabled.
    // We need to move the tick to the checked position in ltr mode when unchecked.
    val rtlOffset = switchTrackLengthPx - 2 * THUMB_RADIUS_CHECKED.toPx() - 4.dp.toPx()

    val distMoved = if (isRtl) rtlOffset - progress * totalDist else progress * totalDist

    // Draw tick icon
    drawTickIcon(thumbIconColor, progress, distMoved)
}

private fun DrawScope.drawTickIcon(tickColor: Color, alpha: Float, distMoved: Float) {
    val tickBaseLength = TICK_BASE_LENGTH.toPx()
    val tickStickLength = TICK_STICK_LENGTH.toPx()
    val tickTotalLength = tickBaseLength + tickStickLength
    val center = Offset(9.dp.toPx(), 9.dp.toPx())
    val angle = TICK_ROTATION - TICK_ROTATION / tickTotalLength * tickTotalLength
    val angleRadians = angle.toRadians()

    val baseStart = Offset(6.7f.dp.toPx() + distMoved, 13.3f.dp.toPx())

    val path = Path()
    path.moveTo(baseStart.rotate(angleRadians, center))
    path.lineTo(
        (baseStart + Offset(tickBaseLength, tickBaseLength)).rotate(angleRadians, center)
    )

    val stickStart = Offset(9.3f.dp.toPx() + distMoved, 16.3f.dp.toPx())
    // Move back to the start of the stick (without drawing)
    path.moveTo(stickStart.rotate(angleRadians, center))
    path.lineTo(
        Offset(stickStart.x + tickStickLength, stickStart.y - tickStickLength).rotate(
            angleRadians,
            center
        )
    )
    // Use StrokeCap.Butt because Square adds an extension on the end of each line.
    drawPath(
        path,
        tickColor,
        style = Stroke(width = 1.dp.toPx(), cap = StrokeCap.Butt),
        alpha = alpha
    )
}

private fun Path.moveTo(offset: Offset) {
    moveTo(offset.x, offset.y)
}

private fun Path.lineTo(offset: Offset) {
    lineTo(offset.x, offset.y)
}

private fun Offset.rotate(angleRadians: Float): Offset {
    val angledDirection = directionVector(angleRadians)
    return angledDirection * x + angledDirection.rotate90() * y
}

private fun Offset.rotate(angleRadians: Float, center: Offset): Offset =
    (this - center).rotate(angleRadians) + center

private fun Offset.rotate90() = Offset(-y, x)

private val BOX_STROKE = 2.dp
private val BOX_RADIUS = 2.dp
private val BOX_SIZE = 18.dp

private val THUMB_RADIUS_UNCHECKED = 7.dp
private val THUMB_RADIUS_CHECKED = 9.dp
private val TRACK_WIDTH = 32.dp
private val TRACK_HEIGHT = 22.dp
private val TICK_BASE_LENGTH = 3.dp
private val TICK_STICK_LENGTH = 7.dp
private const val TICK_ROTATION = 15f

private val COLOR_ANIMATION_SPEC: AnimationSpec<Color> =
    tween(MotionTokens.DurationMedium1, 0, MotionTokens.EasingStandardDecelerate)
private val PROGRESS_ANIMATION_SPEC: TweenSpec<Float> =
    tween(MotionTokens.DurationMedium1, 0, MotionTokens.EasingStandardDecelerate)
private val SWITCH_PROGRESS_ANIMATION_SPEC: TweenSpec<Float> =
    tween(MotionTokens.DurationMedium2, 0, MotionTokens.EasingStandardDecelerate)

private val WIDTH = 32.dp
private val HEIGHT = 24.dp