From b835b668de0bb3533cc2b1a8b3e0e5063faf937c Mon Sep 17 00:00:00 2001 From: Skeletonxf Date: Fri, 1 Aug 2025 16:17:41 +0100 Subject: [PATCH 1/2] Add in memory only presentation mode to toggle scale factor --- .../com/apadmi/mockzilla/desktop/i18n/En.kt | 5 +- .../apadmi/mockzilla/desktop/i18n/Strings.kt | 6 +- .../mockzilla/desktop/ui/theme/Theme.kt | 26 ++++- .../misccontrols/MiscControlsWidget.kt | 96 ++++++++++++++++++- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/En.kt b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/En.kt index 6419ebaa..ef520a9e 100644 --- a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/En.kt +++ b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/En.kt @@ -2,6 +2,7 @@ package com.apadmi.mockzilla.desktop.i18n import cafe.adriel.lyricist.LyricistStrings import io.ktor.http.HttpStatusCode +import kotlin.math.roundToInt @LyricistStrings(languageTag = "En", default = true) val EnStrings = Strings( @@ -128,7 +129,9 @@ val EnStrings = Strings( miscControls = Strings.Widgets.MiscControls( refreshAll = "Re-sync all", clearOverrides = "Reset all overrides", - title = "Tools" + title = "Tools", + presentationMode = "Presentation mode", + fontScaleLabel = { scale -> "${(scale * 100).roundToInt()}%" } ), unsupportedMockzilla = Strings.Widgets.UnsupportedMockzillaVersion( heading = "Unsupported SDK", diff --git a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/Strings.kt b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/Strings.kt index 43f1cb37..7c32ea72 100644 --- a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/Strings.kt +++ b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/i18n/Strings.kt @@ -96,11 +96,15 @@ data class Strings( * @property refreshAll * @property clearOverrides * @property title + * @property presentationMode + * @property fontScaleLabel */ data class MiscControls( val refreshAll: String, val clearOverrides: String, - val title: String + val title: String, + val presentationMode: String, + val fontScaleLabel: (Float) -> String, ) /** diff --git a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/theme/Theme.kt b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/theme/Theme.kt index a2ce88dd..9f45a1d5 100644 --- a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/theme/Theme.kt +++ b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/theme/Theme.kt @@ -10,6 +10,10 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.platform.LocalDensity @@ -86,6 +90,13 @@ private val darkColors = darkColorScheme( @Suppress("VARIABLE_NAME_INCORRECT_FORMAT") val LocalForceDarkMode = compositionLocalOf { false } +@Suppress("VARIABLE_NAME_INCORRECT_FORMAT") +val LocalSetScaleFactor = compositionLocalOf<(Float) -> Unit> { { /* noop */ } } + +data object ScaleFactor { + const val DEFAULT = 0.9F +} + @Composable fun Modifier.alternatingBackground(index: Int) = background( if (index % 2 == 0) { @@ -106,12 +117,17 @@ fun AppTheme( lightColors } + var scaleFactor by rememberSaveable { mutableFloatStateOf(ScaleFactor.DEFAULT) } ProvideLocalisableStrings { - ScaledDensity(scaleFactor = 0.9f) { - MaterialTheme( - colorScheme = colors, - content = content - ) + CompositionLocalProvider( + LocalSetScaleFactor provides { scale -> scaleFactor = scale }, + ) { + ScaledDensity(scaleFactor = scaleFactor) { + MaterialTheme( + colorScheme = colors, + content = content + ) + } } } } diff --git a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt index 834d8da8..de3a0fcb 100644 --- a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt +++ b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt @@ -1,15 +1,42 @@ package com.apadmi.mockzilla.desktop.ui.widgets.misccontrols +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Button +import androidx.compose.material3.Slider +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp import com.apadmi.mockzilla.desktop.di.utils.getViewModel import com.apadmi.mockzilla.desktop.engine.device.Device import com.apadmi.mockzilla.desktop.i18n.LocalStrings import com.apadmi.mockzilla.desktop.i18n.Strings +import com.apadmi.mockzilla.desktop.ui.theme.LocalSetScaleFactor +import com.apadmi.mockzilla.desktop.ui.theme.ScaleFactor import org.koin.core.parameter.parametersOf +private data object PresentationModeScaleFactor { + const val MIN = 0.8F + const val MAX = 1.4F + const val DEFAULT = 1.2F +} + @Composable fun MiscControlsWidget( device: Device? @@ -17,7 +44,7 @@ fun MiscControlsWidget( val viewModel = getViewModel(key = device?.toString()) { parametersOf(device) } MiscControlsWidgetContent( onRefreshAll = viewModel::refreshAllData, - onClearAllOverrides = viewModel::clearAllOverrides + onClearAllOverrides = viewModel::clearAllOverrides, ) } @@ -33,4 +60,71 @@ fun MiscControlsWidgetContent( Button(onClick = onClearAllOverrides) { Text(strings.widgets.miscControls.clearOverrides) } + var presentationMode by rememberSaveable { mutableStateOf(false) } + var presentationModeScaleFactor by rememberSaveable { + mutableFloatStateOf(PresentationModeScaleFactor.DEFAULT) + } + val setScaleFactor = LocalSetScaleFactor.current + PresentationModeSettings( + presentationMode = presentationMode, + onPresentationModeChange = { presentationModeEnabled -> + presentationMode = presentationModeEnabled + if (presentationModeEnabled) { + setScaleFactor(presentationModeScaleFactor) + } else { + setScaleFactor(ScaleFactor.DEFAULT) + } + }, + presentationModeScaleFactor = presentationModeScaleFactor, + onPresentationModeScaleFactorChange = { scaleFactor -> + setScaleFactor(scaleFactor) + presentationModeScaleFactor = scaleFactor + }, + ) +} + +@Composable +private fun PresentationModeSettings( + presentationMode: Boolean, + onPresentationModeChange: (Boolean) -> Unit, + presentationModeScaleFactor: Float, + onPresentationModeScaleFactorChange: (Float) -> Unit, + strings: Strings = LocalStrings.current +) = Column { + Row( + modifier = Modifier + .toggleable( + value = presentationMode, + onValueChange = { checked -> + onPresentationModeChange(checked) + }, + role = Role.Switch, + ) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = strings.widgets.miscControls.presentationMode, + ) + Spacer(modifier = Modifier.width(4.dp)) + Switch( + checked = presentationMode, + onCheckedChange = null, + ) + } + AnimatedVisibility(visible = presentationMode) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp), + ) { + Slider( + value = presentationModeScaleFactor, + onValueChange = { onPresentationModeScaleFactorChange(it) }, + steps = 5, + valueRange = PresentationModeScaleFactor.MIN..PresentationModeScaleFactor.MAX, + ) + Text(text = strings.widgets.miscControls.fontScaleLabel(presentationModeScaleFactor)) + } + } + Spacer(modifier = Modifier.height(4.dp)) } From a2df5360c6fb50b3a43dffbc6daa182a6d1cfaf5 Mon Sep 17 00:00:00 2001 From: Skeletonxf Date: Mon, 11 Aug 2025 14:17:54 +0100 Subject: [PATCH 2/2] Work around settings being lost on tab switches with in-memory cache --- .../widgets/misccontrols/MiscControlsWidget.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt index de3a0fcb..af6401a6 100644 --- a/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt +++ b/mockzilla-management-ui/src/commonMain/kotlin/com/apadmi/mockzilla/desktop/ui/widgets/misccontrols/MiscControlsWidget.kt @@ -35,6 +35,16 @@ private data object PresentationModeScaleFactor { const val MIN = 0.8F const val MAX = 1.4F const val DEFAULT = 1.2F + + // These variables are in-memory caches for the state that we + // may want to save to disk in the future. Without an in-memory + // cache the compose state would reset on tab switches since the + // entire tab tree UI is removed from the composition on tab switching + // These are not themselves mutable state tracked by compose so we must + // ensure we don't reference these values outside of the defaults + // for setting up rememberSaveable states. + var enabled = false + var scaleFactor = DEFAULT } @Composable @@ -60,15 +70,16 @@ fun MiscControlsWidgetContent( Button(onClick = onClearAllOverrides) { Text(strings.widgets.miscControls.clearOverrides) } - var presentationMode by rememberSaveable { mutableStateOf(false) } + var presentationMode by rememberSaveable { mutableStateOf(PresentationModeScaleFactor.enabled) } var presentationModeScaleFactor by rememberSaveable { - mutableFloatStateOf(PresentationModeScaleFactor.DEFAULT) + mutableFloatStateOf(PresentationModeScaleFactor.scaleFactor) } val setScaleFactor = LocalSetScaleFactor.current PresentationModeSettings( presentationMode = presentationMode, onPresentationModeChange = { presentationModeEnabled -> presentationMode = presentationModeEnabled + PresentationModeScaleFactor.enabled = presentationModeEnabled if (presentationModeEnabled) { setScaleFactor(presentationModeScaleFactor) } else { @@ -79,6 +90,7 @@ fun MiscControlsWidgetContent( onPresentationModeScaleFactorChange = { scaleFactor -> setScaleFactor(scaleFactor) presentationModeScaleFactor = scaleFactor + PresentationModeScaleFactor.scaleFactor = scaleFactor }, ) }