diff --git a/foundation/api/foundation.api b/foundation/api/foundation.api index d2af70d029..c3339e4998 100644 --- a/foundation/api/foundation.api +++ b/foundation/api/foundation.api @@ -831,8 +831,9 @@ public final class org/jetbrains/jewel/foundation/modifier/BorderKt { public static synthetic fun border-QWjY48E$default (Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/foundation/Stroke$Alignment;FJLandroidx/compose/ui/graphics/Shape;FILjava/lang/Object;)Landroidx/compose/ui/Modifier; } -public final class org/jetbrains/jewel/foundation/modifier/OnHoverModifierKt { +public final class org/jetbrains/jewel/foundation/modifier/PointerModifiersKt { public static final fun onHover (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier; + public static final fun onMove (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier; } public final class org/jetbrains/jewel/foundation/state/CommonStateBitMask { diff --git a/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/modifier/OnHoverModifier.kt b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/modifier/PointerModifiers.kt similarity index 57% rename from foundation/src/main/kotlin/org/jetbrains/jewel/foundation/modifier/OnHoverModifier.kt rename to foundation/src/main/kotlin/org/jetbrains/jewel/foundation/modifier/PointerModifiers.kt index b3b3851447..13249f5114 100644 --- a/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/modifier/OnHoverModifier.kt +++ b/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/modifier/PointerModifiers.kt @@ -1,8 +1,10 @@ package org.jetbrains.jewel.foundation.modifier import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.awtEventOrNull import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.pointerInput +import java.awt.event.MouseEvent public fun Modifier.onHover(onHover: (Boolean) -> Unit): Modifier = pointerInput(Unit) { @@ -16,3 +18,15 @@ public fun Modifier.onHover(onHover: (Boolean) -> Unit): Modifier = } } } + +public fun Modifier.onMove(onMove: (MouseEvent?) -> Unit): Modifier = + pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent() + when (event.type) { + PointerEventType.Move -> onMove(event.awtEventOrNull) + } + } + } + } diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeComboBox.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeComboBox.kt index 7968b10fd5..b3a8911d0a 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeComboBox.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeComboBox.kt @@ -53,7 +53,7 @@ internal fun readDefaultComboBoxStyle(): ComboBoxStyle { minSize = DpSize(minimumSize.width + arrowWidth, minimumSize.height), cornerSize = componentArc, contentPadding = retrieveInsetsAsPaddingValues("ComboBox.padding"), - popupContentPadding = PaddingValues(6.dp), + popupContentPadding = PaddingValues(vertical = 6.dp), borderWidth = DarculaUIUtil.LW.dp, maxPopupHeight = Dp.Unspecified, ), diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeLazyTree.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeLazyTree.kt index d306d53f14..85b88565ba 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeLazyTree.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeLazyTree.kt @@ -22,22 +22,23 @@ internal fun readLazyTreeStyle(): LazyTreeStyle { val selectedElementBackground = retrieveColorOrUnspecified("Tree.selectionBackground") val inactiveSelectedElementBackground = retrieveColorOrUnspecified("Tree.selectionInactiveBackground") - val colors = + val itemColors = SimpleListItemColors( content = normalContent, - contentFocused = normalContent, + contentActive = normalContent, contentSelected = selectedContent, - contentSelectedFocused = selectedContent, - backgroundFocused = Color.Transparent, + contentSelectedActive = selectedContent, + background = Color.Unspecified, + backgroundActive = Color.Unspecified, backgroundSelected = inactiveSelectedElementBackground, - backgroundSelectedFocused = selectedElementBackground, + backgroundSelectedActive = selectedElementBackground, ) val leftIndent = retrieveIntAsDpOrUnspecified("Tree.leftChildIndent").takeOrElse { 7.dp } val rightIndent = retrieveIntAsDpOrUnspecified("Tree.rightChildIndent").takeOrElse { 11.dp } return LazyTreeStyle( - colors = colors, + colors = itemColors, metrics = LazyTreeMetrics( indentSize = leftIndent + rightIndent, @@ -46,6 +47,7 @@ internal fun readLazyTreeStyle(): LazyTreeStyle { innerPadding = PaddingValues(horizontal = 12.dp), outerPadding = PaddingValues(4.dp), selectionBackgroundCornerSize = CornerSize(JBUI.CurrentTheme.Tree.ARC.dp / 2), + iconTextGap = 2.dp, ), elementMinHeight = retrieveIntAsDpOrUnspecified("Tree.rowHeight").takeOrElse { 24.dp }, chevronContentGap = 2.dp, // See com.intellij.ui.tree.ui.ClassicPainter.GAP diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeSimpleListItem.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeSimpleListItem.kt index 83ae5ce65d..e41b9a2f6f 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeSimpleListItem.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeSimpleListItem.kt @@ -1,10 +1,10 @@ package org.jetbrains.jewel.bridge.theme import androidx.compose.foundation.shape.CornerSize +import androidx.compose.ui.unit.dp import com.intellij.util.ui.JBUI import org.jetbrains.jewel.bridge.dp import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified -import org.jetbrains.jewel.bridge.retrieveInsetsAsPaddingValues import org.jetbrains.jewel.bridge.toPaddingValues import org.jetbrains.jewel.ui.component.styling.SimpleListItemColors import org.jetbrains.jewel.ui.component.styling.SimpleListItemMetrics @@ -15,18 +15,19 @@ internal fun readSimpleListItemStyle() = colors = SimpleListItemColors( background = retrieveColorOrUnspecified("ComboBox.background"), - backgroundFocused = retrieveColorOrUnspecified("ComboBox.selectionBackground"), + backgroundActive = retrieveColorOrUnspecified("ComboBox.background"), backgroundSelected = retrieveColorOrUnspecified("ComboBox.selectionBackground"), - backgroundSelectedFocused = retrieveColorOrUnspecified("ComboBox.selectionBackground"), + backgroundSelectedActive = retrieveColorOrUnspecified("ComboBox.selectionBackground"), content = retrieveColorOrUnspecified("ComboBox.foreground"), - contentFocused = retrieveColorOrUnspecified("ComboBox.foreground"), + contentActive = retrieveColorOrUnspecified("ComboBox.foreground"), contentSelected = retrieveColorOrUnspecified("ComboBox.foreground"), - contentSelectedFocused = retrieveColorOrUnspecified("ComboBox.foreground"), + contentSelectedActive = retrieveColorOrUnspecified("ComboBox.foreground"), ), metrics = SimpleListItemMetrics( - innerPadding = retrieveInsetsAsPaddingValues("ComboBox.padding"), + innerPadding = JBUI.CurrentTheme.PopupMenu.Selection.innerInsets().toPaddingValues(), outerPadding = JBUI.CurrentTheme.PopupMenu.Selection.outerInsets().toPaddingValues(), selectionBackgroundCornerSize = CornerSize(JBUI.CurrentTheme.PopupMenu.Selection.ARC.dp / 2), + iconTextGap = 2.dp, ), ) diff --git a/int-ui/int-ui-standalone/api/int-ui-standalone.api b/int-ui/int-ui-standalone/api/int-ui-standalone.api index fcd91a722e..9fb6c58f20 100644 --- a/int-ui/int-ui-standalone/api/int-ui-standalone.api +++ b/int-ui/int-ui-standalone/api/int-ui-standalone.api @@ -183,8 +183,8 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiDefaultInfo public final class org/jetbrains/jewel/intui/standalone/styling/IntUiDefaultSimpleListItemLazyTreeStyleFactory { public static final field $stable I public static final field INSTANCE Lorg/jetbrains/jewel/intui/standalone/styling/IntUiDefaultSimpleListItemLazyTreeStyleFactory; - public final fun dark-69fazGs (JJJJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; - public final fun light-69fazGs (JJJJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; + public final fun dark-oq7We08 (JJJJJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; + public final fun light-oq7We08 (JJJJJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiDefaultSuccessBannerColorFactory { @@ -297,8 +297,8 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeSty public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeIcons;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle; public static final fun defaults (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeIcons$Companion;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeIcons; public static synthetic fun defaults$default (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeIcons$Companion;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;Lorg/jetbrains/jewel/ui/icon/IconKey;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeIcons; - public static final fun defaults-hRm7RI8 (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics$Companion;FLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FF)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics; - public static synthetic fun defaults-hRm7RI8$default (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics$Companion;FLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics; + public static final fun defaults-FqMU-wI (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics$Companion;FLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFF)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics; + public static synthetic fun defaults-FqMU-wI$default (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics$Companion;FLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics; public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeMetrics;Lorg/jetbrains/jewel/ui/component/styling/LazyTreeIcons;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/LazyTreeStyle; } @@ -421,19 +421,29 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiSegmentedCo } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiSelectableLazyColumnStylingKt { - public static final fun dark-V6-xPKs (Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle$Companion;FJLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;JJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle; - public static final fun light-V6-xPKs (Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle$Companion;FJLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;JJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle; + public static final fun dark-DzVHIIc (Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle$Companion;FLorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle; + public static final fun light-DzVHIIc (Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle$Companion;FLorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/SelectableLazyColumnStyle; } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiSimpleListItemStylingKt { - public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; - public static final fun darkFullWidth-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; - public static synthetic fun darkFullWidth-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static synthetic fun dark$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static final fun dark-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; + public static synthetic fun dark-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; + public static final fun darkFullWidth (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static synthetic fun darkFullWidth$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; public static final fun default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static final fun default-M2VBTUQ (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics$Companion;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/shape/CornerSize;F)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics; + public static synthetic fun default-M2VBTUQ$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics$Companion;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/shape/CornerSize;FILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics; public static final fun fullWidth (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Landroidx/compose/runtime/Composer;I)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; - public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; - public static final fun lightFullWidth-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; - public static synthetic fun lightFullWidth-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static final fun fullWidth-M2VBTUQ (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics$Companion;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/shape/CornerSize;F)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics; + public static synthetic fun fullWidth-M2VBTUQ$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics$Companion;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/shape/CornerSize;FILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics; + public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static synthetic fun light$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static final fun light-iLRpYWo (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors$Companion;JJJJJJJJ)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; + public static synthetic fun light-iLRpYWo$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors$Companion;JJJJJJJJILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors; + public static final fun lightFullWidth (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; + public static synthetic fun lightFullWidth$default (Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle; } public final class org/jetbrains/jewel/intui/standalone/styling/IntUiSliderStylingKt { diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiComboBoxStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiComboBoxStyling.kt index dc129256df..c378d8bab1 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiComboBoxStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiComboBoxStyling.kt @@ -212,7 +212,7 @@ public fun ComboBoxMetrics.Companion.default( minSize: DpSize = DpSize(49.dp, 24.dp), cornerSize: CornerSize = CornerSize(4.dp), contentPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp), - popupContentPadding: PaddingValues = PaddingValues(6.dp), + popupContentPadding: PaddingValues = PaddingValues(vertical = 6.dp), borderWidth: Dp = 1.dp, maxPopupHeight: Dp = 200.dp, ): ComboBoxMetrics = @@ -231,7 +231,7 @@ public fun ComboBoxMetrics.Companion.undecorated( minSize: DpSize = DpSize(49.dp, 24.dp), cornerSize: CornerSize = CornerSize(4.dp), contentPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp), - popupContentPadding: PaddingValues = PaddingValues(6.dp), + popupContentPadding: PaddingValues = PaddingValues(vertical = 6.dp), borderWidth: Dp = 0.dp, maxPopupHeight: Dp = 200.dp, ): ComboBoxMetrics = diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt index 2fdcb4c443..088bbd2988 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiLazyTreeStyling.kt @@ -24,41 +24,45 @@ public object IntUiDefaultSimpleListItemLazyTreeStyleFactory { @Composable public fun light( content: Color = Color.Unspecified, - contentFocused: Color = content, + contentActive: Color = content, contentSelected: Color = content, - contentSelectedFocused: Color = content, - nodeBackgroundFocused: Color = Color.Unspecified, + contentSelectedActive: Color = content, + nodeBackground: Color = Color.Unspecified, + nodeBackgroundActive: Color = Color.Unspecified, nodeBackgroundSelected: Color = IntUiLightTheme.colors.gray(11), - nodeBackgroundSelectedFocused: Color = IntUiLightTheme.colors.blue(11), + nodeBackgroundSelectedActive: Color = IntUiLightTheme.colors.blue(11), ): SimpleListItemColors = SimpleListItemColors( - backgroundFocused = nodeBackgroundFocused, + background = nodeBackground, + backgroundActive = nodeBackgroundActive, backgroundSelected = nodeBackgroundSelected, - backgroundSelectedFocused = nodeBackgroundSelectedFocused, + backgroundSelectedActive = nodeBackgroundSelectedActive, content = content, - contentFocused = contentFocused, + contentActive = contentActive, contentSelected = contentSelected, - contentSelectedFocused = contentSelectedFocused, + contentSelectedActive = contentSelectedActive, ) @Composable public fun dark( content: Color = Color.Unspecified, - contentFocused: Color = content, + contentActive: Color = content, contentSelected: Color = content, - contentSelectedFocused: Color = content, - nodeBackgroundFocused: Color = Color.Unspecified, + contentSelectedActive: Color = content, + nodeBackground: Color = Color.Unspecified, + nodeBackgroundActive: Color = Color.Unspecified, nodeBackgroundSelected: Color = IntUiDarkTheme.colors.gray(4), - nodeBackgroundSelectedFocused: Color = IntUiDarkTheme.colors.blue(2), + nodeBackgroundSelectedActive: Color = IntUiDarkTheme.colors.blue(2), ): SimpleListItemColors = SimpleListItemColors( - backgroundFocused = nodeBackgroundFocused, + background = nodeBackground, + backgroundActive = nodeBackgroundActive, backgroundSelected = nodeBackgroundSelected, - backgroundSelectedFocused = nodeBackgroundSelectedFocused, + backgroundSelectedActive = nodeBackgroundSelectedActive, content = content, - contentFocused = contentFocused, + contentActive = contentActive, contentSelected = contentSelected, - contentSelectedFocused = contentSelectedFocused, + contentSelectedActive = contentSelectedActive, ) } @@ -82,6 +86,7 @@ public fun LazyTreeMetrics.Companion.defaults( elementPadding: PaddingValues = PaddingValues(horizontal = 12.dp), elementContentPadding: PaddingValues = PaddingValues(4.dp), elementMinHeight: Dp = 24.dp, + elementIconTextGap: Dp = 4.dp, chevronContentGap: Dp = 2.dp, ): LazyTreeMetrics = LazyTreeMetrics( @@ -93,6 +98,7 @@ public fun LazyTreeMetrics.Companion.defaults( innerPadding = elementContentPadding, outerPadding = elementPadding, selectionBackgroundCornerSize = elementBackgroundCornerSize, + iconTextGap = elementIconTextGap, ), ) diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSelectableLazyColumnStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSelectableLazyColumnStyling.kt index e85f53f2bd..ec751e0d27 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSelectableLazyColumnStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSelectableLazyColumnStyling.kt @@ -1,12 +1,8 @@ package org.jetbrains.jewel.intui.standalone.styling -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.shape.CornerSize import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme import org.jetbrains.jewel.ui.component.styling.SelectableLazyColumnStyle import org.jetbrains.jewel.ui.component.styling.SimpleListItemColors import org.jetbrains.jewel.ui.component.styling.SimpleListItemMetrics @@ -15,61 +11,13 @@ import org.jetbrains.jewel.ui.component.styling.SimpleListItemStyle @Composable public fun SelectableLazyColumnStyle.Companion.light( itemHeight: Dp = 24.dp, - selectionBackgroundColor: Color = IntUiLightTheme.colors.blue(11), - selectionBackgroundCornerRadius: CornerSize = CornerSize(0.dp), - itemContentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - itemPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - contentColor: Color = Color.Unspecified, - selectedContentColor: Color = Color.Unspecified, -): SelectableLazyColumnStyle = - SelectableLazyColumnStyle( - itemHeight, - SimpleListItemStyle( - SimpleListItemColors( - background = selectionBackgroundColor, - backgroundFocused = selectionBackgroundColor, - backgroundSelected = selectionBackgroundColor, - backgroundSelectedFocused = selectionBackgroundColor, - content = contentColor, - contentFocused = contentColor, - contentSelected = contentColor, - contentSelectedFocused = selectedContentColor, - ), - SimpleListItemMetrics( - innerPadding = itemContentPadding, - outerPadding = itemPadding, - selectionBackgroundCornerSize = selectionBackgroundCornerRadius, - ), - ), - ) + itemColors: SimpleListItemColors = SimpleListItemColors.light(), + itemMetrics: SimpleListItemMetrics = SimpleListItemMetrics.default(), +): SelectableLazyColumnStyle = SelectableLazyColumnStyle(itemHeight, SimpleListItemStyle(itemColors, itemMetrics)) @Composable public fun SelectableLazyColumnStyle.Companion.dark( itemHeight: Dp = 24.dp, - selectionBackgroundColor: Color = IntUiLightTheme.colors.blue(2), - selectionBackgroundCornerRadius: CornerSize = CornerSize(0.dp), - itemContentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - itemPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - contentColor: Color = Color.Unspecified, - selectedContentColor: Color = Color.Unspecified, -): SelectableLazyColumnStyle = - SelectableLazyColumnStyle( - itemHeight, - SimpleListItemStyle( - SimpleListItemColors( - background = selectionBackgroundColor, - backgroundFocused = selectionBackgroundColor, - backgroundSelected = selectionBackgroundColor, - backgroundSelectedFocused = selectionBackgroundColor, - content = contentColor, - contentFocused = contentColor, - contentSelected = contentColor, - contentSelectedFocused = selectedContentColor, - ), - SimpleListItemMetrics( - innerPadding = itemContentPadding, - outerPadding = itemPadding, - selectionBackgroundCornerSize = selectionBackgroundCornerRadius, - ), - ), - ) + itemColors: SimpleListItemColors = SimpleListItemColors.dark(), + itemMetrics: SimpleListItemMetrics = SimpleListItemMetrics.default(), +): SelectableLazyColumnStyle = SelectableLazyColumnStyle(itemHeight, SimpleListItemStyle(itemColors, itemMetrics)) diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSimpleListItemStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSimpleListItemStyling.kt index 67de254b46..1592b7c5d2 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSimpleListItemStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiSimpleListItemStyling.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CornerSize import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme import org.jetbrains.jewel.ui.component.styling.SimpleListItemColors @@ -12,93 +13,84 @@ import org.jetbrains.jewel.ui.component.styling.SimpleListItemMetrics import org.jetbrains.jewel.ui.component.styling.SimpleListItemStyle @Composable -public fun SimpleListItemStyle.Companion.default(): SimpleListItemStyle = - if (isSystemInDarkTheme()) { - dark() - } else { - light() - } +public fun SimpleListItemStyle.Companion.default(): SimpleListItemStyle = if (isSystemInDarkTheme()) dark() else light() + +public fun SimpleListItemStyle.Companion.light( + colors: SimpleListItemColors = SimpleListItemColors.light(), + metrics: SimpleListItemMetrics = SimpleListItemMetrics.default(), +): SimpleListItemStyle = SimpleListItemStyle(colors, metrics) + +public fun SimpleListItemStyle.Companion.dark( + colors: SimpleListItemColors = SimpleListItemColors.dark(), + metrics: SimpleListItemMetrics = SimpleListItemMetrics.default(), +): SimpleListItemStyle = SimpleListItemStyle(colors, metrics) @Composable public fun SimpleListItemStyle.Companion.fullWidth(): SimpleListItemStyle = - if (isSystemInDarkTheme()) { - darkFullWidth() - } else { - lightFullWidth() - } + if (isSystemInDarkTheme()) darkFullWidth() else lightFullWidth() public fun SimpleListItemStyle.Companion.lightFullWidth( + colors: SimpleListItemColors = SimpleListItemColors.light(), + metrics: SimpleListItemMetrics = SimpleListItemMetrics.fullWidth(), +): SimpleListItemStyle = SimpleListItemStyle(colors, metrics) + +public fun SimpleListItemStyle.Companion.darkFullWidth( + colors: SimpleListItemColors = SimpleListItemColors.light(), + metrics: SimpleListItemMetrics = SimpleListItemMetrics.fullWidth(), +): SimpleListItemStyle = SimpleListItemStyle(colors, metrics) + +public fun SimpleListItemColors.Companion.light( background: Color = Color.Unspecified, - backgroundFocused: Color = IntUiLightTheme.colors.blue(11), + backgroundActive: Color = Color.Unspecified, backgroundSelected: Color = IntUiLightTheme.colors.blue(11), - backgroundSelectedFocused: Color = IntUiLightTheme.colors.blue(11), + backgroundSelectedActive: Color = IntUiLightTheme.colors.blue(11), content: Color = Color.Unspecified, - contentFocused: Color = Color.Unspecified, + contentActive: Color = Color.Unspecified, contentSelected: Color = Color.Unspecified, - contentSelectedFocused: Color = Color.Unspecified, -): SimpleListItemStyle = - SimpleListItemStyle( - SimpleListItemColors( - background = background, - backgroundFocused = backgroundFocused, - backgroundSelected = backgroundSelected, - backgroundSelectedFocused = backgroundSelectedFocused, - content = content, - contentFocused = contentFocused, - contentSelected = contentSelected, - contentSelectedFocused = contentSelectedFocused, - ), - SimpleListItemMetrics( - innerPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp), - outerPadding = PaddingValues(), - selectionBackgroundCornerSize = CornerSize(0.dp), - ), + contentSelectedActive: Color = Color.Unspecified, +): SimpleListItemColors = + SimpleListItemColors( + background = background, + backgroundActive = backgroundActive, + backgroundSelected = backgroundSelected, + backgroundSelectedActive = backgroundSelectedActive, + content = content, + contentActive = contentActive, + contentSelected = contentSelected, + contentSelectedActive = contentSelectedActive, ) -public fun SimpleListItemStyle.Companion.darkFullWidth( +public fun SimpleListItemColors.Companion.dark( background: Color = Color.Unspecified, - backgroundFocused: Color = IntUiLightTheme.colors.blue(2), + backgroundActive: Color = IntUiLightTheme.colors.blue(2), backgroundSelected: Color = IntUiLightTheme.colors.blue(2), - backgroundSelectedFocused: Color = IntUiLightTheme.colors.blue(2), + backgroundSelectedActive: Color = IntUiLightTheme.colors.blue(2), content: Color = Color.Unspecified, - contentFocused: Color = Color.Unspecified, + contentActive: Color = Color.Unspecified, contentSelected: Color = Color.Unspecified, - contentSelectedFocused: Color = Color.Unspecified, -): SimpleListItemStyle = - SimpleListItemStyle( - SimpleListItemColors( - background = background, - backgroundFocused = backgroundFocused, - backgroundSelected = backgroundSelected, - backgroundSelectedFocused = backgroundSelectedFocused, - content = content, - contentFocused = contentFocused, - contentSelected = contentSelected, - contentSelectedFocused = contentSelectedFocused, - ), - SimpleListItemMetrics( - innerPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp), - outerPadding = PaddingValues(), - selectionBackgroundCornerSize = CornerSize(0.dp), - ), + contentSelectedActive: Color = Color.Unspecified, +): SimpleListItemColors = + SimpleListItemColors( + background = background, + backgroundActive = backgroundActive, + backgroundSelected = backgroundSelected, + backgroundSelectedActive = backgroundSelectedActive, + content = content, + contentActive = contentActive, + contentSelected = contentSelected, + contentSelectedActive = contentSelectedActive, ) -public fun SimpleListItemStyle.Companion.light(): SimpleListItemStyle = - SimpleListItemStyle( - SimpleListItemStyle.lightFullWidth().colors, - SimpleListItemMetrics( - innerPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp), - outerPadding = PaddingValues(horizontal = 7.dp, vertical = 1.dp), - selectionBackgroundCornerSize = CornerSize(4.dp), - ), - ) +public fun SimpleListItemMetrics.Companion.default( + innerPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp), + outerPadding: PaddingValues = PaddingValues(horizontal = 7.dp, vertical = 1.dp), + selectionBackgroundCornerSize: CornerSize = CornerSize(4.dp), + iconTextGap: Dp = 3.dp, +): SimpleListItemMetrics = SimpleListItemMetrics(innerPadding, outerPadding, selectionBackgroundCornerSize, iconTextGap) -public fun SimpleListItemStyle.Companion.dark(): SimpleListItemStyle = - SimpleListItemStyle( - SimpleListItemStyle.darkFullWidth().colors, - SimpleListItemMetrics( - innerPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp), - outerPadding = PaddingValues(horizontal = 7.dp, vertical = 1.dp), - selectionBackgroundCornerSize = CornerSize(4.dp), - ), - ) +public fun SimpleListItemMetrics.Companion.fullWidth( + innerPadding: PaddingValues = PaddingValues(horizontal = 6.dp, vertical = 2.dp), + outerPadding: PaddingValues = PaddingValues(), + selectionBackgroundCornerSize: CornerSize = CornerSize(0.dp), + iconTextGap: Dp = 3.dp, +): SimpleListItemMetrics = SimpleListItemMetrics(innerPadding, outerPadding, selectionBackgroundCornerSize, iconTextGap) diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt index dd63e8799e..7547e6017e 100644 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/SwingComparisonTabPanel.kt @@ -3,7 +3,6 @@ package org.jetbrains.jewel.samples.ideplugin import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -50,16 +49,15 @@ import org.jetbrains.jewel.bridge.JewelComposePanel import org.jetbrains.jewel.bridge.medium import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.DefaultButton +import org.jetbrains.jewel.ui.component.EditableListComboBox import org.jetbrains.jewel.ui.component.Icon import org.jetbrains.jewel.ui.component.ListComboBox -import org.jetbrains.jewel.ui.component.ListItemState import org.jetbrains.jewel.ui.component.OutlinedButton import org.jetbrains.jewel.ui.component.SimpleListItem import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.TextArea import org.jetbrains.jewel.ui.component.TextField import org.jetbrains.jewel.ui.component.Typography -import org.jetbrains.jewel.ui.theme.simpleListItemStyle import org.jetbrains.jewel.ui.theme.textAreaStyle internal class SwingComparisonTabPanel : BorderLayoutPanel() { @@ -129,7 +127,7 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { row("Long text (Swing)") { text(longText, maxLineLength = 100) } row("Long text (Compose)") { compose { - BoxWithConstraints { + Box { Text( longText, modifier = @@ -150,7 +148,7 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { } row("Titles (Compose)") { compose { - BoxWithConstraints { + Box { val style = Typography.h1TextStyle() Text( "This will wrap over a couple rows", @@ -274,23 +272,23 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { var selectedComboBox1: String? by remember { mutableStateOf(comboBoxItems.first()) } var selectedComboBox2: String? by remember { mutableStateOf(comboBoxItems.first()) } var selectedComboBox3: String? by remember { mutableStateOf(comboBoxItems.first()) } + var selectedComboBox4: String? by remember { mutableStateOf(comboBoxItems.first()) } Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Column { Text("Not editable") - Text(text = "Selected item: $selectedComboBox2") + Text(text = "Selected item: $selectedComboBox1") ListComboBox( items = comboBoxItems, modifier = Modifier.width(200.dp), - isEditable = false, - onSelectedItemChange = { selectedComboBox2 = it }, - listItemContent = { item, isSelected, isFocused, isItemHovered, isListHovered -> + onSelectedItemChange = { _, text -> selectedComboBox1 = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, - style = JewelTheme.simpleListItemStyle, - state = ListItemState(isSelected, isListHovered, isItemHovered), - contentDescription = item, + isSelected = isSelected, + isActive = isActive, + iconContentDescription = item, ) }, ) @@ -303,15 +301,14 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { ListComboBox( items = comboBoxItems, modifier = Modifier.width(200.dp), - isEditable = false, isEnabled = false, - onSelectedItemChange = { selectedComboBox2 = it }, - listItemContent = { item, isSelected, isFocused, isItemHovered, isListHovered -> + onSelectedItemChange = { _, text -> selectedComboBox2 = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, - style = JewelTheme.simpleListItemStyle, - state = ListItemState(isSelected, isListHovered, isItemHovered), - contentDescription = item, + isSelected = isSelected, + isActive = isActive, + iconContentDescription = item, ) }, ) @@ -319,18 +316,18 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { Column { Text("Editable") - Text(text = "Selected item: $selectedComboBox1") - ListComboBox( + Text(text = "Selected item: $selectedComboBox3") + EditableListComboBox( items = comboBoxItems, modifier = Modifier.width(200.dp), maxPopupHeight = 150.dp, - onSelectedItemChange = { selectedComboBox1 = it }, - listItemContent = { item, isSelected, isFocused, isItemHovered, isListHovered -> + onSelectedItemChange = { _, text -> selectedComboBox3 = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, - style = JewelTheme.simpleListItemStyle, - state = ListItemState(isSelected, isListHovered, isItemHovered), - contentDescription = item, + isSelected = isSelected, + isActive = isActive, + iconContentDescription = item, ) }, ) @@ -338,19 +335,18 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { Column { Text("Editable + disabled") - Text(text = "Selected item: $selectedComboBox3") - ListComboBox( + Text(text = "Selected item: $selectedComboBox4") + EditableListComboBox( items = comboBoxItems, modifier = Modifier.width(200.dp), - isEditable = true, isEnabled = false, - onSelectedItemChange = { selectedComboBox3 = it }, - listItemContent = { item, isSelected, isFocused, isItemHovered, isListHovered -> + onSelectedItemChange = { _, text -> selectedComboBox4 = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, - style = JewelTheme.simpleListItemStyle, - state = ListItemState(isSelected, isListHovered, isItemHovered), - contentDescription = item, + isSelected = isSelected, + isActive = isActive, + iconContentDescription = item, ) }, ) diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt index 8738de61c2..58bd7aac78 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/component/Dropdowns.kt @@ -11,19 +11,18 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import kotlin.random.Random -import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.Outline import org.jetbrains.jewel.ui.component.Dropdown +import org.jetbrains.jewel.ui.component.EditableListComboBox import org.jetbrains.jewel.ui.component.ListComboBox -import org.jetbrains.jewel.ui.component.ListItemState import org.jetbrains.jewel.ui.component.SimpleListItem import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.Typography import org.jetbrains.jewel.ui.component.separator import org.jetbrains.jewel.ui.icons.AllIconsKeys -import org.jetbrains.jewel.ui.theme.simpleListItemStyle @Composable fun Dropdowns() { @@ -183,64 +182,66 @@ fun Dropdowns() { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text(text = "ComboBoxes", style = Typography.h1TextStyle()) - Text(text = "Selected item: $selectedComboBox1") Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Column { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("Enabled and Editable") - Text(text = "Selected item: $selectedComboBox1") - ListComboBox( + + Text(text = "Selected item: $selectedComboBox1", maxLines = 1, overflow = TextOverflow.Ellipsis) + + EditableListComboBox( items = comboBoxItems, modifier = Modifier.width(200.dp), maxPopupHeight = 150.dp, - onSelectedItemChange = { selectedComboBox1 = it }, - listItemContent = { item, isSelected, _, isItemHovered, isPreviewSelection -> + onSelectedItemChange = { _, text -> selectedComboBox1 = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, - state = ListItemState(isSelected, isItemHovered, isPreviewSelection), - modifier = Modifier, - style = JewelTheme.simpleListItemStyle, - contentDescription = item, + isSelected = isSelected, + isActive = isActive, + iconContentDescription = item, ) }, ) } - Column { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("Enabled") - Text(text = "Selected item: $selectedComboBox2") + + Text(text = "Selected item: $selectedComboBox2", maxLines = 1, overflow = TextOverflow.Ellipsis) ListComboBox( items = comboBoxItems, modifier = Modifier.width(200.dp), - isEditable = false, maxPopupHeight = 150.dp, - onSelectedItemChange = { selectedComboBox2 = it }, - listItemContent = { item, isSelected, isFocused, isItemHovered, isPreviewSelection -> + onSelectedItemChange = { _, text -> selectedComboBox2 = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, - state = ListItemState(isSelected, isItemHovered, isPreviewSelection), - style = JewelTheme.simpleListItemStyle, - contentDescription = item, + isSelected = isSelected, + isActive = isActive, + iconContentDescription = item, ) }, ) } - Column { + + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("Disabled") - Text(text = "Selected item: $selectedComboBox3") + + Text(text = "Selected item: $selectedComboBox3", maxLines = 1, overflow = TextOverflow.Ellipsis) + ListComboBox( items = comboBoxItems, modifier = Modifier.width(200.dp), - isEditable = false, isEnabled = false, - onSelectedItemChange = { selectedComboBox3 = it }, - listItemContent = { item, isSelected, _, isItemHovered, isPreviewSelection -> + onSelectedItemChange = { _, text -> selectedComboBox3 = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, - state = ListItemState(isSelected, isItemHovered, isPreviewSelection), - style = JewelTheme.simpleListItemStyle, - contentDescription = item, + isSelected = isSelected, + isActive = isActive, + iconContentDescription = item, ) }, ) diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt index 73252dad60..1c745a36e9 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/markdown/MarkdownEditor.kt @@ -26,12 +26,10 @@ import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.Divider import org.jetbrains.jewel.ui.component.ListComboBox -import org.jetbrains.jewel.ui.component.ListItemState import org.jetbrains.jewel.ui.component.OutlinedButton import org.jetbrains.jewel.ui.component.SimpleListItem import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.component.TextArea -import org.jetbrains.jewel.ui.theme.simpleListItemStyle @Composable internal fun MarkdownEditor(state: TextFieldState, modifier: Modifier = Modifier) { @@ -77,20 +75,13 @@ private fun ControlsRow(modifier: Modifier = Modifier, onLoadMarkdown: (String) ListComboBox( items = comboBoxItems, modifier = Modifier.width(170.dp).padding(end = 2.dp), - isEditable = false, maxPopupHeight = 150.dp, - onSelectedItemChange = { - selected = it + onSelectedItemChange = { _, text -> + selected = text onLoadMarkdown(if (selected == "Jewel readme") JewelReadme else MarkdownCatalog) }, - listItemContent = { item, isSelected, _, isItemHovered, isPreviewSelection -> - SimpleListItem( - text = item, - state = ListItemState(isSelected, isItemHovered, isPreviewSelection), - modifier = Modifier, - style = JewelTheme.simpleListItemStyle, - contentDescription = item, - ) + itemContent = { item, isSelected, isActive -> + SimpleListItem(text = item, isSelected = isSelected, isActive = isActive) }, ) } diff --git a/ui-tests/build.gradle.kts b/ui-tests/build.gradle.kts index be8b65a7a5..e4cbcd8c27 100644 --- a/ui-tests/build.gradle.kts +++ b/ui-tests/build.gradle.kts @@ -16,4 +16,3 @@ dependencies { testImplementation(compose.desktop.uiTestJUnit4) testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") } } - diff --git a/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/Assertions.kt b/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/Assertions.kt new file mode 100644 index 0000000000..de199cb17d --- /dev/null +++ b/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/Assertions.kt @@ -0,0 +1,13 @@ +package org.jetbrains.jewel.ui.component + +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.assert +import androidx.compose.ui.text.AnnotatedString + +fun SemanticsNodeInteraction.assertEditableTextEquals(expected: String): SemanticsNodeInteraction = + assert(SemanticsMatcher.expectValue(SemanticsProperties.EditableText, AnnotatedString(expected))) + +fun SemanticsNodeInteraction.assertEditableTextEquals(expected: AnnotatedString): SemanticsNodeInteraction = + assert(SemanticsMatcher.expectValue(SemanticsProperties.EditableText, expected)) diff --git a/ui-tests/src/test/kotlin/ListComboBoxUiTest.kt b/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/ListComboBoxUiTest.kt similarity index 69% rename from ui-tests/src/test/kotlin/ListComboBoxUiTest.kt rename to ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/ListComboBoxUiTest.kt index 85fe0011e3..8eaad8c3cf 100644 --- a/ui-tests/src/test/kotlin/ListComboBoxUiTest.kt +++ b/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/ListComboBoxUiTest.kt @@ -1,3 +1,5 @@ +package org.jetbrains.jewel.ui.component + import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size @@ -10,6 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.key.Key import androidx.compose.ui.platform.testTag import androidx.compose.ui.test.ExperimentalTestApi @@ -20,6 +23,8 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsSelected +import androidx.compose.ui.test.assertTextContains +import androidx.compose.ui.test.assertTextEquals import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.isDisplayed @@ -31,16 +36,13 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.performMouseInput import androidx.compose.ui.test.performTextClearance import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.pressKey import androidx.compose.ui.unit.dp -import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme -import org.jetbrains.jewel.ui.component.EditableComboBox -import org.jetbrains.jewel.ui.component.ListComboBox -import org.jetbrains.jewel.ui.component.ListItemState -import org.jetbrains.jewel.ui.component.SimpleListItem -import org.jetbrains.jewel.ui.theme.simpleListItemStyle +import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -49,7 +51,7 @@ class ListComboBoxUiTest { @get:Rule val composeRule = createComposeRule() private val popupMenu: SemanticsNodeInteraction - get() = composeRule.onNode(hasTestTag("Jewel.ComboBox.PopupMenu")) + get() = composeRule.onNodeWithTag("Jewel.ComboBox.PopupMenu") private val chevronContainer: SemanticsNodeInteraction get() = composeRule.onNodeWithTag("Jewel.ComboBox.ChevronContainer", useUnmergedTree = true) @@ -69,14 +71,14 @@ class ListComboBoxUiTest { @Test fun `when disable clicking the chevron container doesn't open the popup`() { - injectComboBox(FocusRequester(), isEditable = false, isEnabled = false) + injectComboBox(FocusRequester(), isEnabled = false) chevronContainer.assertExists().assertHasNoClickAction().performClick() popupMenu.assertDoesNotExist() } @Test fun `when editable clicking chevron twice opens and closed the popup`() { - injectComboBox(FocusRequester(), isEditable = true, isEnabled = true) + injectEditableComboBox(FocusRequester(), isEnabled = true) chevronContainer.assertHasClickAction().performClick() popupMenu.assertExists().isDisplayed() @@ -90,9 +92,12 @@ class ListComboBoxUiTest { val focusRequester = FocusRequester() composeRule.setContent { IntUiTheme { - Box(modifier = Modifier.size(20.dp).focusRequester(focusRequester).testTag("Pre-Box").focusable(true)) + Box( + modifier = + Modifier.Companion.size(20.dp).focusRequester(focusRequester).testTag("Pre-Box").focusable(true) + ) EditableComboBox( - modifier = Modifier.width(140.dp).testTag("ComboBox"), + modifier = Modifier.Companion.width(140.dp).testTag("ComboBox"), inputTextFieldState = rememberTextFieldState("Item 1"), popupContent = { /* ... */ }, ) @@ -102,15 +107,15 @@ class ListComboBoxUiTest { composeRule.onNodeWithTag("Pre-Box").assertIsDisplayed().assertIsFocused() composeRule.onNodeWithTag("Pre-Box").performKeyInput { - keyDown(Key.Tab) - keyUp(Key.Tab) + keyDown(Key.Companion.Tab) + keyUp(Key.Companion.Tab) } composeRule.onNodeWithText("Item 1").assertIsDisplayed().assertIsFocused() composeRule.onNodeWithText("Item 1").performKeyInput { - keyDown(Key.Tab) - keyUp(Key.Tab) + keyDown(Key.Companion.Tab) + keyUp(Key.Companion.Tab) } composeRule.onNodeWithTag("Pre-Box").assertIsFocused() @@ -118,20 +123,20 @@ class ListComboBoxUiTest { @Test fun `when not-editable both Box and TextField are focused`() { - notEditableFocusedComboBox() + focusedComboBox() composeRule.onNodeWithText("Item 1").assertIsDisplayed().assertIsFocused() } @Test fun `when not-editable click opens popup`() { - val comboBox = notEditableFocusedComboBox() + val comboBox = focusedComboBox() comboBox.performClick() composeRule.onNodeWithText("Item 2").isDisplayed() } @Test fun `when not-editable click on comboBox opens popup`() { - val comboBox = notEditableFocusedComboBox() + val comboBox = focusedComboBox() comboBox.performClick() popupMenu.isDisplayed() @@ -139,7 +144,7 @@ class ListComboBoxUiTest { @Test fun `when not-editable double click on comboBox opens and closes popup`() { - val comboBox = notEditableFocusedComboBox() + val comboBox = focusedComboBox() comboBox.performClick() popupMenu.isDisplayed() @@ -152,42 +157,42 @@ class ListComboBoxUiTest { @Test fun `when not-editable pressing spacebar opens popup`() { - val comboBox = notEditableFocusedComboBox() + val comboBox = focusedComboBox() popupMenu.assertDoesNotExist() comboBox.performKeyInput { - keyDown(Key.Spacebar) - keyUp(Key.Spacebar) + keyDown(Key.Companion.Spacebar) + keyUp(Key.Companion.Spacebar) } popupMenu.isDisplayed() } - // Reference: https://youtrack.jetbrains.com/issue/CMP-3710 - // @Test - // fun `when editable pressing spacebar does not open popup`() { - // val comboBox = editableComboBox() - // popupMenu.assertDoesNotExist() - // - // textField.assertIsFocused().isDisplayed() - // textField.assertTextContains("Item 1") - // textField.assertIsFocused().performKeyInput { - // keyDown(Key.Spacebar) - // keyUp(Key.Spacebar) - // } - // textField.assertTextEquals("Item 1 ") - // popupMenu.assertDoesNotExist() - // } + @Ignore("Due to https://youtrack.jetbrains.com/issue/CMP-3710") + @Test + fun `when editable pressing spacebar does not open popup`() { + injectEditableComboBox(FocusRequester(), isEnabled = true) + popupMenu.assertDoesNotExist() + + textField.assertIsFocused().isDisplayed() + textField.assertTextContains("Item 1") + textField.assertIsFocused().performKeyInput { + keyDown(Key.Companion.Spacebar) + keyUp(Key.Companion.Spacebar) + } + textField.assertTextEquals("Item 1 ") + popupMenu.assertDoesNotExist() + } @Test fun `when not-editable pressing enter does not open popup`() { - val comboBox = notEditableFocusedComboBox() + val comboBox = focusedComboBox() popupMenu.assertDoesNotExist() comboBox.performKeyInput { - keyDown(Key.Enter) - keyUp(Key.Enter) + keyDown(Key.Companion.Enter) + keyUp(Key.Companion.Enter) } composeRule.onNodeWithText("Item 1").isNotDisplayed() popupMenu.assertDoesNotExist() @@ -200,8 +205,8 @@ class ListComboBoxUiTest { popupMenu.assertDoesNotExist() comboBox.performKeyInput { - keyDown(Key.Enter) - keyUp(Key.Enter) + keyDown(Key.Companion.Enter) + keyUp(Key.Companion.Enter) } composeRule.onNodeWithText("Item 1").assertIsDisplayed() popupMenu.assertDoesNotExist() @@ -209,7 +214,7 @@ class ListComboBoxUiTest { @Test fun `when editable, textField is displayed and can receive input`() { - injectComboBox(FocusRequester(), isEditable = true, isEnabled = true) + injectEditableComboBox(FocusRequester(), isEnabled = true) composeRule.onNodeWithTag("ComboBox").assertIsDisplayed().performClick() @@ -222,7 +227,7 @@ class ListComboBoxUiTest { @Suppress("SwallowedException") @Test fun `when not editable, only Text component is displayed and cannot be edited`() { - injectComboBox(FocusRequester(), isEditable = false, isEnabled = true) + injectComboBox(FocusRequester(), isEnabled = true) composeRule.onNodeWithTag("ComboBox").assertIsDisplayed() @@ -232,7 +237,7 @@ class ListComboBoxUiTest { var exceptionThrown = false try { textNode.performTextClearance() - } catch (e: AssertionError) { + } catch (_: AssertionError) { exceptionThrown = true } assert(exceptionThrown) { "Expected an AssertionError to be thrown when attempting to clear text" } @@ -253,7 +258,7 @@ class ListComboBoxUiTest { @Test fun `when editable divider is displayed`() { - injectComboBox(FocusRequester(), isEditable = true, isEnabled = true) + injectEditableComboBox(FocusRequester(), isEnabled = true) composeRule.onNode(hasTestTag("Jewel.ComboBox.Divider"), useUnmergedTree = true).assertExists() @@ -271,7 +276,7 @@ class ListComboBoxUiTest { @Test fun `when not-editable divider is not displayed`() { - injectComboBox(FocusRequester(), isEditable = false, isEnabled = true) + injectComboBox(FocusRequester(), isEnabled = true) composeRule .onNode( hasTestTag("Jewel.ComboBox.Divider") and hasContentDescription("Jewel.ComboBox.Divider"), @@ -281,8 +286,8 @@ class ListComboBoxUiTest { } @Test - fun `when not-editable ChevronContainer is clickable and opens popup`() { - injectComboBox(FocusRequester(), isEditable = false, isEnabled = true) + fun `when not-editable the ChevronContainer is clickable and opens popup`() { + injectComboBox(FocusRequester(), isEnabled = true) chevronContainer.assertExists() @@ -294,6 +299,7 @@ class ListComboBoxUiTest { } chevronContainer.performClick() + popupMenu.assertIsDisplayed() } @@ -302,8 +308,8 @@ class ListComboBoxUiTest { editableComboBox() popupMenu.assertDoesNotExist() textField.performKeyInput { - keyDown(Key.DirectionDown) - keyUp(Key.DirectionDown) + keyDown(Key.Companion.DirectionDown) + keyUp(Key.Companion.DirectionDown) } popupMenu.assertIsDisplayed() } @@ -313,10 +319,10 @@ class ListComboBoxUiTest { editableComboBox() popupMenu.assertDoesNotExist() textField.performKeyInput { - keyDown(Key.DirectionDown) - keyUp(Key.DirectionDown) - keyDown(Key.DirectionDown) - keyUp(Key.DirectionDown) + keyDown(Key.Companion.DirectionDown) + keyUp(Key.Companion.DirectionDown) + keyDown(Key.Companion.DirectionDown) + keyUp(Key.Companion.DirectionDown) } popupMenu.assertIsDisplayed() composeRule.onAllNodesWithText("Item 2").onLast().isDisplayed() @@ -325,7 +331,7 @@ class ListComboBoxUiTest { @Test fun `when enabled but not editable clicking on the comboBox focuses it and open the popup`() { val focusRequester = FocusRequester() - injectComboBox(focusRequester, false, true) + injectComboBox(focusRequester, isEnabled = true) val comboBox = comboBox comboBox.performClick() @@ -335,25 +341,25 @@ class ListComboBoxUiTest { @Test fun `when enabled but not editable spacebar opens the popup`() { - val comboBox = notEditableFocusedComboBox() + val comboBox = focusedComboBox() comboBox.performKeyInput { - keyDown(Key.Spacebar) - keyUp(Key.Spacebar) + keyDown(Key.Companion.Spacebar) + keyUp(Key.Companion.Spacebar) } popupMenu.assertIsDisplayed() } @Test fun `when enabled but not editable pressing spacebar twice opens and closes the popup`() { - val comboBox = notEditableFocusedComboBox() + val comboBox = focusedComboBox() comboBox.performKeyInput { - keyDown(Key.Spacebar) - keyUp(Key.Spacebar) + keyDown(Key.Companion.Spacebar) + keyUp(Key.Companion.Spacebar) } popupMenu.assertIsDisplayed() comboBox.performKeyInput { - keyDown(Key.Spacebar) - keyUp(Key.Spacebar) + keyDown(Key.Companion.Spacebar) + keyUp(Key.Companion.Spacebar) } popupMenu.assertDoesNotExist() } @@ -361,7 +367,7 @@ class ListComboBoxUiTest { @Test fun `when enabled and editable with open popup losing focus closes the popup`() { val focusRequester = FocusRequester() - injectComboBox(focusRequester, isEditable = true, isEnabled = true) + injectEditableComboBox(focusRequester, isEnabled = true) focusRequester.requestFocus() val comboBox = comboBox @@ -385,8 +391,8 @@ class ListComboBoxUiTest { comboBox.performClick() popupMenu.isDisplayed() comboBox.performKeyInput { - keyDown(Key.Enter) - keyUp(Key.Enter) + keyDown(Key.Companion.Enter) + keyUp(Key.Companion.Enter) } popupMenu.assertDoesNotExist() } @@ -412,22 +418,33 @@ class ListComboBoxUiTest { @Test fun `when editable pressing down twice selects the second element`() { editableComboBox() - textField.performKeyInput { - keyDown(Key.DirectionDown) - keyUp(Key.DirectionDown) - } + textField.performKeyInput { pressKey(Key.Companion.DirectionDown) } popupMenu.assertIsDisplayed() - textField.performKeyInput { - keyDown(Key.DirectionDown) - keyUp(Key.DirectionDown) - } + textField.performKeyInput { pressKey(Key.Companion.DirectionDown) } composeRule.onNodeWithTag("Item 2").assertIsDisplayed().assertIsSelected() } + @Test + fun `when not editable pressing enter selects the preview selection if any`() { + injectComboBox(FocusRequester(), isEnabled = true) + comboBox.performClick() + popupMenu.assertIsDisplayed() + + composeRule.onNodeWithTag("Item 2").assertIsDisplayed().performMouseInput { + enter(Offset(2f, 0f)) + moveTo(Offset(10f, 2f)) + advanceEventTime() + } + + composeRule.onNodeWithTag("Item 2").assertIsSelected().performKeyInput { pressKey(Key.Companion.Enter) } + + comboBox.assertTextEquals("Item 2", includeEditableText = false) + } + private fun editableComboBox(): SemanticsNodeInteraction { val focusRequester = FocusRequester() - injectComboBox(focusRequester, isEditable = true, isEnabled = true) + injectEditableComboBox(focusRequester, isEnabled = true) focusRequester.requestFocus() val comboBox = comboBox comboBox.assertIsDisplayed() @@ -438,7 +455,7 @@ class ListComboBoxUiTest { private fun disabledEditableComboBox(): SemanticsNodeInteraction { val focusRequester = FocusRequester() - injectComboBox(focusRequester, true, false) + injectEditableComboBox(focusRequester, isEnabled = false) focusRequester.requestFocus() val comboBox = comboBox comboBox.assertIsDisplayed() @@ -446,34 +463,56 @@ class ListComboBoxUiTest { return comboBox } - private fun notEditableFocusedComboBox( + private fun focusedComboBox( focusRequester: FocusRequester = FocusRequester(), isEnabled: Boolean = true, ): SemanticsNodeInteraction { - injectComboBox(focusRequester, false, isEnabled) + injectComboBox(focusRequester, isEnabled) focusRequester.requestFocus() val comboBox = comboBox comboBox.assertIsDisplayed().assertIsFocused() return comboBox } - private fun injectComboBox(focusRequester: FocusRequester, isEditable: Boolean, isEnabled: Boolean) { + private fun injectComboBox(focusRequester: FocusRequester, isEnabled: Boolean) { composeRule.setContent { IntUiTheme { - var selectedComboBox: String? by remember { mutableStateOf(itemsComboBox.first()) } + var selectedItemText: String? by remember { mutableStateOf(comboBoxItems.first()) } ListComboBox( - items = itemsComboBox, + items = comboBoxItems, + modifier = Modifier.testTag("ComboBox").width(200.dp).focusRequester(focusRequester), + isEnabled = isEnabled, + onSelectedItemChange = { _, text -> selectedItemText = text }, + itemContent = { item, isSelected, isActive -> + SimpleListItem( + text = item, + isSelected = isSelected, + isActive = isActive, + modifier = Modifier.testTag(item), + iconContentDescription = item, + ) + }, + ) + } + } + } + + private fun injectEditableComboBox(focusRequester: FocusRequester, isEnabled: Boolean) { + composeRule.setContent { + IntUiTheme { + var selectedItemText: String? by remember { mutableStateOf(comboBoxItems.first()) } + EditableListComboBox( + items = comboBoxItems, modifier = Modifier.testTag("ComboBox").width(200.dp).focusRequester(focusRequester), - isEditable = isEditable, isEnabled = isEnabled, - onSelectedItemChange = { selectedComboBox = it }, - listItemContent = { item, isSelected, _, isItemHovered, previewSelection -> + onSelectedItemChange = { _, text -> selectedItemText = text }, + itemContent = { item, isSelected, isActive -> SimpleListItem( text = item, + isSelected = isSelected, + isActive = isActive, modifier = Modifier.testTag(item), - state = ListItemState(isSelected, isItemHovered, previewSelection), - style = JewelTheme.simpleListItemStyle, - contentDescription = item, + iconContentDescription = item, ) }, ) @@ -482,7 +521,7 @@ class ListComboBoxUiTest { } } -private val itemsComboBox = +private val comboBoxItems = listOf( "Item 1", "Item 2", diff --git a/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/PopupManagerTest.kt b/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/PopupManagerTest.kt new file mode 100644 index 0000000000..15511402ae --- /dev/null +++ b/ui-tests/src/test/kotlin/org/jetbrains/jewel/ui/component/PopupManagerTest.kt @@ -0,0 +1,119 @@ +package org.jetbrains.jewel.ui.component + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class PopupManagerTest { + @Test + fun `should start with the popup not visible`() { + val popupManager = PopupManager() + assertFalse(popupManager.isPopupVisible.value) + } + + @Test + fun `should toggle the visibility correctly`() { + val popupManager = PopupManager() + + popupManager.togglePopupVisibility() + assertTrue(popupManager.isPopupVisible.value) + + popupManager.togglePopupVisibility() + assertFalse(popupManager.isPopupVisible.value) + + popupManager.togglePopupVisibility() + assertTrue(popupManager.isPopupVisible.value) + } + + @Test + fun `should set the visibility correctly`() { + val popupManager = PopupManager() + + popupManager.setPopupVisible(true) + assertTrue(popupManager.isPopupVisible.value) + + popupManager.setPopupVisible(false) + assertFalse(popupManager.isPopupVisible.value) + + popupManager.setPopupVisible(false) + assertFalse(popupManager.isPopupVisible.value) + + popupManager.setPopupVisible(true) + assertTrue(popupManager.isPopupVisible.value) + + popupManager.setPopupVisible(true) + assertTrue(popupManager.isPopupVisible.value) + } + + @Test + fun `should only call onPopupVisibilityChange when the visibility actually changes`() { + var callCount = 0 + val popupManager = PopupManager({ callCount++ }) + + assertEquals(0, callCount) + + popupManager.setPopupVisible(true) + assertEquals(1, callCount) + + popupManager.setPopupVisible(false) + assertEquals(2, callCount) + + popupManager.setPopupVisible(false) + assertEquals(2, callCount) + + popupManager.setPopupVisible(true) + assertEquals(3, callCount) + + popupManager.setPopupVisible(true) + assertEquals(3, callCount) + } + + @Test + fun `should consider two instances equals when their name and state are equal`() { + var first = PopupManager() + var second = PopupManager() + assertTrue(first == second) + + // Same name, same state (false) + first = PopupManager(name = "banana") + second = PopupManager(name = "banana") + assertTrue(first == second) + + // Same name, same state (true) + first.togglePopupVisibility() + second.togglePopupVisibility() + assertTrue(first == second) + } + + @Test + fun `should ignore onPopupVisibilityChange when checking equality`() { + var first = PopupManager({ "first" }) + var second = PopupManager({ "second" }) + assertTrue(first == second) + } + + @Test + fun `should have same hashcode when name and state are equal`() { + var first = PopupManager() + var second = PopupManager() + assertTrue(first.hashCode() == second.hashCode()) + + // Same name, same state (false) + first = PopupManager(name = "banana") + second = PopupManager(name = "banana") + assertTrue(first.hashCode() == second.hashCode()) + + // Same name, same state (true) + first.togglePopupVisibility() + second.togglePopupVisibility() + assertTrue(first.hashCode() == second.hashCode()) + } + + @Test + fun `should ignore onPopupVisibilityChange when computing hashcode`() { + var first = PopupManager({ "first" }) + var second = PopupManager({ "second" }) + assertTrue(first.hashCode() == second.hashCode()) + } +} diff --git a/ui/api/ui.api b/ui/api/ui.api index 31d18b5235..04b80b4108 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -244,7 +244,7 @@ public final class org/jetbrains/jewel/ui/component/CircularProgressIndicatorKt } public final class org/jetbrains/jewel/ui/component/ComboBoxKt { - public static final fun ComboBox-xYaah8o (Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;ZLorg/jetbrains/jewel/ui/Outline;FLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ComboBoxStyle;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V + public static final fun ComboBox-xYaah8o (Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;ZLorg/jetbrains/jewel/ui/Outline;FLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ComboBoxStyle;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lorg/jetbrains/jewel/ui/component/PopupManager;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V } public final class org/jetbrains/jewel/ui/component/ComboBoxState : org/jetbrains/jewel/foundation/state/FocusableComponentState { @@ -361,7 +361,7 @@ public final class org/jetbrains/jewel/ui/component/DropdownState$Companion { } public final class org/jetbrains/jewel/ui/component/EditableComboBoxKt { - public static final fun EditableComboBox (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/text/input/TextFieldState;Lorg/jetbrains/jewel/ui/Outline;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ComboBoxStyle;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V + public static final fun EditableComboBox (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;ZLandroidx/compose/foundation/text/input/TextFieldState;Lorg/jetbrains/jewel/ui/Outline;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ComboBoxStyle;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lorg/jetbrains/jewel/ui/component/PopupManager;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V } public final class org/jetbrains/jewel/ui/component/FixedCursorPoint : androidx/compose/foundation/TooltipPlacement { @@ -526,16 +526,17 @@ public final class org/jetbrains/jewel/ui/component/LinkState$Companion { } public final class org/jetbrains/jewel/ui/component/ListComboBoxKt { - public static final fun ListComboBox-Fsagccs (Ljava/util/List;Landroidx/compose/ui/Modifier;ZZFLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function7;Landroidx/compose/runtime/Composer;II)V + public static final fun EditableListComboBox-lYrZsNM (Ljava/util/List;Landroidx/compose/ui/Modifier;ZILorg/jetbrains/jewel/ui/Outline;FLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ComboBoxStyle;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun ListComboBox-lYrZsNM (Ljava/util/List;Landroidx/compose/ui/Modifier;ZILorg/jetbrains/jewel/ui/Outline;FLandroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/ComboBoxStyle;Landroidx/compose/ui/text/TextStyle;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V } public final class org/jetbrains/jewel/ui/component/ListItemState { public static final field $stable I - public fun (ZZZ)V + public fun (ZZ)V + public synthetic fun (ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z - public final fun getPreviewSelection ()Z public fun hashCode ()I - public final fun isHovered ()Z + public final fun isActive ()Z public final fun isSelected ()Z public fun toString ()Ljava/lang/String; } @@ -622,6 +623,21 @@ public final class org/jetbrains/jewel/ui/component/PopupContainerKt { public static final fun PopupContainer (Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/ui/component/styling/PopupContainerStyle;Landroidx/compose/ui/window/PopupProperties;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V } +public final class org/jetbrains/jewel/ui/component/PopupManager { + public static final field $stable I + public fun ()V + public fun (Lkotlin/jvm/functions/Function1;Ljava/lang/String;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getOnPopupVisibleChange ()Lkotlin/jvm/functions/Function1; + public fun hashCode ()I + public final fun isPopupVisible ()Landroidx/compose/runtime/State; + public final fun setPopupVisible (Z)V + public fun toString ()Ljava/lang/String; + public final fun togglePopupVisibility ()V +} + public final class org/jetbrains/jewel/ui/component/RadioButtonKt { public static final fun RadioButton (ZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZLorg/jetbrains/jewel/ui/Outline;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/Alignment$Vertical;Landroidx/compose/runtime/Composer;II)V public static final fun RadioButtonRow (Ljava/lang/String;ZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZLorg/jetbrains/jewel/ui/Outline;Landroidx/compose/foundation/interaction/MutableInteractionSource;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/Alignment$Vertical;Landroidx/compose/runtime/Composer;II)V @@ -810,7 +826,8 @@ public final class org/jetbrains/jewel/ui/component/SelectableIconButtonState$Co } public final class org/jetbrains/jewel/ui/component/SimpleListItemKt { - public static final fun SimpleListItem-iHT-50w (Ljava/lang/String;Lorg/jetbrains/jewel/ui/component/ListItemState;Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle;FLorg/jetbrains/jewel/ui/icon/IconKey;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V + public static final fun SimpleListItem-aqv2aB4 (Ljava/lang/String;ZLandroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;ZLorg/jetbrains/jewel/ui/icon/IconKey;Ljava/lang/String;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle;FLandroidx/compose/runtime/Composer;II)V + public static final fun SimpleListItem-eKw1uXw (Ljava/lang/String;Lorg/jetbrains/jewel/ui/component/ListItemState;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/Modifier;Lorg/jetbrains/jewel/ui/icon/IconKey;Ljava/lang/String;Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemStyle;FLandroidx/compose/runtime/Composer;II)V } public final class org/jetbrains/jewel/ui/component/SliderKt { @@ -2406,17 +2423,18 @@ public final class org/jetbrains/jewel/ui/component/styling/SelectableLazyColumn public final class org/jetbrains/jewel/ui/component/styling/SimpleListItemColors { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemColors$Companion; - public synthetic fun (JJJJJJJJILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun (JJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun backgroundFor (Lorg/jetbrains/jewel/ui/component/ListItemState;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; + public final fun contentFor (Lorg/jetbrains/jewel/ui/component/ListItemState;Landroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State; public fun equals (Ljava/lang/Object;)Z public final fun getBackground-0d7_KjU ()J - public final fun getBackgroundFocused-0d7_KjU ()J + public final fun getBackgroundActive-0d7_KjU ()J public final fun getBackgroundSelected-0d7_KjU ()J - public final fun getBackgroundSelectedFocused-0d7_KjU ()J + public final fun getBackgroundSelectedActive-0d7_KjU ()J public final fun getContent-0d7_KjU ()J - public final fun getContentFocused-0d7_KjU ()J + public final fun getContentActive-0d7_KjU ()J public final fun getContentSelected-0d7_KjU ()J - public final fun getContentSelectedFocused-0d7_KjU ()J + public final fun getContentSelectedActive-0d7_KjU ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -2427,8 +2445,9 @@ public final class org/jetbrains/jewel/ui/component/styling/SimpleListItemColors public final class org/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics { public static final field $stable I public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/SimpleListItemMetrics$Companion; - public fun (Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/shape/CornerSize;)V + public synthetic fun (Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/shape/CornerSize;FLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z + public final fun getIconTextGap-D9Ej5fM ()F public final fun getInnerPadding ()Landroidx/compose/foundation/layout/PaddingValues; public final fun getOuterPadding ()Landroidx/compose/foundation/layout/PaddingValues; public final fun getSelectionBackgroundCornerSize ()Landroidx/compose/foundation/shape/CornerSize; diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ComboBox.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ComboBox.kt index 84385e837d..bb6d5dbcea 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ComboBox.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ComboBox.kt @@ -40,7 +40,6 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle @@ -63,7 +62,7 @@ import org.jetbrains.jewel.ui.util.thenIf public fun ComboBox( labelText: String, modifier: Modifier = Modifier, - menuModifier: Modifier = Modifier, + popupModifier: Modifier = Modifier, isEnabled: Boolean = true, outline: Outline = Outline.None, maxPopupHeight: Dp = Dp.Unspecified, @@ -72,16 +71,12 @@ public fun ComboBox( textStyle: TextStyle = JewelTheme.defaultTextStyle, onArrowDownPress: () -> Unit = {}, onArrowUpPress: () -> Unit = {}, - onPopupStateChange: (Boolean) -> Unit = {}, + popupManager: PopupManager = PopupManager(), popupContent: @Composable () -> Unit, ) { - var popupExpanded by remember { mutableStateOf(false) } var chevronHovered by remember { mutableStateOf(false) } - fun setPopupExpanded(expanded: Boolean) { - popupExpanded = expanded - onPopupStateChange(expanded) - } + val popupVisible by popupManager.isPopupVisible var comboBoxState by remember { mutableStateOf(ComboBoxState.of(enabled = isEnabled)) } val comboBoxFocusRequester = remember { FocusRequester() } @@ -112,7 +107,7 @@ public fun ComboBox( .onFocusChanged { focusState -> comboBoxState = comboBoxState.copy(focused = focusState.isFocused) if (!focusState.isFocused) { - setPopupExpanded(false) + popupManager.setPopupVisible(false) } } .thenIf(isEnabled) { @@ -121,29 +116,29 @@ public fun ComboBox( .pointerInput(interactionSource) { detectPressAndCancel( onPress = { - setPopupExpanded(!popupExpanded) + popupManager.setPopupVisible(!popupVisible) comboBoxFocusRequester.requestFocus() }, - onCancel = { setPopupExpanded(false) }, + onCancel = { popupManager.setPopupVisible(false) }, ) } .semantics(mergeDescendants = true) { role = Role.DropdownList } .onPreviewKeyEvent { if (it.type == KeyEventType.KeyDown && it.key == Key.Spacebar) { - setPopupExpanded(!popupExpanded) + popupManager.setPopupVisible(!popupVisible) } if (it.type == KeyEventType.KeyDown && it.key == Key.DirectionDown) { - if (popupExpanded) { + if (popupVisible) { onArrowDownPress() } else { - setPopupExpanded(true) + popupManager.setPopupVisible(true) } } - if (it.type == KeyEventType.KeyDown && it.key == Key.DirectionUp && popupExpanded) { + if (it.type == KeyEventType.KeyDown && it.key == Key.DirectionUp && popupVisible) { onArrowUpPress() } - if (it.type == KeyEventType.KeyDown && it.key == Key.Escape && popupExpanded) { - setPopupExpanded(false) + if (it.type == KeyEventType.KeyDown && it.key == Key.Escape && popupVisible) { + popupManager.setPopupVisible(false) } false } @@ -191,7 +186,7 @@ public fun ComboBox( } } - if (popupExpanded) { + if (popupVisible) { val maxHeight = if (maxPopupHeight == Dp.Unspecified) { JewelTheme.comboBoxStyle.metrics.maxPopupHeight @@ -202,16 +197,15 @@ public fun ComboBox( PopupContainer( onDismissRequest = { if (!chevronHovered) { - setPopupExpanded(false) + popupManager.setPopupVisible(false) } }, modifier = - menuModifier + popupModifier .testTag("Jewel.ComboBox.PopupMenu") - .semantics { contentDescription = "Jewel.ComboBox.PopupMenu" } .heightIn(max = maxHeight) .width(comboBoxWidth) - .onClick { setPopupExpanded(false) }, + .onClick { popupManager.setPopupVisible(false) }, horizontalAlignment = Alignment.Start, popupProperties = PopupProperties(focusable = false), content = popupContent, diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/EditableComboBox.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/EditableComboBox.kt index b1459db86f..497772148b 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/EditableComboBox.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/EditableComboBox.kt @@ -88,30 +88,16 @@ public fun EditableComboBox( onArrowDownPress: () -> Unit = {}, onArrowUpPress: () -> Unit = {}, onEnterPress: () -> Unit = {}, - onPopupStateChange: (Boolean) -> Unit = {}, + popupManager: PopupManager = PopupManager(), popupContent: @Composable () -> Unit, ) { - var popupExpanded by remember { mutableStateOf(false) } + var popupVisible by remember { mutableStateOf(false) } var chevronHovered by remember { mutableStateOf(false) } var textFieldHovered by remember { mutableStateOf(false) } val textFieldInteractionSource = remember { MutableInteractionSource() } val textFieldFocusRequester = remember { FocusRequester() } - fun setPopupExpanded(expanded: Boolean) { - popupExpanded = expanded - onPopupStateChange(expanded) - } - - fun togglePopup() { - setPopupExpanded(!popupExpanded) - } - - val onPressWhenEnabled = { - togglePopup() - textFieldFocusRequester.requestFocus() - } - var comboBoxState by remember { mutableStateOf(ComboBoxState.of(enabled = isEnabled)) } remember(isEnabled) { comboBoxState = comboBoxState.copy(enabled = isEnabled) } @@ -163,24 +149,24 @@ public fun EditableComboBox( Modifier.onFocusChanged { comboBoxState = comboBoxState.copy(focused = it.isFocused) if (!it.isFocused) { - setPopupExpanded(false) + popupManager.setPopupVisible(false) } }, verticalAlignment = Alignment.CenterVertically, ) { - TextInput( + TextField( isEnabled = isEnabled, inputTextFieldState = inputTextFieldState, isFocused = comboBoxState.isFocused, textFieldFocusRequester = textFieldFocusRequester, style = style, - popupExpanded = popupExpanded, + popupExpanded = popupVisible, textStyle = textStyle, textFieldInteractionSource = textFieldInteractionSource, onArrowDownPress = onArrowDownPress, onArrowUpPress = onArrowUpPress, onEnterPress = onEnterPress, - onSetPopupExpanded = { popupExpanded = it }, + onSetPopupExpanded = { popupVisible = it }, onFocusedChange = { comboBoxState = comboBoxState.copy(focused = it) }, onHoveredChange = { textFieldHovered = it }, modifier = Modifier.fillMaxWidth().weight(1f), @@ -191,17 +177,20 @@ public fun EditableComboBox( style = style, interactionSource = interactionSource, onHoveredChange = { chevronHovered = it }, - setPopupExpanded = { popupExpanded = it }, - onPressWhenEnabled = onPressWhenEnabled, + setPopupExpanded = { popupVisible = it }, + onPressWhenEnabled = { + popupManager.togglePopupVisibility() + textFieldFocusRequester.requestFocus() + }, ) } } - if (popupExpanded) { + if (popupVisible) { PopupContainer( onDismissRequest = { if (!chevronHovered && !textFieldHovered) { - setPopupExpanded(false) + popupManager.setPopupVisible(false) } }, modifier = @@ -209,7 +198,7 @@ public fun EditableComboBox( .testTag("Jewel.ComboBox.PopupMenu") .semantics { contentDescription = "Jewel.ComboBox.PopupMenu" } .width(comboBoxWidth) - .onClick { setPopupExpanded(false) }, + .onClick { popupManager.setPopupVisible(false) }, horizontalAlignment = Alignment.Start, popupProperties = PopupProperties(focusable = false), content = popupContent, @@ -218,9 +207,8 @@ public fun EditableComboBox( } } -@Suppress("ktlint:compose:modifier-without-default-check") @Composable -private fun TextInput( +private fun TextField( modifier: Modifier, isEnabled: Boolean, inputTextFieldState: TextFieldState, diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/LazyTree.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/LazyTree.kt index af75cb685d..1e5807a145 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/LazyTree.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/LazyTree.kt @@ -38,8 +38,8 @@ public fun LazyTree( BasicLazyTree( tree = tree, onElementClick = onElementClick, - elementBackgroundFocused = colors.backgroundFocused, - elementBackgroundSelectedFocused = colors.backgroundSelectedFocused, + elementBackgroundFocused = colors.backgroundActive, + elementBackgroundSelectedFocused = colors.backgroundSelectedActive, elementBackgroundSelected = colors.backgroundSelected, indentSize = metrics.indentSize, elementBackgroundCornerSize = metrics.simpleListItemMetrics.selectionBackgroundCornerSize, diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ListComboBox.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ListComboBox.kt index e693822a1b..3e60ee37ce 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ListComboBox.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/ListComboBox.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -18,42 +17,65 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import kotlinx.coroutines.launch import org.jetbrains.jewel.foundation.lazy.SelectableLazyColumn -import org.jetbrains.jewel.foundation.lazy.SelectableLazyListScope import org.jetbrains.jewel.foundation.lazy.SelectableLazyListState import org.jetbrains.jewel.foundation.lazy.SelectionMode -import org.jetbrains.jewel.foundation.lazy.items +import org.jetbrains.jewel.foundation.lazy.itemsIndexed import org.jetbrains.jewel.foundation.lazy.rememberSelectableLazyListState import org.jetbrains.jewel.foundation.lazy.visibleItemsRange -import org.jetbrains.jewel.foundation.modifier.onHover +import org.jetbrains.jewel.foundation.modifier.onMove import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.foundation.util.JewelLogger import org.jetbrains.jewel.ui.Outline +import org.jetbrains.jewel.ui.component.styling.ComboBoxStyle import org.jetbrains.jewel.ui.theme.comboBoxStyle @Composable public fun ListComboBox( items: List, modifier: Modifier = Modifier, - isEditable: Boolean = true, isEnabled: Boolean = true, + initialSelectedIndex: Int = 0, + outline: Outline = Outline.None, maxPopupHeight: Dp = Dp.Unspecified, - onSelectedItemChange: (String) -> Unit = {}, - onHoverItemChange: (String) -> Unit = {}, - onListHoverChange: (Boolean) -> Unit = {}, - onPopupStateChange: (Boolean) -> Unit = {}, - listItemContent: @Composable (String, Boolean, Boolean, Boolean, Boolean) -> Unit, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: ComboBoxStyle = JewelTheme.comboBoxStyle, + textStyle: TextStyle = JewelTheme.defaultTextStyle, + onSelectedItemChange: (Int, String) -> Unit = { _, _ -> }, + onPopupVisibleChange: (visible: Boolean) -> Unit = {}, + itemContent: @Composable (text: String, isSelected: Boolean, isActive: Boolean) -> Unit, ) { - val initialTextFieldContent = items.firstOrNull().orEmpty() - val inputTextFieldState = rememberTextFieldState(initialTextFieldContent) - val scrollState = rememberSelectableLazyListState() - var selectedItem by remember { mutableIntStateOf(0) } - var isListHovered by remember { mutableStateOf(false) } - var hoverItemIndex: Int? by remember { mutableStateOf(null) } + val listState = rememberSelectableLazyListState() + var labelText by remember { mutableStateOf(items.firstOrNull().orEmpty()) } + var previewSelectedIndex by remember { mutableIntStateOf(-1) } val scope = rememberCoroutineScope() - LaunchedEffect(selectedItem) { scrollState.selectedKeys = setOf(items[selectedItem]) } + LaunchedEffect(Unit) { + // Select the first item in the list automatically when creating + if (items.isNotEmpty()) { + listState.selectedKeys = setOf(initialSelectedIndex.coerceIn(0, items.lastIndex)) + } + } + + fun setSelectedItem(index: Int) { + if (index > 0 && index < items.lastIndex) { + listState.selectedKeys = setOf(index) + labelText = items[index] + onSelectedItemChange(index, items[index]) + scope.launch { listState.lazyListState.scrollToIndex(index) } + } else { + JewelLogger.getInstance("ListComboBox").trace("Ignoring item index $index as it's invalid") + } + } + val contentPadding = JewelTheme.comboBoxStyle.metrics.popupContentPadding val popupMaxHeight = if (maxPopupHeight == Dp.Unspecified) { @@ -62,160 +84,202 @@ public fun ListComboBox( maxPopupHeight } - val onArrowDownPress: () -> Unit = { - hoverItemIndex?.let { hoveredIndex -> - selectedItem = hoveredIndex - hoverItemIndex = null - } - selectedItem = selectedItem.plus(1).coerceAtMost(items.lastIndex) - scope.launch { scrollState.lazyListState.scrollToIndex(selectedItem) } - } - val onArrowUpPress: () -> Unit = { - hoverItemIndex?.let { hoveredIndex -> - selectedItem = hoveredIndex - hoverItemIndex = null - } - selectedItem = selectedItem.minus(1).coerceAtLeast(0) - scope.launch { scrollState.lazyListState.scrollToIndex(selectedItem) } - } - val onEnterPress = { - val indexOfSelected = items.indexOf(inputTextFieldState.text) - if (indexOfSelected != -1) { - selectedItem = indexOfSelected - } + val popupManager = remember { + PopupManager( + onPopupVisibleChange = { visible -> + previewSelectedIndex = -1 + onPopupVisibleChange(visible) + }, + name = "ListComboBoxPopup", + ) } - fun onSelectedIndexChange(selectedItemIndex: Int) { - selectedItem = selectedItemIndex - inputTextFieldState.setTextAndPlaceCursorAtEnd(items[selectedItemIndex]) - onSelectedItemChange(items[selectedItemIndex]) - } + ComboBox( + modifier = + modifier.onPreviewKeyEvent { + if (it.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false - fun contentItems( - items: List, - onHoverItemChange: (String) -> Unit, - listItemContent: @Composable (String, Boolean, Boolean, Boolean, Boolean) -> Unit, - ): SelectableLazyListScope.() -> Unit = { - items( - items = items, - itemContent = { item -> - var isItemHovered by remember { mutableStateOf(false) } - Box( - modifier = - Modifier.onHover { - isItemHovered = it - if (isItemHovered) { - hoverItemIndex = items.indexOf(item) - onHoverItemChange(item) - } - } - ) { - listItemContent( - item, - isSelected, - isActive, - isItemHovered || items.indexOf(item) == hoverItemIndex, - hoverItemIndex != null, - ) + if (it.key == Key.Enter || it.key == Key.NumPadEnter) { + if (popupManager.isPopupVisible.value && previewSelectedIndex >= 0) { + setSelectedItem(previewSelectedIndex) + previewSelectedIndex = -1 + popupManager.setPopupVisible(false) + } + true + } else { + false } }, + isEnabled = isEnabled, + labelText = labelText, + maxPopupHeight = popupMaxHeight, + onArrowDownPress = { + var currentSelectedIndex = listState.selectedItemIndex() + + // When there is a preview-selected item, pressing down will actually change the + // selected value to the one underneath it (unless it's the last one) + if (previewSelectedIndex >= 0 && previewSelectedIndex < items.lastIndex) { + currentSelectedIndex = previewSelectedIndex + previewSelectedIndex = -1 + } + + setSelectedItem((currentSelectedIndex + 1).coerceAtMost(items.lastIndex)) + }, + onArrowUpPress = { + var currentSelectedIndex = listState.selectedItemIndex() + + // When there is a preview-selected item, pressing up will actually change the + // selected value to the one above it (unless it's the first one) + if (previewSelectedIndex > 0) { + currentSelectedIndex = previewSelectedIndex + previewSelectedIndex = -1 + } + + setSelectedItem((currentSelectedIndex - 1).coerceAtLeast(0)) + }, + style = style, + textStyle = textStyle, + interactionSource = interactionSource, + outline = outline, + popupManager = popupManager, + ) { + PopupContent( + items = items, + previewSelectedItemIndex = previewSelectedIndex, + scrollState = listState, + popupMaxHeight = popupMaxHeight, + contentPadding = contentPadding, + onHoveredItemChange = { previewSelectedIndex = it }, + onSelectedItemChange = ::setSelectedItem, + itemContent = itemContent, ) } +} - @Composable - fun list( - items: List, - scrollState: SelectableLazyListState, - popupMaxHeight: Dp, - contentPadding: PaddingValues, - onListHoverChange: (Boolean) -> Unit, - onHoverItemChange: (String) -> Unit, - listItemContent: @Composable (String, Boolean, Boolean, Boolean, Boolean) -> Unit, - ) { - VerticallyScrollableContainer( - scrollState = scrollState.lazyListState, - modifier = - Modifier.heightIn(max = popupMaxHeight).onHover { - isListHovered = it - onListHoverChange(it) - }, - ) { - SelectableLazyColumn( - modifier = Modifier.fillMaxWidth().heightIn(max = popupMaxHeight).padding(contentPadding), - selectionMode = SelectionMode.Single, - state = scrollState, - onSelectedIndexesChange = { selectedItemsIndexes -> - if (selectedItemsIndexes.isEmpty()) return@SelectableLazyColumn - if (selectedItemsIndexes.first() == selectedItem) return@SelectableLazyColumn - onSelectedIndexChange(selectedItemsIndexes.first()) - }, - content = contentItems(items, onHoverItemChange, listItemContent), - ) +@Composable +public fun EditableListComboBox( + items: List, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, + initialSelectedIndex: Int = 0, + outline: Outline = Outline.None, + maxPopupHeight: Dp = Dp.Unspecified, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + style: ComboBoxStyle = JewelTheme.comboBoxStyle, + textStyle: TextStyle = JewelTheme.defaultTextStyle, + onSelectedItemChange: (Int, String) -> Unit = { _, _ -> }, + onPopupVisibleChange: (visible: Boolean) -> Unit = {}, + itemContent: @Composable (text: String, isSelected: Boolean, isActive: Boolean) -> Unit, +) { + val listState = rememberSelectableLazyListState() + val textFieldState = rememberTextFieldState(items.firstOrNull().orEmpty()) + var previewSelectedIndex by remember { mutableIntStateOf(-1) } + val scope = rememberCoroutineScope() + + LaunchedEffect(Unit) { + // Select the first item in the list automatically when creating + if (items.isNotEmpty()) { + listState.selectedKeys = setOf(initialSelectedIndex.coerceIn(0, items.lastIndex)) } } - if (isEditable) { - EditableComboBox( - modifier = modifier, - isEnabled = isEnabled, - inputTextFieldState = inputTextFieldState, - outline = Outline.None, - interactionSource = remember { MutableInteractionSource() }, - style = JewelTheme.comboBoxStyle, - textStyle = JewelTheme.defaultTextStyle, - onArrowDownPress = onArrowDownPress, - onArrowUpPress = onArrowUpPress, - onEnterPress = onEnterPress, - onPopupStateChange = onPopupStateChange, - ) { - list( - items, - scrollState, - popupMaxHeight, - contentPadding, - onListHoverChange, - onHoverItemChange, - listItemContent, - ) + + fun setSelectedItem(index: Int) { + if (index > 0 && index < items.lastIndex) { + // Note: it's important to do the edit _before_ updating the list state, + // since updating the list state will cause another, asynchronous and + // potentially nested call to edit, which is not supported. + // This is because setting the selected keys on the SLC will eventually + // cause a call to this very function via SLC's onSelectedIndexesChange. + textFieldState.edit { replace(0, length, items[index]) } + + if (listState.selectedKeys.size != 1 || listState.selectedItemIndex() != index) { + // This guard condition should also help avoid issues caused by side effects + // of setting new selected keys, as per the comment above. + listState.selectedKeys = setOf(index) + } + onSelectedItemChange(index, items[index]) + scope.launch { listState.lazyListState.scrollToIndex(index) } + } else { + JewelLogger.getInstance("EditableListComboBox").trace("Ignoring item index $index as it's invalid") } - } else { - ComboBox( - modifier = modifier, - isEnabled = isEnabled, - labelText = items[selectedItem], - outline = Outline.None, - maxPopupHeight = popupMaxHeight, - interactionSource = remember { MutableInteractionSource() }, - style = JewelTheme.comboBoxStyle, - textStyle = JewelTheme.defaultTextStyle, - onArrowDownPress = onArrowDownPress, - onArrowUpPress = onArrowUpPress, - onPopupStateChange = onPopupStateChange, - ) { - list( - items, - scrollState, - popupMaxHeight, - contentPadding, - onListHoverChange, - onHoverItemChange, - listItemContent, - ) + } + + val contentPadding = JewelTheme.comboBoxStyle.metrics.popupContentPadding + val popupMaxHeight = + if (maxPopupHeight == Dp.Unspecified) { + JewelTheme.comboBoxStyle.metrics.maxPopupHeight + } else { + maxPopupHeight } + + EditableComboBox( + modifier = modifier, + isEnabled = isEnabled, + inputTextFieldState = textFieldState, + onArrowDownPress = { + var currentSelectedIndex = listState.selectedItemIndex() + + // When there is a preview-selected item, pressing down will actually change the + // selected value to the one underneath it (unless it's the last one) + if (previewSelectedIndex >= 0 && previewSelectedIndex < items.lastIndex) { + currentSelectedIndex = previewSelectedIndex + previewSelectedIndex = -1 + } + + setSelectedItem((currentSelectedIndex + 1).coerceAtMost(items.lastIndex)) + }, + onArrowUpPress = { + var currentSelectedIndex = listState.selectedItemIndex() + + // When there is a preview-selected item, pressing up will actually change the + // selected value to the one above it (unless it's the first one) + if (previewSelectedIndex > 0) { + currentSelectedIndex = previewSelectedIndex + previewSelectedIndex = -1 + } + + setSelectedItem((currentSelectedIndex - 1).coerceAtLeast(0)) + }, + onEnterPress = { + val indexOfSelected = items.indexOf(textFieldState.text) + if (indexOfSelected != -1) { + setSelectedItem(indexOfSelected) + } + }, + style = style, + textStyle = textStyle, + interactionSource = interactionSource, + outline = outline, + popupManager = + PopupManager( + onPopupVisibleChange = { + previewSelectedIndex = -1 + onPopupVisibleChange(it) + }, + name = "EditableListComboBoxPopup", + ), + ) { + PopupContent( + items = items, + previewSelectedItemIndex = previewSelectedIndex, + scrollState = listState, + popupMaxHeight = popupMaxHeight, + contentPadding = contentPadding, + onHoveredItemChange = { previewSelectedIndex = it }, + onSelectedItemChange = ::setSelectedItem, + itemContent = itemContent, + ) } } private suspend fun LazyListState.scrollToIndex(itemIndex: Int) { val isFirstItemFullyVisible = firstVisibleItemScrollOffset == 0 - val lastItemInfo = layoutInfo.visibleItemsInfo.lastOrNull() - val isLastItemFullyVisible = - if (lastItemInfo != null) { - layoutInfo.viewportEndOffset - lastItemInfo.offset >= lastItemInfo.size - } else { - false - } + // If there are no visible items, just return + val lastItemInfo = layoutInfo.visibleItemsInfo.lastOrNull() ?: return + val isLastItemFullyVisible = layoutInfo.viewportEndOffset - lastItemInfo.offset >= lastItemInfo.size - val lastItemInfoSize = lastItemInfo?.size ?: 0 + val lastItemInfoSize = lastItemInfo.size when { itemIndex < visibleItemsRange.first -> scrollToItem((itemIndex - 1).coerceAtLeast(0)) itemIndex == visibleItemsRange.first && !isFirstItemFullyVisible -> scrollToItem(itemIndex) @@ -235,3 +299,50 @@ private suspend fun LazyListState.scrollToIndex(itemIndex: Int) { } } } + +/** Returns the index of the selected item in the list, returning -1 if there is no selected item. */ +private fun SelectableLazyListState.selectedItemIndex(): Int = selectedKeys.firstOrNull() as Int? ?: -1 + +@Composable +private fun PopupContent( + items: List, + previewSelectedItemIndex: Int, + scrollState: SelectableLazyListState, + popupMaxHeight: Dp, + contentPadding: PaddingValues, + onHoveredItemChange: (Int) -> Unit, + onSelectedItemChange: (Int) -> Unit, + itemContent: @Composable (text: String, isSelected: Boolean, isActive: Boolean) -> Unit, +) { + VerticallyScrollableContainer( + scrollState = scrollState.lazyListState, + modifier = Modifier.heightIn(max = popupMaxHeight), + ) { + SelectableLazyColumn( + modifier = Modifier.fillMaxWidth().heightIn(max = popupMaxHeight).padding(contentPadding), + selectionMode = SelectionMode.Single, + state = scrollState, + onSelectedIndexesChange = { selectedItemsIndexes -> + val selectedIndex = selectedItemsIndexes.firstOrNull() + if (selectedIndex != null) onSelectedItemChange(selectedIndex) + }, + ) { -> + itemsIndexed( + items = items, + key = { itemIndex, _ -> itemIndex }, + itemContent = { index, item -> + Box( + modifier = Modifier.onMove { if (previewSelectedItemIndex != index) onHoveredItemChange(index) } + ) { + // Items can be "actually" selected, or "preview" selected (e.g., hovered), + // but if we have a "preview" selection, we hide the "actual" selection + val showAsSelected = + (isSelected && previewSelectedItemIndex < 0) || previewSelectedItemIndex == index + + itemContent(item, showAsSelected, isActive) + } + }, + ) + } + } +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/PopupManager.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/PopupManager.kt new file mode 100644 index 0000000000..f043be3410 --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/PopupManager.kt @@ -0,0 +1,60 @@ +package org.jetbrains.jewel.ui.component + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import org.jetbrains.jewel.foundation.ExperimentalJewelApi + +/** + * Manages a popup visibility. + * + * Note: two instances are equals and share the same hashcode if: + * * They have the same [name] + * * The [isPopupVisible] value is the same + * + * @param name An optional name given to the instance. + * @param onPopupVisibleChange A lambda to call when the popup visibility changes. + */ +@ExperimentalJewelApi +public class PopupManager(public val onPopupVisibleChange: (Boolean) -> Unit = {}, public val name: String? = null) { + private val _isPopupVisible: MutableState = mutableStateOf(false) + + /** Indicates whether the popup is currently visible. */ + public val isPopupVisible: State = _isPopupVisible + + /** Toggle the popup visibility. */ + public fun togglePopupVisibility() { + setPopupVisible(!_isPopupVisible.value) + } + + /** + * Set the popup visibility. + * + * @param visible true when the popup should be shown, false otherwise. + */ + public fun setPopupVisible(visible: Boolean) { + if (_isPopupVisible.value == visible) return + _isPopupVisible.value = visible + onPopupVisibleChange(visible) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PopupManager + + if (name != other.name) return false + if (isPopupVisible.value != other.isPopupVisible.value) return false + + return true + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + isPopupVisible.value.hashCode() + return result + } + + override fun toString(): String = "PopupManager(isPopupVisible=${isPopupVisible.value}, name=$name)" +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/SimpleListItem.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/SimpleListItem.kt index d1b4bd46dd..fac80ad640 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/SimpleListItem.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/SimpleListItem.kt @@ -1,6 +1,7 @@ package org.jetbrains.jewel.ui.component import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -8,9 +9,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.selected import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow @@ -22,44 +23,74 @@ import org.jetbrains.jewel.ui.component.styling.SimpleListItemStyle import org.jetbrains.jewel.ui.icon.IconKey import org.jetbrains.jewel.ui.theme.simpleListItemStyle +/** + * A simple list item layout comprising of a text and an optional icon to its start side. + * + * The text will only take up one line and is ellipsized if too long to fit. The item will draw a background based on + * the [state]. + */ @Composable public fun SimpleListItem( text: String, - state: ListItemState, + isSelected: Boolean, modifier: Modifier = Modifier, + textModifier: Modifier = Modifier, + iconModifier: Modifier = Modifier, + isActive: Boolean = true, + icon: IconKey? = null, + iconContentDescription: String? = null, style: SimpleListItemStyle = JewelTheme.simpleListItemStyle, height: Dp = JewelTheme.globalMetrics.rowHeight, - icon: IconKey? = null, - contentDescription: String? = null, ) { - val color = - when { - state.previewSelection && state.isHovered -> style.colors.backgroundSelectedFocused - state.isSelected && !state.previewSelection -> style.colors.backgroundSelectedFocused - else -> Color.Transparent - } + val state = remember(isSelected, isActive) { ListItemState(isSelected, isActive) } + SimpleListItem(text, state, modifier, textModifier, iconModifier, icon, iconContentDescription, style, height) +} +/** + * A simple list item layout comprising of a text and an optional icon to its start side. + * + * The text will only take up one line and is ellipsized if too long to fit. The item will draw a background based on + * the [state]. + */ +@Composable +public fun SimpleListItem( + text: String, + state: ListItemState, + modifier: Modifier = Modifier, + textModifier: Modifier = Modifier, + iconModifier: Modifier = Modifier, + icon: IconKey? = null, + iconContentDescription: String? = null, + style: SimpleListItemStyle = JewelTheme.simpleListItemStyle, + height: Dp = JewelTheme.globalMetrics.rowHeight, +) { Row( - verticalAlignment = Alignment.CenterVertically, modifier = modifier .semantics { selected = state.isSelected } .fillMaxWidth() .height(height) .padding(style.metrics.outerPadding) - .background(color, RoundedCornerShape(style.metrics.selectionBackgroundCornerSize)) + .background( + color = style.colors.backgroundFor(state).value, + shape = RoundedCornerShape(style.metrics.selectionBackgroundCornerSize), + ) .padding(style.metrics.innerPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(style.metrics.iconTextGap), ) { if (icon != null) { - Icon(modifier = Modifier.size(16.dp), key = icon, contentDescription = contentDescription) + Icon(modifier = iconModifier.size(16.dp), key = icon, contentDescription = iconContentDescription) } - Text(text = text, maxLines = 1, overflow = TextOverflow.Ellipsis, style = JewelTheme.defaultTextStyle) + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = JewelTheme.defaultTextStyle, + color = style.colors.contentFor(state).value, + modifier = textModifier, + ) } } -@GenerateDataFunctions -public class ListItemState( - public val isSelected: Boolean, - public val isHovered: Boolean, - public val previewSelection: Boolean, -) +@GenerateDataFunctions public class ListItemState(public val isSelected: Boolean, public val isActive: Boolean = true) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/LazyTreeStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/LazyTreeStyling.kt index a13f2f6a95..7c3459b29e 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/LazyTreeStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/LazyTreeStyling.kt @@ -28,8 +28,8 @@ public class LazyTreeStyle( public fun SimpleListItemColors.contentFor(state: TreeElementState): State = rememberUpdatedState( when { - state.isSelected && state.isFocused -> contentSelectedFocused - state.isFocused -> contentFocused + state.isSelected && state.isFocused -> contentSelectedActive + state.isFocused -> contentActive state.isSelected -> contentSelected else -> content } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SimpleListItemStyle.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SimpleListItemStyle.kt index b0c039be6d..b769f2b3b8 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SimpleListItemStyle.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/SimpleListItemStyle.kt @@ -2,11 +2,16 @@ package org.jetbrains.jewel.ui.component.styling import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.shape.CornerSize +import androidx.compose.runtime.Composable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import org.jetbrains.jewel.foundation.GenerateDataFunctions +import org.jetbrains.jewel.ui.component.ListItemState @GenerateDataFunctions public class SimpleListItemStyle(public val colors: SimpleListItemColors, public val metrics: SimpleListItemMetrics) { @@ -16,15 +21,37 @@ public class SimpleListItemStyle(public val colors: SimpleListItemColors, public @Stable @GenerateDataFunctions public class SimpleListItemColors( - public val background: Color = Color.Unspecified, - public val backgroundFocused: Color, + public val background: Color, + public val backgroundActive: Color, public val backgroundSelected: Color, - public val backgroundSelectedFocused: Color, + public val backgroundSelectedActive: Color, public val content: Color, - public val contentFocused: Color, + public val contentActive: Color, public val contentSelected: Color, - public val contentSelectedFocused: Color, + public val contentSelectedActive: Color, ) { + @Composable + public fun contentFor(state: ListItemState): State = + rememberUpdatedState( + when { + state.isSelected && state.isActive -> contentSelectedActive + state.isSelected && !state.isActive -> contentSelected + state.isActive -> contentActive + else -> content + } + ) + + @Composable + public fun backgroundFor(state: ListItemState): State = + rememberUpdatedState( + when { + state.isSelected && state.isActive -> backgroundSelectedActive + state.isSelected && !state.isActive -> backgroundSelected + state.isActive -> backgroundActive + else -> background + } + ) + public companion object } @@ -34,6 +61,7 @@ public class SimpleListItemMetrics( public val innerPadding: PaddingValues, public val outerPadding: PaddingValues, public val selectionBackgroundCornerSize: CornerSize, + public val iconTextGap: Dp, ) { public companion object }