diff --git a/app/shared/src/androidMain/kotlin/iris/platform/AndroidSecureStore.kt b/app/shared/src/androidMain/kotlin/iris/platform/AndroidSecureStore.kt index b3164e3..42474cb 100644 --- a/app/shared/src/androidMain/kotlin/iris/platform/AndroidSecureStore.kt +++ b/app/shared/src/androidMain/kotlin/iris/platform/AndroidSecureStore.kt @@ -134,6 +134,10 @@ class AndroidSecureStore(context: Context) : SecureStore { get() = prefs.getString(KEY_BACKGROUND_IMAGE_PATH, "").orEmpty() 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) { serverUrl = url this.token = token @@ -162,5 +166,6 @@ class AndroidSecureStore(context: Context) : SecureStore { const val KEY_BACKGROUND_MODE = "background_mode" const val KEY_BACKGROUND_COLOR = "background_color" const val KEY_BACKGROUND_IMAGE_PATH = "background_image_path" + const val KEY_FONT_SIZE_SCALE = "font_size_scale" } } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/iris/IrisApp.kt b/app/shared/src/commonMain/kotlin/iris/IrisApp.kt index 34ca887..95cbb01 100644 --- a/app/shared/src/commonMain/kotlin/iris/IrisApp.kt +++ b/app/shared/src/commonMain/kotlin/iris/IrisApp.kt @@ -13,6 +13,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density import iris.data.SecureStore import iris.net.GatewayClient import iris.platform.setActiveController @@ -55,9 +57,20 @@ fun IrisApp( } val state by controller.client.state.collectAsState() val theme by controller.theme.collectAsState() + val fontScale by controller.fontSizeScale.collectAsState() + val baseDensity = LocalDensity.current 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 // Modifier.background on the Surface would be painted *under* the // Surface's own colorScheme.surface fill and never show through. diff --git a/app/shared/src/commonMain/kotlin/iris/data/SecureStore.kt b/app/shared/src/commonMain/kotlin/iris/data/SecureStore.kt index 14fb78d..5a0d482 100644 --- a/app/shared/src/commonMain/kotlin/iris/data/SecureStore.kt +++ b/app/shared/src/commonMain/kotlin/iris/data/SecureStore.kt @@ -57,6 +57,10 @@ interface SecureStore { /** UI setting: background image path (empty when unused). */ 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 clear() } \ No newline at end of file diff --git a/app/shared/src/commonMain/kotlin/iris/state/IrisController.kt b/app/shared/src/commonMain/kotlin/iris/state/IrisController.kt index f81c3c3..18e2175 100644 --- a/app/shared/src/commonMain/kotlin/iris/state/IrisController.kt +++ b/app/shared/src/commonMain/kotlin/iris/state/IrisController.kt @@ -204,6 +204,24 @@ class IrisController( 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 = _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 ────────────────────────────────────────────────── private val _searchResults = MutableStateFlow>(emptyList()) val searchResults: StateFlow> = _searchResults.asStateFlow() diff --git a/app/shared/src/commonMain/kotlin/iris/ui/screens/SettingsScreen.kt b/app/shared/src/commonMain/kotlin/iris/ui/screens/SettingsScreen.kt index 02477d5..82a4431 100644 --- a/app/shared/src/commonMain/kotlin/iris/ui/screens/SettingsScreen.kt +++ b/app/shared/src/commonMain/kotlin/iris/ui/screens/SettingsScreen.kt @@ -65,6 +65,7 @@ fun SettingsScreen(controller: IrisController, onBack: () -> Unit) { val streamingEnabled by controller.streamingEnabled.collectAsState() val reasoningAutoCollapse by controller.reasoningAutoCollapse.collectAsState() val toolDetail by controller.toolDetail.collectAsState() + val fontSizeScale by controller.fontSizeScale.collectAsState() val theme = LocalUserTheme.current var pickerTarget by remember { mutableStateOf(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 { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/app/shared/src/desktopMain/kotlin/iris/platform/DesktopSecureStore.kt b/app/shared/src/desktopMain/kotlin/iris/platform/DesktopSecureStore.kt index 6f82a29..baba4ad 100644 --- a/app/shared/src/desktopMain/kotlin/iris/platform/DesktopSecureStore.kt +++ b/app/shared/src/desktopMain/kotlin/iris/platform/DesktopSecureStore.kt @@ -44,6 +44,7 @@ class DesktopSecureStore : SecureStore { val backgroundMode: String = "color", val backgroundColor: Int = UserTheme.DEFAULT_BACKGROUND, val backgroundImagePath: String = "", + val fontSizeScale: Float = 1.0f, ) init { @@ -206,6 +207,13 @@ class DesktopSecureStore : SecureStore { 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) { val d = load() save(d.copy(serverUrl = url.trim()))