diff --git a/images/compose-sources/compose-desktop-swing-composepanel.animated.gif b/images/compose-sources/compose-desktop-swing-composepanel.animated.gif new file mode 100644 index 00000000..925cf3e1 Binary files /dev/null and b/images/compose-sources/compose-desktop-swing-composepanel.animated.gif differ diff --git a/images/compose-sources/compose-desktop-swing-composepanel.png b/images/compose-sources/compose-desktop-swing-composepanel.png new file mode 100644 index 00000000..069f7246 Binary files /dev/null and b/images/compose-sources/compose-desktop-swing-composepanel.png differ diff --git a/images/compose-sources/compose-desktop-swing-layout.animated.gif b/images/compose-sources/compose-desktop-swing-layout.animated.gif new file mode 100644 index 00000000..d3bf903b Binary files /dev/null and b/images/compose-sources/compose-desktop-swing-layout.animated.gif differ diff --git a/images/compose-sources/compose-desktop-swing-layout.png b/images/compose-sources/compose-desktop-swing-layout.png new file mode 100644 index 00000000..9ae38c31 Binary files /dev/null and b/images/compose-sources/compose-desktop-swing-layout.png differ diff --git a/images/compose-sources/compose-desktop-swinglabel.animated.gif b/images/compose-sources/compose-desktop-swinglabel.animated.gif new file mode 100644 index 00000000..95a19bff Binary files /dev/null and b/images/compose-sources/compose-desktop-swinglabel.animated.gif differ diff --git a/images/compose-sources/compose-desktop-swinglabel.png b/images/compose-sources/compose-desktop-swinglabel.png new file mode 100644 index 00000000..a4ebe4fb Binary files /dev/null and b/images/compose-sources/compose-desktop-swinglabel.png differ diff --git a/images/compose-sources/compose-desktop-swingpanel.animated.gif b/images/compose-sources/compose-desktop-swingpanel.animated.gif new file mode 100644 index 00000000..e6d7298a Binary files /dev/null and b/images/compose-sources/compose-desktop-swingpanel.animated.gif differ diff --git a/images/compose-sources/compose-desktop-swingpanel.png b/images/compose-sources/compose-desktop-swingpanel.png new file mode 100644 index 00000000..27efec4a Binary files /dev/null and b/images/compose-sources/compose-desktop-swingpanel.png differ diff --git a/mpd.tree b/mpd.tree index 5b928662..a2cbefc3 100644 --- a/mpd.tree +++ b/mpd.tree @@ -66,6 +66,7 @@ + diff --git a/topics/compose/compose-desktop-swing-interoperability.md b/topics/compose/compose-desktop-swing-interoperability.md new file mode 100644 index 00000000..670fc6e4 --- /dev/null +++ b/topics/compose/compose-desktop-swing-interoperability.md @@ -0,0 +1,675 @@ +[//]: # (title: Swing interoperability) + +Here, you'll learn about using Swing components in the Compose Multiplatform application and vice versa, +the limitations and advantages of this interoperability, and when you should or shouldn't use this approach. + +The interoperability between Compose Multiplatform and Swing aims to help you: +* Simplify and smooth the migration process of Swing applications to Compose Multiplatform. +* Enhance Compose Multiplatform applications using Swing components when no Compose analogues are available. + +In many cases, it's more effective to implement a missing component directly in Compose Multiplatform (and contribute it to the community) +rather than using a Swing component within a Compose Multiplatform application. + +## Swing interop use cases and limitations + +### Compose Multiplatform component in a Swing app + +The first use case involves adding a Compose Multiplatform component to a Swing application. +You can achieve it using the `ComposePanel` Swing component to render the Compose Multiplatform part of the application. +From Swing's perspective, `ComposePanel` is another Swing component, and handles it accordingly. + +Note that all Compose Multiplatform components, including popups, tooltips, and context menus, are rendered within Swing's `ComposePanel` and positioned and resized inside it. +Therefore, consider replacing these components with Swing-based implementations, or try two new experimental features: + +[Off-screen rendering](#experimental-off-screen-rendering) +: Allows rendering of compose panels directly on Swing components. + +[Separate platform views for popups, dialogs, and dropdowns](#experimental-separate-views-for-popups) +: Popups are no longer limited by the initial composable canvas or the app window. + + +Here are several scenarios for using `ComposePanel`: +* Embed animated objects or a whole panel of animated objects into your application (for example, selection of emoticons or a toolbar with animated reactions to events). +* Implement an interactive rendering area such as graphics or infographics in your application, +which is easier and more convenient to accomplish using Compose Multiplatform. +* Integrate a complex rendering area (potentially even animated) into your application, which is simpler with Compose Multiplatform. +* Replace complex parts of the user interface in your Swing-based application, as Compose Multiplatform provides a convenient +component layout system and a wide range of built-in components and options for quickly creating custom components. + +### Swing component in a Compose Multiplatform app + +Another use case is when you need to use a component that exists in Swing but has no analog in Compose Multiplatform. +If creating its new implementation from scratch is too time-consuming, try `SwingPanel`. The `SwingPanel` function serves +as a wrapper that manages the size, position, and rendering of a Swing component placed on top of a Compose Multiplatform component. + +Note that the Swing component within `SwingPanel` will always be layered above the Compose Multiplatform component, +so anything positioned underneath your `SwingPanel` will be clipped by the Swing component. To avoid clipping and overlapping issues, +try [experimental interop blending](#experimental-interop-blending). If there is still a risk of incorrect rendering, +you can redesign the UI accordingly or avoid using `SwingPanel` and try implementing the missing component, contributing to technology development. + +Here are scenarios for using `SwingPanel`: +* Your application does not require popups, tooltips, or context menus, or at least they are not inside your `SwingPanel`. +* `SwingPanel` remains in a fixed position. In this case, you reduce the risk of glitches and artifacts when the Swing component's position changes. +However, this condition is not mandatory and should be tested for each particular case. + +Compose Multiplatform and Swing can be combined in both ways, allowing for flexible UI design. You can place a `SwingPanel` inside a `ComposePanel`, +which can also be inside another `SwingPanel`. +However, before using such nested combinations, consider potential rendering glitches. +Refer to [Layout with nested `SwingPanel` and `ComposePanel`](#layout-with-nested-swing-and-compose-multiplatform-components) for a code sample. + +## Use Compose Multiplatform in a Swing application + +`ComposePanel` allows you to create a UI with Compose Multiplatform within a Swing-based application. +Add an instance of `ComposePanel` to your Swing layout and define the composition inside `setContent`: + +```kotlin +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import java.awt.BorderLayout +import java.awt.Dimension +import javax.swing.JButton +import javax.swing.JFrame +import javax.swing.SwingUtilities +import javax.swing.WindowConstants + +val northClicks = mutableStateOf(0) +val westClicks = mutableStateOf(0) +val eastClicks = mutableStateOf(0) + +fun main() = SwingUtilities.invokeLater { + val window = JFrame() + + // Create ComposePanel + val composePanel = ComposePanel() + window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE + window.title = "SwingComposeWindow" + + window.contentPane.add(actionButton("NORTH", action = { northClicks.value++ }), BorderLayout.NORTH) + window.contentPane.add(actionButton("WEST", action = { westClicks.value++ }), BorderLayout.WEST) + window.contentPane.add(actionButton("EAST", action = { eastClicks.value++ }), BorderLayout.EAST) + window.contentPane.add( + actionButton( + text = "SOUTH/REMOVE COMPOSE", + action = { + window.contentPane.remove(composePanel) + } + ), + BorderLayout.SOUTH + ) + + // Add ComposePanel to JFrame + window.contentPane.add(composePanel, BorderLayout.CENTER) + + // Set the content + composePanel.setContent { + ComposeContent() + } + + window.setSize(800, 600) + window.isVisible = true +} + +fun actionButton(text: String, action: () -> Unit): JButton { + val button = JButton(text) + button.toolTipText = "Tooltip for $text button." + button.preferredSize = Dimension(100, 100) + button.addActionListener { action() } + return button +} + +@Composable +fun ComposeContent() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Row { + Counter("West", westClicks) + Spacer(modifier = Modifier.width(25.dp)) + Counter("North", northClicks) + Spacer(modifier = Modifier.width(25.dp)) + Counter("East", eastClicks) + } + } +} + +@Composable +fun Counter(text: String, counter: MutableState) { + Surface( + modifier = Modifier.size(130.dp, 130.dp), + color = Color(180, 180, 180), + shape = RoundedCornerShape(4.dp) + ) { + Column { + Box( + modifier = Modifier.height(30.dp).fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text(text = "${text}Clicks: ${counter.value}") + } + Spacer(modifier = Modifier.height(25.dp)) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Button(onClick = { counter.value++ }) { + Text(text = text, color = Color.White) + } + } + } + } +} +``` +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="composePanel.setContent { ComposeContent() }"} + +IntegrationWithSwing + +### Experimental off-screen rendering + +An experimental mode allows rendering compose panels directly on Swing components. +This prevents transitional rendering issues when panels are shown, hidden, or resized. +It also enables proper layering when combining Swing components and compose panels: a Swing component can be shown +above or beneath a `ComposePanel`. + +> Off-screen rendering is [Experimental](supported-platforms.md#compose-multiplatform-ui-framework-stability-levels), +> and you should use it only for evaluation purposes. +> +{style="warning"} + +To enable off-screen rendering, use the `compose.swing.render.on.graphics` system property. +The property must be set before executing any Compose code in your application, so it is recommended to enable it using +the `-D` command-line JVM argument at startup: + +```Console +-Dcompose.swing.render.on.graphics=true +``` + +Alternatively, use `System.setProperty()` at the entry point: + +```kotlin +fun main() { + System.setProperty("compose.swing.render.on.graphics", "true") + ... +} +``` + +### Experimental separate views for popups + +It can be important that popup elements such as tooltips and dropdown menus are not limited by the initial composable canvas +or the app window. For example, when the composable view does not occupy the full screen but needs to spawn an alert dialog. + +> Creating separate views or windows for popups is [Experimental](supported-platforms.md#compose-multiplatform-ui-framework-stability-levels). Opt-in is required (see details below), +> and you should use it only for evaluation purposes. +> +{style="warning"} + +To create separate views or windows for popups on desktop, set the `compose.layers.type` system property. Supported values: +* `WINDOW` creates `Popup` and `Dialog` components as separate undecorated windows. +* `COMPONENT` creates `Popup` or `Dialog` as a separate Swing component in the same window. Note that the setting requires enabled +off-screen rendering (see the [Experimental off-screen rendering](#experimental-off-screen-rendering) section), and off-screen rendering only works +for `ComposePanel` components, not full window applications. + +Note that popups and dialogs are still unable to draw anything outside their own bounds (for example, the shadow of the topmost container). + +Here is an example of the code that uses the `COMPONENT` property: + +```kotlin +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import javax.swing.JFrame +import javax.swing.JLayeredPane +import javax.swing.SwingUtilities +import javax.swing.WindowConstants + +@OptIn(ExperimentalComposeUiApi::class) +fun main() = SwingUtilities.invokeLater { + System.setProperty("compose.swing.render.on.graphics", "true") + System.setProperty("compose.layers.type", "COMPONENT") + + val window = JFrame() + window.defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE + + val contentPane = JLayeredPane() + contentPane.layout = null + + val composePanel = ComposePanel() + composePanel.setBounds(200, 200, 200, 200) + composePanel.setContent { + ComposeContent() + } + + // Use the full window for dialogs + composePanel.windowContainer = contentPane + contentPane.add(composePanel) + + window.contentPane.add(contentPane) + window.setSize(800, 600) + window.isVisible = true +} + +@Composable +fun ComposeContent() { + Box(Modifier.fillMaxSize().background(Color.Green)) { + Dialog(onDismissRequest = {}) { + Box(Modifier.size(100.dp).background(Color.Yellow)) + } + } +} +``` +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="@OptIn(ExperimentalComposeUiApi::class) fun main()"} + +## Use Swing in a Compose Multiplatform application + +`SwingPanel` allows you to create a UI with Swing within a Compose Multiplatform application. +Use the `factory` parameter of `SwingPanel` to create a Swing `JPanel`: + +```kotlin +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.singleWindowApplication +import java.awt.Component +import javax.swing.BoxLayout +import javax.swing.JButton +import javax.swing.JPanel + +fun main() = singleWindowApplication(title = "SwingPanel") { + val counter = remember { mutableStateOf(0) } + + val inc: () -> Unit = { counter.value++ } + val dec: () -> Unit = { counter.value-- } + + Box( + modifier = Modifier.fillMaxWidth().height(60.dp).padding(top = 20.dp), + contentAlignment = Alignment.Center + ) { + Text("Counter: ${counter.value}") + } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.padding(top = 80.dp, bottom = 20.dp) + ) { + Button("1. Compose Button: increment", inc) + Spacer(modifier = Modifier.height(20.dp)) + + SwingPanel( + background = Color.LightGray, + modifier = Modifier.size(270.dp, 90.dp), + factory = { + JPanel().apply { + layout = BoxLayout(this, BoxLayout.Y_AXIS) + add(actionButton("1. Swing Button: decrement", dec)) + add(actionButton("2. Swing Button: decrement", dec)) + add(actionButton("3. Swing Button: decrement", dec)) + } + } + ) + + Spacer(modifier = Modifier.height(20.dp)) + Button("2. Compose Button: increment", inc) + } + } +} + +@Composable +fun Button(text: String = "", action: (() -> Unit)? = null) { + Button( + modifier = Modifier.size(270.dp, 30.dp), + onClick = { action?.invoke() } + ) { + Text(text) + } +} + +fun actionButton( + text: String, + action: () -> Unit +): JButton { + val button = JButton(text) + button.alignmentX = Component.CENTER_ALIGNMENT + button.addActionListener { action() } + + return button +} +``` +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="factory = { JPanel().apply { layout = BoxLayout(this, BoxLayout.Y_AXIS)"} + +SwingPanel + +### Update Swing components when Compose state changes + +To keep a Swing component up to date, provide an `update: (T) -> Unit` callback, which is invoked whenever the composable +state changes or the layout is inflated. +The following code sample demonstrates how to update a Swing component within a `SwingPanel` whenever the composable state changes: + +```kotlin +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.width +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.awt.SwingPanel +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.application +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.rememberWindowState +import java.awt.BorderLayout +import javax.swing.JPanel +import javax.swing.JLabel + +val swingLabel = JLabel() + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 400.dp, height = 200.dp), + title = "SwingLabel" + ) { + val clicks = remember { mutableStateOf(0) } + Column( + modifier = Modifier.fillMaxSize().padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SwingPanel( + modifier = Modifier.fillMaxWidth().height(40.dp), + factory = { + JPanel().apply { + add(swingLabel, BorderLayout.CENTER) + } + }, + update = { + swingLabel.text = "SwingLabel clicks: ${clicks.value}" + } + ) + Spacer(modifier = Modifier.height(40.dp)) + Row ( + modifier = Modifier.height(40.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button(onClick = { clicks.value++ }) { + Text(text = "Increment") + } + Spacer(modifier = Modifier.width(20.dp)) + Button(onClick = { clicks.value-- }) { + Text(text = "Decrement") + } + } + } + } +} +``` +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="factory = { JPanel().apply { add(swingLabel, BorderLayout.CENTER)} }, update = {"} + +SwingLabel + +### Experimental interop blending + +By default, the interop view implemented using the `SwingPanel` wrapper is rectangular and in the foreground, +on top of any Compose Multiplatform components. To make popup elements easier to use, we introduced experimental support for +interop blending. + +> Interop blending is [Experimental](supported-platforms.md#compose-multiplatform-ui-framework-stability-levels), +> and you should use it only for evaluation purposes. +> +{style="warning"} + +To enable this experimental feature, set the `compose.interop.blending` system property to `true`. +The property must be set before executing any Compose code in your application, +so use `System.setProperty()` at the entry point: + +```kotlin +fun main() { + System.setProperty("compose.interop.blending", "true") + ... +} +``` + +With interop blending enabled, you can rely on Swing in the following use cases: + +* **Clipping**. You're no longer limited by a rectangular shape: the `clip` and `shadow` modifiers work correctly with `SwingPanel`. +* **Overlapping**. It is possible to draw any Compose Multiplatform content on top of a `SwingPanel` and interact with it as usual. + +For details and known limitations, see the [description on GitHub](https://github.com/JetBrains/compose-multiplatform-core/pull/915). + +## Layout with nested Swing and Compose Multiplatform components + +With interoperability, you can combine Swing and Compose Multiplatform in both ways: adding Swing components to a Compose Multiplatform +application and adding Compose Multiplatform components to a Swing application. If you want to nest several components and +freely combine approaches, this scenario is also supported. + +The following code sample demonstrates how to add a `SwingPanel` to a `ComposePanel`, which is already inside another `SwingPanel`, +creating a Swing-Compose Multiplatform-Swing structure: + +```kotlin +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.runtime.* +import androidx.compose.ui.awt.* +import androidx.compose.ui.* +import androidx.compose.ui.draw.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.window.* +import androidx.compose.ui.unit.* +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.Insets +import javax.swing.* +import javax.swing.border.EmptyBorder + +val Gray = java.awt.Color(64, 64, 64) +val DarkGray = java.awt.Color(32, 32, 32) +val LightGray = java.awt.Color(210, 210, 210) + +data class Item( + val text: String, + val icon: ImageVector, + val color: Color, + val state: MutableState = mutableStateOf(false) +) +val panelItemsList = listOf( + Item(text = "Person", icon = Icons.Filled.Person, color = Color(10, 232, 162)), + Item(text = "Favorite", icon = Icons.Filled.Favorite, color = Color(150, 232, 150)), + Item(text = "Search", icon = Icons.Filled.Search, color = Color(232, 10, 162)), + Item(text = "Settings", icon = Icons.Filled.Settings, color = Color(232, 162, 10)), + Item(text = "Close", icon = Icons.Filled.Close, color = Color(232, 100, 100)) +) +val itemSize = 50.dp + +fun java.awt.Color.toCompose(): Color { + return Color(red, green, blue) +} + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState(width = 500.dp, height = 500.dp), + title = "Layout" + ) { + Column( + modifier = Modifier.fillMaxSize().background(color = Gray.toCompose()).padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Compose Area", color = LightGray.toCompose()) + Spacer(modifier = Modifier.height(40.dp)) + SwingPanel( + background = DarkGray.toCompose(), + modifier = Modifier.fillMaxSize(), + factory = { + ComposePanel().apply { + setContent { + Box { + SwingPanel( + modifier = Modifier.fillMaxSize(), + factory = { SwingComponent() } + ) + Box ( + modifier = Modifier.align(Alignment.TopStart) + .padding(start = 20.dp, top = 80.dp) + .background(color = DarkGray.toCompose()) + ) { + SwingPanel( + modifier = Modifier.size(itemSize * panelItemsList.size, itemSize), + factory = { + ComposePanel().apply { + setContent { + ComposeOverlay() + } + } + } + ) + } + } + } + } + } + ) + } + } +} + +fun SwingComponent() : JPanel { + return JPanel().apply { + background = DarkGray + border = EmptyBorder(20, 20, 20, 20) + layout = BorderLayout() + add( + JLabel("TextArea Swing Component").apply { + foreground = LightGray + verticalAlignment = SwingConstants.NORTH + horizontalAlignment = SwingConstants.CENTER + preferredSize = Dimension(40, 160) + }, + BorderLayout.NORTH + ) + add( + JTextArea().apply { + background = LightGray + lineWrap = true + wrapStyleWord = true + margin = Insets(10, 10, 10, 10) + text = "The five boxing wizards jump quickly. " + + "Crazy Fredrick bought many very exquisite opal jewels. " + + "Pack my box with five dozen liquor jugs.\n" + + "Cozy sphinx waves quart jug of bad milk. " + + "The jay, pig, fox, zebra and my wolves quack!" + }, + BorderLayout.CENTER + ) + } +} + +@Composable +fun ComposeOverlay() { + Box( + modifier = Modifier.fillMaxSize(). + background(color = DarkGray.toCompose()), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier.background( + shape = RoundedCornerShape(4.dp), + color = Color.DarkGray.copy(alpha = 0.5f) + ) + ) { + for (item in panelItemsList) { + SelectableItem( + text = item.text, + icon = item.icon, + color = item.color, + selected = item.state + ) + } + } + } +} + +@Composable +fun SelectableItem( + text: String, + icon: ImageVector, + color: Color, + selected: MutableState +) { + Box( + modifier = Modifier.size(itemSize) + .clickable { selected.value = !selected.value }, + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.alpha(if (selected.value) 1.0f else 0.5f), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon(modifier = Modifier.size(32.dp), imageVector = icon, contentDescription = null, tint = color) + Text(text = text, color = Color.White, fontSize = 10.sp) + } + } +} +``` +{initial-collapse-state="collapsed" collapsible="true" collapsed-title="fun SwingComponent() : JPanel { return JPanel().apply {"} + +Swing layout + +## What's next? + +Explore the tutorials about [other desktop-specific components](https://github.com/JetBrains/compose-multiplatform/tree/master/tutorials#desktop).