1 /*
<lambda>null2 * Copyright (C) 2025 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 @file:OptIn(ExperimentalMaterial3Api::class)
18
19 package com.android.mechanics.demo.tuneable
20
21 import androidx.compose.foundation.clickable
22 import androidx.compose.foundation.interaction.MutableInteractionSource
23 import androidx.compose.foundation.layout.Row
24 import androidx.compose.foundation.layout.fillMaxWidth
25 import androidx.compose.foundation.layout.padding
26 import androidx.compose.foundation.layout.wrapContentWidth
27 import androidx.compose.foundation.selection.selectable
28 import androidx.compose.material3.DropdownMenuItem
29 import androidx.compose.material3.ExperimentalMaterial3Api
30 import androidx.compose.material3.ExposedDropdownMenuBox
31 import androidx.compose.material3.ExposedDropdownMenuDefaults
32 import androidx.compose.material3.Label
33 import androidx.compose.material3.MaterialTheme
34 import androidx.compose.material3.PlainTooltip
35 import androidx.compose.material3.RadioButton
36 import androidx.compose.material3.Slider
37 import androidx.compose.material3.SliderDefaults
38 import androidx.compose.material3.Text
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.getValue
41 import androidx.compose.runtime.mutableStateOf
42 import androidx.compose.runtime.remember
43 import androidx.compose.runtime.setValue
44 import androidx.compose.ui.Alignment
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.draw.clip
47 import androidx.compose.ui.semantics.Role
48 import androidx.compose.ui.text.style.TextAlign
49 import androidx.compose.ui.unit.Dp
50 import androidx.compose.ui.unit.dp
51 import kotlin.math.log10
52 import kotlin.math.pow
53 import kotlin.math.roundToInt
54
55 @Composable
56 fun SliderWithPreview(
57 value: Float,
58 onValueChange: (Float) -> Unit,
59 valueRange: ClosedFloatingPointRange<Float>,
60 render: (Float) -> String,
61 modifier: Modifier = Modifier,
62 normalize: (Float) -> Float = { it },
63 steps: Int = 0,
64 ) {
<lambda>null65 var sliderPosition by remember { mutableStateOf(value) }
<lambda>null66 val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
67 Slider(
68 value = sliderPosition,
<lambda>null69 onValueChangeFinished = { onValueChange(sliderPosition) },
<lambda>null70 onValueChange = { sliderPosition = normalize(it) },
71 valueRange = valueRange.start..valueRange.endInclusive,
72 interactionSource = interactionSource,
73 steps = steps,
<lambda>null74 thumb = {
75 Label(
76 label = {
77 PlainTooltip(modifier = Modifier.wrapContentWidth(unbounded = true)) {
78 Text(render(sliderPosition), textAlign = TextAlign.Center)
79 }
80 },
81 interactionSource = interactionSource,
82 ) {
83 SliderDefaults.Thumb(interactionSource = interactionSource)
84 }
85 },
86 modifier = modifier,
87 )
88 }
89
90 @Composable
DpSlidernull91 fun DpSlider(
92 value: Dp,
93 onValueChange: (Dp) -> Unit,
94 valueRange: ClosedRange<Dp>,
95 modifier: Modifier = Modifier,
96 steps: Int = 0,
97 ) {
98 var sliderPosition by remember { mutableStateOf(value.value) }
99 val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
100 Slider(
101 value = sliderPosition,
102 onValueChangeFinished = { onValueChange(sliderPosition.dp) },
103 onValueChange = { sliderPosition = it.roundToInt().toFloat() },
104 valueRange = valueRange.start.value..valueRange.endInclusive.value,
105 interactionSource = interactionSource,
106 steps = steps,
107 thumb = {
108 Label(
109 label = {
110 PlainTooltip(modifier = Modifier.wrapContentWidth(unbounded = true)) {
111 Text("${sliderPosition.toInt()}dp", textAlign = TextAlign.Center)
112 }
113 },
114 interactionSource = interactionSource,
115 ) {
116 SliderDefaults.Thumb(interactionSource = interactionSource)
117 }
118 },
119 modifier = modifier,
120 )
121 }
122
123 @Composable
LogarithmicSliderWithPreviewnull124 fun LogarithmicSliderWithPreview(
125 value: Float,
126 onValueChange: (Float) -> Unit,
127 valueRange: ClosedFloatingPointRange<Float>,
128 render: (Float) -> String,
129 modifier: Modifier = Modifier,
130 normalize: (Float) -> Float = { it },
131 steps: Int = 0,
132 ) {
133
134 SliderWithPreview(
135 value = log10(value),
<lambda>null136 onValueChange = { onValueChange(10f.pow(it)) },
137 valueRange = log10(valueRange.start)..log10(valueRange.endInclusive),
<lambda>null138 render = { render(10f.pow(it)) },
<lambda>null139 normalize = { log10(normalize(10f.pow(it))) },
140 steps = steps,
141 modifier = modifier,
142 )
143 }
144
145 @Composable
Dropdownnull146 fun <T> Dropdown(
147 value: T,
148 options: List<T>,
149 render: (T) -> String,
150 onChange: (T) -> Unit,
151 modifier: Modifier = Modifier,
152 ) {
153 var expanded by remember { mutableStateOf(false) }
154
155 ExposedDropdownMenuBox(
156 expanded = expanded,
157 onExpandedChange = { expanded = !expanded },
158 modifier = modifier.fillMaxWidth(),
159 ) {
160 Row(
161 verticalAlignment = Alignment.CenterVertically,
162 modifier = Modifier.fillMaxWidth().menuAnchor(),
163 ) {
164 Text(render(value), style = MaterialTheme.typography.labelMedium)
165 ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
166 }
167
168 ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
169 options.forEach { option ->
170 DropdownMenuItem(
171 text = { Text(render(option)) },
172 onClick = {
173 onChange(option)
174 expanded = false
175 },
176 contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
177 )
178 }
179 }
180 }
181 }
182
183 @Composable
LabelledCheckboxnull184 fun LabelledCheckbox(
185 label: String,
186 checked: Boolean,
187 onCheckedChange: (Boolean) -> Unit,
188 modifier: Modifier = Modifier,
189 ) {
190 Row(
191 modifier
192 .fillMaxWidth()
193 .clip(MaterialTheme.shapes.small)
194 .clickable(onClick = { onCheckedChange(!checked) }),
195 verticalAlignment = Alignment.CenterVertically,
196 ) {
197 androidx.compose.material3.Checkbox(checked, onCheckedChange)
198 Text(label, Modifier.padding(start = 8.dp))
199 }
200 }
201
202 @Composable
LabelledRadioButtonnull203 fun LabelledRadioButton(
204 label: String,
205 isSelected: Boolean,
206 onSelected: () -> Unit,
207 modifier: Modifier = Modifier,
208 ) {
209 Row(
210 modifier
211 .fillMaxWidth()
212 .clip(MaterialTheme.shapes.small)
213 .selectable(selected = isSelected, onClick = { onSelected() }, role = Role.RadioButton)
214 .padding(horizontal = 16.dp),
215 verticalAlignment = Alignment.CenterVertically,
216 ) {
217 RadioButton(
218 selected = isSelected,
219 onClick = null, // null recommended for accessibility with screenreaders
220 )
221 Text(text = label, modifier = Modifier.padding(start = 8.dp))
222 }
223 }
224