From fe2c4a7e2a4e3de83d6adec59090d0b8312c426e Mon Sep 17 00:00:00 2001 From: Mansimran Singh Date: Tue, 18 Aug 2026 22:13:07 -0400 Subject: [PATCH] Fix unscaled text sizes and match iOS typography scale Several screens fell back to flat Material3 defaults instead of this app's own per-breakpoint dimens.xml convention, most notably the keyboard's letter-key and typed-input text - traced via git blame to an orphaned dimen left behind by the 2020 Compose rewrite. Wires those (and two similarly orphaned dimens on EditCategoryMenuScreen/SensitivityScreen) back in, corrects settings_title_text_size's phone value to match iOS's compact size (tablet already matched), and gives the six remaining genuine hardcoded text sizes proper per-breakpoint dimens. Font family (Roboto/San Francisco, each platform's OS default) was already correct on both sides and is unchanged. Co-Authored-By: Claude Sonnet 5 --- .../work-log/681-typography-parity.md | 137 ++++++++++++++++++ .../EditCategoryMenuScreen.kt | 11 +- .../editphrases/EditCategoryPhrasesScreen.kt | 2 +- .../ui/facetracking/FaceTrackingScreen.kt | 3 +- .../vocable/ui/keyboard/KeyboardScreen.kt | 24 ++- .../ui/sensitivity/SensitivityScreen.kt | 15 +- .../ui/voiceselection/VoiceSelectionScreen.kt | 3 +- app/src/main/res/values-sw600dp/dimens.xml | 5 + app/src/main/res/values/dimens.xml | 9 +- 9 files changed, 192 insertions(+), 17 deletions(-) create mode 100644 Documentation/work-log/681-typography-parity.md diff --git a/Documentation/work-log/681-typography-parity.md b/Documentation/work-log/681-typography-parity.md new file mode 100644 index 000000000..88c27f4a8 --- /dev/null +++ b/Documentation/work-log/681-typography-parity.md @@ -0,0 +1,137 @@ +# Typography parity with iOS + consolidate hardcoded text sizes + +**Issue:** #681 ("Match Android typography to iOS font-size scale, consolidate hardcoded text +sizes"). + +## What was needed + +vocable-ios uses `UIFont.systemFont` (San Francisco, the OS default) everywhere, sized in a +compact/regular scale (22/24/28/40/48pt, almost always bold). Android's `Type.kt` already uses +`FontFamily.Default` (Roboto, the OS default) — the font family already matched, no change needed +there. But a few Android per-breakpoint sizes drifted from their iOS compact/regular equivalents +more than unit/density rounding explains, and a number of hardcoded `.sp` literals bypassed this +app's own `dimens.xml` per-breakpoint convention entirely, so those spots didn't scale on tablet +the way the rest of the app does. + +## What changed + +### An accidental Compose-rewrite regression, found via git blame — not just an iOS mismatch + +Several screens used `MaterialTheme.typography.headlineLarge`/`headlineMedium` with no `fontSize` +override, silently falling back to Material3's flat, non-scaling defaults (32sp/28sp) instead of a +deliberate per-breakpoint size. `git log -S "keyboard_text_size"` showed this dimen (and its +sibling `keyboard_input_text_size`) dates to the pre-Compose View-based keyboard layouts +(`e9a6f96d`, `bcf61a58`), while the Compose rewrite (`f5b06b93`, "Refactor with jetpack compose +commit#1") introduced the keyboard key `Text(...)` fresh and never reconnected it to that dimen — +it sat in `dimens.xml`, fully unused, ever since. Fixed by wiring the already-designed (just +orphaned) dimens back in, rather than inventing new values: + +- **Keyboard letter keys** (`KeyboardScreen.kt`) — the actual key glyphs, now use + `keyboard_text_size` (48sp phone/land, 34sp tablet portrait, 40sp tablet landscape — all + pre-existing breakpoint values, untouched). +- **Keyboard typed-input display** (`KeyboardScreen.kt`, both the head-tracking `Text` and the + touch-fallback `TextField` + placeholder, in both the landscape/tablet and portrait layout + branches — 6 spots total) — now use `keyboard_input_text_size` (34sp across all breakpoints). + +Two similarly-orphaned dimens, found the same way, revived for their evident original targets: + +- **`EditCategoryMenuScreen`'s category title** — now uses `settings_edit_individual_category_text_size` + (30sp phone; added a 42sp tablet override, since none existed — sized in proportion to + `edit_categories_title_text_size`'s 34→48sp ratio, the sibling list-screen's title). +- **`SensitivityScreen`'s "Hover Time"/"Cursor Sensitivity" section labels** — now use + `timing_subtitle_text_size` (24sp phone, 34sp tablet). Its **dwell-time value display** ("1 + second", "2.5 seconds") now uses `hover_time_text_size` (24sp phone; added a 34sp tablet override + to match `timing_subtitle_text_size`'s ratio, since none existed). The screen's own main title + ("Timing and Sensitivity") was left alone — it already uses `BasicText`/`TextAutoSize.StepBased` + for responsive sizing, a different (and already-adequate) strategy than the breakpoint-dimens + approach used everywhere else. +- **`VoiceSelectionScreen`'s "Change Voice" header** — this is the screen's own title, playing the + same role as every other Settings-adjacent screen's title, so it now reuses + `settings_title_text_size` directly rather than getting a new near-duplicate `voice_*` dimen. + +### `settings_title_text_size` phone value corrected to match iOS + +iOS's `VocableNavigationBar` title is 28pt compact / 48pt regular. Android's tablet value already +matched exactly (48sp); only the phone value was off (34sp) beyond what unit/density differences +explain. Changed to 28sp. This dimen backs most Settings-adjacent screen titles (`SettingsScreen`, +`SettingsVoiceScreen`, `SelectionModeScreen`, `ResetSettingsScreen`, and now `VoiceSelectionScreen` +above), so the fix applies consistently everywhere in one change. + +### Hardcoded `.sp` sweep + +A broad literal grep (`[0-9]+\.sp`) initially suggested ~50 hardcoded sizes, but nearly all of them +turned out to be `minFontSize`/`maxFontSize`/`stepSize` values inside already-responsive +`TextAutoSize.StepBased(...)` configs, or `letterSpacing`/`lineHeight` (spacing, not size), or the +base `Typography` scale definition in `Type.kt` itself (not a screen bypassing anything — it's the +canonical place that scale lives, and per-screen dimens already layer on top of it, as this whole +pass confirmed). None of those represent an actual "doesn't scale on tablet" bug: auto-sizing +already adapts to available space at runtime regardless of breakpoint. Filtering those out left +exactly 6 genuine fixed, non-scaling hardcoded sizes, all coincidentally `18.sp`: + +- `EditCategoryMenuScreen`'s four action-button labels (Rename/Toggle/Edit Phrases/Remove) — new + `edit_category_menu_action_text_size` (18sp phone, 24sp tablet). +- `EditCategoryPhrasesScreen`'s phrase-row text — new `edit_category_phrases_item_text_size` (18sp + phone, 24sp tablet). +- `FaceTrackingScreen`'s "Head tracking is paused" message — new `head_tracking_paused_text_size` + (18sp phone, 24sp tablet). + +Three `PresetsScreen.kt` instances of `fontSize = 20.sp` were investigated and deliberately left +alone: each sits inside a `TextStyle` immediately followed by +`TextAutoSize.StepBased(minFontSize = 10.sp, maxFontSize = 20.sp, ...)` — the base `fontSize` +exactly matches `maxFontSize`, meaning auto-sizing already governs the actual rendered size at +runtime regardless of screen breakpoint. Moving these to a dimen would be cosmetic churn with zero +behavior change. + +## Key decisions, and why + +### No font-family change on either platform + +iOS uses `.systemFont` (San Francisco) everywhere; Android's `Type.kt` already uses +`FontFamily.Default` (Roboto). Both are already using their OS's default font, which is correct — +bundling one platform's typeface onto the other would look foreign to users of that platform, and +neither app has ever shipped a custom font asset. Out of scope by design, not an oversight. + +### `timing_title_text_size` restored after initially removing it + +Confirmed via code search that this dimen (and `SensitivityScreen`'s main title in general) wasn't +used by any Compose screen — the title uses `BasicText`/`TextAutoSize` instead. Removed it as dead +weight, which broke `processDebugResources`: `styles.xml`'s `TimingSensitivityTitle` style (a +leftover from the pre-Compose, XML-View era — this app is 100% Compose today, no XML layouts) still +references it via `autoSizeMaxTextSize`. That style is itself unreferenced by any layout or Kotlin +code, but Android's resource linker doesn't care whether a *referencing* resource is itself dead — +it only cares that the reference resolves. Restored the dimen rather than also hunting down and +deleting the legacy XML style tree, which is a separate, broader "dead pre-Compose resource +cleanup" concern outside this ticket's scope. + +### Screen-scoped dimen names over one shared generic one + +For the three genuine `18.sp` hardcodes (three unrelated screens, coincidentally the same value), +named each dimen after its own screen/role (`edit_category_menu_action_text_size`, +`edit_category_phrases_item_text_size`, `head_tracking_paused_text_size`) rather than introducing +one shared `action_text_size`-style constant. Matches this codebase's existing naming convention +(`edit_categories_title_text_size`, `settings_button_text_size`, etc., all screen-prefixed) and +keeps each screen independently tunable later without an unrelated screen's needs colliding. + +### Added tablet overrides even where sibling dimens don't have one + +`settings_button_text_size` and `edit_categories_list_text_size` (pre-existing, untouched) have no +`values-sw600dp` override either — the codebase's existing tablet-scaling coverage is itself +incomplete. Chose to add tablet overrides for every dimen touched or created in this pass anyway: +the point of this ticket is closing tablet-scaling gaps, so shipping a new dimen that reproduces the +same gap (mechanically compliant with "use dimens.xml," but not actually fixing the user-facing +issue) would defeat the purpose. Pre-existing dimens with the same gap were left alone — fixing +every instance of that separate, smaller issue across the whole app is out of scope here. + +## Verification + +`./gradlew compileDebugKotlin testDebugUnitTest` and +`./gradlew assembleDebug assembleDebugAndroidTest` all pass. Not done: manual verification in a +running app/emulator (none available in this session) and no design sign-off. + +## Pointers + +- Issue: #681, branch `feature/681/typography-parity` (linked via `createLinkedBranch`, branched + from `main`). +- iOS reference: `willowtreeapps/vocable-ios`, `Vocable/Extensions/UIFont+Helpers.swift`, + `Vocable/Common/VocableNavigationBar.swift`, `Vocable/Common/PresetItemCollectionViewCell.swift`, + `Vocable/Common/Views/EmptyStateView.swift`. diff --git a/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt index b844c3fd0..67bad125e 100644 --- a/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt +++ b/app/src/main/java/com/willowtree/vocable/ui/editcategorymenu/EditCategoryMenuScreen.kt @@ -155,7 +155,8 @@ private fun EditCategoryMenuContent( text = categoryName, style = MaterialTheme.typography.headlineMedium.copy( fontWeight = FontWeight.Bold, - color = TextColor + color = TextColor, + fontSize = dimensionResource(id = R.dimen.settings_edit_individual_category_text_size).value.sp ), textAlign = TextAlign.Center, modifier = Modifier @@ -184,7 +185,7 @@ private fun EditCategoryMenuContent( text = stringResource(R.string.rename_category), color = TextColor, fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, + fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp, modifier = Modifier .weight(1f) .padding(horizontal = 16.dp) @@ -208,7 +209,7 @@ private fun EditCategoryMenuContent( text = stringResource(R.string.show_category), color = TextColor, fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, + fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp, modifier = Modifier .weight(1f) .padding(horizontal = 16.dp) @@ -244,7 +245,7 @@ private fun EditCategoryMenuContent( text = stringResource(R.string.edit_phrases), color = TextColor, fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, + fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp, modifier = Modifier .weight(1f) .padding(horizontal = 16.dp) @@ -276,7 +277,7 @@ private fun EditCategoryMenuContent( text = stringResource(R.string.remove_category), color = Color.White, fontWeight = FontWeight.Bold, - fontSize = 18.sp, + fontSize = dimensionResource(id = R.dimen.edit_category_menu_action_text_size).value.sp, modifier = Modifier.padding(horizontal = 16.dp) ) } diff --git a/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt index 80a8e7d2a..7e72c1408 100644 --- a/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt +++ b/app/src/main/java/com/willowtree/vocable/ui/editphrases/EditCategoryPhrasesScreen.kt @@ -358,7 +358,7 @@ private fun PhraseEditItem( text = phraseText, color = TextColor, fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, + fontSize = dimensionResource(id = R.dimen.edit_category_phrases_item_text_size).value.sp, modifier = Modifier .weight(1f) .padding(horizontal = 16.dp), diff --git a/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt index 4f629fe80..c21cabf7c 100644 --- a/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt +++ b/app/src/main/java/com/willowtree/vocable/ui/facetracking/FaceTrackingScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -113,7 +114,7 @@ private fun ErrorBanner() { text = stringResource(id = R.string.head_tracking_paused_message), color = Color.White, fontWeight = FontWeight.Bold, - fontSize = 18.sp + fontSize = dimensionResource(id = R.dimen.head_tracking_paused_text_size).value.sp ) } } diff --git a/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt index 2f7538858..8279dee40 100644 --- a/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt +++ b/app/src/main/java/com/willowtree/vocable/ui/keyboard/KeyboardScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.integerResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringArrayResource @@ -40,6 +41,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.willowtree.vocable.R import com.willowtree.vocable.ui.base.MviScreen import com.willowtree.vocable.ui.components.GazeButton @@ -115,6 +117,8 @@ fun KeyboardContent( ) { val keys = stringArrayResource(id = R.array.keyboard_keys) val numColumns = integerResource(id = R.integer.keyboard_columns) + val inputTextSize = dimensionResource(id = R.dimen.keyboard_input_text_size).value.sp + val keyTextSize = dimensionResource(id = R.dimen.keyboard_text_size).value.sp val isLandscapeOrTablet = numColumns > 5 @@ -165,6 +169,7 @@ fun KeyboardContent( Text( text = inputText.ifEmpty { stringResource(R.string.keyboard_select_letters) }, style = MaterialTheme.typography.headlineMedium.copy( + fontSize = inputTextSize, fontWeight = FontWeight.Bold, color = if (inputText.isEmpty()) Color.Gray else Color.White ), @@ -178,12 +183,14 @@ fun KeyboardContent( Text( stringResource(R.string.keyboard_select_letters), style = MaterialTheme.typography.headlineMedium.copy( + fontSize = inputTextSize, fontWeight = FontWeight.Bold, color = Color.Gray ) ) }, textStyle = MaterialTheme.typography.headlineMedium.copy( + fontSize = inputTextSize, fontWeight = FontWeight.Bold, color = Color.White ), @@ -206,8 +213,8 @@ fun KeyboardContent( autoCorrectEnabled = true ), keyboardActions = KeyboardActions( - onDone = { - onSpeak() + onDone = { + onSpeak() } ) ) @@ -324,6 +331,7 @@ fun KeyboardContent( Text( text = inputText.ifEmpty { stringResource(R.string.keyboard_select_letters) }, style = MaterialTheme.typography.headlineMedium.copy( + fontSize = inputTextSize, fontWeight = FontWeight.Bold, color = if (inputText.isEmpty()) Color.Gray else Color.White ), @@ -337,12 +345,14 @@ fun KeyboardContent( Text( stringResource(R.string.keyboard_select_letters), style = MaterialTheme.typography.headlineMedium.copy( + fontSize = inputTextSize, fontWeight = FontWeight.Bold, color = Color.Gray ) ) }, textStyle = MaterialTheme.typography.headlineMedium.copy( + fontSize = inputTextSize, fontWeight = FontWeight.Bold, color = Color.White ), @@ -365,8 +375,8 @@ fun KeyboardContent( autoCorrectEnabled = true ), keyboardActions = KeyboardActions( - onDone = { - onSpeak() + onDone = { + onSpeak() } ) ) @@ -507,7 +517,11 @@ fun KeyboardContent( .weight(1f) .fillMaxHeight() ) { - Text(text = key, style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold) + Text( + text = key, + style = MaterialTheme.typography.headlineLarge.copy(fontSize = keyTextSize), + fontWeight = FontWeight.Bold + ) } } if (rowKeys.size < numColumns) { diff --git a/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt index 3ca8fa84b..39cbe43f7 100644 --- a/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt +++ b/app/src/main/java/com/willowtree/vocable/ui/sensitivity/SensitivityScreen.kt @@ -110,7 +110,10 @@ private fun SensitivityContent( // Hover Time Section Text( text = stringResource(id = R.string.hover_time_text), - style = MaterialTheme.typography.titleLarge.copy(color = TextColor), + style = MaterialTheme.typography.titleLarge.copy( + color = TextColor, + fontSize = dimensionResource(id = R.dimen.timing_subtitle_text_size).value.sp + ), modifier = Modifier.constrainAs(hoverTitleRef) { top.linkTo(backButtonRef.bottom, margin = 32.dp) start.linkTo(parent.start) @@ -143,7 +146,10 @@ private fun SensitivityContent( Text( text = dwellTimeText, - style = MaterialTheme.typography.headlineMedium.copy(color = TextColor), + style = MaterialTheme.typography.headlineMedium.copy( + color = TextColor, + fontSize = dimensionResource(id = R.dimen.hover_time_text_size).value.sp + ), modifier = Modifier .weight(1f) .padding(horizontal = 16.dp), @@ -161,7 +167,10 @@ private fun SensitivityContent( // Cursor Sensitivity Section Text( text = stringResource(id = R.string.cursor_sensitivity_text), - style = MaterialTheme.typography.titleLarge.copy(color = TextColor), + style = MaterialTheme.typography.titleLarge.copy( + color = TextColor, + fontSize = dimensionResource(id = R.dimen.timing_subtitle_text_size).value.sp + ), modifier = Modifier.constrainAs(cursorTitleRef) { top.linkTo(hoverControlsRef.bottom, margin = 32.dp) start.linkTo(parent.start) diff --git a/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt b/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt index 20f137219..012c82d36 100644 --- a/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt +++ b/app/src/main/java/com/willowtree/vocable/ui/voiceselection/VoiceSelectionScreen.kt @@ -185,7 +185,8 @@ fun VoiceSelectionScreen( Text( text = stringResource(R.string.voice_settings_change_voice), style = MaterialTheme.typography.headlineMedium.copy( - fontWeight = FontWeight.Bold + fontWeight = FontWeight.Bold, + fontSize = dimensionResource(id = R.dimen.settings_title_text_size).value.sp ) ) diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml index 8e0e20365..6aed58682 100644 --- a/app/src/main/res/values-sw600dp/dimens.xml +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -103,6 +103,7 @@ 44dp 48sp 34sp + 34sp 88dp 44dp @@ -116,6 +117,10 @@ 44dp 48sp 76dp + 42sp + 24sp + 24sp + 24sp 56dp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index b5b9953e0..aaa9b1fc8 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -42,7 +42,7 @@ 16dp 88dp 40dp - 34sp + 28sp 8dp 24dp 80dp @@ -182,6 +182,9 @@ 8dp 30sp 80dp + 18sp + 18sp + 18sp 45dp 8dp 10dp @@ -214,6 +217,10 @@ 16dp 8dp 16dp + 24sp 24sp 24sp