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:
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()))
|
||||||
|
|||||||
Reference in new issue
Block a user