Font size setting: app-wide text scale (0.8x-1.5x, default 1.0x) via Settings > Appearance slider — applied through LocalDensity.fontScale so all sp text scales while dp layout keeps its proportions; the multiplier stacks on top of the system font scale; persisted per platform (EncryptedSharedPreferences / settings.json) like the other appearance settings

This commit is contained in:
ARIA committed 2026-08-21 08:54:48 +02:00
1 parent f79c54fead
commit abf1374c1e
6 files changed
+90 -1

No files matched your search

@@ -134,6 +134,10 @@ class AndroidSecureStore(context: Context) : SecureStore {
get() = prefs.getString(KEY_BACKGROUND_IMAGE_PATH, "").orEmpty() get() = prefs.getString(KEY_BACKGROUND_IMAGE_PATH, "").orEmpty()
set(value) = prefs.edit().putString(KEY_BACKGROUND_IMAGE_PATH, value).apply() set(value) = prefs.edit().putString(KEY_BACKGROUND_IMAGE_PATH, value).apply()
override var fontSizeScale: Float
get() = prefs.getFloat(KEY_FONT_SIZE_SCALE, 1.0f)
set(value) = prefs.edit().putFloat(KEY_FONT_SIZE_SCALE, value).apply()
override fun savePairing(url: String, token: String) { override fun savePairing(url: String, token: String) {
serverUrl = url serverUrl = url
this.token = token this.token = token
@@ -162,5 +166,6 @@ class AndroidSecureStore(context: Context) : SecureStore {
const val KEY_BACKGROUND_MODE = "background_mode" const val KEY_BACKGROUND_MODE = "background_mode"
const val KEY_BACKGROUND_COLOR = "background_color" const val KEY_BACKGROUND_COLOR = "background_color"
const val KEY_BACKGROUND_IMAGE_PATH = "background_image_path" const val KEY_BACKGROUND_IMAGE_PATH = "background_image_path"
const val KEY_FONT_SIZE_SCALE = "font_size_scale"
} }
} }
@@ -13,6 +13,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import iris.data.SecureStore import iris.data.SecureStore
import iris.net.GatewayClient import iris.net.GatewayClient
import iris.platform.setActiveController import iris.platform.setActiveController
@@ -55,9 +57,20 @@ fun IrisApp(
} }
val state by controller.client.state.collectAsState() val state by controller.client.state.collectAsState()
val theme by controller.theme.collectAsState() val theme by controller.theme.collectAsState()
val fontScale by controller.fontSizeScale.collectAsState()
val baseDensity = LocalDensity.current
IrisTheme { IrisTheme {
CompositionLocalProvider(LocalUserTheme provides theme) { // Font-size setting (Settings → Appearance): a multiplier on top of the
// system font scale. fontScale affects sp text only — dp layout is
// untouched, so the UI keeps its proportions at any size.
CompositionLocalProvider(
LocalUserTheme provides theme,
LocalDensity provides Density(
density = baseDensity.density,
fontScale = baseDensity.fontScale * fontScale,
),
) {
// The color goes through Surface's `color` parameter: a // The color goes through Surface's `color` parameter: a
// Modifier.background on the Surface would be painted *under* the // Modifier.background on the Surface would be painted *under* the
// Surface's own colorScheme.surface fill and never show through. // Surface's own colorScheme.surface fill and never show through.
@@ -57,6 +57,10 @@ interface SecureStore {
/** UI setting: background image path (empty when unused). */ /** UI setting: background image path (empty when unused). */
var backgroundImagePath: String var backgroundImagePath: String
/** UI setting: text size multiplier (1.0 = default; applied on top of the
* system font scale). */
var fontSizeScale: Float
fun savePairing(url: String, token: String) fun savePairing(url: String, token: String)
fun clear() fun clear()
} }
@@ -204,6 +204,24 @@ class IrisController(
store.backgroundImagePath = t.backgroundImagePath.orEmpty() store.backgroundImagePath = t.backgroundImagePath.orEmpty()
} }
// ── Font size (Settings → Appearance) ─────────────────────────────────
// Text-size multiplier applied app-wide via LocalDensity.fontScale (scales
// all sp text, not dp layout). Persisted; 1.0 = default.
private val _fontSizeScale = MutableStateFlow(store.fontSizeScale.coerceIn(FONT_SCALE_MIN, FONT_SCALE_MAX))
val fontSizeScale: StateFlow<Float> = _fontSizeScale.asStateFlow()
fun setFontSizeScale(scale: Float) {
val clamped = scale.coerceIn(FONT_SCALE_MIN, FONT_SCALE_MAX)
if (clamped == _fontSizeScale.value) return
_fontSizeScale.value = clamped
store.fontSizeScale = clamped
}
companion object {
const val FONT_SCALE_MIN = 0.8f
const val FONT_SCALE_MAX = 1.5f
}
// ── M3: search state ────────────────────────────────────────────────── // ── M3: search state ──────────────────────────────────────────────────
private val _searchResults = MutableStateFlow<List<SearchHit>>(emptyList()) private val _searchResults = MutableStateFlow<List<SearchHit>>(emptyList())
val searchResults: StateFlow<List<SearchHit>> = _searchResults.asStateFlow() val searchResults: StateFlow<List<SearchHit>> = _searchResults.asStateFlow()
@@ -65,6 +65,7 @@ fun SettingsScreen(controller: IrisController, onBack: () -> Unit) {
val streamingEnabled by controller.streamingEnabled.collectAsState() val streamingEnabled by controller.streamingEnabled.collectAsState()
val reasoningAutoCollapse by controller.reasoningAutoCollapse.collectAsState() val reasoningAutoCollapse by controller.reasoningAutoCollapse.collectAsState()
val toolDetail by controller.toolDetail.collectAsState() val toolDetail by controller.toolDetail.collectAsState()
val fontSizeScale by controller.fontSizeScale.collectAsState()
val theme = LocalUserTheme.current val theme = LocalUserTheme.current
var pickerTarget by remember { mutableStateOf<PickerTarget?>(null) } var pickerTarget by remember { mutableStateOf<PickerTarget?>(null) }
@@ -241,6 +242,46 @@ fun SettingsScreen(controller: IrisController, onBack: () -> Unit) {
} }
} }
} }
SettingsCard {
Text("🔤 Font size", fontSize = 14.sp)
Text(
"Scale the text across the app",
fontSize = 12.sp,
color = IrisColors.textDim,
)
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Aa", fontSize = 12.sp, color = IrisColors.textDim)
Slider(
value = fontSizeScale,
onValueChange = { controller.setFontSizeScale(it) },
valueRange = IrisController.FONT_SCALE_MIN..IrisController.FONT_SCALE_MAX,
modifier = Modifier.weight(1f),
)
Text("Aa", fontSize = 18.sp, color = IrisColors.textDim)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
Text(
"The quick brown fox jumps over the lazy dog",
fontSize = 13.sp,
color = IrisColors.textSecondary,
modifier = Modifier.weight(1f),
)
Text(
"${(fontSizeScale * 100).roundToInt()}%",
fontSize = 12.sp,
color = IrisColors.textDim,
)
if (fontSizeScale != 1.0f) {
TextButton(onClick = { controller.setFontSizeScale(1.0f) }) {
Text("Reset", fontSize = 12.sp)
}
}
}
}
SettingsCard { SettingsCard {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -44,6 +44,7 @@ class DesktopSecureStore : SecureStore {
val backgroundMode: String = "color", val backgroundMode: String = "color",
val backgroundColor: Int = UserTheme.DEFAULT_BACKGROUND, val backgroundColor: Int = UserTheme.DEFAULT_BACKGROUND,
val backgroundImagePath: String = "", val backgroundImagePath: String = "",
val fontSizeScale: Float = 1.0f,
) )
init { init {
@@ -206,6 +207,13 @@ class DesktopSecureStore : SecureStore {
save(d.copy(backgroundImagePath = value)) save(d.copy(backgroundImagePath = value))
} }
override var fontSizeScale: Float
get() = load().fontSizeScale
set(value) {
val d = load()
save(d.copy(fontSizeScale = value))
}
override fun savePairing(url: String, token: String) { override fun savePairing(url: String, token: String) {
val d = load() val d = load()
save(d.copy(serverUrl = url.trim())) save(d.copy(serverUrl = url.trim()))