Skip to content

Commit

Permalink
Fix Scrollbars [part 14/n]
Browse files Browse the repository at this point in the history
* Fix native crash when TextField/TextArea don't have a fixed width
* Redo the entire BTF2-based InputField implementation, better
* Expose all the new parameters that BTF2-based InputField has to the
  users of TextField and TextArea
* Fix TextArea decoration and scrollbars
  • Loading branch information
rock3r committed Aug 23, 2024
1 parent ea7e175 commit a601251
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 194 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -968,7 +968,7 @@ private fun readTextFieldStyle(): TextFieldStyle {
TextFieldMetrics(
cornerSize = CornerSize(DarculaUIUtil.COMPONENT_ARC.dp / 2),
contentPadding = PaddingValues(horizontal = 9.dp, vertical = 2.dp),
minSize = DpSize(minimumSize.width, minimumSize.height),
minSize = DpSize(144.dp, minimumSize.height),
borderWidth = DarculaUIUtil.LW.dp,
),
)
Expand Down
4 changes: 2 additions & 2 deletions ui/api/ui.api
Original file line number Diff line number Diff line change
Expand Up @@ -880,13 +880,13 @@ public final class org/jetbrains/jewel/ui/component/TabsKt {
}

public final class org/jetbrains/jewel/ui/component/TextAreaKt {
public static final fun TextArea (Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/ui/Modifier;ZZLorg/jetbrains/jewel/ui/Outline;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;ILorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/Modifier;ZLorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;III)V
public static final fun TextArea (Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/foundation/text/input/InputTransformation;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/input/KeyboardActionHandler;Landroidx/compose/foundation/text/input/TextFieldLineLimits;Lkotlin/jvm/functions/Function2;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Lorg/jetbrains/jewel/ui/Outline;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/text/input/OutputTransformation;ZLandroidx/compose/foundation/ScrollState;Lorg/jetbrains/jewel/ui/component/styling/ScrollbarStyle;Landroidx/compose/runtime/Composer;III)V
public static final fun TextArea (Landroidx/compose/ui/text/input/TextFieldValue;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZZLkotlin/jvm/functions/Function2;ZLorg/jetbrains/jewel/ui/Outline;Landroidx/compose/ui/text/input/VisualTransformation;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ILkotlin/jvm/functions/Function1;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;III)V
public static final fun TextArea (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZZLorg/jetbrains/jewel/ui/Outline;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/ui/text/input/VisualTransformation;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;ILkotlin/jvm/functions/Function1;Lorg/jetbrains/jewel/ui/component/styling/TextAreaStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;III)V
}

public final class org/jetbrains/jewel/ui/component/TextFieldKt {
public static final fun TextField (Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/ui/Modifier;ZZLorg/jetbrains/jewel/ui/Outline;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/foundation/text/KeyboardOptions;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/runtime/Composer;III)V
public static final fun TextField (Landroidx/compose/foundation/text/input/TextFieldState;Landroidx/compose/ui/Modifier;ZZLandroidx/compose/foundation/text/input/InputTransformation;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/input/KeyboardActionHandler;Lkotlin/jvm/functions/Function2;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Lorg/jetbrains/jewel/ui/Outline;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/foundation/text/input/OutputTransformation;ZLandroidx/compose/runtime/Composer;III)V
public static final fun TextField (Landroidx/compose/ui/text/input/TextFieldValue;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZZLorg/jetbrains/jewel/ui/Outline;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/ui/text/input/VisualTransformation;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/runtime/Composer;III)V
public static final fun TextField (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;ZZLorg/jetbrains/jewel/ui/Outline;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ZLandroidx/compose/ui/text/input/VisualTransformation;Landroidx/compose/foundation/text/KeyboardOptions;Landroidx/compose/foundation/text/KeyboardActions;Lkotlin/jvm/functions/Function1;Lorg/jetbrains/jewel/ui/component/styling/TextFieldStyle;Landroidx/compose/foundation/interaction/MutableInteractionSource;Landroidx/compose/runtime/Composer;III)V
}
Expand Down
159 changes: 59 additions & 100 deletions ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InputField.kt
Original file line number Diff line number Diff line change
@@ -1,36 +1,34 @@
package org.jetbrains.jewel.ui.component

import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.KeyboardActionHandler
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldDecorator
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
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.SolidColor
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Density
import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval
import org.jetbrains.jewel.foundation.Stroke
import org.jetbrains.jewel.foundation.modifier.border
Expand All @@ -42,57 +40,53 @@ import org.jetbrains.jewel.foundation.state.CommonStateBitMask.Pressed
import org.jetbrains.jewel.foundation.state.FocusableComponentState
import org.jetbrains.jewel.ui.Outline
import org.jetbrains.jewel.ui.component.styling.InputFieldStyle
import org.jetbrains.jewel.ui.component.styling.ScrollbarStyle
import org.jetbrains.jewel.ui.focusOutline
import org.jetbrains.jewel.ui.outline
import org.jetbrains.jewel.ui.util.thenIf

@Suppress("DuplicatedCode") // The dupe is deprecated and is scheduled for removal
@Composable
internal fun InputField(
state: TextFieldState,
modifier: Modifier,
enabled: Boolean,
readOnly: Boolean,
outline: Outline,
undecorated: Boolean,
inputTransformation: InputTransformation?,
textStyle: TextStyle,
keyboardOptions: KeyboardOptions,
singleLine: Boolean,
maxLines: Int,
onKeyboardAction: KeyboardActionHandler?,
lineLimits: TextFieldLineLimits,
onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)?,
interactionSource: MutableInteractionSource,
style: InputFieldStyle,
textStyle: TextStyle,
scrollbarStyle: ScrollbarStyle?,
showScrollbar: Boolean,
modifier: Modifier,
decorationBox: @Composable (innerTextField: @Composable () -> Unit, state: InputFieldState) -> Unit,
outline: Outline,
outputTransformation: OutputTransformation?,
decorator: TextFieldDecorator?,
scrollState: ScrollState,
) {
var inputState by remember(interactionSource) {
mutableStateOf(InputFieldState.of(enabled = enabled))
}
remember(enabled) { inputState = inputState.copy(enabled = enabled) }
var inputFieldState by remember(interactionSource) { mutableStateOf(InputFieldState.of(enabled = enabled)) }
remember(enabled) { inputFieldState = inputFieldState.copy(enabled = enabled) }

LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> inputState = inputState.copy(focused = true)
is FocusInteraction.Unfocus -> inputState = inputState.copy(focused = false)
is FocusInteraction.Focus -> inputFieldState = inputFieldState.copy(focused = true)
is FocusInteraction.Unfocus -> inputFieldState = inputFieldState.copy(focused = false)
}
}
}

val colors = style.colors
val backgroundColor by colors.backgroundFor(inputState)
val backgroundColor by colors.backgroundFor(inputFieldState)
val shape = RoundedCornerShape(style.metrics.cornerSize)

val isUndecorated = decorator == null
val backgroundModifier =
Modifier.thenIf(!undecorated && backgroundColor.isSpecified) {
background(backgroundColor, shape)
}
Modifier.thenIf(!isUndecorated && backgroundColor.isSpecified) { background(backgroundColor, shape) }

val borderColor by style.colors.borderFor(inputState)
val borderColor by style.colors.borderFor(inputFieldState)
val hasNoOutline = outline == Outline.None
val borderModifier =
Modifier.thenIf(!undecorated && borderColor.isSpecified && hasNoOutline) {
Modifier.thenIf(!isUndecorated && borderColor.isSpecified && hasNoOutline) {
border(
alignment = Stroke.Alignment.Center,
width = style.metrics.borderWidth,
Expand All @@ -101,68 +95,44 @@ internal fun InputField(
)
}

val contentColor by colors.contentFor(inputState)
val contentColor by colors.contentFor(inputFieldState)
val mergedTextStyle = textStyle.copy(color = contentColor)
val caretColor by colors.caretFor(inputState)

val lineLimits =
when {
singleLine -> SingleLine
else -> MultiLine(maxLines)
}
val caretColor by colors.caretFor(inputFieldState)

val scrollState = rememberScrollState()
val canScroll by remember {
derivedStateOf {
scrollState.canScrollBackward || scrollState.canScrollForward
}
}

Box(
modifier = modifier
.then(backgroundModifier)
.then(borderModifier)
.thenIf(!undecorated && hasNoOutline) { focusOutline(inputState, shape) }
.outline(inputState, outline, shape, Stroke.Alignment.Center),
) {
BasicTextField(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterStart)
.thenIf(canScroll && showScrollbar) { padding(end = 12.dp) },
state = state,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
cursorBrush = SolidColor(caretColor),
keyboardOptions = keyboardOptions,
lineLimits = lineLimits,
interactionSource = interactionSource,
decorator = { innerTextField: @Composable () -> Unit -> decorationBox(innerTextField, inputState) },
scrollState = scrollState,
)

if (showScrollbar && scrollbarStyle != null) {
VerticalScrollbar(
scrollState = scrollState,
modifier = Modifier.align(Alignment.CenterEnd),
interactionSource = interactionSource,
style = scrollbarStyle
)
}
}
BasicTextField(
state = state,
modifier =
modifier
.then(backgroundModifier)
.then(borderModifier)
.thenIf(!isUndecorated && hasNoOutline) { focusOutline(inputFieldState, shape) }
.outline(inputFieldState, outline, shape, Stroke.Alignment.Center),
enabled = enabled,
readOnly = readOnly,
inputTransformation = inputTransformation,
textStyle = mergedTextStyle,
keyboardOptions = keyboardOptions,
onKeyboardAction = onKeyboardAction,
lineLimits = lineLimits,
onTextLayout = onTextLayout,
interactionSource = interactionSource,
cursorBrush = SolidColor(caretColor),
outputTransformation = outputTransformation,
decorator = decorator,
scrollState = scrollState,
)
}

@ScheduledForRemoval(inVersion = "Before 1.0")
@Suppress("DuplicatedCode") // This is deprecated and will be removed before 1.0
@Deprecated("Please use InputField(state) instead. If you want to observe text changes, use snapshotFlow { state.text }")
@Deprecated("NO")
@ScheduledForRemoval
@Composable
internal fun InputField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier,
enabled: Boolean,
outline: Outline,
readOnly: Boolean,
outline: Outline,
undecorated: Boolean,
visualTransformation: VisualTransformation,
keyboardOptions: KeyboardOptions,
Expand All @@ -173,7 +143,6 @@ internal fun InputField(
interactionSource: MutableInteractionSource,
style: InputFieldStyle,
textStyle: TextStyle,
modifier: Modifier,
decorationBox: @Composable (innerTextField: @Composable () -> Unit, state: InputFieldState) -> Unit,
) {
var inputState by remember(interactionSource) { mutableStateOf(InputFieldState.of(enabled = enabled)) }
Expand All @@ -193,9 +162,7 @@ internal fun InputField(
val shape = RoundedCornerShape(style.metrics.cornerSize)

val backgroundModifier =
Modifier.thenIf(!undecorated && backgroundColor.isSpecified) {
background(backgroundColor, shape)
}
Modifier.thenIf(!undecorated && backgroundColor.isSpecified) { background(backgroundColor, shape) }

val borderColor by style.colors.borderFor(inputState)
val hasNoOutline = outline == Outline.None
Expand Down Expand Up @@ -233,9 +200,8 @@ internal fun InputField(
interactionSource = interactionSource,
singleLine = singleLine,
maxLines = maxLines,
decorationBox = @Composable { innerTextField: @Composable () -> Unit ->
decorationBox(innerTextField, inputState)
},
decorationBox =
@Composable { innerTextField: @Composable () -> Unit -> decorationBox(innerTextField, inputState) },
)
}

Expand Down Expand Up @@ -263,14 +229,7 @@ public value class InputFieldState(public val state: ULong) : FocusableComponent
pressed: Boolean = isPressed,
hovered: Boolean = isHovered,
active: Boolean = isActive,
): InputFieldState =
of(
enabled = enabled,
focused = focused,
pressed = pressed,
hovered = hovered,
active = active,
)
): InputFieldState = of(enabled = enabled, focused = focused, pressed = pressed, hovered = hovered, active = active)

override fun toString(): String =
"${javaClass.simpleName}(isEnabled=$isEnabled, isFocused=$isFocused, " +
Expand All @@ -290,7 +249,7 @@ public value class InputFieldState(public val state: ULong) : FocusableComponent
(if (focused) Focused else 0UL) or
(if (hovered) Hovered else 0UL) or
(if (pressed) Pressed else 0UL) or
(if (active) Active else 0UL),
(if (active) Active else 0UL)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
Expand Down Expand Up @@ -64,6 +66,33 @@ public fun VerticallyScrollableContainer(
}
}

@Composable
internal fun TextAreaScrollableContainer(
scrollState: ScrollState,
style: ScrollbarStyle,
contentModifier: Modifier,
content: @Composable () -> Unit,
) {
var keepVisible by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()

ScrollableContainerImpl(
verticalScrollbar = {
VerticalScrollbar(
scrollState,
style = style,
modifier = Modifier.pointerHoverIcon(PointerIcon.Default),
keepVisible = keepVisible,
)
},
horizontalScrollbar = null,
modifier = Modifier.withKeepVisible(style.scrollbarVisibility.lingerDuration, scope) { keepVisible = it },
scrollbarStyle = style,
) {
Box(contentModifier.layoutId(ID_CONTENT)) { content() }
}
}

@Composable
public fun VerticallyScrollableContainer(
scrollState: LazyListState,
Expand Down
Loading

0 comments on commit a601251

Please sign in to comment.