
net.peanuuutz.fork.ui.preset.Switch.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of fork-ui Show documentation
Show all versions of fork-ui Show documentation
Comprehensive API designed for Minecraft modders
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 kotlinx.coroutines.launch
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.draw.painter.LayeredPainter
import net.peanuuutz.fork.ui.foundation.input.AxisAnchoredDragState
import net.peanuuutz.fork.ui.foundation.input.AxisAnchors
import net.peanuuutz.fork.ui.foundation.input.LocalVisualIndication
import net.peanuuutz.fork.ui.foundation.input.axisAnchoredDraggable
import net.peanuuutz.fork.ui.foundation.input.focusable
import net.peanuuutz.fork.ui.foundation.input.hoverable
import net.peanuuutz.fork.ui.foundation.input.interaction.MutableInteractionSource
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.rememberAxisAnchoredDragState
import net.peanuuutz.fork.ui.foundation.input.toggleable
import net.peanuuutz.fork.ui.foundation.input.visualIndication
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.minSize
import net.peanuuutz.fork.ui.foundation.layout.offset
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.draw.Painter
import net.peanuuutz.fork.ui.ui.layout.Alignment
import net.peanuuutz.fork.ui.ui.layout.LayoutDirection.Right
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.node.LayoutCallbackModifierNode
import net.peanuuutz.fork.ui.ui.node.LayoutInfo
import net.peanuuutz.fork.ui.ui.node.ModifierNode
import net.peanuuutz.fork.ui.ui.unit.IntOffset
import net.peanuuutz.fork.ui.ui.unit.IntSize
import net.peanuuutz.fork.util.common.Color
// TODO Add LayoutDirection
@Composable
fun Switch(
isChecked: Boolean,
onValueChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
thumbStyle: SliderThumbStyle = Theme.switchThumb,
thumbPadding: PaddingValues = SwitchDefaults.ThumbPadding,
thumbSizer: ThumbSizer = SwitchDefaults.Horizontal.DefaultThumbSizer,
trackStyle: SliderTrackStyle = Theme.switchTrack
) {
Box(
modifier = modifier.minSize(SwitchDefaults.Horizontal.MinSize),
propagateMinConstraints = true
) {
val dragState = rememberAxisAnchoredDragState(isChecked).apply {
updateCandidatePredicate { candidate ->
onValueChanged(candidate)
true
}
updateThresholds(DefaultThresholds)
}
LaunchedEffect(isChecked) {
if (isChecked != dragState.targetValue) {
dragState.animateTo(isChecked)
}
}
SwitchTrack(
dragState = dragState,
onValueChanged = onValueChanged,
isEnabled = isEnabled,
interactionSource = interactionSource,
trackStyle = trackStyle,
modifier = Modifier
)
SwitchThumb(
dragState = dragState,
isEnabled = isEnabled,
interactionSource = interactionSource,
thumbStyle = thumbStyle,
thumbPadding = thumbPadding,
thumbSizer = thumbSizer,
modifier = Modifier
)
}
}
object SwitchDefaults {
val ThumbPadding: PaddingValues = NoPadding
object Horizontal {
val MinSize: IntSize = IntSize(12, 8)
val DefaultSize: IntSize = IntSize(20, 10)
val DefaultThumbSizer: ThumbSizer = object : ThumbSizer {
override fun calculateThumbSize(availableSpace: IntSize): IntSize {
return IntSize(
width = 8,
height = availableSpace.height + 4
)
}
override fun toString(): String {
return "SwitchDefaults.Horizontal.DefaultThumbSizer"
}
}
}
object Vertical {
val MinSize: IntSize = IntSize(8, 12)
val DefaultSize: IntSize = IntSize(10, 20)
val DefaultThumbSizer: ThumbSizer = object : ThumbSizer {
override fun calculateThumbSize(availableSpace: IntSize): IntSize {
return IntSize(
width = availableSpace.width + 4,
height = 8
)
}
override fun toString(): String {
return "SwitchDefaults.Vertical.DefaultThumbSizer"
}
}
}
}
val Theme.switchTrack: SliderTrackStyle
@ReadOnlyComposable
@Composable
get() = LocalSwitchTrack.current
val Theme.switchThumb: SliderThumbStyle
@ReadOnlyComposable
@Composable
get() = LocalSwitchThumb.current
@NonRestartableComposable
@Composable
fun SwitchStyleProvider(
thumbStyle: SliderThumbStyle,
trackStyle: SliderTrackStyle,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalSwitchThumb provides thumbStyle,
LocalSwitchTrack provides trackStyle,
content = content
)
}
@Stable
class DefaultSwitchTrackStyle(
val border: BorderStroke,
val background: Painter,
val checkedBorder: BorderStroke,
val checkedBackground: Painter,
val selectedBorder: BorderStroke,
val selectedBackground: Painter,
val selectedCheckedBorder: BorderStroke,
val selectedCheckedBackground: Painter,
val disabledBorder: BorderStroke,
val disabledBackground: Painter,
val disabledCheckedBorder: BorderStroke,
val disabledCheckedBackground: Painter
) : SliderTrackStyle {
@Composable
override fun border(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
val borderStroke = when {
!isEnabled -> if (fraction != 1.0f) disabledBorder else disabledCheckedBorder
!isSelected -> {
if (border == checkedBorder) {
border
} else {
BorderStroke.lerp(
from = border,
to = checkedBorder,
fraction = fraction
)
}
}
else -> {
if (selectedBorder == selectedCheckedBorder) {
selectedBorder
} else {
BorderStroke.lerp(
from = selectedBorder,
to = selectedCheckedBorder,
fraction = fraction
)
}
}
}
return rememberUpdatedState(borderStroke)
}
@Composable
override fun background(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
val painter = when {
!isEnabled -> if (fraction != 1.0f) disabledBackground else disabledCheckedBackground
!isSelected -> {
if (background == checkedBackground) {
background
} else {
LayeredPainter.lerp(
from = background,
to = checkedBackground,
fraction = fraction
)
}
}
else -> {
if (selectedBackground == selectedCheckedBackground) {
selectedBackground
} else {
LayeredPainter.lerp(
from = selectedBackground,
to = selectedCheckedBackground,
fraction = fraction
)
}
}
}
return rememberUpdatedState(painter)
}
@Composable
override fun content(isEnabled: Boolean, fraction: Float): State {
// Switch track doesn't have content
return rememberUpdatedState(Color.Transparent)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DefaultSwitchTrackStyle) return false
if (border != other.border) return false
if (background != other.background) return false
if (checkedBorder != other.checkedBorder) return false
if (checkedBackground != other.checkedBackground) return false
if (selectedBorder != other.selectedBorder) return false
if (selectedBackground != other.selectedBackground) return false
if (selectedCheckedBorder != other.selectedCheckedBorder) return false
if (selectedCheckedBackground != other.selectedCheckedBackground) return false
if (disabledBorder != other.disabledBorder) return false
if (disabledBackground != other.disabledBackground) return false
if (disabledCheckedBorder != other.disabledCheckedBorder) return false
if (disabledCheckedBackground != other.disabledCheckedBackground) return false
return true
}
override fun hashCode(): Int {
var result = border.hashCode()
result = 31 * result + background.hashCode()
result = 31 * result + checkedBorder.hashCode()
result = 31 * result + checkedBackground.hashCode()
result = 31 * result + selectedBorder.hashCode()
result = 31 * result + selectedBackground.hashCode()
result = 31 * result + selectedCheckedBorder.hashCode()
result = 31 * result + selectedCheckedBackground.hashCode()
result = 31 * result + disabledBorder.hashCode()
result = 31 * result + disabledBackground.hashCode()
result = 31 * result + disabledCheckedBorder.hashCode()
result = 31 * result + disabledCheckedBackground.hashCode()
return result
}
override fun toString(): String {
return "DefaultSwitchTrackStyle(border=$border, background=$background, " +
"checkedBorder=$checkedBorder, checkedBackground=$checkedBackground, " +
"selectedBorder=$selectedBorder, selectedBackground=$selectedBackground, " +
"selectedCheckedBorder=$selectedCheckedBorder, selectedCheckedBackground=$selectedCheckedBackground, " +
"disabledBorder=$disabledBorder, disabledBackground=$disabledBackground, " +
"disabledCheckedBorder=$disabledCheckedBorder, disabledCheckedBackground=$disabledCheckedBackground)"
}
}
@Stable
class DefaultSwitchThumbStyle(
val border: BorderStroke,
val background: Painter,
val checkedBorder: BorderStroke,
val checkedBackground: Painter,
val selectedBorder: BorderStroke,
val selectedBackground: Painter,
val selectedCheckedBorder: BorderStroke,
val selectedCheckedBackground: Painter,
val disabledBorder: BorderStroke,
val disabledBackground: Painter,
val disabledCheckedBorder: BorderStroke,
val disabledCheckedBackground: Painter
) : SliderThumbStyle {
@Composable
override fun border(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
val borderStroke = when {
!isEnabled -> if (fraction != 1.0f) disabledBorder else disabledCheckedBorder
!isSelected -> {
if (border == checkedBorder) {
border
} else {
BorderStroke.lerp(
from = border,
to = checkedBorder,
fraction = fraction
)
}
}
else -> {
if (selectedBorder == selectedCheckedBorder) {
selectedBorder
} else {
BorderStroke.lerp(
from = selectedBorder,
to = selectedCheckedBorder,
fraction = fraction
)
}
}
}
return rememberUpdatedState(borderStroke)
}
@Composable
override fun background(isEnabled: Boolean, isSelected: Boolean, fraction: Float): State {
val painter = when {
!isEnabled -> if (fraction != 1.0f) disabledBackground else disabledCheckedBackground
!isSelected -> {
if (background == checkedBackground) {
background
} else {
LayeredPainter.lerp(
from = background,
to = checkedBackground,
fraction = fraction
)
}
}
else -> {
if (selectedBackground == selectedCheckedBackground) {
selectedBackground
} else {
LayeredPainter.lerp(
from = selectedBackground,
to = selectedCheckedBackground,
fraction = fraction
)
}
}
}
return rememberUpdatedState(painter)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DefaultSwitchThumbStyle) return false
if (border != other.border) return false
if (background != other.background) return false
if (checkedBorder != other.checkedBorder) return false
if (checkedBackground != other.checkedBackground) return false
if (disabledBorder != other.disabledBorder) return false
if (disabledBackground != other.disabledBackground) return false
if (disabledCheckedBorder != other.disabledCheckedBorder) return false
if (disabledCheckedBackground != other.disabledCheckedBackground) return false
return true
}
override fun hashCode(): Int {
var result = border.hashCode()
result = 31 * result + background.hashCode()
result = 31 * result + checkedBorder.hashCode()
result = 31 * result + checkedBackground.hashCode()
result = 31 * result + disabledBorder.hashCode()
result = 31 * result + disabledBackground.hashCode()
result = 31 * result + disabledCheckedBorder.hashCode()
result = 31 * result + disabledCheckedBackground.hashCode()
return result
}
override fun toString(): String {
return "DefaultSwitchThumbStyle(border=$border, background=$background, " +
"checkedBorder=$checkedBorder, checkedBackground=$checkedBackground, " +
"disabledBorder=$disabledBorder, disabledBackground=$disabledBackground, " +
"disabledCheckedBorder=$disabledCheckedBorder, disabledCheckedBackground=$disabledCheckedBackground)"
}
}
// ======== Internal ========
private val AnchorsPlaceholder: AxisAnchors = mapOf(0.0f to false)
private val OnThumbSizeChanged: (IntSize) -> Unit = {}
// -------- Composables --------
@Composable
private fun BoxScope.SwitchTrack(
dragState: AxisAnchoredDragState,
onValueChanged: (Boolean) -> Unit,
isEnabled: Boolean,
interactionSource: MutableInteractionSource,
trackStyle: SliderTrackStyle,
modifier: Modifier
) {
val fraction = dragState.fraction
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
)
Box(
modifier = modifier
.matchParentSize()
.conditional(isEnabled) {
this
.toggleable(
interactionSource = interactionSource,
label = TrackThumbInteractions.Thumb
) {
onValueChanged(dragState.targetValue.not())
}
.hoverable(
interactionSource = interactionSource,
label = TrackThumbInteractions.Thumb
)
.focusable(
interactionSource = interactionSource,
label = TrackThumbInteractions.Track
)
}
.border(border)
.background(background)
)
}
@Composable
private fun BoxScope.SwitchThumb(
dragState: AxisAnchoredDragState,
isEnabled: Boolean,
interactionSource: MutableInteractionSource,
thumbStyle: SliderThumbStyle,
thumbPadding: PaddingValues,
thumbSizer: ThumbSizer,
modifier: Modifier
) {
val fraction = dragState.fraction
val isHovered by interactionSource.collectHoverState(TrackThumbInteractions.Thumb)
val isSelected = isHovered
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
.matchParentSize()
.wrapContentSize(Alignment.CenterLeft)
.padding(thumbPadding)
.offset { IntOffset(dragState.roundedOffset, 0) }
.thumbSizer(
thumbSizeUpdater = OnThumbSizeChanged,
sizer = thumbSizer
)
.switchAnchorsCalculator(
dragState = dragState,
thumbPadding = thumbPadding
)
.conditional(isEnabled) {
this
.hoverable(
interactionSource = interactionSource,
label = TrackThumbInteractions.Thumb
)
.axisAnchoredDraggable(
state = dragState,
interactionSource = interactionSource,
label = TrackThumbInteractions.Thumb
)
.visualIndication(
interactionSource = interactionSource,
indication = LocalVisualIndication.current,
label = TrackThumbInteractions.Thumb
)
}
.border(border)
.background(background)
)
}
// -------- Switch Anchors Calculator Modifier --------
private fun Modifier.switchAnchorsCalculator(
dragState: AxisAnchoredDragState,
thumbPadding: PaddingValues
): Modifier {
val element = SwitchAnchorsCalculatorModifier(
dragState = dragState,
thumbPadding = thumbPadding
)
return this then element
}
private data class SwitchAnchorsCalculatorModifier(
val dragState: AxisAnchoredDragState,
val thumbPadding: PaddingValues
) : ModifierNodeElement() {
override fun create(): SwitchAnchorsCalculatorModifierNode {
return SwitchAnchorsCalculatorModifierNode(
dragState = dragState,
thumbPadding = thumbPadding
)
}
override fun update(node: SwitchAnchorsCalculatorModifierNode) {
node.thumbPadding = thumbPadding
}
override fun InspectInfo.inspect() {
set("thumbPadding", thumbPadding)
}
}
private class SwitchAnchorsCalculatorModifierNode(
val dragState: AxisAnchoredDragState,
thumbPadding: PaddingValues
) : ModifierNode(), LayoutCallbackModifierNode {
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 intMaxOffset = calculateMaxDragOffset(
thumbInfo = info,
direction = Right,
thumbPadding = thumbPadding
)
val maxOffset = intMaxOffset.toFloat()
val anchors = mapOf(
0.0f to false,
maxOffset to true
)
nodeScope.launch {
dragState.updateAnchors(anchors)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy