Swap percent fields for DisplaySizeLevel enum end-to-end

The three v1 free sliders (UI scale, Card size, Spacing) become two
discrete enum-based controls (Card size, Spacing) and a new Text size
control, each selecting one of four DisplaySizeLevel values
(Extra small / Small / Default / Large). UI scale is dropped entirely;
the density override at the MainContent root goes with it.

Proto: replaces ui_scale_percent / card_size_percent / spacing_percent
with text_size_level / card_size_level / spacing_level on InterfacePreferences.
DisplaySizeLevel enum is ordered DEFAULT = 0 so proto-zero-default lands
new installs and any unset upgrader on DEFAULT for free; no upgrade-reset
block needed, no serializer default-setting needed.

AppPreference UiScale / CardSize / Spacing slider objects become three
AppChoicePreference<AppPreferences, DisplaySizeLevel> entries
(TextSize, CardSize, Spacing). DisplaySizeLevels.kt holds the user-facing
display order and the three per-control percent-mapping extensions
(textPercent / cardPercent / spacingPercent).

InterfaceCustomization stores the three level fields and exposes
cardSizePercent / spacingPercent as derived getters off the levels, so
every consumer of cardHeightDp / episodeCardHeightDp / spacingDp stays
unchanged.

DisplaySizePage keeps the preview infrastructure (sample-item viewmodel,
HomePageContent-based canvas, fake nav rail, two-column layout) and
swaps the slider column for three ChoicePreference selectors bound to
the new AppPreferences. The density-override wrap on the preview pane
is removed.

InterfaceHelpersTest withinBoundsOrDefault now exercises SkipForward
since CardSize / Spacing are no longer AppSliderPreference. The two
InterfaceCustomization scaling tests assert against cardSizeLevel = LARGE
(116%) and spacingLevel = LARGE (125%).
This commit is contained in:
Justin Visser 2026-05-28 11:26:27 +02:00
parent b6c4f0dd4f
commit c9433fbf57
10 changed files with 220 additions and 228 deletions

View file

@ -18,7 +18,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
@ -40,7 +39,6 @@ import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import com.github.damontecres.wholphin.ui.util.InterfaceCustomization
import com.github.damontecres.wholphin.ui.util.LocalInterfaceCustomization
import com.github.damontecres.wholphin.ui.util.scaledDensity
@Composable
fun MainContent(
@ -62,14 +60,8 @@ fun MainContent(
// setupNavigationManager.backStack = backStack
val interfaceCustomization =
remember(appPreferences) { InterfaceCustomization(appPreferences) }
val baseDensity = LocalDensity.current
val uiScaleDensity =
remember(baseDensity, interfaceCustomization.uiScalePercent) {
scaledDensity(baseDensity, interfaceCustomization.uiScalePercent)
}
CompositionLocalProvider(
LocalInterfaceCustomization provides interfaceCustomization,
LocalDensity provides uiScaleDensity,
) {
NavDisplay(
backStack = backStack,

View file

@ -504,46 +504,67 @@ sealed interface AppPreference<Pref, T> {
summaryOff = R.string.disabled,
)
val UiScale =
AppSliderPreference<AppPreferences>(
title = R.string.ui_scale,
defaultValue = 100,
min = 50,
max = 150,
interval = 5,
getter = { it.interfacePreferences.uiScalePercent.toLong() },
setter = { prefs, value ->
prefs.updateInterfacePreferences { uiScalePercent = value.toInt() }
val TextSize =
AppChoicePreference<AppPreferences, DisplaySizeLevel>(
title = R.string.text_size,
defaultValue = DisplaySizeLevel.DEFAULT,
displayValues = R.array.display_size_levels,
indexToValue = {
displaySizeLevelDisplayOrder.getOrElse(it) { DisplaySizeLevel.DEFAULT }
},
valueToIndex = {
displaySizeLevelDisplayOrder
.indexOf(it)
.let { idx ->
if (idx >= 0) idx else displaySizeLevelDisplayOrder.indexOf(DisplaySizeLevel.DEFAULT)
}
},
getter = { it.interfacePreferences.textSizeLevel },
setter = { prefs, value ->
prefs.updateInterfacePreferences { textSizeLevel = value }
},
summarizer = { it?.let { "$it%" } },
)
val CardSize =
AppSliderPreference<AppPreferences>(
AppChoicePreference<AppPreferences, DisplaySizeLevel>(
title = R.string.card_size,
defaultValue = 100,
min = 50,
max = 150,
interval = 5,
getter = { it.interfacePreferences.cardSizePercent.toLong() },
setter = { prefs, value ->
prefs.updateInterfacePreferences { cardSizePercent = value.toInt() }
defaultValue = DisplaySizeLevel.DEFAULT,
displayValues = R.array.display_size_levels,
indexToValue = {
displaySizeLevelDisplayOrder.getOrElse(it) { DisplaySizeLevel.DEFAULT }
},
valueToIndex = {
displaySizeLevelDisplayOrder
.indexOf(it)
.let { idx ->
if (idx >= 0) idx else displaySizeLevelDisplayOrder.indexOf(DisplaySizeLevel.DEFAULT)
}
},
getter = { it.interfacePreferences.cardSizeLevel },
setter = { prefs, value ->
prefs.updateInterfacePreferences { cardSizeLevel = value }
},
summarizer = { it?.let { "$it%" } },
)
val Spacing =
AppSliderPreference<AppPreferences>(
AppChoicePreference<AppPreferences, DisplaySizeLevel>(
title = R.string.spacing,
defaultValue = 100,
min = 25,
max = 175,
interval = 10,
getter = { it.interfacePreferences.spacingPercent.toLong() },
setter = { prefs, value ->
prefs.updateInterfacePreferences { spacingPercent = value.toInt() }
defaultValue = DisplaySizeLevel.DEFAULT,
displayValues = R.array.display_size_levels,
indexToValue = {
displaySizeLevelDisplayOrder.getOrElse(it) { DisplaySizeLevel.DEFAULT }
},
valueToIndex = {
displaySizeLevelDisplayOrder
.indexOf(it)
.let { idx ->
if (idx >= 0) idx else displaySizeLevelDisplayOrder.indexOf(DisplaySizeLevel.DEFAULT)
}
},
getter = { it.interfacePreferences.spacingLevel },
setter = { prefs, value ->
prefs.updateInterfacePreferences { spacingLevel = value }
},
summarizer = { it?.let { "$it%" } },
)
val DisplaySizePref =

View file

@ -100,9 +100,6 @@ class AppPreferencesSerializer
showLogos = AppPreference.ShowLogos.defaultValue
clearDisplayToggles()
addAllDisplayToggles(AppPreference.DisplayTogglesPref.defaultValue)
uiScalePercent = AppPreference.UiScale.defaultValue.toInt()
cardSizePercent = AppPreference.CardSize.defaultValue.toInt()
spacingPercent = AppPreference.Spacing.defaultValue.toInt()
searchPreferences =
SearchPreferences

View file

@ -0,0 +1,33 @@
package com.github.damontecres.wholphin.preferences
val displaySizeLevelDisplayOrder: List<DisplaySizeLevel> =
listOf(
DisplaySizeLevel.EXTRA_SMALL,
DisplaySizeLevel.SMALL,
DisplaySizeLevel.DEFAULT,
DisplaySizeLevel.LARGE,
)
fun DisplaySizeLevel.textPercent(): Int =
when (this) {
DisplaySizeLevel.EXTRA_SMALL -> 88
DisplaySizeLevel.SMALL -> 94
DisplaySizeLevel.LARGE -> 108
DisplaySizeLevel.DEFAULT, DisplaySizeLevel.UNRECOGNIZED -> 100
}
fun DisplaySizeLevel.cardPercent(): Int =
when (this) {
DisplaySizeLevel.EXTRA_SMALL -> 78
DisplaySizeLevel.SMALL -> 90
DisplaySizeLevel.LARGE -> 116
DisplaySizeLevel.DEFAULT, DisplaySizeLevel.UNRECOGNIZED -> 100
}
fun DisplaySizeLevel.spacingPercent(): Int =
when (this) {
DisplaySizeLevel.EXTRA_SMALL -> 75
DisplaySizeLevel.SMALL -> 88
DisplaySizeLevel.LARGE -> 125
DisplaySizeLevel.DEFAULT, DisplaySizeLevel.UNRECOGNIZED -> 100
}

View file

@ -350,15 +350,5 @@ class AppUpgradeHandler
}
}
}
if (previous.isEqualOrBefore(Version.fromString("0.6.4-31-g0"))) {
appPreferences.updateData {
it.updateInterfacePreferences {
uiScalePercent = AppPreference.UiScale.defaultValue.toInt()
cardSizePercent = AppPreference.CardSize.defaultValue.toInt()
spacingPercent = AppPreference.Spacing.defaultValue.toInt()
}
}
}
}
}

View file

@ -16,17 +16,13 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Star
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -40,37 +36,31 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.preferences.AppChoicePreference
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AppSliderPreference
import com.github.damontecres.wholphin.preferences.DisplaySizeLevel
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.ui.FontAwesome
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsListItem
import com.github.damontecres.wholphin.ui.main.settings.TitleText
import com.github.damontecres.wholphin.ui.main.settings.settingsWidth
import com.github.damontecres.wholphin.ui.preferences.ChoicePreference
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.SliderPreference
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.ResStringProvider
import com.github.damontecres.wholphin.ui.util.scaledDensity
import com.github.damontecres.wholphin.ui.util.withinBoundsOrDefault
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@ -98,8 +88,6 @@ private val PreviewNavRailIcons: List<ImageVector> =
Icons.Default.Search,
Icons.Default.Add,
Icons.Default.Star,
Icons.Default.PlayArrow,
Icons.Default.ArrowDropDown,
Icons.Default.Settings,
)
@ -163,31 +151,9 @@ fun DisplaySizePage(
val scope = rememberCoroutineScope()
val iface = preferences.interfacePreferences
val uiScale = iface.uiScalePercent.withinBoundsOrDefault(AppPreference.UiScale)
val cardSize = iface.cardSizePercent.withinBoundsOrDefault(AppPreference.CardSize)
val spacing = iface.spacingPercent.withinBoundsOrDefault(AppPreference.Spacing)
val posters by previewViewModel.posters.collectAsState()
val episodes by previewViewModel.episodes.collectAsState()
// Freezes the rail width and slider labels so they don't reflow under the user's finger
// while dragging UI scale.
val ambientDensity = LocalDensity.current
val pageDensity = remember { ambientDensity }
val context = LocalContext.current
val systemDensity =
remember {
val dm = context.resources.displayMetrics
Density(
density = dm.density,
fontScale = context.resources.configuration.fontScale,
)
}
val previewDensity =
remember(systemDensity, uiScale) { scaledDensity(systemDensity, uiScale) }
CompositionLocalProvider(LocalDensity provides pageDensity) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier.fillMaxSize(),
@ -201,38 +167,27 @@ fun DisplaySizePage(
.padding(8.dp),
) {
DisplaySizeSettingsColumn(
uiScale = uiScale,
cardSize = cardSize,
spacing = spacing,
onUiScaleChange = { value ->
textLevel = iface.textSizeLevel,
cardLevel = iface.cardSizeLevel,
spacingLevel = iface.spacingLevel,
onChangeTextLevel = { value ->
scope.launch {
viewModel.preferenceDataStore.updateData {
it.updateInterfacePreferences { uiScalePercent = value }
it.updateInterfacePreferences { textSizeLevel = value }
}
}
},
onCardSizeChange = { value ->
onChangeCardLevel = { value ->
scope.launch {
viewModel.preferenceDataStore.updateData {
it.updateInterfacePreferences { cardSizePercent = value }
it.updateInterfacePreferences { cardSizeLevel = value }
}
}
},
onSpacingChange = { value ->
onChangeSpacingLevel = { value ->
scope.launch {
viewModel.preferenceDataStore.updateData {
it.updateInterfacePreferences { spacingPercent = value }
}
}
},
onClickReset = {
scope.launch {
viewModel.preferenceDataStore.updateData {
it.updateInterfacePreferences {
uiScalePercent = AppPreference.UiScale.defaultValue.toInt()
cardSizePercent = AppPreference.CardSize.defaultValue.toInt()
spacingPercent = AppPreference.Spacing.defaultValue.toInt()
}
it.updateInterfacePreferences { spacingLevel = value }
}
}
},
@ -245,7 +200,6 @@ fun DisplaySizePage(
.fillMaxHeight()
.weight(1f),
) {
CompositionLocalProvider(LocalDensity provides previewDensity) {
DisplaySizePreview(
posters = posters,
episodes = episodes,
@ -254,19 +208,16 @@ fun DisplaySizePage(
)
}
}
}
}
}
@Composable
private fun DisplaySizeSettingsColumn(
uiScale: Int,
cardSize: Int,
spacing: Int,
onUiScaleChange: (Int) -> Unit,
onCardSizeChange: (Int) -> Unit,
onSpacingChange: (Int) -> Unit,
onClickReset: () -> Unit,
textLevel: DisplaySizeLevel,
cardLevel: DisplaySizeLevel,
spacingLevel: DisplaySizeLevel,
onChangeTextLevel: (DisplaySizeLevel) -> Unit,
onChangeCardLevel: (DisplaySizeLevel) -> Unit,
onChangeSpacingLevel: (DisplaySizeLevel) -> Unit,
) {
val firstFocus = remember { FocusRequester() }
LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
@ -282,39 +233,25 @@ private fun DisplaySizeSettingsColumn(
.focusRestorer(firstFocus),
) {
item {
DisplaySizeSlider(
preference = AppPreference.UiScale,
value = uiScale,
onChange = onUiScaleChange,
LevelChoicePreference(
preference = AppPreference.TextSize,
currentValue = textLevel,
onChange = onChangeTextLevel,
modifier = Modifier.focusRequester(firstFocus),
)
}
item {
DisplaySizeSlider(
LevelChoicePreference(
preference = AppPreference.CardSize,
value = cardSize,
onChange = onCardSizeChange,
currentValue = cardLevel,
onChange = onChangeCardLevel,
)
}
item {
DisplaySizeSlider(
LevelChoicePreference(
preference = AppPreference.Spacing,
value = spacing,
onChange = onSpacingChange,
)
}
item { HorizontalDivider() }
item {
HomeSettingsListItem(
selected = false,
headlineText = stringResource(R.string.reset_to_defaults),
leadingContent = {
Text(
text = stringResource(R.string.fa_arrows_rotate),
fontFamily = FontAwesome,
)
},
onClick = onClickReset,
currentValue = spacingLevel,
onChange = onChangeSpacingLevel,
)
}
}
@ -322,19 +259,20 @@ private fun DisplaySizeSettingsColumn(
}
@Composable
private fun DisplaySizeSlider(
preference: AppSliderPreference<AppPreferences>,
value: Int,
onChange: (Int) -> Unit,
private fun LevelChoicePreference(
preference: AppChoicePreference<AppPreferences, DisplaySizeLevel>,
currentValue: DisplaySizeLevel,
onChange: (DisplaySizeLevel) -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
SliderPreference(
preference = preference,
val levelNames = stringArrayResource(preference.displayValues).toList()
val selectedIndex = preference.valueToIndex(currentValue)
ChoicePreference(
title = stringResource(preference.title),
summary = preference.summary(context, value.toLong()),
value = value.toLong(),
onChange = { onChange(it.toInt()) },
summary = levelNames.getOrNull(selectedIndex),
possibleValues = levelNames,
selectedIndex = selectedIndex,
onValueChange = { onChange(preference.indexToValue(it)) },
modifier = modifier,
)
}

View file

@ -1,9 +1,11 @@
package com.github.damontecres.wholphin.ui.util
import androidx.compose.runtime.staticCompositionLocalOf
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.DisplaySizeLevel
import com.github.damontecres.wholphin.preferences.DisplayToggle
import com.github.damontecres.wholphin.preferences.cardPercent
import com.github.damontecres.wholphin.preferences.spacingPercent
import com.github.damontecres.wholphin.ui.BASE_SPACING_DP
import com.github.damontecres.wholphin.ui.Cards
import java.util.EnumSet
@ -13,9 +15,9 @@ import java.util.EnumSet
*/
data class InterfaceCustomization(
val enabledDisplayToggles: EnumSet<DisplayToggle>,
val uiScalePercent: Int = AppPreference.UiScale.defaultValue.toInt(),
val cardSizePercent: Int = AppPreference.CardSize.defaultValue.toInt(),
val spacingPercent: Int = AppPreference.Spacing.defaultValue.toInt(),
val textSizeLevel: DisplaySizeLevel = DisplaySizeLevel.DEFAULT,
val cardSizeLevel: DisplaySizeLevel = DisplaySizeLevel.DEFAULT,
val spacingLevel: DisplaySizeLevel = DisplaySizeLevel.DEFAULT,
) {
constructor(prefs: AppPreferences) : this(
enabledDisplayToggles =
@ -26,14 +28,17 @@ data class InterfaceCustomization(
EnumSet.copyOf(it)
}
},
uiScalePercent =
prefs.interfacePreferences.uiScalePercent.withinBoundsOrDefault(AppPreference.UiScale),
cardSizePercent =
prefs.interfacePreferences.cardSizePercent.withinBoundsOrDefault(AppPreference.CardSize),
spacingPercent =
prefs.interfacePreferences.spacingPercent.withinBoundsOrDefault(AppPreference.Spacing),
textSizeLevel = prefs.interfacePreferences.textSizeLevel,
cardSizeLevel = prefs.interfacePreferences.cardSizeLevel,
spacingLevel = prefs.interfacePreferences.spacingLevel,
)
val cardSizePercent: Int
get() = cardSizeLevel.cardPercent()
val spacingPercent: Int
get() = spacingLevel.spacingPercent()
val cardHeightDp: Int
get() = Cards.HEIGHT_2X3_DP * cardSizePercent / 100

View file

@ -177,6 +177,13 @@ enum DisplayToggle{
COMMUNITY_RATING = 2;
}
enum DisplaySizeLevel{
DEFAULT = 0;
EXTRA_SMALL = 1;
SMALL = 2;
LARGE = 3;
}
message InterfacePreferences {
ThemeSongVolume play_theme_songs = 1;
bool remember_selected_tab = 2;
@ -193,9 +200,9 @@ message InterfacePreferences {
bool show_logos = 13;
SearchPreferences search_preferences = 14;
repeated DisplayToggle display_toggles = 15;
int32 ui_scale_percent = 16;
int32 card_size_percent = 17;
int32 spacing_percent = 18;
DisplaySizeLevel text_size_level = 19;
DisplaySizeLevel card_size_level = 20;
DisplaySizeLevel spacing_level = 21;
}
message AdvancedPreferences {

View file

@ -399,8 +399,12 @@
<string name="spacing">Spacing</string>
<string name="card_size">Card size</string>
<string name="display_size">Display size</string>
<string name="display_size_summary">Scale the app UI, card size, and spacing</string>
<string name="ui_scale">UI scale</string>
<string name="display_size_summary">Adjust text size, card size, and spacing</string>
<string name="text_size">Text size</string>
<string name="display_size_level_extra_small">Extra small</string>
<string name="display_size_level_small">Small</string>
<string name="display_size_level_default">Default</string>
<string name="display_size_level_large">Large</string>
<string name="aspect_ratio">Aspect Ratio</string>
<string name="show_details">Show details</string>
<string name="image_type">Image type</string>
@ -597,6 +601,13 @@
<item>@string/black</item>
</string-array>
<string-array name="display_size_levels">
<item>@string/display_size_level_extra_small</item>
<item>@string/display_size_level_small</item>
<item>@string/display_size_level_default</item>
<item>@string/display_size_level_large</item>
</string-array>
<string name="skip_ignore">Ignore</string>
<string name="skip_automatically">Skip automatically</string>
<string name="skip_ask">Ask to skip</string>

View file

@ -5,6 +5,7 @@ import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.DisplaySizeLevel
import com.github.damontecres.wholphin.services.migrateLegacyRowSpacing
import com.github.damontecres.wholphin.ui.AspectRatio
import com.github.damontecres.wholphin.ui.Cards
@ -16,31 +17,28 @@ import java.util.EnumSet
class InterfaceHelpersTest {
@Test
fun `withinBoundsOrDefault returns value when in range`() {
assertEquals(100, 100.withinBoundsOrDefault(AppPreference.CardSize))
assertEquals(75, 75.withinBoundsOrDefault(AppPreference.CardSize))
assertEquals(150, 150.withinBoundsOrDefault(AppPreference.CardSize))
assertEquals(30, 30.withinBoundsOrDefault(AppPreference.SkipForward))
assertEquals(60, 60.withinBoundsOrDefault(AppPreference.SkipForward))
assertEquals(120, 120.withinBoundsOrDefault(AppPreference.SkipForward))
}
@Test
fun `withinBoundsOrDefault returns default when below min`() {
// CardSize is 50..150 default 100; Spacing is 25..175 default 100.
assertEquals(100, 49.withinBoundsOrDefault(AppPreference.CardSize))
assertEquals(100, 0.withinBoundsOrDefault(AppPreference.Spacing))
assertEquals(30, 4.withinBoundsOrDefault(AppPreference.SkipForward))
assertEquals(30, 0.withinBoundsOrDefault(AppPreference.SkipForward))
}
@Test
fun `withinBoundsOrDefault returns default when above max`() {
assertEquals(100, 151.withinBoundsOrDefault(AppPreference.CardSize))
assertEquals(100, 200.withinBoundsOrDefault(AppPreference.Spacing))
assertEquals(30, 301.withinBoundsOrDefault(AppPreference.SkipForward))
assertEquals(30, 1000.withinBoundsOrDefault(AppPreference.SkipForward))
}
@Test
fun `withinBoundsOrDefault includes both bounds inclusively`() {
// Sanity: min and max themselves are accepted, NOT replaced with default.
assertEquals(50, 50.withinBoundsOrDefault(AppPreference.CardSize))
assertEquals(150, 150.withinBoundsOrDefault(AppPreference.CardSize))
assertEquals(25, 25.withinBoundsOrDefault(AppPreference.Spacing))
assertEquals(175, 175.withinBoundsOrDefault(AppPreference.Spacing))
assertEquals(5, 5.withinBoundsOrDefault(AppPreference.SkipForward))
assertEquals(300, 300.withinBoundsOrDefault(AppPreference.SkipForward))
}
@Test
@ -134,38 +132,38 @@ class InterfaceHelpersTest {
}
@Test
fun `InterfaceCustomization cardHeightDp computes global from slider percent`() {
fun `InterfaceCustomization cardHeightDp at DEFAULT level matches natural base`() {
val customization = InterfaceCustomization(prefs = AppPreferences.getDefaultInstance())
// Default cardSizePercent is 100, so cardHeightDp == HEIGHT_2X3_DP.
// proto-zero-default lands cardSizeLevel on DEFAULT (100%), so cardHeightDp == HEIGHT_2X3_DP.
assertEquals(Cards.HEIGHT_2X3_DP, customization.cardHeightDp)
}
@Test
fun `InterfaceCustomization cardHeightDp scales linearly`() {
fun `InterfaceCustomization cardHeightDp scales by Card level percent`() {
val customization =
InterfaceCustomization(
enabledDisplayToggles =
EnumSet.noneOf(
com.github.damontecres.wholphin.preferences.DisplayToggle::class.java,
),
cardSizePercent = 150,
cardSizeLevel = DisplaySizeLevel.LARGE,
)
// 172 * 150 / 100 = 258.
assertEquals(258, customization.cardHeightDp)
// LARGE → cardPercent() = 116. 172 * 116 / 100 = 199 (integer truncation).
assertEquals(199, customization.cardHeightDp)
}
@Test
fun `InterfaceCustomization spacingDp scales linearly`() {
fun `InterfaceCustomization spacingDp scales by Spacing level percent`() {
val customization =
InterfaceCustomization(
enabledDisplayToggles =
EnumSet.noneOf(
com.github.damontecres.wholphin.preferences.DisplayToggle::class.java,
),
spacingPercent = 175,
spacingLevel = DisplaySizeLevel.LARGE,
)
// BASE_SPACING_DP = 16. 16 * 175 / 100 = 28.
assertEquals(28, customization.spacingDp)
// LARGE → spacingPercent() = 125. BASE_SPACING_DP = 16. 16 * 125 / 100 = 20.
assertEquals(20, customization.spacingDp)
}
@Test