From 6131b37b4ab084f8110fc2e5cb0d5c34a12f0911 Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Mon, 15 Jan 2024 07:33:31 +0100 Subject: [PATCH] Fix TextField parameter ordering and sizing shenanigans (#285) * Fix TextField parameters order * Fix TextField layout: ignore placeholders Also use IconButton in TextField sample --- .../samples/ideplugin/ComponentShowcaseTab.kt | 2 +- .../standalone/view/component/TextFields.kt | 117 +++++++++++------- .../jetbrains/jewel/ui/component/TextField.kt | 43 +++---- 3 files changed, 92 insertions(+), 70 deletions(-) diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt index 37de7255e..f801a42e8 100644 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt @@ -104,8 +104,8 @@ private fun RowScope.ColumnOne() { TextField( value = textFieldValue, onValueChange = { textFieldValue = it }, - placeholder = { Text("Write something...") }, modifier = Modifier.width(200.dp), + placeholder = { Text("Write something...") }, ) var checked by remember { mutableStateOf(false) } diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/TextFields.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/TextFields.kt index 30f6b6112..1bdb06dfc 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/TextFields.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/TextFields.kt @@ -5,30 +5,36 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.HoverInteraction -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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.graphics.Color import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon -import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.intui.standalone.styling.dark +import org.jetbrains.jewel.intui.standalone.styling.defaults +import org.jetbrains.jewel.intui.standalone.styling.light import org.jetbrains.jewel.samples.standalone.StandaloneSampleIcons import org.jetbrains.jewel.samples.standalone.viewmodel.View import org.jetbrains.jewel.ui.Outline import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.component.IconButton import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.TextField +import org.jetbrains.jewel.ui.component.styling.IconButtonColors +import org.jetbrains.jewel.ui.component.styling.IconButtonMetrics +import org.jetbrains.jewel.ui.component.styling.IconButtonStyle +import org.jetbrains.jewel.ui.painter.hints.Stateful import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider @Composable @@ -60,9 +66,11 @@ fun TextFields() { ) { var text1 by remember { mutableStateOf("") } TextField( - text1, - { text1 = it }, - enabled = true, + value = text1, + onValueChange = { text1 = it }, + placeholder = { + Text("With leading icon") + }, leadingIcon = { Icon( resource = "icons/search.svg", @@ -71,62 +79,75 @@ fun TextFields() { modifier = Modifier.size(16.dp), ) }, - placeholder = { - Text("With leading icon") - }, ) var text2 by remember { mutableStateOf("") } TextField( - text2, - { text2 = it }, - enabled = true, - trailingIcon = { - AnimatedVisibility( - visible = text2.isNotEmpty(), - enter = fadeIn() + slideInHorizontally { it / 2 }, - exit = fadeOut() + slideOutHorizontally { it / 2 }, - ) { - CloseIconButton { text2 = "" } - } - }, + value = text2, + onValueChange = { text2 = it }, placeholder = { Text("With trailing button") }, + trailingIcon = { + CloseIconButton(text2.isNotEmpty()) { text2 = "" } + }, ) } } @Composable -private fun CloseIconButton(onClick: () -> Unit) { - val interactionSource = remember { MutableInteractionSource() } - var hovered by remember { mutableStateOf(false) } +private fun CloseIconButton( + isVisible: Boolean, + onClick: () -> Unit, +) { + Box(Modifier.size(16.dp)) { + AnimatedVisibility( + visible = isVisible, + enter = fadeIn() + slideInHorizontally { it / 2 }, + exit = fadeOut() + slideOutHorizontally { it / 2 }, + ) { + // TODO replace when IconButton supports no-background style + val isDark = JewelTheme.isDark - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { - when (it) { - is HoverInteraction.Enter -> hovered = true - is HoverInteraction.Exit -> hovered = false + val colors = noBackgroundIconButtonColors(isDark) + val style = remember(isDark, colors) { + IconButtonStyle(colors, IconButtonMetrics.defaults()) } - } - } - val closeIconProvider = rememberResourcePainterProvider("icons/close.svg", StandaloneSampleIcons::class.java) - val closeIcon by closeIconProvider.getPainter() + IconButton( + onClick, + style = style, + modifier = Modifier.pointerHoverIcon(PointerIcon.Default), + ) { state -> + val painterProvider = + rememberResourcePainterProvider("icons/close.svg", StandaloneSampleIcons::class.java) + val painter by painterProvider.getPainter(Stateful(state)) - val hoveredCloseIconProvider = - rememberResourcePainterProvider("icons/closeHovered.svg", StandaloneSampleIcons::class.java) - val hoveredCloseIcon by hoveredCloseIconProvider.getPainter() + Icon(painter, contentDescription = "Clear") + } + } + } +} - Icon( - painter = if (hovered) hoveredCloseIcon else closeIcon, - contentDescription = "Clear", - modifier = Modifier - .pointerHoverIcon(PointerIcon.Default) - .clickable( - interactionSource = interactionSource, - indication = null, - role = Role.Button, - ) { onClick() }, +@Composable +private fun noBackgroundIconButtonColors(isDark: Boolean) = if (isDark) { + IconButtonColors.dark( + background = Color.Unspecified, + backgroundDisabled = Color.Unspecified, + backgroundSelected = Color.Unspecified, + backgroundSelectedActivated = Color.Unspecified, + backgroundFocused = Color.Unspecified, + backgroundPressed = Color.Unspecified, + backgroundHovered = Color.Unspecified, + ) +} else { + IconButtonColors.light( + background = Color.Unspecified, + backgroundDisabled = Color.Unspecified, + backgroundSelected = Color.Unspecified, + backgroundSelectedActivated = Color.Unspecified, + backgroundFocused = Color.Unspecified, + backgroundPressed = Color.Unspecified, + backgroundHovered = Color.Unspecified, ) } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TextField.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TextField.kt index 5c49ffb35..fdaac2a2b 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TextField.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/TextField.kt @@ -45,8 +45,8 @@ public fun TextField( readOnly: Boolean = false, outline: Outline = Outline.None, placeholder: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, undecorated: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, @@ -139,8 +139,8 @@ public fun TextField( textStyle = textStyle, placeholderTextColor = style.colors.placeholder, placeholder = if (value.text.isEmpty()) placeholder else null, - trailingIcon = trailingIcon, leadingIcon = leadingIcon, + trailingIcon = trailingIcon, ) } } @@ -152,8 +152,8 @@ private fun TextFieldDecorationBox( textStyle: TextStyle, placeholderTextColor: Color, placeholder: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, ) { Layout( modifier = modifier, @@ -206,72 +206,73 @@ private fun TextFieldDecorationBox( ?.measure(placeholderConstraints) val width = calculateWidth( - trailingPlaceable, leadingPlaceable, + trailingPlaceable, textFieldPlaceable, - placeholderPlaceable, incomingConstraints, ) val height = calculateHeight( - trailingPlaceable, leadingPlaceable, + trailingPlaceable, textFieldPlaceable, - placeholderPlaceable, incomingConstraints, ) layout(width, height) { - place(height, width, trailingPlaceable, leadingPlaceable, textFieldPlaceable, placeholderPlaceable) + place( + height = height, + width = width, + leadingPlaceable = leadingPlaceable, + trailingPlaceable = trailingPlaceable, + textFieldPlaceable = textFieldPlaceable, + placeholderPlaceable = placeholderPlaceable, + ) } } } private fun calculateWidth( - trailingPlaceable: Placeable?, leadingPlaceable: Placeable?, + trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, - placeholderPlaceable: Placeable?, constraints: Constraints, ): Int { - val middleSection = - maxOf(textFieldPlaceable.width, placeholderPlaceable?.width ?: 0) + val middleSection = textFieldPlaceable.width val wrappedWidth = middleSection + (trailingPlaceable?.width ?: 0) + (leadingPlaceable?.width ?: 0) return max(wrappedWidth, constraints.minWidth) } private fun calculateHeight( - trailingPlaceable: Placeable?, leadingPlaceable: Placeable?, + trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, - placeholderPlaceable: Placeable?, constraints: Constraints, ): Int = maxOf( textFieldPlaceable.height, - placeholderPlaceable?.height ?: 0, - trailingPlaceable?.height ?: 0, leadingPlaceable?.height ?: 0, + trailingPlaceable?.height ?: 0, constraints.minHeight, ) private fun Placeable.PlacementScope.place( height: Int, width: Int, - trailingPlaceable: Placeable?, leadingPlaceable: Placeable?, + trailingPlaceable: Placeable?, textFieldPlaceable: Placeable, placeholderPlaceable: Placeable?, ) { // placed center vertically and to the end edge horizontally - trailingPlaceable?.placeRelative( - width - trailingPlaceable.width, - Alignment.CenterVertically.align(trailingPlaceable.height, height), - ) leadingPlaceable?.placeRelative( 0, Alignment.CenterVertically.align(leadingPlaceable.height, height), ) + trailingPlaceable?.placeRelative( + width - trailingPlaceable.width, + Alignment.CenterVertically.align(trailingPlaceable.height, height), + ) // placed center vertically textFieldPlaceable.placeRelative(