[JEWEL-1205] Fix Menu Items Looking Too Tall in Presentation Mode

closes https://github.com/JetBrains/intellij-community/pull/3419

(cherry picked from commit 134b87df4a183304340fb5e85c3fc16fa4cba527)


(cherry picked from commit d6e0ba4e678ffdcb5b361eeb75b598e51cc644dd)

IJ-MR-211495

GitOrigin-RevId: c32b93d0e223bbb9e410e4721d0aceccdca9cd01
This commit is contained in:
Daniel Bertoldi
2026-07-17 17:01:41 +00:00
committed by intellij-monorepo-bot
parent 917cc8cb18
commit 8768cbb083
11 changed files with 408 additions and 128 deletions
@@ -28,6 +28,7 @@ f:org.jetbrains.jewel.bridge.BridgeUtilsKt
- sf:createVerticalBrush-8A-3gB4(java.util.List,F,F,I):androidx.compose.ui.graphics.Brush
- bs:createVerticalBrush-8A-3gB4$default(java.util.List,F,F,I,I,java.lang.Object):androidx.compose.ui.graphics.Brush
- sf:getDp(com.intellij.util.ui.JBValue):F
- sf:getUnscaledDp(I):F
- sf:retrieveArcAsCornerSize(java.lang.String):androidx.compose.foundation.shape.CornerSize
- sf:retrieveArcAsCornerSizeOrDefault(java.lang.String,androidx.compose.foundation.shape.CornerSize):androidx.compose.foundation.shape.CornerSize
- sf:retrieveArcAsCornerSizeWithFallbacks(java.lang.String[]):androidx.compose.foundation.shape.CornerSize
@@ -49,6 +50,8 @@ f:org.jetbrains.jewel.bridge.BridgeUtilsKt
- bs:retrieveTextStyle-WdJyH8Q$default(java.lang.String,J,J,Z,I,J,I,java.lang.Object):androidx.compose.ui.text.TextStyle
- sf:retrieveTextStyle-tD9LlGs(java.lang.String,java.lang.String,J,Z,I,J):androidx.compose.ui.text.TextStyle
- bs:retrieveTextStyle-tD9LlGs$default(java.lang.String,java.lang.String,J,Z,I,J,I,java.lang.Object):androidx.compose.ui.text.TextStyle
- sf:retrieveUnscaledIntAsDpOrUnspecified(java.lang.String):F
- sf:retrieveUnscaledIntAsNonNegativeDpOrUnspecified(java.lang.String):F
- sf:toAwtColor-8_81llA(J):java.awt.Color
- sf:toAwtColorOrNull-8_81llA(J):java.awt.Color
- sf:toComposeColor(java.awt.Color):J
@@ -63,6 +63,8 @@ package org.jetbrains.jewel.bridge {
method public static androidx.compose.ui.text.PlatformTextStyle retrievePlatformTextStyle();
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String fontKey, optional String? colorKey, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String key, optional long color, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
method public static float retrieveUnscaledIntAsDpOrUnspecified(String key);
method public static float retrieveUnscaledIntAsNonNegativeDpOrUnspecified(String key);
method public static java.awt.Color toAwtColor(long);
method public static java.awt.Color? toAwtColorOrNull(long);
method public static long toComposeColor(java.awt.Color);
@@ -74,6 +76,7 @@ package org.jetbrains.jewel.bridge {
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(com.intellij.util.ui.JBInsets);
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(java.awt.Insets);
property public static androidx.compose.ui.unit.Dp com.intellij.util.ui.JBValue.dp;
property public static androidx.compose.ui.unit.Dp int.unscaledDp;
}
public abstract sealed class JewelBridgeException extends java.lang.RuntimeException {
@@ -61,6 +61,8 @@ package org.jetbrains.jewel.bridge {
method public static androidx.compose.ui.text.PlatformTextStyle retrievePlatformTextStyle();
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String fontKey, optional String? colorKey, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
method public static androidx.compose.ui.text.TextStyle retrieveTextStyle(String key, optional long color, optional long lineHeight, optional boolean bold, optional int fontStyle, optional long size);
method public static float retrieveUnscaledIntAsDpOrUnspecified(String key);
method public static float retrieveUnscaledIntAsNonNegativeDpOrUnspecified(String key);
method public static java.awt.Color toAwtColor(long);
method public static java.awt.Color? toAwtColorOrNull(long);
method public static long toComposeColor(java.awt.Color);
@@ -70,6 +72,7 @@ package org.jetbrains.jewel.bridge {
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(com.intellij.util.ui.JBInsets);
method public static androidx.compose.foundation.layout.PaddingValues toPaddingValues(java.awt.Insets);
property public static androidx.compose.ui.unit.Dp com.intellij.util.ui.JBValue.dp;
property public static androidx.compose.ui.unit.Dp int.unscaledDp;
}
public abstract sealed class JewelBridgeException extends java.lang.RuntimeException {
@@ -42,6 +42,7 @@ import com.intellij.ui.scale.JBUIScale.scale
import com.intellij.util.ui.JBDimension
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBInsets
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.JBValue
import java.awt.Dimension
import java.awt.Insets
@@ -520,3 +521,58 @@ public fun retrieveEditorColorScheme(): EditorColorsScheme {
val manager = EditorColorsManager.getInstance() as EditorColorsManagerImpl
return manager.schemeManager.activeScheme ?: DefaultColorSchemesManager.getInstance().firstScheme
}
/**
* Retrieves a pre-scaled integer value from the current LaF as a non-negative [Dp], or returns [Dp.Unspecified] if the
* key is not found.
*
* Any negative values read from the LaF will be coerced to `0.dp`.
*
* Use this instead of [retrieveIntAsNonNegativeDpOrUnspecified] for keys whose values are explicitly rescaled by
* `patchHiDPI` in `LafManagerImpl`. See [unscaledDp] for the full list of affected keys and an explanation of why
* unscaling is needed.
*
* @param key The key to look up the integer with.
* @return The unscaled integer value from the LaF as a [Dp], or [Dp.Unspecified] if the key is not found.
* @see unscaledDp
*/
public fun retrieveUnscaledIntAsNonNegativeDpOrUnspecified(key: String): Dp =
retrieveUnscaledIntAsDpOrUnspecified(key).takeIf { it.isSpecified }?.safeValue() ?: Dp.Unspecified
/**
* Retrieves a pre-scaled integer value from the current LaF as a [Dp], or returns [Dp.Unspecified] if the key is not
* found.
*
* Use this instead of [retrieveIntAsDpOrUnspecified] for keys whose values are explicitly rescaled by `patchHiDPI` in
* `LafManagerImpl`. See [unscaledDp] for the full list of affected keys and an explanation of why unscaling is needed.
*
* @param key The key to look up the integer with.
* @return The unscaled integer value from the LaF as a [Dp], or [Dp.Unspecified] if the key is not found.
* @see unscaledDp
*/
public fun retrieveUnscaledIntAsDpOrUnspecified(key: String): Dp {
val rawValue = UIManager.get(key)
if (rawValue is Int) return rawValue.unscaledDp
return Dp.Unspecified
}
/**
* Converts a pre-scaled JBUI value into a Compose [Dp].
*
* Certain integer values in UIDefaults are explicitly rescaled by `patchHiDPI` in `LafManagerImpl` after the theme JSON
* is applied. This means they already reflect the current JBUI scale factor. For example, `List.rowHeight` is stored as
* `24` in the theme JSON, but `patchHiDPI` overwrites it with `scale(24)`. So in Presentation Mode (scale=1.75),
* [com.intellij.util.ui.JBUI.CurrentTheme.List.rowHeight] returns `42`, not `24`.
*
* Passing such a pre-scaled value directly to `.dp` causes double-scaling:
* 1. `patchHiDPI` scales the stored value (e.g. 24 → 42 in Presentation Mode)
* 2. Compose scales it again via `LocalDensity` (e.g. `42.dp` → way too big)
*
* The keys currently explicitly rescaled by `patchHiDPI` are: `List.rowHeight`, `Table.rowHeight`, `Tree.rowHeight`,
* VCS log row height, `Tree.leftChildIndent`, `Tree.rightChildIndent`, and `SettingsTree.rowHeight`.
*
* For all other keys — those NOT rescaled by `patchHiDPI` — the value in UIDefaults is the raw logical integer from the
* theme JSON, and `.dp` is correct. Do NOT use this extension for those (e.g. via [retrieveIntAsDp] or similar).
*/
public val Int.unscaledDp: Dp
get() = JBUI.unscale(this).dp
@@ -8,7 +8,7 @@ import androidx.compose.ui.unit.takeOrElse
import com.intellij.util.ui.JBUI
import org.jetbrains.jewel.bridge.dp
import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified
import org.jetbrains.jewel.bridge.retrieveIntAsNonNegativeDpOrUnspecified
import org.jetbrains.jewel.bridge.retrieveUnscaledIntAsNonNegativeDpOrUnspecified
import org.jetbrains.jewel.ui.component.styling.LazyTreeIcons
import org.jetbrains.jewel.ui.component.styling.LazyTreeMetrics
import org.jetbrains.jewel.ui.component.styling.LazyTreeStyle
@@ -34,8 +34,8 @@ internal fun readLazyTreeStyle(): LazyTreeStyle {
backgroundSelectedActive = selectedElementBackground,
)
val leftIndent = retrieveIntAsNonNegativeDpOrUnspecified("Tree.leftChildIndent").takeOrElse { 7.dp }
val rightIndent = retrieveIntAsNonNegativeDpOrUnspecified("Tree.rightChildIndent").takeOrElse { 11.dp }
val leftIndent = retrieveUnscaledIntAsNonNegativeDpOrUnspecified("Tree.leftChildIndent").takeOrElse { 7.dp }
val rightIndent = retrieveUnscaledIntAsNonNegativeDpOrUnspecified("Tree.rightChildIndent").takeOrElse { 11.dp }
return LazyTreeStyle(
colors = itemColors,
@@ -49,7 +49,8 @@ internal fun readLazyTreeStyle(): LazyTreeStyle {
selectionBackgroundCornerSize = CornerSize(JBUI.CurrentTheme.Tree.ARC.dp / 2),
iconTextGap = 2.dp,
),
elementMinHeight = retrieveIntAsNonNegativeDpOrUnspecified("Tree.rowHeight").takeOrElse { 24.dp },
elementMinHeight =
retrieveUnscaledIntAsNonNegativeDpOrUnspecified("Tree.rowHeight").takeOrElse { 24.dp },
chevronContentGap = 2.dp, // See com.intellij.ui.tree.ui.ClassicPainter.GAP
),
icons =
@@ -16,6 +16,7 @@ import org.jetbrains.jewel.bridge.retrieveColorOrUnspecified
import org.jetbrains.jewel.bridge.retrieveIntAsNonNegativeDpOrUnspecified
import org.jetbrains.jewel.bridge.safeValue
import org.jetbrains.jewel.bridge.toPaddingValues
import org.jetbrains.jewel.bridge.unscaledDp
import org.jetbrains.jewel.ui.component.styling.MenuColors
import org.jetbrains.jewel.ui.component.styling.MenuIcons
import org.jetbrains.jewel.ui.component.styling.MenuItemColors
@@ -107,7 +108,7 @@ internal fun readMenuStyle(): MenuStyle {
iconSize = 16.dp,
minHeight =
if (isNewUiTheme()) {
JBUI.CurrentTheme.List.rowHeight().dp.safeValue()
JBUI.CurrentTheme.List.rowHeight().unscaledDp.safeValue()
} else {
Dp.Unspecified
},
@@ -4,7 +4,7 @@ package org.jetbrains.jewel.ui.component
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.input.InputMode
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.v2.createComposeRule
import javax.swing.KeyStroke
import org.jetbrains.jewel.intui.standalone.menuShortcut.StandaloneShortcutProvider
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
@@ -0,0 +1,120 @@
package org.jetbrains.jewel.ui.component
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.semantics.SemanticsActions
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isPopup
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performSemanticsAction
import org.jetbrains.jewel.intui.standalone.theme.IntUiTheme
import org.jetbrains.jewel.ui.component.interactions.performKeyPress
import org.junit.Rule
import org.junit.Test
class MenuSubmenuItemTest {
@get:Rule val rule = createComposeRule()
@Test
fun `clicking submenu item opens submenu`() {
rule.setContent {
IntUiTheme {
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
MenuSubmenuItem(
showIcon = false,
selected = false,
submenu = { selectableItem(false, onClick = {}) { Text("Submenu") } },
content = { Text("Parent") },
)
}
}
}
// submenu not yet visible
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertDoesNotExist()
rule.onNodeWithText("Parent").performClick()
rule.waitForIdle()
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertIsDisplayed()
}
@Test
fun `disabling while submenu is open closes the submenu`() {
val enabled = mutableStateOf(true)
rule.setContent {
IntUiTheme {
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
MenuSubmenuItem(
showIcon = false,
selected = false,
enabled = enabled.value,
submenu = { selectableItem(false, onClick = {}) { Text("Submenu") } },
content = { Text("Parent") },
)
}
}
}
rule.onNodeWithText("Parent").performClick()
rule.waitForIdle()
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertIsDisplayed()
enabled.value = false
rule.waitForIdle()
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertDoesNotExist()
}
@Test
fun `disabled items cannot be opened`() {
rule.setContent {
IntUiTheme {
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
MenuSubmenuItem(
showIcon = false,
selected = false,
enabled = false,
submenu = { selectableItem(selected = false, onClick = {}) { Text("Submenu") } },
content = { Text("Parent") },
)
}
}
}
rule.onNodeWithText("Parent").performClick()
rule.waitForIdle()
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertDoesNotExist()
}
@Test
fun `right arrow opens submenu`() {
rule.setContent {
IntUiTheme {
CompositionLocalProvider(LocalMenuController provides FakeMenuController()) {
MenuSubmenuItem(
showIcon = false,
selected = false,
submenu = { selectableItem(selected = false, onClick = {}) { Text("Submenu") } },
content = { Text("Parent") },
)
}
}
}
rule.onNodeWithText("Parent").performSemanticsAction(SemanticsActions.RequestFocus)
rule.waitForIdle()
rule.onNodeWithText("Parent").performKeyPress(Key.DirectionRight)
rule.waitForIdle()
rule.onNode(hasText("Submenu").and(hasAnyAncestor(isPopup()))).assertIsDisplayed()
}
}
@@ -31,6 +31,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -821,36 +822,18 @@ internal fun MenuItemBase(
style: MenuStyle = JewelTheme.menuStyle,
@Suppress("DEPRECATION") content: @Composable (itemState: MenuItemState) -> Unit,
) {
var itemState by
remember(interactionSource) {
@Suppress("DEPRECATION") mutableStateOf(MenuItemState.of(selected = selected, enabled = enabled))
}
remember(enabled, selected) { itemState = itemState.copy(selected = selected, enabled = enabled) }
val itemState by rememberMenuItemState(selected, enabled, interactionSource)
val focusRequester = remember { FocusRequester() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> itemState = itemState.copy(pressed = true)
is PressInteraction.Cancel,
is PressInteraction.Release -> itemState = itemState.copy(pressed = false)
is HoverInteraction.Enter -> {
itemState = itemState.copy(hovered = true)
focusRequester.requestFocus()
}
is HoverInteraction.Exit -> itemState = itemState.copy(hovered = false)
is FocusInteraction.Focus -> itemState = itemState.copy(focused = true)
is FocusInteraction.Unfocus -> itemState = itemState.copy(focused = false)
}
}
}
val menuController = LocalMenuController.current
val localInputModeManager = LocalInputModeManager.current
LaunchedEffect(itemState.isHovered) {
if (itemState.isHovered) {
focusRequester.requestFocus()
}
}
Box(
modifier =
modifier
@@ -872,52 +855,25 @@ internal fun MenuItemBase(
onDispose {}
}
val itemColors = style.colors.itemColors
val itemMetrics = style.metrics.itemMetrics
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
val updatedTextStyle = LocalTextStyle.current.copy(color = itemColors.contentFor(itemState).value)
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
CompositionLocalProvider(
LocalContentColor provides itemColors.contentFor(itemState).value,
LocalTextStyle provides updatedTextStyle,
) {
val backgroundColor by itemColors.backgroundFor(itemState)
Row(
modifier =
Modifier.fillMaxWidth()
.defaultMinSize(minHeight = itemMetrics.minHeight)
.drawItemBackground(itemMetrics, backgroundColor)
.padding(itemMetrics.contentPadding),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (canShowIcon) {
val iconModifier = Modifier.size(style.metrics.itemMetrics.iconSize)
if (iconKey != null) {
Icon(
key = iconKey,
contentDescription = null,
modifier = iconModifier.thenIf(!enabled) { disabledAppearance() },
)
} else {
Box(modifier = iconModifier)
}
}
Box(modifier = Modifier.weight(1f, true)) { content(itemState) }
MenuItemLayout(
itemState = itemState,
style = style,
iconKey = iconKey,
canShowIcon = canShowIcon,
enabled = enabled,
trailingContent =
if (canShowKeybinding) {
Text(
modifier = Modifier.padding(style.metrics.itemMetrics.keybindingsPadding),
text = keybindingHint,
color = itemColors.keybindingTintFor(itemState).value,
)
}
}
}
{
@Suppress("DEPRECATION") // Not really deprecated, MenuItemColors will be made internal
Text(
modifier = Modifier.padding(style.metrics.itemMetrics.keybindingsPadding),
text = keybindingHint,
color = style.colors.itemColors.keybindingTintFor(itemState).value,
)
}
} else null,
content = { content(itemState) },
)
}
}
@@ -939,8 +895,11 @@ public fun MenuSubmenuItem(
}
}
@VisibleForTesting
@ApiStatus.Internal
@InternalJewelApi
@Composable
internal fun MenuSubmenuItem(
public fun MenuSubmenuItem(
showIcon: Boolean,
selected: Boolean,
submenu: MenuScope.() -> Unit,
@@ -951,43 +910,17 @@ internal fun MenuSubmenuItem(
style: MenuStyle = JewelTheme.menuStyle,
@Suppress("DEPRECATION") content: @Composable (itemState: MenuItemState) -> Unit,
) {
var itemState by
remember(interactionSource) {
@Suppress("DEPRECATION") mutableStateOf(MenuItemState.of(selected = selected, enabled = enabled))
}
remember(enabled) { itemState = itemState.copy(selected = false, enabled = enabled) }
var itemState by rememberMenuItemState(selected, enabled, interactionSource)
// When the item becomes disabled, close any open submenu
remember(enabled) { if (!enabled) itemState = itemState.copy(selected = false) }
val focusRequester = remember { FocusRequester() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> itemState = itemState.copy(pressed = true)
is PressInteraction.Cancel,
is PressInteraction.Release -> itemState = itemState.copy(pressed = false)
is HoverInteraction.Enter -> itemState = itemState.copy(hovered = true)
is HoverInteraction.Exit -> itemState = itemState.copy(hovered = false)
is FocusInteraction.Focus -> itemState = itemState.copy(focused = true)
is FocusInteraction.Unfocus -> itemState = itemState.copy(focused = false)
}
}
}
remember(selected) { itemState = itemState.copy(selected = selected) }
LaunchedEffect(itemState.isSelected) { if (itemState.isSelected) focusRequester.requestFocus() }
val itemColors = style.colors.itemColors
val menuMetrics = style.metrics
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
val backgroundColor by itemColors.backgroundFor(itemState)
Box(
modifier =
modifier
.fillMaxWidth()
.drawItemBackground(menuMetrics.itemMetrics, backgroundColor)
.focusRequester(focusRequester)
.clickable(
onClick = { itemState = itemState.copy(selected = !itemState.isSelected) },
@@ -1004,32 +937,23 @@ internal fun MenuSubmenuItem(
}
}
) {
@Suppress("DEPRECATION") // Not really deprecated, will be made internal
CompositionLocalProvider(LocalContentColor provides itemColors.contentFor(itemState).value) {
Row(
Modifier.fillMaxWidth().padding(menuMetrics.itemMetrics.contentPadding),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
if (showIcon) {
if (iconKey != null) {
Icon(key = iconKey, contentDescription = null)
} else {
Box(Modifier.size(style.metrics.itemMetrics.iconSize))
}
}
Box(Modifier.weight(1f)) { content(itemState) }
MenuItemLayout(
itemState = itemState,
style = style,
iconKey = iconKey,
canShowIcon = showIcon,
trailingContent = {
@Suppress("DEPRECATION") // Not really deprecated, MenuItemColors will be made internal
Icon(
key = style.icons.submenuChevron,
tint = itemColors.iconTintFor(itemState).value,
contentDescription = null,
modifier = Modifier.size(style.metrics.itemMetrics.iconSize),
tint = style.colors.itemColors.iconTintFor(itemState).value,
hint = Stateful(itemState),
)
}
}
},
content = { content(itemState) },
)
if (itemState.isSelected) {
Submenu(
@@ -1048,6 +972,60 @@ internal fun MenuSubmenuItem(
}
}
@Suppress("DEPRECATION") // Not really deprecated, MenuItemColors be made internal
@Composable
internal fun MenuItemLayout(
itemState: MenuItemState,
modifier: Modifier = Modifier,
style: MenuStyle = JewelTheme.menuStyle,
iconKey: IconKey? = null,
canShowIcon: Boolean = true,
enabled: Boolean = true,
trailingContent: (@Composable () -> Unit)? = null,
content: @Composable () -> Unit,
) {
val itemColors = style.colors.itemColors
val itemMetrics = style.metrics.itemMetrics
val contentColor = itemColors.contentFor(itemState).value
val backgroundColor by itemColors.backgroundFor(itemState)
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides LocalTextStyle.current.copy(color = contentColor),
) {
Row(
modifier =
modifier
.fillMaxWidth()
.defaultMinSize(minHeight = itemMetrics.minHeight)
.drawItemBackground(itemMetrics, backgroundColor)
.padding(itemMetrics.contentPadding),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (canShowIcon) {
val iconModifier = Modifier.size(itemMetrics.iconSize)
if (iconKey != null) {
Icon(
key = iconKey,
contentDescription = null,
modifier = iconModifier.thenIf(!enabled) { disabledAppearance() },
)
} else {
Box(modifier = iconModifier)
}
}
Box(modifier = Modifier.weight(1f)) { content() }
if (trailingContent != null) {
trailingContent()
}
}
}
}
private fun Modifier.drawItemBackground(itemMetrics: MenuItemMetrics, backgroundColor: Color) = drawBehind {
val cornerSizePx = itemMetrics.selectionCornerSize.toPx(size, density = this)
val cornerRadius = CornerRadius(cornerSizePx, cornerSizePx)
@@ -1201,3 +1179,32 @@ public value class MenuItemState(public val state: ULong) : SelectableComponentS
}
}
}
@Suppress("DEPRECATION") // Not really deprecated, MenuItemState will be made internal
@Composable
internal fun rememberMenuItemState(
selected: Boolean,
enabled: Boolean,
interactionSource: MutableInteractionSource,
): MutableState<MenuItemState> {
val itemState =
remember(interactionSource) { mutableStateOf(MenuItemState.of(selected = selected, enabled = enabled)) }
remember(enabled, selected) { itemState.value = itemState.value.copy(selected = selected, enabled = enabled) }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> itemState.value = itemState.value.copy(pressed = true)
is PressInteraction.Cancel,
is PressInteraction.Release -> itemState.value = itemState.value.copy(pressed = false)
is HoverInteraction.Enter -> itemState.value = itemState.value.copy(hovered = true)
is HoverInteraction.Exit -> itemState.value = itemState.value.copy(hovered = false)
is FocusInteraction.Focus -> itemState.value = itemState.value.copy(focused = true)
is FocusInteraction.Unfocus -> itemState.value = itemState.value.copy(focused = false)
}
}
}
return itemState
}
@@ -76,6 +76,9 @@ jewel.swing.badge.green=Green
jewel.swing.badge.green.secondary=Green secondary
jewel.swing.badge.purple.secondary=Purple secondary
jewel.swing.badge.gray.secondary=Gray secondary
jewel.section.menu.label=Menus:
jewel.section.menu.swing.button=Open IntelliJ Platform Menu
jewel.section.menu.swing.submenu=Submenu
compose.sandbox=Compose Sandbox
compose.sandbox.show.automatically.on.project.open=Show automatically on project open
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@@ -19,6 +20,7 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
@@ -30,6 +32,11 @@ import com.intellij.devkit.compose.DevkitComposeBundle
import com.intellij.devkit.compose.icons.DevkitComposeIcons
import com.intellij.icons.AllIcons
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.editor.colors.EditorFontType
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.popup.JBPopup
@@ -63,23 +70,24 @@ import org.jetbrains.jewel.ui.component.DefaultSplitButton
import org.jetbrains.jewel.ui.component.EditableListComboBox
import org.jetbrains.jewel.ui.component.ExternalLink
import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.InformationDefaultBanner
import org.jetbrains.jewel.ui.component.InlineInformationBanner
import org.jetbrains.jewel.ui.component.ListComboBox
import org.jetbrains.jewel.ui.component.OutlinedButton
import org.jetbrains.jewel.ui.component.OutlinedSplitButton
import org.jetbrains.jewel.ui.component.PopupMenu
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.separator
import org.jetbrains.jewel.ui.disabledAppearance
import org.jetbrains.jewel.ui.icons.AllIconsKeys
import org.jetbrains.jewel.ui.theme.badgeStyle
import org.jetbrains.jewel.ui.theme.textAreaStyle
import org.jetbrains.jewel.ui.typography
import java.awt.Dimension
import java.awt.MouseInfo
import javax.swing.BoxLayout
import javax.swing.DefaultComboBoxModel
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import org.jetbrains.jewel.ui.component.Badge as JewelBadge
@@ -108,6 +116,8 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
separator()
badgesRow()
separator()
menusRow()
separator()
}
.apply {
border = JBUI.Borders.empty(0, 10)
@@ -713,6 +723,79 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() {
}.layout(RowLayout.PARENT_GRID)
}
private fun Panel.menusRow() {
row(DevkitComposeBundle.message("jewel.section.menu.label")) {
button(DevkitComposeBundle.message("jewel.section.menu.swing.button")) {
val actionGroup = createIntelliJMenuActionGroup()
val popupMenu = ActionManager.getInstance().createActionPopupMenu(ActionPlaces.POPUP, actionGroup)
val button = it.source as? JComponent
button?.let { comp ->
popupMenu.setTargetComponent(comp)
popupMenu.component.show(comp, 0, comp.height)
}
}
.align(AlignY.CENTER)
compose {
var showJewelMenu by remember { mutableStateOf(false) }
Box(modifier = Modifier.height(150.dp)) {
OutlinedButton(onClick = { showJewelMenu = true }, modifier = Modifier.align(Alignment.Center)) {
Text("Open Jewel Menu")
}
if (showJewelMenu) {
PopupMenu(
onDismissRequest = { showJewelMenu = false; true },
horizontalAlignment = Alignment.Start,
) {
selectableItem(selected = false, onClick = {}) { Text("Menu Item 1") }
selectableItem(selected = false, onClick = {}) { Text("Menu Item 2") }
selectableItem(selected = false, onClick = {}) { Text("Menu Item 3") }
separator()
submenu(submenu = {
selectableItem(selected = false, onClick = {}) { Text("Submenu Item 1") }
selectableItem(selected = false, onClick = {}) { Text("Submenu Item 2") }
selectableItem(selected = false, onClick = {}) { Text("Submenu Item 3") }
}) {
Text("Submenu")
}
separator()
selectableItem(selected = false, onClick = {}) { Text("Menu Item 4") }
}
}
}
}.align(AlignY.CENTER)
}
.layout(RowLayout.PARENT_GRID)
}
private fun createIntelliJMenuActionGroup(): DefaultActionGroup {
return DefaultActionGroup().apply {
add(createSimpleAction("Menu Item 1"))
add(createSimpleAction("Menu Item 2"))
add(createSimpleAction("Menu Item 3"))
addSeparator()
val submenu = DefaultActionGroup(DevkitComposeBundle.message("jewel.section.menu.swing.submenu"), true)
submenu.add(createSimpleAction("Submenu Item 1"))
submenu.add(createSimpleAction("Submenu Item 2"))
submenu.add(createSimpleAction("Submenu Item 3"))
add(submenu)
addSeparator()
add(createSimpleAction("Menu Item 4"))
}
}
private fun createSimpleAction(text: String): AnAction {
return object : AnAction(text) {
override fun actionPerformed(e: AnActionEvent) {
// No-op for demo purposes
}
}
}
private fun PaddingValues.vertical(): Dp = calculateTopPadding() + calculateBottomPadding()
private fun PaddingValues.horizontal(layoutDirection: LayoutDirection): Dp =