BIT-1260: Fix line breaks for generated passwords (#424)

This commit is contained in:
Brian Yencho 2023-12-20 13:01:49 -06:00 committed by Álison Fernandes
parent 719bf52420
commit 44afc44829
7 changed files with 101 additions and 10 deletions

View file

@ -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.
*

View file

@ -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].
*/

View file

@ -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)

View file

@ -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,
)

View file

@ -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.

View file

@ -309,6 +309,7 @@ private fun GeneratedStringItem(
)
},
textStyle = LocalNonMaterialTypography.current.sensitiveInfoSmall,
shouldAddCustomLineBreaks = true,
visualTransformation = nonLetterColorVisualTransformation(),
modifier = Modifier.padding(horizontal = 16.dp),
)

View file

@ -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(