fix(ui): restore outlined visual on EditPostView + ForwardZapTo TextFields

Migrating the mention TextFields to BasicTextField(state) routed them
through ThinPaddingTextField, which uses TextFieldDefaults.DecorationBox
(filled, surfaceVariant container). That replaced the original
OutlinedTextField look — visible rounded border + transparent inside —
with a filled gray rectangle. On ForwardZapTo the change was especially
visible since the original outlined border was the only visual cue that
the search row was an input.

Add OutlinedThinPaddingTextField, a sibling of ThinPaddingTextField that
wraps the same TextFieldState pipeline with
OutlinedTextFieldDefaults.DecorationBox + Container. Same thin padding,
same onTextChanged/inputTransformation/outputTransformation surface, but
genuinely outlined (notched label, transparent inside).

  - ForwardZapTo: switch to the new component, drop the
    focused/unfocusedIndicatorColor=Transparent overrides — defaults give
    the proper outlined border the original OutlinedTextField had.
  - EditPostView.MessageField: switch to the new component and use
    OutlinedTextFieldDefaults.colors(focusedBorderColor=Transparent,
    unfocusedBorderColor=Transparent) to keep the inner border invisible
    so the existing 1dp Modifier.border(surface, RoundedCornerShape(8.dp))
    remains the only visible frame, matching the original.
This commit is contained in:
Claude
2026-05-20 22:21:20 +00:00
parent b2959dcb75
commit 05d958d182
3 changed files with 185 additions and 15 deletions
@@ -42,10 +42,10 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -80,7 +80,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
import com.vitorpamplona.amethyst.ui.components.BechLink
import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.ui.note.NoteCompose
@@ -380,7 +380,7 @@ private fun MessageField(postViewModel: EditPostViewModel) {
}
}
ThinPaddingTextField(
OutlinedThinPaddingTextField(
state = postViewModel.message,
onTextChanged = { postViewModel.onMessageChanged() },
inputTransformation = MentionPreservingInputTransformation,
@@ -409,9 +409,9 @@ private fun MessageField(postViewModel: EditPostViewModel) {
)
},
colors =
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
OutlinedTextFieldDefaults.colors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
@@ -0,0 +1,177 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText
/**
* Outlined sibling of [ThinPaddingTextField]. Wraps the new [TextFieldState] API
* with [OutlinedTextFieldDefaults.DecorationBox] so callers that want the
* Material3 outlined look (visible rounded border, transparent inside, notched
* label) can use the same `outputTransformation` / `inputTransformation`
* mention pipeline as the filled `ThinPaddingTextField`.
*/
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun OutlinedThinPaddingTextField(
state: TextFieldState,
modifier: Modifier = Modifier,
onTextChanged: (() -> Unit)? = null,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
inputTransformation: InputTransformation? = null,
outputTransformation: OutputTransformation? = null,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
prefix: @Composable (() -> Unit)? = null,
suffix: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource? = null,
shape: Shape = OutlinedTextFieldDefaults.shape,
colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
contentPadding: PaddingValues =
OutlinedTextFieldDefaults.contentPadding(
start = 10.dp,
top = 12.dp,
end = 10.dp,
bottom = 12.dp,
),
) {
@Suppress("NAME_SHADOWING")
val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
if (onTextChanged != null) {
val callback by rememberUpdatedState(onTextChanged)
LaunchedEffect(state) {
snapshotFlow { state.text }
.collect { callback() }
}
}
val textColor =
textStyle.color.takeOrElse {
val focused by interactionSource.collectIsFocusedAsState()
when {
!enabled -> MaterialTheme.colorScheme.placeholderText
isError -> MaterialTheme.colorScheme.onSurface
focused -> MaterialTheme.colorScheme.onSurface
else -> MaterialTheme.colorScheme.onSurface
}
}
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
val lineLimits =
if (singleLine) {
TextFieldLineLimits.SingleLine
} else {
TextFieldLineLimits.MultiLine(minLines, maxLines)
}
CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
BasicTextField(
state = state,
modifier =
modifier
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = 36.dp,
),
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
keyboardOptions = keyboardOptions,
lineLimits = lineLimits,
interactionSource = interactionSource,
inputTransformation = inputTransformation,
outputTransformation = outputTransformation,
decorator = { innerTextField ->
OutlinedTextFieldDefaults.DecorationBox(
value = state.text.toString(),
visualTransformation = VisualTransformation.None,
innerTextField = innerTextField,
placeholder = placeholder,
label = label,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
singleLine = singleLine,
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
colors = colors,
contentPadding = contentPadding,
container = {
OutlinedTextFieldDefaults.Container(
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
colors = colors,
shape = shape,
)
},
)
},
)
}
}
@@ -31,11 +31,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
@@ -44,7 +42,7 @@ import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.MentionPreservingInputTransformation
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagOutputTransformation
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.ui.components.OutlinedThinPaddingTextField
import com.vitorpamplona.amethyst.ui.note.BaseUserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -126,7 +124,7 @@ fun ForwardZapTo(
}
}
ThinPaddingTextField(
OutlinedThinPaddingTextField(
state = postViewModel.forwardZapToEditting,
onTextChanged = postViewModel::onForwardZapTextChanged,
inputTransformation = MentionPreservingInputTransformation,
@@ -140,11 +138,6 @@ fun ForwardZapTo(
)
},
singleLine = true,
colors =
TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
}