• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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