diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeBanner.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeBanner.kt index 383fc4b01ab25..dda2fa2bf7d1a 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeBanner.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridgeBanner.kt @@ -1,5 +1,8 @@ package org.jetbrains.jewel.bridge.theme +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.ui.unit.dp import com.intellij.util.ui.JBUI import org.jetbrains.jewel.bridge.toComposeColor import org.jetbrains.jewel.ui.component.styling.BannerColors @@ -18,7 +21,7 @@ internal fun readDefaultBannerStyle(): DefaultBannerStyles = background = JBUI.CurrentTheme.Banner.INFO_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.INFO_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), success = DefaultBannerStyle( @@ -27,7 +30,7 @@ internal fun readDefaultBannerStyle(): DefaultBannerStyles = background = JBUI.CurrentTheme.Banner.SUCCESS_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.SUCCESS_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), warning = DefaultBannerStyle( @@ -36,7 +39,7 @@ internal fun readDefaultBannerStyle(): DefaultBannerStyles = background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), error = DefaultBannerStyle( @@ -45,7 +48,7 @@ internal fun readDefaultBannerStyle(): DefaultBannerStyles = background = JBUI.CurrentTheme.Banner.ERROR_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.ERROR_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), ) @@ -58,7 +61,7 @@ internal fun readInlineBannerStyle(): InlineBannerStyles = background = JBUI.CurrentTheme.Banner.INFO_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.INFO_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), success = InlineBannerStyle( @@ -67,7 +70,7 @@ internal fun readInlineBannerStyle(): InlineBannerStyles = background = JBUI.CurrentTheme.Banner.SUCCESS_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.SUCCESS_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), warning = InlineBannerStyle( @@ -76,7 +79,7 @@ internal fun readInlineBannerStyle(): InlineBannerStyles = background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), error = InlineBannerStyle( @@ -85,6 +88,13 @@ internal fun readInlineBannerStyle(): InlineBannerStyles = background = JBUI.CurrentTheme.Banner.ERROR_BACKGROUND.toComposeColor(), border = JBUI.CurrentTheme.Banner.ERROR_BORDER_COLOR.toComposeColor(), ), - metrics = BannerMetrics(borderWidth = borderWidth), + metrics = BannerMetrics.defaultBannerMetrics(), ), ) + +private fun BannerMetrics.Companion.defaultBannerMetrics() = + BannerMetrics( + borderWidth, + CornerSize(8.dp), // Swing uses arc diameter of 16, which is radius of 8 + PaddingValues(vertical = 12.dp, horizontal = 12.dp), // Swing uses JBUI.Borders.empty(12) + ) diff --git a/platform/jewel/int-ui/int-ui-standalone/api-dump.txt b/platform/jewel/int-ui/int-ui-standalone/api-dump.txt index 8986387419cd9..a79a61a75cf1d 100644 --- a/platform/jewel/int-ui/int-ui-standalone/api-dump.txt +++ b/platform/jewel/int-ui/int-ui-standalone/api-dump.txt @@ -44,8 +44,10 @@ f:org.jetbrains.jewel.intui.standalone.StandalonePainterHintsProvider - hints(java.lang.String,androidx.compose.runtime.Composer,I):java.util.List f:org.jetbrains.jewel.intui.standalone.StandalonePainterHintsProvider$Companion f:org.jetbrains.jewel.intui.standalone.styling.IntUIBannerStylingKt -- sf:default-3ABfNKs(org.jetbrains.jewel.ui.component.styling.BannerMetrics$Companion,F):org.jetbrains.jewel.ui.component.styling.BannerMetrics +- bsf:default-3ABfNKs(org.jetbrains.jewel.ui.component.styling.BannerMetrics$Companion,F):org.jetbrains.jewel.ui.component.styling.BannerMetrics - bs:default-3ABfNKs$default(org.jetbrains.jewel.ui.component.styling.BannerMetrics$Companion,F,I,java.lang.Object):org.jetbrains.jewel.ui.component.styling.BannerMetrics +- sf:default-ziNgDLE(org.jetbrains.jewel.ui.component.styling.BannerMetrics$Companion,F,androidx.compose.foundation.shape.CornerSize,androidx.compose.foundation.layout.PaddingValues):org.jetbrains.jewel.ui.component.styling.BannerMetrics +- bs:default-ziNgDLE$default(org.jetbrains.jewel.ui.component.styling.BannerMetrics$Companion,F,androidx.compose.foundation.shape.CornerSize,androidx.compose.foundation.layout.PaddingValues,I,java.lang.Object):org.jetbrains.jewel.ui.component.styling.BannerMetrics - sf:getDefault(org.jetbrains.jewel.ui.component.styling.DefaultBannerStyles$Companion):org.jetbrains.jewel.intui.standalone.styling.IntUiDefaultBannerStylesFactory - sf:getDefault(org.jetbrains.jewel.ui.component.styling.InlineBannerStyles$Companion):org.jetbrains.jewel.intui.standalone.styling.IntUiInlineBannerStylesFactory - sf:getError(org.jetbrains.jewel.ui.component.styling.BannerColors$Companion):org.jetbrains.jewel.intui.standalone.styling.IntUiErrorBannerColorFactory diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUIBannerStyling.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUIBannerStyling.kt index 69f68695487e7..303ad8992fd05 100644 --- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUIBannerStyling.kt +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUIBannerStyling.kt @@ -1,5 +1,7 @@ package org.jetbrains.jewel.intui.standalone.styling +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -183,7 +185,15 @@ public object IntUiInlineBannerStylesFactory { InlineBannerStyles(information = information, success = success, warning = warning, error = error) } -public fun BannerMetrics.Companion.default(borderWidth: Dp = 1.dp): BannerMetrics = BannerMetrics(borderWidth) +@Deprecated("Use the `default()` function with `cornerSize` and `paddingValues`", level = DeprecationLevel.HIDDEN) +public fun BannerMetrics.Companion.default(borderWidth: Dp = 1.dp): BannerMetrics = + BannerMetrics(borderWidth, CornerSize(8.dp), PaddingValues(12.dp)) + +public fun BannerMetrics.Companion.default( + borderWidth: Dp = 1.dp, + cornerSize: CornerSize = CornerSize(8.dp), + paddingValues: PaddingValues = PaddingValues(12.dp), +): BannerMetrics = BannerMetrics(borderWidth, cornerSize, paddingValues) // region Inline Information Banner public val InlineBannerStyle.Companion.Information: IntUiInlineInformationBannerStyleFactory diff --git a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt index 0bd4801f0316d..d8e646a7d5706 100644 --- a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt +++ b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Banners.kt @@ -64,6 +64,7 @@ public fun Banners(modifier: Modifier = Modifier) { GroupHeader("Default banner (aka editor banners)") DefaultInformationBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.defaultBannerStyle.information, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", linkActions = { @@ -73,6 +74,7 @@ public fun Banners(modifier: Modifier = Modifier) { ) DefaultInformationBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.defaultBannerStyle.information, iconActions = { iconAction( @@ -90,6 +92,7 @@ public fun Banners(modifier: Modifier = Modifier) { ) DefaultInformationBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.defaultBannerStyle.information, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", icon = null, @@ -99,32 +102,40 @@ public fun Banners(modifier: Modifier = Modifier) { }, ) - DefaultInformationBanner(style = JewelTheme.defaultBannerStyle.information, text = LONG_IPSUM) + DefaultInformationBanner( + modifier = Modifier.fillMaxWidth(), + style = JewelTheme.defaultBannerStyle.information, + text = LONG_IPSUM, + ) DefaultInformationBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.defaultBannerStyle.information, text = LONG_IPSUM, icon = null, ) DefaultInformationBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.defaultBannerStyle.information, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", ) DefaultSuccessBanner( + modifier = Modifier.fillMaxWidth(), text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", style = JewelTheme.defaultBannerStyle.success, ) DefaultWarningBanner( + modifier = Modifier.fillMaxWidth(), text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", style = JewelTheme.defaultBannerStyle.warning, ) DefaultErrorBanner( + modifier = Modifier.fillMaxWidth(), text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", - modifier = Modifier, icon = { Icon(AllIconsKeys.General.BalloonError, null) }, linkActions = { action("Action A", onClick = { clickLabel = "Error default Action A clicked" }) @@ -166,6 +177,7 @@ public fun Banners(modifier: Modifier = Modifier) { } InlineInformationBanner( + modifier = Modifier.fillMaxWidth(), text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", title = optionalTitle, icon = null, @@ -173,6 +185,7 @@ public fun Banners(modifier: Modifier = Modifier) { ) InlineInformationBanner( + modifier = Modifier.fillMaxWidth(), title = optionalTitle, icon = null, iconActions = { @@ -202,6 +215,7 @@ public fun Banners(modifier: Modifier = Modifier) { } InlineInformationBanner( + modifier = Modifier.fillMaxWidth(), title = optionalTitle, icon = null, iconActions = { iconAction(AllIconsKeys.General.Refresh, "Restart", onClick = { restart += 1 }) }, @@ -210,6 +224,7 @@ public fun Banners(modifier: Modifier = Modifier) { ) InlineInformationBanner( + modifier = Modifier.fillMaxWidth(), icon = null, style = JewelTheme.inlineBannerStyle.information, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", @@ -236,6 +251,7 @@ public fun Banners(modifier: Modifier = Modifier) { ) InlineInformationBanner( + modifier = Modifier.fillMaxWidth(), text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", title = optionalTitle, icon = null, @@ -254,12 +270,14 @@ public fun Banners(modifier: Modifier = Modifier) { ) InlineInformationBanner( + modifier = Modifier.fillMaxWidth(), text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", title = optionalTitle, linkActions = null, style = JewelTheme.inlineBannerStyle.information, ) InlineErrorBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.inlineBannerStyle.error, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", title = optionalTitle, @@ -272,6 +290,7 @@ public fun Banners(modifier: Modifier = Modifier) { }, ) InlineInformationBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.inlineBannerStyle.information, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", title = optionalTitle, @@ -281,6 +300,7 @@ public fun Banners(modifier: Modifier = Modifier) { }, ) InlineSuccessBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.inlineBannerStyle.success, text = LONG_IPSUM, title = optionalTitle, @@ -312,6 +332,7 @@ public fun Banners(modifier: Modifier = Modifier) { }, ) InlineWarningBanner( + modifier = Modifier.fillMaxWidth(), style = JewelTheme.inlineBannerStyle.warning, text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt", title = optionalTitle, diff --git a/platform/jewel/ui/api-dump.txt b/platform/jewel/ui/api-dump.txt index e7d09117cd855..bf063ffbf5df7 100644 --- a/platform/jewel/ui/api-dump.txt +++ b/platform/jewel/ui/api-dump.txt @@ -988,6 +988,8 @@ f:org.jetbrains.jewel.ui.component.styling.BannerMetrics - sf:Companion:org.jetbrains.jewel.ui.component.styling.BannerMetrics$Companion - equals(java.lang.Object):Z - f:getBorderWidth-D9Ej5fM():F +- f:getCornerSize():androidx.compose.foundation.shape.CornerSize +- f:getPadding():androidx.compose.foundation.layout.PaddingValues - hashCode():I f:org.jetbrains.jewel.ui.component.styling.BannerMetrics$Companion f:org.jetbrains.jewel.ui.component.styling.BannerStylingKt diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/DefaultBanner.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/DefaultBanner.kt index 46d9074eb3dcf..4fc6d33cff1d1 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/DefaultBanner.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/DefaultBanner.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer @@ -850,10 +851,10 @@ private fun DefaultBannerImpl( modifier: Modifier = Modifier, content: @Composable (() -> Unit), ) { - Column(modifier = modifier) { + Column(modifier = modifier.width(IntrinsicSize.Max)) { Divider(orientation = Orientation.Horizontal, color = style.colors.border, modifier = Modifier.fillMaxWidth()) Row( - modifier = Modifier.background(style.colors.background).padding(10.dp), + modifier = Modifier.background(style.colors.background).padding(style.metrics.padding), verticalAlignment = Alignment.CenterVertically, ) { if (icon != null) { diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InlineBanner.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InlineBanner.kt index b477dd46cd127..20a54409f029a 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InlineBanner.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/InlineBanner.kt @@ -16,25 +16,27 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +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 import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight.Companion.Bold import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import org.jetbrains.annotations.Nls -import org.jetbrains.jewel.foundation.modifier.thenIf import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.foundation.theme.LocalContentColor import org.jetbrains.jewel.ui.component.banner.BannerActionsRow @@ -45,6 +47,9 @@ import org.jetbrains.jewel.ui.component.styling.InlineBannerStyle import org.jetbrains.jewel.ui.icons.AllIconsKeys import org.jetbrains.jewel.ui.theme.inlineBannerStyle +private const val BANNER_ICON_SIZE = 16 +private const val BANNER_CONTENT_SPACING = 8 + /** * Displays an informational inline banner providing subtle, non-intrusive context or feedback. * @@ -1222,46 +1227,120 @@ private fun InlineBannerImpl( content: @Composable (() -> Unit), ) { val borderColor = style.colors.border + val layoutDirection = LocalLayoutDirection.current + val originalPadding = style.metrics.padding + + val endPadding = if (actionIcons != null) 0.dp else originalPadding.calculateEndPadding(layoutDirection) + val adjustedPadding = + PaddingValues( + start = originalPadding.calculateStartPadding(layoutDirection), + top = originalPadding.calculateTopPadding(), + bottom = originalPadding.calculateBottomPadding(), + end = endPadding, + ) + RoundedCornerBox( modifier = modifier.testTag("InlineBanner"), borderColor = borderColor, backgroundColor = style.colors.background, contentColor = JewelTheme.contentColor, - borderWidth = 1.dp, - cornerSize = CornerSize(8.dp), - padding = PaddingValues(), + borderWidth = style.metrics.borderWidth, + cornerSize = style.metrics.cornerSize, + padding = adjustedPadding, ) { - Row(modifier = Modifier.padding(start = 12.dp)) { - if (icon != null) { - Box(modifier = Modifier.padding(top = 12.dp, bottom = 12.dp).size(16.dp)) { icon() } - Spacer(Modifier.width(8.dp)) - } + SubcomposeLayout { constraints -> + val spacingPx = BANNER_CONTENT_SPACING.dp.roundToPx() - Column( - modifier = - Modifier.weight(1f) - .padding(top = 12.dp, bottom = 12.dp) // kftmt plz behave - .thenIf(actionIcons == null) { padding(end = 12.dp) } - ) { - if (title != null) { - Text(text = title, style = textStyle, fontWeight = Bold) - Spacer(Modifier.height(8.dp)) - } - content() + val unconstrained = constraints.copy(minWidth = 0) + val actionIconsPlaceables = + subcompose("actionIcons") { + if (actionIcons != null) { + Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { actionIcons() } + } + } + .map { it.measure(unconstrained) } + val actionIconsWidth = actionIconsPlaceables.maxOfOrNull { it.width } ?: 0 - if (actions != null) { - Spacer(Modifier.height(8.dp)) - FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { actions() } - } - } + val iconPlaceables = + subcompose("icon") { + if (icon != null) { + Box(modifier = Modifier.size(BANNER_ICON_SIZE.dp)) { icon() } + } + } + .map { it.measure(unconstrained) } + val iconWidth = iconPlaceables.firstOrNull()?.width ?: 0 + val iconHeight = iconPlaceables.firstOrNull()?.height ?: 0 + + // Calculate width available for the main content column + // Total - Icon - Spacing + val startOffset = if (iconWidth > 0) iconWidth + spacingPx else 0 + val contentAvailableWidth = (constraints.maxWidth - startOffset).coerceAtLeast(0) + + // Text MUST respect Action Icons (i.e, subtract their width) + val textConstraints = + constraints.copy(minWidth = 0, maxWidth = (contentAvailableWidth - actionIconsWidth).coerceAtLeast(0)) + val textPlaceables = + subcompose("text") { + Column { + if (title != null) { + Text(text = title, style = textStyle, fontWeight = Bold) + Spacer(Modifier.height(BANNER_CONTENT_SPACING.dp)) + } + content() + } + } + .map { it.measure(textConstraints) } + val textHeight = textPlaceables.maxOfOrNull { it.height } ?: 0 + val textWidth = textPlaceables.maxOfOrNull { it.width } ?: 0 - if (actionIcons != null) { - Spacer(Modifier.width(8.dp)) - Row( - modifier = Modifier.align(Alignment.Top).padding(top = 8.dp, end = 8.dp, bottom = 8.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - actionIcons() + // Link Actions must IGNORE Action Icons (use full available width) + // This allows buttons to render underneath the top-right icons + val linkConstraints = constraints.copy(minWidth = 0, maxWidth = contentAvailableWidth) + val linkPlaceables = + subcompose("links") { + if (actions != null) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { actions() } + } + } + .map { it.measure(linkConstraints) } + val linksHeight = linkPlaceables.maxOfOrNull { it.height } ?: 0 + val linksWidth = linkPlaceables.maxOfOrNull { it.width } ?: 0 + + val contentHeight = textHeight + (if (linksHeight > 0) spacingPx else 0) + linksHeight + val totalHeight = maxOf(iconHeight, contentHeight) + // calculating the width the banner actually wants to occupy, coerced to + // fit within the incoming minWidth/maxWidth constraints + val naturalWidth = + (startOffset + maxOf(textWidth + actionIconsWidth, linksWidth)).coerceIn( + constraints.minWidth, + constraints.maxWidth, + ) + + layout(naturalWidth, totalHeight) { + iconPlaceables.forEach { it.placeRelative(0, 0) } + + // Starts after icon + textPlaceables.forEach { it.placeRelative(startOffset, 0) } + + // Starts after icon, below text + val linkY = textHeight + (if (linksHeight > 0) spacingPx else 0) + linkPlaceables.forEach { it.placeRelative(startOffset, linkY) } + + if (actionIconsPlaceables.isNotEmpty()) { + // We always offset the action icon to half the padding + val topPaddingPx = adjustedPadding.calculateTopPadding().roundToPx() + val halfPaddingTop = (originalPadding.calculateTopPadding().toPx() / 2).toInt() + val halfPaddingEnd = (originalPadding.calculateEndPadding(layoutDirection).toPx() / 2).toInt() + + // Offset Logic: + val yPos = halfPaddingTop - topPaddingPx + + actionIconsPlaceables.forEach { placeable -> + // For the X calculation, naturalWidth is the edge (since we killed end padding) + // Move left by icon width + half original padding + val xPos = naturalWidth - placeable.width - halfPaddingEnd + placeable.placeRelative(x = xPos, y = yPos) + } } } } @@ -1280,14 +1359,16 @@ private fun RoundedCornerBox( content: @Composable () -> Unit, ) { val shape = RoundedCornerShape(cornerSize) - Box( + Layout( modifier = modifier .border(borderWidth, borderColor, shape) .background(backgroundColor, shape) .clip(shape) - .padding(padding) - ) { - CompositionLocalProvider(LocalContentColor provides contentColor) { content() } + .padding(padding), + content = { CompositionLocalProvider(LocalContentColor provides contentColor) { content() } }, + ) { measurables, constraints -> + val placeable = measurables.first().measure(constraints) + layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } } diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/BannerStyling.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/BannerStyling.kt index f2d6f8a21dbac..b1a3c538db00d 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/BannerStyling.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/BannerStyling.kt @@ -1,11 +1,14 @@ package org.jetbrains.jewel.ui.component.styling +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CornerSize import androidx.compose.runtime.Immutable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import org.jetbrains.jewel.foundation.GenerateDataFunctions @Stable @@ -172,19 +175,40 @@ public class BannerColors(public val background: Color, public val border: Color @Stable @GenerateDataFunctions -public class BannerMetrics(public val borderWidth: Dp) { +public class BannerMetrics( + public val borderWidth: Dp, + public val cornerSize: CornerSize, + public val padding: PaddingValues, +) { + @Deprecated( + "Use the constructor with `cornerSize` and `padding` parameters", + replaceWith = ReplaceWith("BannerMetrics(borderWidth, cornerSize = TODO(), padding = TODO())"), + level = DeprecationLevel.HIDDEN, + ) + public constructor(borderWidth: Dp) : this(borderWidth, CornerSize(8.dp), PaddingValues(12.dp)) + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as BannerMetrics - return borderWidth == other.borderWidth + if (borderWidth != other.borderWidth) return false + if (cornerSize != other.cornerSize) return false + if (padding != other.padding) return false + + return true } - override fun hashCode(): Int = borderWidth.hashCode() + override fun hashCode(): Int { + var result = borderWidth.hashCode() + result = 31 * result + cornerSize.hashCode() + result = 31 * result + padding.hashCode() + return result + } - override fun toString(): String = "BannerMetrics(borderWidth=$borderWidth)" + override fun toString(): String = + "BannerMetrics(" + "borderWidth=$borderWidth, " + "cornerSize=$cornerSize, " + "padding=$padding" + ")" public companion object } diff --git a/plugins/devkit/intellij.devkit.compose/resources/messages/DevkitComposeBundle.properties b/plugins/devkit/intellij.devkit.compose/resources/messages/DevkitComposeBundle.properties index 0172bbf74e83f..340aa519bf2c3 100644 --- a/plugins/devkit/intellij.devkit.compose/resources/messages/DevkitComposeBundle.properties +++ b/plugins/devkit/intellij.devkit.compose/resources/messages/DevkitComposeBundle.properties @@ -57,6 +57,12 @@ jewel.swing.editable.disabled=Editable + Disabled jewel.swing.text.areas=Text areas: jewel.swing.label=Swing jewel.compose.label=Compose +jewel.inline.banners.label=Inline Banners +jewel.swing.inline.banners=Swing inline banner +jewel.compose.inline.banners=Compose inline banner +jewel.banners.label=Banners +jewel.swing.banners=Swing default banner +jewel.compose.banners=Compose default banner compose.sandbox=Compose Sandbox compose.sandbox.show.automatically.on.project.open=Show automatically on project open diff --git a/plugins/devkit/intellij.devkit.compose/src/demo/SwingComparisonTabPanel.kt b/plugins/devkit/intellij.devkit.compose/src/demo/SwingComparisonTabPanel.kt index 58ca79a7a5a71..1dc7aab6a9313 100644 --- a/plugins/devkit/intellij.devkit.compose/src/demo/SwingComparisonTabPanel.kt +++ b/plugins/devkit/intellij.devkit.compose/src/demo/SwingComparisonTabPanel.kt @@ -31,6 +31,8 @@ import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.util.IconLoader +import com.intellij.ui.EditorNotificationPanel +import com.intellij.ui.InlineBanner import com.intellij.ui.JBColor import com.intellij.ui.components.BrowserLink import com.intellij.ui.components.JBLabel @@ -48,9 +50,12 @@ import org.jetbrains.jewel.bridge.JewelComposePanel import org.jetbrains.jewel.bridge.retrieveEditorColorScheme import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.component.DefaultButton +import org.jetbrains.jewel.ui.component.DefaultInformationBanner 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.Text @@ -60,6 +65,7 @@ import org.jetbrains.jewel.ui.disabledAppearance import org.jetbrains.jewel.ui.icons.AllIconsKeys import org.jetbrains.jewel.ui.theme.textAreaStyle import org.jetbrains.jewel.ui.typography +import java.awt.Dimension import javax.swing.BoxLayout import javax.swing.DefaultComboBoxModel import javax.swing.JLabel @@ -77,6 +83,10 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { separator() linksRow() separator() + inlineBannersRow() + separator() + bannersRow() + separator() textFieldsRow() separator() textAreasRow() @@ -123,6 +133,80 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { .layout(RowLayout.PARENT_GRID) } + private fun Panel.inlineBannersRow() { + row(DevkitComposeBundle.message("jewel.inline.banners.label")) { + val inlineBanner = InlineBanner( + messageText = DevkitComposeBundle.message("jewel.swing.inline.banners"), + status = EditorNotificationPanel.Status.Info, + ) + inlineBanner.setGearAction("", {}) + inlineBanner.addAction("Action A") {} + inlineBanner.addAction("Action B") {} + inlineBanner.addAction("Action C") {} + inlineBanner.addAction("Action D") {} + + cell( + component = inlineBanner + ) + .align(AlignY.CENTER) + + compose { + Box(Modifier.width(256.dp)) { + InlineInformationBanner( + text = DevkitComposeBundle.message("jewel.compose.inline.banners"), + iconActions = { + iconAction( + AllIconsKeys.General.GearPlain, + "Close", + onClick = {}, + ) + iconAction( + AllIconsKeys.General.Close, + "Close", + onClick = {}, + ) + }, + linkActions = { + action("Action A", onClick = {}) + action("Action B", onClick = {}) + action("Action C", onClick = {}) + action("Action D", onClick = {}) + } + ) + } + } + } + .layout(RowLayout.PARENT_GRID) + } + + private fun Panel.bannersRow() { + row(DevkitComposeBundle.message("jewel.banners.label")) { + val defaultBanner = + EditorNotificationPanel(EditorNotificationPanel.Status.Info).text(DevkitComposeBundle.message("jewel.swing.banners")) + defaultBanner.setCloseAction { } + cell( + component = defaultBanner + ) + .align(AlignY.CENTER) + + compose { + Box { + DefaultInformationBanner( + text = DevkitComposeBundle.message("jewel.compose.banners"), + iconActions = { + iconAction( + AllIconsKeys.General.Close, + "Close", + onClick = {}, + ) + }, + ) + } + } + } + .layout(RowLayout.PARENT_GRID) + } + private val scrollingContainer = JBScrollPane( mainContent, @@ -214,7 +298,8 @@ internal class SwingComparisonTabPanel : BorderLayoutPanel() { } row(DevkitComposeBundle.message("jewel.swing.titles.swing")) { - text(DevkitComposeBundle.message("jewel.swing.label.this.will.wrap.over.couple.rows"), maxLineLength = 30).component.font = JBFont.h1() + text(DevkitComposeBundle.message("jewel.swing.label.this.will.wrap.over.couple.rows"), maxLineLength = 30).component.font = + JBFont.h1() } row(DevkitComposeBundle.message("jewel.swing.titles.compose")) { compose {