tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 0be5ba54d30a9da6db2039a9ba76529ffff7071a
parent 738aad16509302b669354d90ac832a543ccb9691
Author: RebecaTudor <rebecatudor273@gmail.com>
Date:   Mon, 29 Dec 2025 22:14:20 +0000

Bug 1975017 - Part 1 - Add compose slider. r=android-reviewers,gmalekpour

Differential Revision: https://phabricator.services.mozilla.com/D268664

Diffstat:
Amobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/FontSizeSlider.kt | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 252 insertions(+), 0 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/FontSizeSlider.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/FontSizeSlider.kt @@ -0,0 +1,252 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import mozilla.components.ui.colors.PhotonColors +import org.mozilla.fenix.R +import kotlin.math.roundToInt + +private const val HALF_ALPHA = 0.5F +private const val BASE_SAMPLE_TEXT_SIZE = 14 +private const val BASE_SAMPLE_HEIGHT_LINE_DIFFERENCE = 6 +private const val START_VALUE = 50 +private const val END_VALUE = 200 +private const val INCREASE_STEP = 5 + +/** + * The slider that changes websites font size. + * + * @param isEnabled Whether or not the slider can be used. + * @param value The current value of the slider. + * @param onValueChange Callback invoked continuously while the user moves the thumb. + * @param onValueChangeFinished Callback invoked once the dragging end. + */ +@Composable +fun FontSizePreference( + isEnabled: Boolean, + value: Float, + onValueChange: (Float) -> Unit, + onValueChangeFinished: () -> Unit, +) { + val alpha = if (isEnabled) 1f else HALF_ALPHA + // The values used to align with the top bar + val paddingFontSizeSection = PaddingValues(start = 72.dp, top = 16.dp, end = 16.dp, bottom = 16.dp) + + Column( + modifier = Modifier + .alpha(alpha) + .padding(paddingFontSizeSection), + ) { + Text( + text = stringResource(id = R.string.preference_accessibility_font_size_title), + style = MaterialTheme.typography.bodyLarge, + ) + + Text( + text = stringResource(id = R.string.preference_accessibility_text_size_summary), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + FontSizeSlider( + isEnabled = isEnabled, + value = value, + onValueChange = onValueChange, + onValueChangeFinished = onValueChangeFinished, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + SampleText(fontSize = value) + } +} + +@Composable +private fun SampleText(fontSize: Float) { + val textSize = (BASE_SAMPLE_TEXT_SIZE * (fontSize / 100f)) + + Box( + modifier = Modifier + .wrapContentSize() + .background(color = PhotonColors.Violet05) + .padding(16.dp), + ) { + Text( + text = stringResource(id = R.string.accessibility_text_size_sample_text_1), + style = MaterialTheme.typography.bodyMedium, + fontSize = textSize.sp, + lineHeight = (textSize + BASE_SAMPLE_HEIGHT_LINE_DIFFERENCE).sp, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun FontSizeSlider( + isEnabled: Boolean, + start: Int = START_VALUE, + end: Int = END_VALUE, + step: Int = INCREASE_STEP, + value: Float, + onValueChange: (Float) -> Unit, + onValueChangeFinished: () -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Slider( + value = value, + onValueChange = { newInput -> + // The slider input is continuous, but we want the value to change by 5. + val newSliderValue = newInput.snapToStep(start, step) + + onValueChange(newSliderValue.toFloat()) + }, + valueRange = start.toFloat()..end.toFloat(), + modifier = Modifier.weight(1f), + enabled = isEnabled, + onValueChangeFinished = onValueChangeFinished, + thumb = { Thumb(isEnabled) }, + track = { _ -> + // Calculate fraction of the slider that is active + val fraction by remember(value) { + derivedStateOf { + (value - start) / (end - start) + } + } + + Track(fraction, isEnabled) + }, + ) + + Text( + text = "${value.toInt()} %", + modifier = Modifier.padding(start = 8.dp), + ) + } +} + +/** + * Rounds the value to the nearest step. + */ +fun Float.snapToStep(start: Int, step: Int) = ((this - start) / step).roundToInt() * step + start + +/** + * Thumb is the draggable handle of the slider that user moves to change the value. + */ +@Composable +private fun Thumb(isEnabled: Boolean) { + if (isEnabled) { + Box( + modifier = Modifier + .padding(vertical = 6.dp) + .size(12.dp) + .background(MaterialTheme.colorScheme.primary, CircleShape), + ) + } else { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(vertical = 6.dp) + .size(8.dp) + .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape) + .padding(6.dp), + ) {} + } +} + +@Composable +private fun Track(fraction: Float, isEnabled: Boolean) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(2.dp) + .background( + MaterialTheme.colorScheme.surfaceContainerHighest, + RoundedCornerShape(12.dp), + ), + verticalAlignment = Alignment.CenterVertically, + ) { + FilledTrack(fraction = fraction, isEnabled = isEnabled) + } +} + +@Composable +private fun FilledTrack(fraction: Float, isEnabled: Boolean) { + val color = + if (isEnabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceContainerHighest + + Row( + modifier = Modifier + .fillMaxWidth(fraction) + .height(2.dp) + .background( + color = color, + shape = RoundedCornerShape(12.dp), + ), + ) {} +} + +@Preview +@Composable +private fun FontSizePreferencePreview() { + MaterialTheme { + Box(Modifier.background(MaterialTheme.colorScheme.surface)) { + FontSizePreference( + isEnabled = true, + value = 100f, + onValueChange = {}, + onValueChangeFinished = {}, + ) + } + } +} + +@Preview +@Composable +private fun FontSizePreferenceDisabledPreview() { + MaterialTheme { + Box(Modifier.background(MaterialTheme.colorScheme.surface)) { + FontSizePreference( + isEnabled = false, + value = 200f, + onValueChange = {}, + onValueChangeFinished = {}, + ) + } + } +}