Skip to content

Commit 4894ae3

Browse files
[JEWEL-1184] Add DefaultSplitButton Without Jewel's Dropdown
1 parent 5073ccc commit 4894ae3

5 files changed

Lines changed: 298 additions & 7 deletions

File tree

platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/popup/JDialogRenderer.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,19 +298,38 @@ private fun JPopupImpl(
298298
// - The current properties allow for the popup to be dismissed on click outside;
299299
// - The event must be a "WINDOW_LOST_FOCUS" event;
300300
// - The window receiving the focus should not be a child of this dialog;
301+
// - The window receiving the focus must not be the parent window: when the user clicks back in the parent
302+
// window, WINDOW_LOST_FOCUS fires during MOUSE_PRESSED (before Compose processes the event). Dismissing
303+
// here would cause the popup to close and immediately reopen when a toggle button (e.g., chevron) is
304+
// clicked. Instead, we handle this case on MOUSE_RELEASED via invokeLater (see below), which ensures
305+
// Compose has already processed the click before the dismiss runs.
301306
fun shouldDismissPopup(event: WindowEvent, dialog: Window, currentProperties: PopupProperties): Boolean =
302307
event.window == dialog &&
303308
currentProperties.focusable &&
304309
currentProperties.dismissOnClickOutside &&
305310
event.id == WindowEvent.WINDOW_LOST_FOCUS &&
306-
!dialog.isAncestorOf(event.oppositeWindow)
311+
!dialog.isAncestorOf(event.oppositeWindow) &&
312+
event.oppositeWindow != window
307313

308314
val listener = AWTEventListener { event ->
309315
when (event) {
310316
is MouseEvent -> {
311317
if (shouldDismissPopup(event, dialog, currentProperties)) {
312318
currentOnDismissRequest?.invoke()
313319
}
320+
// For focusable popups, WINDOW_LOST_FOCUS is skipped when the user clicks in the parent window
321+
// (see shouldDismissPopup(WindowEvent) above). We handle dismissal on MOUSE_RELEASED instead.
322+
// invokeLater queues after the current event finishes dispatching, so Compose has already
323+
// processed the click (e.g., a toggle button's onClick) by the time the dismiss runs.
324+
if (
325+
currentProperties.focusable &&
326+
currentProperties.dismissOnClickOutside &&
327+
event.id == MouseEvent.MOUSE_RELEASED &&
328+
!dialog.isAncestorOf(event.component) &&
329+
!dialog.bounds.contains(event.locationOnScreen)
330+
) {
331+
SwingUtilities.invokeLater { currentOnDismissRequest?.invoke() }
332+
}
314333
}
315334
is WindowEvent -> {
316335
if (shouldDismissPopup(event, dialog, currentProperties)) {

platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Buttons.kt

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
package org.jetbrains.jewel.samples.showcase.components
44

5+
import androidx.compose.foundation.background
6+
import androidx.compose.foundation.border
57
import androidx.compose.foundation.layout.Arrangement
68
import androidx.compose.foundation.layout.Box
79
import androidx.compose.foundation.layout.Column
@@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.height
1315
import androidx.compose.foundation.layout.padding
1416
import androidx.compose.foundation.layout.size
1517
import androidx.compose.foundation.layout.width
18+
import androidx.compose.foundation.shape.RoundedCornerShape
1619
import androidx.compose.runtime.Composable
1720
import androidx.compose.runtime.getValue
1821
import androidx.compose.runtime.mutableIntStateOf
@@ -22,8 +25,16 @@ import androidx.compose.runtime.setValue
2225
import androidx.compose.ui.Alignment
2326
import androidx.compose.ui.Modifier
2427
import androidx.compose.ui.graphics.Color
28+
import androidx.compose.ui.layout.onGloballyPositioned
29+
import androidx.compose.ui.layout.positionInParent
30+
import androidx.compose.ui.platform.LocalDensity
2531
import androidx.compose.ui.text.style.TextOverflow
32+
import androidx.compose.ui.unit.DpOffset
33+
import androidx.compose.ui.unit.IntOffset
2634
import androidx.compose.ui.unit.dp
35+
import androidx.compose.ui.unit.round
36+
import androidx.compose.ui.window.PopupProperties
37+
import androidx.compose.ui.window.rememberComponentRectPositionProvider
2738
import org.jetbrains.jewel.foundation.theme.JewelTheme
2839
import org.jetbrains.jewel.foundation.util.JewelLogger
2940
import org.jetbrains.jewel.ui.component.ActionButton
@@ -39,6 +50,7 @@ import org.jetbrains.jewel.ui.component.MenuScope
3950
import org.jetbrains.jewel.ui.component.OutlinedButton
4051
import org.jetbrains.jewel.ui.component.OutlinedSlimButton
4152
import org.jetbrains.jewel.ui.component.OutlinedSplitButton
53+
import org.jetbrains.jewel.ui.component.Popup
4254
import org.jetbrains.jewel.ui.component.SelectableIconActionButton
4355
import org.jetbrains.jewel.ui.component.SelectableIconButton
4456
import org.jetbrains.jewel.ui.component.Text
@@ -54,6 +66,7 @@ import org.jetbrains.jewel.ui.painter.badge.DotBadgeShape
5466
import org.jetbrains.jewel.ui.painter.hints.Badge
5567
import org.jetbrains.jewel.ui.painter.hints.Selected
5668
import org.jetbrains.jewel.ui.painter.hints.Stroke
69+
import org.jetbrains.jewel.ui.theme.defaultSplitButtonStyle
5770
import org.jetbrains.jewel.ui.theme.outlinedSplitButtonStyle
5871
import org.jetbrains.jewel.ui.theme.transparentIconButtonStyle
5972

@@ -399,6 +412,16 @@ private fun SplitButtons() {
399412
},
400413
)
401414

415+
val defaultExpandedOutline =
416+
with(LocalDensity.current) {
417+
JewelTheme.defaultSplitButtonStyle.button.metrics.focusOutlineExpand.roundToPx()
418+
}
419+
SplitButtonWithComposePopup(defaultExpandedOutline) { mod, expanded, onChange ->
420+
DefaultSplitButton(modifier = mod, onClick = {}, expanded = expanded, onExpandedChange = onChange) {
421+
SingleLineText("Split button w/ Compose Popup")
422+
}
423+
}
424+
402425
Tooltip(
403426
tooltip = {
404427
Text("This button is intentionally too narrow, to check that it works when space-constrained.")
@@ -420,6 +443,16 @@ private fun SplitButtons() {
420443
menuContent = { blankNotice() },
421444
modifier = Modifier.height(JewelTheme.outlinedSplitButtonStyle.button.metrics.minSize.height * 1.25f),
422445
)
446+
447+
val outlinedExpandedOutline =
448+
with(LocalDensity.current) {
449+
JewelTheme.outlinedSplitButtonStyle.button.metrics.focusOutlineExpand.roundToPx()
450+
}
451+
SplitButtonWithComposePopup(outlinedExpandedOutline) { mod, expanded, onChange ->
452+
OutlinedSplitButton(modifier = mod, onClick = {}, expanded = expanded, onExpandedChange = onChange) {
453+
SingleLineText("Outlined Split button w/ Compose Popup")
454+
}
455+
}
423456
}
424457
}
425458
}
@@ -432,3 +465,40 @@ private fun MenuScope.blankNotice() {
432465
private fun SingleLineText(text: String) {
433466
Text(text, overflow = TextOverflow.Ellipsis, maxLines = 1)
434467
}
468+
469+
@Composable
470+
private fun SplitButtonWithComposePopup(
471+
expandedOutlinePx: Int,
472+
button: @Composable (modifier: Modifier, expanded: Boolean, onExpandedChange: (Boolean) -> Unit) -> Unit,
473+
) {
474+
var showPopup by remember { mutableStateOf(false) }
475+
var position by remember { mutableStateOf(IntOffset.Zero) }
476+
477+
Box {
478+
button(
479+
Modifier.onGloballyPositioned { position = it.positionInParent().round() },
480+
showPopup,
481+
{ showPopup = it },
482+
)
483+
if (showPopup) {
484+
Popup(
485+
popupPositionProvider =
486+
rememberComponentRectPositionProvider(
487+
offset = DpOffset(position.x.dp, (position.y + expandedOutlinePx).dp)
488+
),
489+
onDismissRequest = { showPopup = false },
490+
properties = PopupProperties(focusable = true),
491+
) {
492+
Box(
493+
modifier =
494+
Modifier.background(JewelTheme.globalColors.panelBackground, RoundedCornerShape(8.dp))
495+
.border(3.dp, JewelTheme.globalColors.outlines.warning, RoundedCornerShape(8.dp))
496+
.padding(16.dp),
497+
contentAlignment = Alignment.Center,
498+
) {
499+
Text("Compose Popup")
500+
}
501+
}
502+
}
503+
}
504+
}

platform/jewel/ui/api-dump.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,14 @@ f:org.jetbrains.jewel.ui.component.ButtonKt
138138
- sf:DefaultSlimButton(kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.ButtonStyle,androidx.compose.ui.text.TextStyle,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
139139
- bsf:DefaultSplitButton(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function1,androidx.compose.runtime.Composer,I,I):V
140140
- bsf:DefaultSplitButton(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
141+
- sf:DefaultSplitButton(kotlin.jvm.functions.Function0,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
141142
- sf:DefaultSplitButton-zTVvCW4(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.ui.Modifier,F,F,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I,I):V
142143
- sf:DefaultSplitButton-zTVvCW4(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,androidx.compose.ui.Modifier,androidx.compose.ui.Modifier,F,F,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I,I):V
143144
- sf:OutlinedButton(kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.ButtonStyle,androidx.compose.ui.text.TextStyle,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
144145
- sf:OutlinedSlimButton(kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.ButtonStyle,androidx.compose.ui.text.TextStyle,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
145146
- bsf:OutlinedSplitButton(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function1,androidx.compose.runtime.Composer,I,I):V
146147
- bsf:OutlinedSplitButton(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function0,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
148+
- sf:OutlinedSplitButton(kotlin.jvm.functions.Function0,Z,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I):V
147149
- sf:OutlinedSplitButton-zTVvCW4(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,androidx.compose.ui.Modifier,F,F,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I,I):V
148150
- sf:OutlinedSplitButton-zTVvCW4(kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,androidx.compose.ui.Modifier,androidx.compose.ui.Modifier,F,F,Z,androidx.compose.foundation.interaction.MutableInteractionSource,org.jetbrains.jewel.ui.component.styling.SplitButtonStyle,androidx.compose.ui.text.TextStyle,org.jetbrains.jewel.ui.component.styling.MenuStyle,kotlin.jvm.functions.Function0,kotlin.jvm.functions.Function2,androidx.compose.runtime.Composer,I,I,I):V
149151
f:org.jetbrains.jewel.ui.component.ButtonState

0 commit comments

Comments
 (0)