diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/DensityExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/DensityExtensions.kt index a3ce332e05..9ad637acfb 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/DensityExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/DensityExtensions.kt @@ -31,6 +31,12 @@ fun IntSize.toDpSize(density: Density): DpSize = with(density) { ) } +/** + * A function for converting [Dp] to pixels within a composable function. + */ +@Composable +fun Dp.toPx(): Float = with(LocalDensity.current) { this@toPx.toPx() } + /** * Converts a [Dp] value to [TextUnit] with [TextUnitType.Sp] as its type. * diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/StringExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/StringExtensions.kt index b1c77950e2..0c423de004 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/StringExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/StringExtensions.kt @@ -5,10 +5,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.rememberTextMeasurer import androidx.core.graphics.toColorInt import java.net.URI import java.util.Locale +import kotlin.math.floor /** * This character takes up no space but can be used to ensure a string is not empty. It can also @@ -79,6 +82,36 @@ fun String.withVisualTransformation( visualTransformation.filter(toAnnotatedString()).text } +/** + * Returns a new [String] that includes line breaks after [widthPx] worth of text. This is useful + * for long values that need to smoothly flow onto the next line without the OS inserting line + * breaks earlier at special characters. + * + * Note that the internal calculation used assumes that [monospacedTextStyle] is based on a + * monospaced font like Roboto Mono. + */ +@Composable +fun String.withLineBreaksAtWidth( + widthPx: Float, + monospacedTextStyle: TextStyle, +): String { + val measurer = rememberTextMeasurer() + return remember(this, widthPx, monospacedTextStyle) { + val characterSizePx = measurer + .measure("*", monospacedTextStyle) + .size + .width + val perLineCharacterLimit = floor(widthPx / characterSizePx).toInt() + if (widthPx > 0) { + this + .chunked(perLineCharacterLimit) + .joinToString(separator = "\n") + } else { + this + } + } +} + /** * Returns the [String] as an [AnnotatedString]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenReadOnlyTextFieldWithActions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenReadOnlyTextFieldWithActions.kt index 5e1882ca82..0f6b04e662 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenReadOnlyTextFieldWithActions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenReadOnlyTextFieldWithActions.kt @@ -33,6 +33,8 @@ import com.x8bit.bitwarden.R * @param singleLine when `true`, this text field becomes a single line that horizontally scrolls * instead of wrapping onto multiple lines. * @param textStyle An optional style that may be used to override the default used. + * @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling + * an entire line before breaking. `false` by default. * @param visualTransformation Transforms the visual representation of the input [value]. * @param actions A lambda containing the set of actions (usually icons or similar) to display * next to the text field. This lambda extends [RowScope], @@ -45,6 +47,7 @@ fun BitwardenReadOnlyTextFieldWithActions( modifier: Modifier = Modifier, singleLine: Boolean = true, textStyle: TextStyle? = null, + shouldAddCustomLineBreaks: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, actions: @Composable RowScope.() -> Unit = {}, ) { @@ -67,6 +70,7 @@ fun BitwardenReadOnlyTextFieldWithActions( value = value, onValueChange = {}, textStyle = textStyle, + shouldAddCustomLineBreaks = shouldAddCustomLineBreaks, visualTransformation = visualTransformation, ) BitwardenRowOfActions(actions) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt index 444f6e48d0..e57c44b296 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenTextField.kt @@ -7,11 +7,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.ui.platform.base.util.toPx +import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth import com.x8bit.bitwarden.ui.platform.components.model.IconResource /** @@ -30,6 +38,8 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconResource * @param readOnly `true` if the input should be read-only and not accept user interactions. * @param enabled Whether or not the text field is enabled. * @param textStyle An optional style that may be used to override the default used. + * @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling + * an entire line before breaking. `false` by default. * @param visualTransformation Transforms the visual representation of the input [value]. * @param keyboardType the preferred type of keyboard input. */ @@ -46,14 +56,29 @@ fun BitwardenTextField( readOnly: Boolean = false, enabled: Boolean = true, textStyle: TextStyle? = null, + shouldAddCustomLineBreaks: Boolean = false, keyboardType: KeyboardType = KeyboardType.Text, visualTransformation: VisualTransformation = VisualTransformation.None, ) { + var widthPx by remember { mutableStateOf(0) } + + val currentTextStyle = textStyle ?: LocalTextStyle.current + val formattedText = if (shouldAddCustomLineBreaks) { + value.withLineBreaksAtWidth( + // Adjust for built in padding + widthPx = widthPx - 16.dp.toPx(), + monospacedTextStyle = currentTextStyle, + ) + } else { + value + } + OutlinedTextField( - modifier = modifier, + modifier = modifier + .onGloballyPositioned { widthPx = it.size.width }, enabled = enabled, label = { Text(text = label) }, - value = value, + value = formattedText, leadingIcon = leadingIconResource?.let { iconResource -> { Icon( @@ -77,7 +102,7 @@ fun BitwardenTextField( onValueChange = onValueChange, singleLine = singleLine, readOnly = readOnly, - textStyle = textStyle ?: LocalTextStyle.current, + textStyle = currentTextStyle, keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType), visualTransformation = visualTransformation, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/util/NonLetterColorVisualTransformation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/util/NonLetterColorVisualTransformation.kt index fed4824451..c7d3db719f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/util/NonLetterColorVisualTransformation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/util/NonLetterColorVisualTransformation.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.components.util import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -15,11 +16,16 @@ import androidx.compose.ui.text.withStyle * applying different colors to the digits and special characters, letters will remain unaffected. */ @Composable -fun nonLetterColorVisualTransformation(): VisualTransformation = - NonLetterColorVisualTransformation( - digitColor = MaterialTheme.colorScheme.primary, - specialCharacterColor = MaterialTheme.colorScheme.error, - ) +fun nonLetterColorVisualTransformation(): VisualTransformation { + val digitColor = MaterialTheme.colorScheme.primary + val specialCharacterColor = MaterialTheme.colorScheme.error + return remember(digitColor, specialCharacterColor) { + NonLetterColorVisualTransformation( + digitColor = digitColor, + specialCharacterColor = specialCharacterColor, + ) + } +} /** * Alters the visual output of the text in an input field. diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt index 93685764d3..4581e65bd8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreen.kt @@ -309,6 +309,7 @@ private fun GeneratedStringItem( ) }, textStyle = LocalNonMaterialTypography.current.sensitiveInfoSmall, + shouldAddCustomLineBreaks = true, visualTransformation = nonLetterColorVisualTransformation(), modifier = Modifier.padding(horizontal = 16.dp), ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt index 2c5b5194a4..c802a5128d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/tools/feature/generator/passwordhistory/PasswordHistoryListItem.kt @@ -10,16 +10,23 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R +import com.x8bit.bitwarden.ui.platform.base.util.withLineBreaksAtWidth import com.x8bit.bitwarden.ui.platform.base.util.withVisualTransformation import com.x8bit.bitwarden.ui.platform.components.util.nonLetterColorVisualTransformation import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme +import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography /** * A composable function for displaying a password history list item. @@ -44,12 +51,21 @@ fun PasswordHistoryListItem( ) { Column(modifier = Modifier.weight(1f)) { + var widthPx by remember(label) { mutableStateOf(0) } + val textStyle = LocalNonMaterialTypography.current.sensitiveInfoMedium + val formattedText = label.withLineBreaksAtWidth( + widthPx = widthPx.toFloat(), + monospacedTextStyle = textStyle, + ) Text( - text = label.withVisualTransformation( + text = formattedText.withVisualTransformation( visualTransformation = nonLetterColorVisualTransformation(), ), - style = MaterialTheme.typography.bodyLarge, + style = textStyle, color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { widthPx = it.size.width }, ) Text(