Kotlin Multiplatform Development Help

Desktop-specific components and events

You can use Compose Multiplatform the macOS, Linux, and Windows desktop applications. This page gives a short overview of the desktop-specific components and events. Each section includes a link to a detailed tutorial.

Components

Windows and dialogs

You can use the Window composable to create a regular window and the DialogWindow composable for a modal window that locks its parent until the user closes the modal window:

import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.window.DialogWindow import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberDialogState fun main() = application { Window( onCloseRequest = ::exitApplication, ) { var isDialogOpen by remember { mutableStateOf(false) } Button(onClick = { isDialogOpen = true }) { Text(text = "Open dialog") } if (isDialogOpen) { DialogWindow( onCloseRequest = { isDialogOpen = false }, state = rememberDialogState(position = WindowPosition(Alignment.Center)) ) { // Dialog's content } } } }

Compose Multiplatform provides various features for windows. You can adapt the window size, change its state (size, position), hide it in the tray, make the window draggable, transparent, and so on.

For more information, see the Top-level window management tutorial.

Context menus

Context menus are supported by default for the TextField composable (and the Text composable, if it's selectable):

import androidx.compose.foundation.ContextMenuDataProvider import androidx.compose.foundation.ContextMenuItem import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication(title = "Context menu") { val text = remember { mutableStateOf("Hello!") } Column { ContextMenuDataProvider( items = { listOf( ContextMenuItem("User-defined Action") {/*do something here*/ }, ContextMenuItem("Another user-defined action") {/*do something else*/ } ) } ) { TextField( value = text.value, onValueChange = { text.value = it }, label = { Text(text = "Input") } ) Spacer(Modifier.height(16.dp)) SelectionContainer { Text("Hello World!") } } } }

Default context menu options include copy, cut, paste, and select all. You can add more menu items, customize style and texts, and so on.

For more information, see the Context menu in Compose Multiplatform tutorial.

The system tray

You can use the Tray composable to send notifications to users in the system tray:

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.getValue 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.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.window.Tray import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberNotification import androidx.compose.ui.window.rememberTrayState fun main() = application { var count by remember { mutableStateOf(0) } var isOpen by remember { mutableStateOf(true) } if (isOpen) { val trayState = rememberTrayState() val notification = rememberNotification("Notification", "Message from MyApp!") Tray( state = trayState, icon = TrayIcon, menu = { Item( "Increment value", onClick = { count++ } ) Item( "Send notification", onClick = { trayState.sendNotification(notification) } ) Item( "Exit", onClick = { isOpen = false } ) } ) Window( onCloseRequest = { isOpen = false }, icon = MyAppIcon ) { // Content: Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text(text = "Value: $count") } } } } object MyAppIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color.Green, Offset(size.width / 4, 0f), Size(size.width / 2f, size.height)) drawOval(Color.Blue, Offset(0f, size.height / 4), Size(size.width, size.height / 2f)) drawOval(Color.Red, Offset(size.width / 4, size.height / 4), Size(size.width / 2f, size.height / 2f)) } } object TrayIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color(0xFFFFA500)) } }

There are three types of notifications:

  • notify, a simple notification.

  • warn, a warning notification.

  • error, an error notification.

You can also add an application icon to the system tray.

For more information, see the Menu, tray, and notifications tutorial.

You can use the MenuBar composable to create and customize the menu bar for a particular window:

import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyShortcut import androidx.compose.ui.window.MenuBar import androidx.compose.ui.window.Window import androidx.compose.ui.window.application @OptIn(ExperimentalComposeUiApi::class) fun main() = application { var action by remember { mutableStateOf("Last action: None") } var isOpen by remember { mutableStateOf(true) } if (isOpen) { var isSubmenuShowing by remember { mutableStateOf(false) } Window(onCloseRequest = { isOpen = false }) { MenuBar { Menu("File", mnemonic = 'F') { Item("Copy", onClick = { action = "Last action: Copy" }, shortcut = KeyShortcut(Key.C, ctrl = true)) Item( "Paste", onClick = { action = "Last action: Paste" }, shortcut = KeyShortcut(Key.V, ctrl = true) ) } Menu("Actions", mnemonic = 'A') { CheckboxItem( "Advanced settings", checked = isSubmenuShowing, onCheckedChange = { isSubmenuShowing = !isSubmenuShowing } ) if (isSubmenuShowing) { Menu("Settings") { Item("Setting 1", onClick = { action = "Last action: Setting 1" }) Item("Setting 2", onClick = { action = "Last action: Setting 2" }) } } Separator() Item("About", icon = AboutIcon, onClick = { action = "Last action: About" }) Item("Exit", onClick = { isOpen = false }, shortcut = KeyShortcut(Key.Escape), mnemonic = 'E') } } Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text(text = action) } } } } object AboutIcon : Painter() { override val intrinsicSize = Size(256f, 256f) override fun DrawScope.onDraw() { drawOval(Color(0xFFFFA500)) } }

For more information, see the Menu, tray, and notifications tutorial.

Scrollbars

You can apply scrollbars to scrollable components. The scrollbar and scrollable components share a common state to synchronize with each other:

import androidx.compose.foundation.HorizontalScrollbar import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight 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.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication( title = "Scrollbars", state = WindowState(width = 250.dp, height = 400.dp) ) { Box( modifier = Modifier.fillMaxSize() .background(color = Color(180, 180, 180)) .padding(10.dp) ) { val stateVertical = rememberScrollState(0) val stateHorizontal = rememberScrollState(0) Box( modifier = Modifier .fillMaxSize() .verticalScroll(stateVertical) .padding(end = 12.dp, bottom = 12.dp) .horizontalScroll(stateHorizontal) ) { Column { for (item in 0..30) { TextBox("Item #$item") if (item < 30) { Spacer(modifier = Modifier.height(5.dp)) } } } } VerticalScrollbar( modifier = Modifier.align(Alignment.CenterEnd) .fillMaxHeight(), adapter = rememberScrollbarAdapter(stateVertical) ) HorizontalScrollbar( modifier = Modifier.align(Alignment.BottomStart) .fillMaxWidth() .padding(end = 12.dp), adapter = rememberScrollbarAdapter(stateHorizontal) ) } } @Composable fun TextBox(text: String = "Item") { Box( modifier = Modifier.height(32.dp) .width(400.dp) .background(color = Color(200, 0, 0, 20)) .padding(start = 10.dp), contentAlignment = Alignment.CenterStart ) { Text(text = text) } }

For example, you can attach the VerticalScrollbar composable to the Modifier.verticalScroll and LazyColumn components and the HorizontalScrollbar composable to the Modifier.horizontalScroll and LazyRow components.

For more information, see the Desktop components tutorial.

Tooltips

You can add a tooltip to any component using the TooltipArea composable. TooltipArea is similar to the Box component that can show a tooltip:

import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.TooltipArea import androidx.compose.foundation.TooltipPlacement import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Button import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState @OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class) fun main() = application { Window( onCloseRequest = ::exitApplication, title = "Tooltip Example", state = rememberWindowState(width = 300.dp, height = 300.dp) ) { val buttons = listOf("Button A", "Button B", "Button C", "Button D", "Button E", "Button F") Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { buttons.forEachIndexed { index, name -> // Wrap the button in BoxWithTooltip TooltipArea( tooltip = { // Composable tooltip content: Surface( modifier = Modifier.shadow(4.dp), color = Color(255, 255, 210), shape = RoundedCornerShape(4.dp) ) { Text( text = "Tooltip for ${name}", modifier = Modifier.padding(10.dp) ) } }, modifier = Modifier.padding(start = 40.dp), delayMillis = 600, // In milliseconds tooltipPlacement = TooltipPlacement.CursorPoint( alignment = Alignment.BottomEnd, offset = if (index % 2 == 0) DpOffset(-16.dp, 0.dp) else DpOffset.Zero // Tooltip offset ) ) { Button(onClick = {}) { Text(text = name) } } } } } }

The main parameters of the TooltipArea composable are:

  • tooltip, composable content for a tooltip.

  • tooltipPlacement, defines the tooltip placement. You can specify an anchor (the mouse cursor or the component), an offset, and an alignment.

  • delay, the time in milliseconds after which the tooltip is shown. The default value is 500 ms.

For more information, see the Desktop components tutorial.

Events

Mouse event listeners

You can listen to various mouse events in your desktop project: clicking, moving, scrolling, entering, and exiting.

For example, here is how to set up click listeners with onClick, onDoubleClick, and onLongClick modifiers:

import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material.Text import androidx.compose.runtime.getValue 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.unit.sp import androidx.compose.ui.window.singleWindowApplication fun main() = singleWindowApplication { var count by remember { mutableStateOf(0) } Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth()) { var text by remember { mutableStateOf("Click magenta box!") } Column { @OptIn(ExperimentalFoundationApi::class) Box( modifier = Modifier .background(Color.Magenta) .fillMaxWidth(0.7f) .fillMaxHeight(0.2f) .combinedClickable( onClick = { text = "Click! ${count++}" }, onDoubleClick = { text = "Double click! ${count++}" }, onLongClick = { text = "Long click! ${count++}" } ) ) Text(text = text, fontSize = 40.sp) } } }

For more information, see the Mouse events tutorial.

Keyboard event handlers

You can set up keyboard event handlers with the onKeyEvent and onPreviewKeyEvent properties. Use onPreviewKeyEvent to define shortcuts because it guarantees that children components do not consume key events.

For example, here is how to set up an event handler for the active element in focus, the TextField composable:

import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.singleWindowApplication @OptIn(ExperimentalComposeUiApi::class) fun main() = singleWindowApplication { MaterialTheme { var consumedText by remember { mutableStateOf(0) } var text by remember { mutableStateOf("") } Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) { Text("Consumed text: $consumedText") TextField( value = text, onValueChange = { text = it }, modifier = Modifier.onPreviewKeyEvent { when { (it.isCtrlPressed && it.key == Key.Minus && it.type == KeyEventType.KeyUp) -> { consumedText -= text.length text = "" true } (it.isCtrlPressed && it.key == Key.Equals && it.type == KeyEventType.KeyUp) -> { consumedText += text.length text = "" true } else -> false } } ) } } }

You can also define keyboard event handlers that are always active in the current window for the Window, singleWindowApplication, and Dialog composables.

For more information, see the Keyboard event handling tutorial.

Tabbing navigation between components

You can set up navigation between components with the Tab keyboard shortcut for the next component and ⇧ + Tab for the previous one.

By default, the tabbed navigation allows you to move between focusable components in the order of their appearance. Focusable components include TextField, OutlinedTextField, and BasicTextField composables, as well as components that use Modifier.clickable, such as Button, IconButton, and MenuItem.

For example, here's a window where users can navigate between five text fields using standard shortcuts:

import androidx.compose.ui.window.application import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.Spacer import androidx.compose.material.OutlinedTextField import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp fun main() = application { Window( state = WindowState(size = DpSize(350.dp, 500.dp)), onCloseRequest = ::exitApplication ) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(50.dp) ) { for (x in 1..5) { val text = remember { mutableStateOf("") } OutlinedTextField( value = text.value, singleLine = true, onValueChange = { text.value = it } ) Spacer(modifier = Modifier.height(20.dp)) } } } } }

You can also make a non-focusable component focusable, customize the order of tabbing navigation, and put components into focus.

For more information, see the Tabbing navigation and keyboard focus tutorial.

What's next

Last modified: 01 March 2024