diff options
| author | Kalle Lindström <karl.lindstrom@mullvad.net> | 2026-04-20 09:40:28 +0200 |
|---|---|---|
| committer | Kalle Lindström <karl.lindstrom@mullvad.net> | 2026-04-20 09:40:28 +0200 |
| commit | 819de86a77c006a180d41707d14fa1be6ef87bb3 (patch) | |
| tree | 75b679826731f37db7e14a7e9c12dba969e11d6c | |
| parent | bc64759830f77c0f14051e868e72223a007de07a (diff) | |
| parent | fbfdbb487348413da1b9a6828e3bab99fadff843 (diff) | |
| download | mullvadvpn-819de86a77c006a180d41707d14fa1be6ef87bb3.tar.xz mullvadvpn-819de86a77c006a180d41707d14fa1be6ef87bb3.zip | |
Merge branch 'add-monospace-font-droid-2499'
5 files changed, 76 insertions, 18 deletions
diff --git a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt index a2e4de851f..6438ea166c 100644 --- a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt +++ b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/AccountNumberView.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.feature.account.impl import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily import net.mullvad.mullvadvpn.lib.common.util.groupPasswordModeWithSpaces import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces @@ -16,6 +17,7 @@ fun AccountNumberView( if (obfuscateWithPasswordDots) accountNumber.groupPasswordModeWithSpaces() else accountNumber.groupWithSpaces(), modifier = modifier, + fontFamily = FontFamily.Monospace, whenMissing = MissingPolicy.SHOW_SPINNER, ) } diff --git a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt index 14cf201bb9..0d1831c0de 100644 --- a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt +++ b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/InformationView.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.tooling.preview.Preview import net.mullvad.mullvadvpn.lib.ui.designsystem.MullvadCircularProgressIndicatorSmall import net.mullvad.mullvadvpn.lib.ui.theme.Dimens @@ -28,6 +29,7 @@ fun InformationView( modifier: Modifier = Modifier, whenMissing: MissingPolicy = MissingPolicy.SHOW_VIEW, maxLines: Int = 1, + fontFamily: FontFamily? = null, ) { return if (content.isNotEmpty()) { AutoResizeText( @@ -37,6 +39,7 @@ fun InformationView( maxTextSize = MaterialTheme.typography.titleMedium.fontSize, maxLines = maxLines, modifier = modifier.padding(vertical = Dimens.smallPadding), + fontFamily = fontFamily, ) } else { when (whenMissing) { diff --git a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt index 333bf95f94..c3c775d585 100644 --- a/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt +++ b/android/lib/feature/account/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/account/impl/Text.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp @@ -28,6 +29,7 @@ fun AutoResizeText( style: TextStyle = LocalTextStyle.current, maxLines: Int = Int.MAX_VALUE, color: Color = MaterialTheme.colorScheme.onSurface, + fontFamily: FontFamily? = null, ) { var adjustedFontSize by remember { mutableFloatStateOf(maxTextSize.value) } var isReadyToDraw by remember { mutableStateOf(false) } @@ -38,6 +40,7 @@ fun AutoResizeText( style = style, color = color, fontSize = adjustedFontSize.sp, + fontFamily = fontFamily, onTextLayout = { if (it.didOverflowHeight && isReadyToDraw.not()) { val nextFontSizeValue = adjustedFontSize - textSizeStep.value diff --git a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt index 4aaedd3a97..d2bc1d9b1a 100644 --- a/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt +++ b/android/lib/feature/home/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/home/impl/welcome/WelcomeScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -280,6 +281,7 @@ private fun AccountNumberRow(snackbarHostState: SnackbarHostState, state: Welcom text = state.accountNumber?.value?.groupWithSpaces() ?: "", modifier = Modifier.weight(1f), style = MaterialTheme.typography.headlineSmall, + fontFamily = FontFamily.Monospace, color = MaterialTheme.colorScheme.onSurface, ) diff --git a/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt b/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt index 000faa6f16..3662608e5e 100644 --- a/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt +++ b/android/lib/feature/login/impl/src/main/kotlin/net/mullvad/mullvadvpn/feature/login/impl/LoginScreen.kt @@ -46,6 +46,9 @@ import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.layout.AlignmentLine +import androidx.compose.ui.layout.FirstBaseline +import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalResources @@ -56,8 +59,10 @@ import androidx.compose.ui.semantics.contentType import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.text.style.TextOverflow @@ -157,6 +162,7 @@ fun Login( message = resources.getString(R.string.error_occurred) ) } + is ApiUnreachableInfoDialogResult.Success -> { when (it.arg.action) { LoginAction.LOGIN -> vm.login(state.accountNumberInput) @@ -170,14 +176,19 @@ fun Login( when (it) { LoginUiSideEffect.NavigateToWelcome -> navigator.navigate(WelcomeNavKey, clearBackStack = true) + is LoginUiSideEffect.NavigateToConnect -> navigator.navigate(ConnectNavKey, clearBackStack = true) + is LoginUiSideEffect.TooManyDevices -> navigator.navigate(DeviceListNavKey(it.accountNumber)) + LoginUiSideEffect.NavigateToOutOfTime -> navigator.navigate(OutOfTimeNavKey, clearBackStack = true) + LoginUiSideEffect.NavigateToCreateAccountConfirmation -> navigator.navigate(CreateAccountConfirmationNavKey) + LoginUiSideEffect.GenericError -> snackbarHostState.showSnackbarImmediately( message = resources.getString(R.string.error_occurred) @@ -366,30 +377,21 @@ private fun ColumnScope.LoginInput( accountNumberVisualTransformation(showPassword, if (showLastChar) 1 else 0), enabled = state.loginState is LoginState.Idle, colors = mullvadWhiteTextFieldColors(), - textStyle = MaterialTheme.typography.bodyLarge.copy(textDirection = TextDirection.Ltr), + textStyle = + MaterialTheme.typography.bodyLarge.copy( + textDirection = TextDirection.Ltr, + fontFamily = FontFamily.Monospace, + ), isError = state.loginState.isError(), ) AnimatedVisibility( visible = state.lastUsedAccount != null && state.loginState is LoginState.Idle ) { - val token = state.lastUsedAccount?.value.orEmpty() - val accountTransformation = - remember(showPassword) { - accountNumberVisualTransformation( - showPassword, - showLastX = ACCOUNT_NUMBER_CHUNK_SIZE, - ) - } - val transformedText = - remember(token, accountTransformation) { - accountTransformation.filter(AnnotatedString(token)).text - } - - // Since content is number we should always do Ltr CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { AccountDropDownItem( - accountNumber = transformedText.toString(), + accountNumber = state.lastUsedAccount?.value.orEmpty(), + showPassword = showPassword, onClick = { state.lastUsedAccount?.let { onAccountNumberChange(it.value) @@ -416,6 +418,7 @@ private fun LoginIcon(loginState: LoginState, modifier: Modifier = Modifier) { } else { // If view is Idle, we display empty box to keep the same size as other states } + is LoginState.Loading -> MullvadCircularProgressIndicatorLarge() LoginState.Success -> Image( @@ -436,8 +439,10 @@ private fun LoginState.title(): String = is LoginUiStateError.LoginError -> R.string.login_fail_title is LoginUiStateError.CreateAccountError -> R.string.create_account_fail_title + null -> R.string.log_in } + is LoginState.Loading -> R.string.logging_in_title LoginState.Success -> R.string.logged_in_title } @@ -472,6 +477,7 @@ private fun LoginState.supportingText( (loginUiStateError is LoginUiStateError.LoginError.ApiUnreachable || loginUiStateError is LoginUiStateError.CreateAccountError.ApiUnreachable) -> apiUnreachableText(loginUiStateError, onShowApiUnreachableDialog) + is LoginState.Idle -> { when (loginUiStateError) { LoginUiStateError.LoginError.InvalidCredentials -> R.string.login_fail_description @@ -479,16 +485,20 @@ private fun LoginState.supportingText( LoginUiStateError.LoginError.NoInternetConnection, LoginUiStateError.CreateAccountError.NoInternetConnection -> R.string.no_internet_connection + LoginUiStateError.LoginError.ApiUnreachable, LoginUiStateError.CreateAccountError.ApiUnreachable -> R.string.api_unreachable + LoginUiStateError.LoginError.TooManyAttempts, LoginUiStateError.CreateAccountError.TooManyAttempts -> R.string.login_error_too_many_attempts + is LoginUiStateError.LoginError.Unknown -> R.string.error_occurred LoginUiStateError.CreateAccountError.Unknown -> R.string.failed_to_create_account null -> null }?.toAnnotatedString() } + is LoginState.Loading.CreatingAccount -> R.string.creating_new_account.toAnnotatedString() is LoginState.Loading.LoggingIn -> R.string.logging_in_description.toAnnotatedString() LoginState.Success -> R.string.logged_in_description.toAnnotatedString() @@ -516,11 +526,21 @@ private fun Int.toAnnotatedString(): AnnotatedString = AnnotatedString(stringRes @Composable private fun AccountDropDownItem( modifier: Modifier = Modifier, + showPassword: Boolean, accountNumber: String, enabled: Boolean, onClick: () -> Unit, onDeleteClick: () -> Unit, ) { + val accountTransformation = + remember(showPassword) { + accountNumberVisualTransformation(showPassword, showLastX = ACCOUNT_NUMBER_CHUNK_SIZE) + } + val transformedText = + remember(accountNumber, accountTransformation) { + accountTransformation.filter(AnnotatedString(accountNumber)).text + } + Row( modifier = modifier @@ -534,6 +554,19 @@ private fun AccountDropDownItem( .height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically, ) { + + // Hack, our PASSWORD_UNICODE dot char changes the baseline height, so this workaround + // ensures we always place it at the same baseline + val textStyle = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace) + // Measure the digit baseline once to use as a fixed reference + val textMeasurer = rememberTextMeasurer() + val digitBaseline = + remember(textStyle) { + textMeasurer + .measure(text = AnnotatedString("0"), style = textStyle, maxLines = 1) + .firstBaseline + } + Box( modifier = Modifier.clickable(enabled = enabled, onClick = onClick) @@ -543,9 +576,24 @@ private fun AccountDropDownItem( contentAlignment = Alignment.CenterStart, ) { Text( - text = accountNumber, + text = transformedText, overflow = TextOverflow.Clip, - style = MaterialTheme.typography.bodyLarge, + style = textStyle, + maxLines = 1, + // Place text according to baseline so text does not jump as user hide/show password + modifier = + Modifier.layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + val actualBaseline = placeable[FirstBaseline] + // Shift the text so its baseline aligns with the digit baseline + val yOffset = + if (actualBaseline != AlignmentLine.Unspecified) { + digitBaseline.toInt() - actualBaseline + } else { + 0 + } + layout(placeable.width, placeable.height) { placeable.place(0, yOffset) } + }, ) } IconButton( |
