Browse Source

moved updated desktop tutorials to kmp portal (#5136)

Part 2 of moving tutorials from GitHub to the [Kotlin Multiplatform
Development](https://www.jetbrains.com/help/kotlin-multiplatform-dev)
portal
pull/5174/merge
ElviraMustafina 1 day ago committed by GitHub
parent
commit
a093ae6cc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 61
      tutorials/Accessibility/README.md
  2. 18
      tutorials/Accessibility/Windows.md
  3. BIN
      tutorials/Accessibility/images/custom-widget.png
  4. BIN
      tutorials/Accessibility/images/nvda-compat.png
  5. 385
      tutorials/Context_Menu/README.md
  6. 247
      tutorials/Desktop_Components/README.md
  7. BIN
      tutorials/Desktop_Components/lazy_scrollbar.gif
  8. BIN
      tutorials/Desktop_Components/scrollbars.gif
  9. BIN
      tutorials/Desktop_Components/tooltips.gif
  10. 168
      tutorials/Keyboard/README.md
  11. BIN
      tutorials/Keyboard/keyInputFilter.gif
  12. BIN
      tutorials/Keyboard/window_keyboard.gif
  13. 492
      tutorials/Mouse_Events/README.md
  14. BIN
      tutorials/Mouse_Events/mouse_click.gif
  15. BIN
      tutorials/Mouse_Events/mouse_enter.gif
  16. BIN
      tutorials/Mouse_Events/mouse_event.gif
  17. BIN
      tutorials/Mouse_Events/mouse_move.gif
  18. 665
      tutorials/Native_distributions_and_local_execution/README.md
  19. BIN
      tutorials/Native_distributions_and_local_execution/attrs-error.png
  20. 20
      tutorials/README.md
  21. 504
      tutorials/Swing_Integration/README.md
  22. BIN
      tutorials/Swing_Integration/screenshot.png
  23. BIN
      tutorials/Swing_Integration/swing_compose_layouting.gif
  24. BIN
      tutorials/Swing_Integration/swing_panel.gif
  25. BIN
      tutorials/Swing_Integration/swing_panel_update.gif
  26. 656
      tutorials/Window_API_new/README.md
  27. BIN
      tutorials/Window_API_new/adaptive.png
  28. BIN
      tutorials/Window_API_new/ask_to_close.gif
  29. BIN
      tutorials/Window_API_new/draggable_area.gif
  30. BIN
      tutorials/Window_API_new/hide_instead_of_close.gif
  31. BIN
      tutorials/Window_API_new/multiple_windows.gif
  32. BIN
      tutorials/Window_API_new/state.gif
  33. BIN
      tutorials/Window_API_new/window_properties.gif
  34. BIN
      tutorials/Window_API_new/window_splash.gif

61
tutorials/Accessibility/README.md

@ -1,61 +0,0 @@
# Accessibility support
## Platform Support
| Platform | Status |
|----------|-----------------------------------|
| MacOS | Supported |
| Windows | Supported with Java Access Bridge |
| Linux | Not supported |
## Custom widget with semantic rules
```kotlin
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.*
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.*
fun main() = singleWindowApplication(
title = "Custom Button", state = WindowState(size = DpSize(300.dp, 200.dp))
) {
var count by remember { mutableStateOf(0) }
Box(modifier = Modifier.padding(50.dp)) {
Box(modifier = Modifier
.background(Color.LightGray)
.fillMaxSize()
.clickable { count += 1 }
.semantics(mergeDescendants = true /* Use text from the contents (1) */) {
// This is a button (2)
role = Role.Button
// Add some help text to button (3)
contentDescription = "Click to increment value"
}
) {
val text = when (count) {
0 -> "Click Me!"
1 -> "Clicked"
else -> "Clicked $count times"
}
Text(text, modifier = Modifier.align(Alignment.Center), fontSize = 24.sp)
}
}
}
```
![Custom Widget](./images/custom-widget.png)
# Windows
Accessibility on Windows is provided by Java Access Bridge and is disabled by default. To enable it, run the following command in Command Prompt.
```cmd
%JAVA_HOME%\bin\jabswitch.exe /enable
```
There are some issues with HiDPI display support on windows, see [Desktop Accessibility on Windows](Windows.md) for details.

18
tutorials/Accessibility/Windows.md

@ -1,18 +0,0 @@
# Desktop Accessibility on Windows
## Enabling Java Accessibility on Windows
Java Access Bridge is disabled by default. To enable it, run
```cmd
%JAVA_HOME%\bin\jabswitch.exe /enable
```
## HiDPI issues
### JDK support
HiDPI support in Access Bridge was landed in [JDK-8279227](https://bugs.openjdk.java.net/browse/JDK-8279227). As for Feb/01/2022 this feature is not included in any released JDK, OpenJDK 17.0.4 release with this feature is planned for May 2022.
### NVDA workaround
NVDA 2021.3.1 does not handle widget position properly. Until they fix it we can override DPI awareness in NVDA compatibility settings as shown:
![NVDA compatibility settings](./images/nvda-compat.png)

BIN
tutorials/Accessibility/images/custom-widget.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 KiB

BIN
tutorials/Accessibility/images/nvda-compat.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

385
tutorials/Context_Menu/README.md

@ -1,385 +0,0 @@
# Context Menu in Compose for Desktop
## What is covered
In this tutorial we will cover all aspects of work with Context Menu
using the Compose UI framework.
## Default context menu
There is out-of-the box context menu support for TextField and Selectable text.
To enable standard context menu for a TextField you just need to put it inside DesktopMaterialTheme:
```kotlin
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication(title = "Context menu") {
val text = remember { mutableStateOf("Hello!") }
TextField(
value = text.value,
onValueChange = { text.value = it },
label = { Text(text = "Input") }
)
}
```
<img width="396" alt="image" src="https://user-images.githubusercontent.com/5963351/190021028-c207164d-df04-4294-ad8f-da3106c16fb6.png">
Standard context menu for TextField contains the following items based on text selection: Copy, Cut, Paste, Select All.
Enabling standard context menu for a Text component is similar - you just need to make it selectable:
```kotlin
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication(title = "Context menu") {
SelectionContainer {
Text("Hello World!")
}
}
```
Context menu for text contains just Copy action:
<img width="391" alt="image" src="https://user-images.githubusercontent.com/5963351/190020951-0cc539a2-f698-4e2b-bc20-9d4aa1b11c6f.png">
## User-defined context menu
To enable additional context menu items for TextField and Text components, ContextMenuDataProvider and ContextMenuItem elements are used:
```kotlin
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!")
}
}
}
}
```
In this example Text/TextField context menus will be extended with two additional items:
<img width="430" alt="image" src="https://user-images.githubusercontent.com/5963351/190020831-9b87b191-a351-4f70-a726-d5a53577ad53.png">
## Context menu for an arbitrary area
There is a possibility to create a context menu for an arbitrary application window area. This is implemented using ContextMenuArea API that is
similar to ContextMenuDataProvider.
```kotlin
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication(title = "Context menu") {
ContextMenuArea(items = {
listOf(
ContextMenuItem("User-defined Action") {/*do something here*/},
ContextMenuItem("Another user-defined action") {/*do something else*/}
)
}) {
Box(modifier = Modifier.background(Color.Blue).height(100.dp).width(100.dp))
}
}
```
Right click on the Blue Square will show a context menu with two items:
<img width="423" alt="image" src="https://user-images.githubusercontent.com/5963351/190020592-15e851f8-e356-413c-b5c3-225393712292.png">
## Styling context menu
Style of context menu doesn't comply MaterialTheme. To change its colors, you should override `LocalContextMenuRepresentation`:
```kotlin
import androidx.compose.foundation.DarkDefaultContextMenuRepresentation
import androidx.compose.foundation.LightDefaultContextMenuRepresentation
import androidx.compose.foundation.LocalContextMenuRepresentation
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.TextField
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
isSystemInDarkTheme()
MaterialTheme(
colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
) {
val contextMenuRepresentation = if (isSystemInDarkTheme()) {
DarkDefaultContextMenuRepresentation
} else {
LightDefaultContextMenuRepresentation
}
CompositionLocalProvider(LocalContextMenuRepresentation provides contextMenuRepresentation) {
Surface(Modifier.fillMaxSize()) {
Box {
var value by remember { mutableStateOf("") }
TextField(value, { value = it })
}
}
}
}
}
```
<img width="392" alt="image" src="https://user-images.githubusercontent.com/5963351/190514663-d345a0ba-0b4c-4920-b6cd-743a753d7d83.png">
## Custom text context menu
You can override text menu for all texts and text fields in your application, overriding `TextContextMenu`:
```kotlin
import androidx.compose.foundation.ContextMenuDataProvider
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ContextMenuState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.LocalTextContextMenu
import androidx.compose.foundation.text.TextContextMenu
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.awt.ComposePanel
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import java.awt.Dimension
import java.net.URLEncoder
import java.nio.charset.Charset
import javax.swing.JFrame
import javax.swing.SwingUtilities
fun main() = SwingUtilities.invokeLater {
val panel = ComposePanel()
panel.setContent {
CustomTextMenuProvider {
Column {
SelectionContainer {
Text("Hello, Compose!")
}
var text by remember { mutableStateOf("") }
TextField(text, { text = it })
}
}
}
val window = JFrame()
window.contentPane.add(panel)
window.size = Dimension(800, 600)
window.isVisible = true
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun CustomTextMenuProvider(content: @Composable () -> Unit) {
val textMenu = LocalTextContextMenu.current
val uriHandler = LocalUriHandler.current
CompositionLocalProvider(
LocalTextContextMenu provides object : TextContextMenu {
@Composable
override fun Area(
textManager: TextContextMenu.TextManager,
state: ContextMenuState,
content: @Composable () -> Unit
) {
// Here we reuse the original TextContextMenu, but add an additional item to item on the bottom.
ContextMenuDataProvider({
val shortText = textManager.selectedText.crop()
if (shortText.isNotEmpty()) {
val encoded = URLEncoder.encode(shortText, Charset.defaultCharset())
listOf(ContextMenuItem("Search $shortText") {
uriHandler.openUri("https://google.com/search?q=$encoded")
})
} else {
emptyList()
}
}) {
textMenu.Area(textManager, state, content = content)
}
}
},
content = content
)
}
private fun AnnotatedString.crop() = if (length <= 5) toString() else "${take(5)}..."
```
<img width="453" alt="image" src="https://user-images.githubusercontent.com/5963351/190509388-92cff018-2880-4cfe-95c4-4c023ecac09d.png">
## Swing interoperability
If you are embedding Compose into an existing application, you may want the text context menu to look the same as in other parts of the application. To do this, there is `JPopupTextMenu`:
```kotlin
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.JPopupTextMenu
import androidx.compose.foundation.text.LocalTextContextMenu
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.awt.ComposePanel
import androidx.compose.ui.platform.LocalLocalization
import java.awt.Color
import java.awt.Component
import java.awt.Dimension
import java.awt.Graphics
import java.awt.event.KeyEvent
import java.awt.event.KeyEvent.CTRL_DOWN_MASK
import java.awt.event.KeyEvent.META_DOWN_MASK
import javax.swing.Icon
import javax.swing.JFrame
import javax.swing.JMenuItem
import javax.swing.JPopupMenu
import javax.swing.KeyStroke.getKeyStroke
import javax.swing.SwingUtilities
import org.jetbrains.skiko.hostOs
fun main() = SwingUtilities.invokeLater {
val panel = ComposePanel()
panel.setContent {
JPopupTextMenuProvider(panel) {
Column {
SelectionContainer {
Text("Hello, Compose!")
}
var text by remember { mutableStateOf("") }
TextField(text, { text = it })
}
}
}
val window = JFrame()
window.contentPane.add(panel)
window.size = Dimension(800, 600)
window.isVisible = true
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun JPopupTextMenuProvider(owner: Component, content: @Composable () -> Unit) {
val localization = LocalLocalization.current
CompositionLocalProvider(
LocalTextContextMenu provides JPopupTextMenu(owner) { textManager, items ->
JPopupMenu().apply {
textManager.cut?.also {
add(
swingItem(localization.cut, Color.RED, KeyEvent.VK_X, it)
)
}
textManager.copy?.also {
add(
swingItem(localization.copy, Color.GREEN, KeyEvent.VK_C, it)
)
}
textManager.paste?.also {
add(
swingItem(localization.paste, Color.BLUE, KeyEvent.VK_V, it)
)
}
textManager.selectAll?.also {
add(JPopupMenu.Separator())
add(
swingItem(localization.selectAll, Color.BLACK, KeyEvent.VK_A, it)
)
}
// Here we add other items that can be defined additionaly in the other places of the application via ContextMenuDataProvider
for (item in items) {
add(
JMenuItem(item.label).apply {
addActionListener { item.onClick() }
}
)
}
}
},
content = content
)
}
private fun swingItem(
label: String,
color: Color,
key: Int,
onClick: () -> Unit
) = JMenuItem(label).apply {
icon = circleIcon(color)
accelerator = getKeyStroke(key, if (hostOs.isMacOS) META_DOWN_MASK else CTRL_DOWN_MASK)
addActionListener { onClick() }
}
private fun circleIcon(color: Color) = object : Icon {
override fun paintIcon(c: Component?, g: Graphics, x: Int, y: Int) {
g.create().apply {
this.color = color
translate(16, 2)
fillOval(0, 0, 16, 16)
}
}
override fun getIconWidth() = 16
override fun getIconHeight() = 16
}
```
<img width="448" alt="image" src="https://user-images.githubusercontent.com/5963351/191312702-f455ab2c-4c47-4e11-b615-fc67af1af3f9.png">

247
tutorials/Desktop_Components/README.md

@ -1,247 +0,0 @@
# Desktop components
## What is covered
In this tutorial, we will show you how to use desktop-specific components of Compose for Desktop such as scrollbars and tooltips.
## Scrollbars
You can apply scrollbars to scrollable components. The scrollbar and scrollable components share a common state to synchronize with each other. For example, `VerticalScrollbar` can be attached to `Modifier.verticalScroll`, and `LazyColumn` and `HorizontalScrollbar` can be attached to `Modifier.horizontalScroll` and `LazyRow`.
```kotlin
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)
}
}
```
Scrollbars can be moved by dragging the bars and using the mouse wheel or the touchpad. Horizontal scrolling with the mouse wheel can be performed by side-clicking the wheel or by holding down `Shift`.
<img alt="Scrollbars" src="scrollbars.gif" height="412" />
## Lazy scrollable components with Scrollbar
You can use scrollbars with lazy scrollable components, for example, `LazyColumn`.
```kotlin
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollbarAdapter
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.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Scrollbars",
state = rememberWindowState(width = 250.dp, height = 400.dp)
) {
LazyScrollable()
}
}
@Composable
fun LazyScrollable() {
Box(
modifier = Modifier.fillMaxSize()
.background(color = Color(180, 180, 180))
.padding(10.dp)
) {
val state = rememberLazyListState()
LazyColumn(Modifier.fillMaxSize().padding(end = 12.dp), state) {
items(1000) { x ->
TextBox("Item #$x")
Spacer(modifier = Modifier.height(5.dp))
}
}
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
adapter = rememberScrollbarAdapter(
scrollState = state
)
)
}
}
@Composable
fun TextBox(text: String = "Item") {
Box(
modifier = Modifier.height(32.dp)
.fillMaxWidth()
.background(color = Color(0, 0, 0, 20))
.padding(start = 10.dp),
contentAlignment = Alignment.CenterStart
) {
Text(text = text)
}
}
```
<img alt="Lazy component" src="lazy_scrollbar.gif" height="412" />
## Tooltips
You can add tooltip to any components using `TooltipArea`. `TooltipArea` is similar to a `Box`, but with the ability to show a tooltip.
The main arguments of the `TooltipArea` function:
- tooltip - composable content representing tooltip
- tooltipPlacement - describes how to place tooltip. You can specify an anchor (the mouse cursor or the component), an offset and an alignment
- delay - time delay in milliseconds after which the tooltip will be shown (default is 500 ms)
```kotlin
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 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) }
}
}
}
}
}
```
<img alt="Tooltip" src="tooltips.gif" height="314" />

BIN
tutorials/Desktop_Components/lazy_scrollbar.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

BIN
tutorials/Desktop_Components/scrollbars.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

BIN
tutorials/Desktop_Components/tooltips.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

168
tutorials/Keyboard/README.md

@ -1,168 +0,0 @@
# Keyboard events handling
## Prerequisites
This tutorial expects that you have already set up the Compose project as described in the [Getting Started tutorial](../Getting_Started)
## What is covered
In this tutorial, we will look at two different ways of handling keyboard events in Compose for Desktop as well as the utilities that we have to do this.
## Event handlers
There are two ways to handle key events in Compose for Desktop:
- By setting up an event handler based on the element that is in focus
- By setting up an event handler in the scope of the window
## Focus related events
It works the same as Compose for Android, for details see [API Reference](https://developer.android.com/reference/kotlin/androidx/compose/ui/input/key/package-summary#keyinputfilter)
The most common use case is to define keyboard handlers for active controls like `TextField`. You can use both `onKeyEvent` and `onPreviewKeyEvent` but the last one is usually preferable to define shortcuts while it guarantees you that key events will not be consumed by children components. Here is an example:
```kotlin
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
}
}
)
}
}
}
```
Note the annotation `@OptIn(ExperimentalComposeUiApi::class)`. Some keys related APIs are still an experimental feature of Compose, and later API changes are possible. So it requires the use of a special annotation to emphasize the experimental nature of the code.
<img alt="keyInputFilter" src="keyInputFilter.gif" height="272" />
## Window-scoped events
`Window`, `singleWindowApplication` and `Dialog` functions have a `onPreviewKeyEvent` and a `onKeyEvent` properties. It is possible to use them to define keyboard event handlers that are always active in the current window. You possibly want to use `onPreviewKeyEvent` here to intercept events. Here is an example:
``` kotlin
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.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
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.KeyEventType
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.singleWindowApplication
private var cleared by mutableStateOf(false)
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication(
onKeyEvent = {
if (
it.isCtrlPressed &&
it.isShiftPressed &&
it.key == Key.C &&
it.type == KeyEventType.KeyDown
) {
cleared = true
true
} else {
false
}
}
) {
MaterialTheme {
if (cleared) {
Text("The App was cleared!")
} else {
App()
}
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun App() {
var isDialogOpen by remember { mutableStateOf(false) }
if (isDialogOpen) {
Dialog(
onCloseRequest = { isDialogOpen = false },
onPreviewKeyEvent = {
if (it.key == Key.Escape && it.type == KeyEventType.KeyDown) {
isDialogOpen = false
true
} else {
false
}
}) {
Text("I'm dialog!")
}
}
Column(Modifier.fillMaxSize(), Arrangement.spacedBy(5.dp)) {
Button(
modifier = Modifier.padding(4.dp),
onClick = { isDialogOpen = true }
) {
Text("Open dialog")
}
}
}
```
<img alt="window_keyboard" src="window_keyboard.gif" height="280" />

BIN
tutorials/Keyboard/keyInputFilter.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

BIN
tutorials/Keyboard/window_keyboard.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

492
tutorials/Mouse_Events/README.md

@ -1,492 +0,0 @@
# Mouse events
## What is covered
In this tutorial we will see how to install mouse event listeners on components
in Compose for Desktop.
## Mouse event listeners
### Click listeners
Click listeners are available in both Compose on Android and Compose for Desktop, so code like this will work on both platforms:
```kotlin
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)
}
}
}
```
<img alt="Application running" src="mouse_click.gif" height="500" />
`combinedClickable` supports only the Primary button (Left Mouse Button) and touch events. If there is a need to handle other buttons differently, please have a look at `Modifier.onClick` below (note: `Modifier.onClick` is currently avilable only for Desktop-JVM platform).
### Mouse move listeners
Let's create a window and install a pointer move listener on it that changes the background color according to the mouse pointer position:
```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
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.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
var color by remember { mutableStateOf(Color(0, 0, 0)) }
Box(
modifier = Modifier
.wrapContentSize(Alignment.Center)
.fillMaxSize()
.background(color = color)
.onPointerEvent(PointerEventType.Move) {
val position = it.changes.first().position
color = Color(position.x.toInt() % 256, position.y.toInt() % 256, 0)
}
)
}
```
*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*.
<img alt="Application running" src="mouse_move.gif" height="519" />
### Mouse enter listeners
Compose for Desktop also supports pointer enter and exit handlers, like this:
```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
Column(
Modifier.background(Color.White),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
repeat(10) { index ->
var active by remember { mutableStateOf(false) }
Text(
modifier = Modifier
.fillMaxWidth()
.background(color = if (active) Color.Green else Color.White)
.onPointerEvent(PointerEventType.Enter) { active = true }
.onPointerEvent(PointerEventType.Exit) { active = false },
fontSize = 30.sp,
fontStyle = if (active) FontStyle.Italic else FontStyle.Normal,
text = "Item $index"
)
}
}
}
```
*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*.
<img alt="Application running" src="mouse_enter.gif" height="500" />
### Mouse scroll listeners
```kotlin
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.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
var number by remember { mutableStateOf(0f) }
Box(
Modifier
.fillMaxSize()
.onPointerEvent(PointerEventType.Scroll) {
number += it.changes.first().scrollDelta.y
},
contentAlignment = Alignment.Center
) {
Text("Scroll to change the number: $number", fontSize = 30.sp)
}
}
```
*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*.
### Swing interoperability
Compose for Desktop uses Swing underneath and allows to access raw AWT events:
```kotlin
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.awt.awtEventOrNull
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalComposeUiApi::class)
fun main() = singleWindowApplication {
var text by remember { mutableStateOf("") }
Box(
Modifier
.fillMaxSize()
.onPointerEvent(PointerEventType.Press) {
text = it.awtEventOrNull?.locationOnScreen?.toString().orEmpty()
},
contentAlignment = Alignment.Center
) {
Text(text)
}
}
```
*Note that onPointerEvent is experimental and can be changed in the future. For more stable API look at [Modifier.pointerInput](#listenining-raw-events-in-commonmain-via-modifierpointerinput)*.
### Listenining raw events in commonMain via Modifier.pointerInput
In the snippets above we use `Modifier.onPointerEvent`, which is a helper function to subscribe to some type of pointer events. It is a shorter variant of `Modifier.pointerInput`. For now it is experimental, and desktop-only (you can't use it in commonMain code). If you need to subscribe to events in commonMain or you need stable API, you can use `Modifier.pointerInput`:
```kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
val list = remember { mutableStateListOf<String>() }
Column(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
val position = event.changes.first().position
// on every relayout Compose will send synthetic Move event,
// so we skip it to avoid event spam
if (event.type != PointerEventType.Move) {
list.add(0, "${event.type} $position")
}
}
}
},
) {
for (item in list.take(20)) {
Text(item)
}
}
}
```
### New experimental onClick handlers (only for Desktop-JVM platform)
`Modifier.onClick` provides independent callbacks for clicks, double clicks, long clicks. It handles clicks originated only from pointer events and accessibility `click` event is not handled out of a box.
Each `onClick` can be configured to target specific pointer events (using `matcher: PointerMatcher` and `keyboardModifiers: PointerKeyboardModifiers.() -> Boolean`). `matcher` can be specified to choose what mouse button should trigger a click. `keyboardModifiers` allows for filtering pointer events which have specified keyboardModifiers pressed.
Multiple `onClick` modifiers can be chained to handle different clicks with different conditions (matcher and keyboard modifiers).
Unlike `clickable`, `onClick` doesn't have a default `Modifier.indication`, `Modifier.semantics`, and it doesn't trigger a click event when `Enter` pressed. These modifiers need to be added separately if necessary.
The most generic (with the least number of conditions) `onClick` handlers should be declared before other to ensure correct events propagation.
```kotlin
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.background
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.onClick
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.isAltPressed
import androidx.compose.ui.input.pointer.isShiftPressed
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
fun main() = singleWindowApplication {
Column {
var topBoxText by remember { mutableStateOf("Click me\nusing LMB or LMB + Shift") }
var topBoxCount by remember { mutableStateOf(0) }
// No indication on interaction
Box(modifier = Modifier.size(200.dp, 100.dp).background(Color.Blue)
// the most generic click handler (without extra conditions) should be the first one
.onClick {
// it will receive all LMB clicks except when Shift is pressed
println("Click with primary button")
topBoxText = "LMB ${topBoxCount++}"
}.onClick(
keyboardModifiers = { isShiftPressed } // accept clicks only when Shift pressed
) {
// it will receive all LMB clicks when Shift is pressed
println("Click with primary button and shift pressed")
topBoxCount++
topBoxText = "LMB + Shift ${topBoxCount++}"
}
) {
AnimatedContent(
targetState = topBoxText,
modifier = Modifier.align(Alignment.Center)
) {
Text(text = it, textAlign = TextAlign.Center)
}
}
var bottomBoxText by remember { mutableStateOf("Click me\nusing LMB or\nRMB + Alt") }
var bottomBoxCount by remember { mutableStateOf(0) }
val interactionSource = remember { MutableInteractionSource() }
// With indication on interaction
Box(modifier = Modifier.size(200.dp, 100.dp).background(Color.Yellow)
.onClick(
enabled = true,
interactionSource = interactionSource,
matcher = PointerMatcher.mouse(PointerButton.Secondary), // Right Mouse Button
keyboardModifiers = { isAltPressed }, // accept clicks only when Alt pressed
onLongClick = { // optional
bottomBoxText = "RMB Long Click + Alt ${bottomBoxCount++}"
println("Long Click with secondary button and Alt pressed")
},
onDoubleClick = { // optional
bottomBoxText = "RMB Double Click + Alt ${bottomBoxCount++}"
println("Double Click with secondary button and Alt pressed")
},
onClick = {
bottomBoxText = "RMB Click + Alt ${bottomBoxCount++}"
println("Click with secondary button and Alt pressed")
}
)
.onClick(interactionSource = interactionSource) { // use default parameters
bottomBoxText = "LMB Click ${bottomBoxCount++}"
println("Click with primary button (mouse left button)")
}
.indication(interactionSource, LocalIndication.current)
) {
AnimatedContent(
targetState = bottomBoxText,
modifier = Modifier.align(Alignment.Center)
) {
Text(text = it, textAlign = TextAlign.Center)
}
}
}
}
```
### New experimental onDrag modifier (only for Desktop-JVM platform)
`Modifier.onDrag` allows for configuration of the pointer that should trigger the drag (see `matcher: PointerMatcher`).
Many `onDrag` modifiers can be chained together.
The example below also shows how to access the state of keyboard modifiers (via `LocalWindowInfo.current.keyboardModifier`) for cases when keyboard modifiers can alter the behaviour of the drag (for example: move an item if we perform a simple drag; or copy/paste an item if dragged with Ctrl pressed)
```kotlin
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.onDrag
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
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.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerButton
import androidx.compose.ui.input.pointer.isCtrlPressed
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalFoundationApi::class)
fun main() = singleWindowApplication {
val windowInfo = LocalWindowInfo.current
Column {
var topBoxOffset by remember { mutableStateOf(Offset(0f, 0f)) }
Box(modifier = Modifier.offset {
IntOffset(topBoxOffset.x.toInt(), topBoxOffset.y.toInt())
}.size(100.dp)
.background(Color.Green)
.onDrag { // all default: enabled = true, matcher = PointerMatcher.Primary (left mouse button)
topBoxOffset += it
}
) {
Text(text = "Drag with LMB", modifier = Modifier.align(Alignment.Center))
}
var bottomBoxOffset by remember { mutableStateOf(Offset(0f, 0f)) }
Box(modifier = Modifier.offset {
IntOffset(bottomBoxOffset.x.toInt(), bottomBoxOffset.y.toInt())
}.size(100.dp)
.background(Color.LightGray)
.onDrag(
enabled = true,
matcher = PointerMatcher.mouse(PointerButton.Secondary), // right mouse button
onDragStart = {
println("Gray Box: drag start")
},
onDragEnd = {
println("Gray Box: drag end")
}
) {
val keyboardModifiers = windowInfo.keyboardModifiers
bottomBoxOffset += if (keyboardModifiers.isCtrlPressed) it * 2f else it
}
) {
Text(text = "Drag with RMB,\ntry with CTRL", modifier = Modifier.align(Alignment.Center))
}
}
}
```
There is also a non-modifier way to handle drags using `suspend fun PointerInputScope.detectDragGestures`:
```kotlin
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.PointerMatcher
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
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.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.singleWindowApplication
@OptIn(ExperimentalFoundationApi::class)
fun main() = singleWindowApplication {
var topBoxOffset by remember { mutableStateOf(Offset(0f, 0f)) }
Box(modifier = Modifier.offset {
IntOffset(topBoxOffset.x.toInt(), topBoxOffset.y.toInt())
}.size(100.dp)
.background(Color.Green)
.pointerInput(Unit) {
detectDragGestures(
matcher = PointerMatcher.Primary
) {
topBoxOffset += it
}
}
) {
Text(text = "Drag with LMB", modifier = Modifier.align(Alignment.Center))
}
}
```

BIN
tutorials/Mouse_Events/mouse_click.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

BIN
tutorials/Mouse_Events/mouse_enter.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

BIN
tutorials/Mouse_Events/mouse_event.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

BIN
tutorials/Mouse_Events/mouse_move.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

665
tutorials/Native_distributions_and_local_execution/README.md

@ -1,665 +0,0 @@
# Native distributions & local execution
## What is covered
In this tutorial, we'll show you how to create native distributions (installers/packages) for all the supported systems.
We will also demonstrate how to run an application locally with the same settings as for distributions.
## Available tools
There are two tools available for packaging Compose applications:
1. The Compose Multiplatform Gradle plugin which provides tasks for basic packaging, obfuscation and (macOS only) signing.
2. [Conveyor](https://www.hydraulic.software), which is a separate tool not made by JetBrains.
This tutorial covers how to use the built-in tasks. Conveyor has [its own tutorial](https://conveyor.hydraulic.dev/latest/tutorial/hare/jvm). The choice of which to use boils down to features/ease of use vs price. Conveyor provides support for online updates, cross-building and [various other features](packaging-tools-comparison.md) but requires [a license](https://hydraulic.software/pricing.html) for non-open source projects. The packaging tasks come with the Compose Desktop Gradle plugin, but the resulting packages don't support online updates and will require a multi-platform CI setup to create packages for each OS.
## Gradle plugin
`org.jetbrains.compose` Gradle plugin simplifies the packaging of applications into native distributions using `jpackage` and running an application locally.
Distributable applications are self-contained, installable binaries which include all the Java runtime components they need,
without requiring an installed JDK on the target system.
[Jlink](https://openjdk.java.net/jeps/282) will take care of bundling only the necessary Java Modules in
the distributable package to minimize package size,
but you must still configure the Gradle plugin to tell it which modules you need
(see the `Configuring included JDK modules` section).
## Basic usage
The basic unit of configuration in the plugin is an `application`.
An `application` defines a shared configuration for a set of final binaries.
In other words, an `application` in DSL allows you to pack a bunch of files,
together with a JDK distribution, into a set of compressed binary installers
in various formats (`.dmg`, `.deb`, `.msi`, `.exe`, etc).
``` kotlin
import org.jetbrains.compose.compose
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("jvm")
id("org.jetbrains.compose")
}
dependencies {
implementation(compose.desktop.currentOS)
}
compose.desktop {
application {
mainClass = "example.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
}
}
}
```
The plugin creates the following tasks:
* `package<FormatName>` (e.g. `packageDmg` or `packageMsi`) are used for packaging the app into the corresponding format.
Note, that there is no cross-compilation support available at the moment,
so the formats can only be built using the specific OS (e.g. to build `.dmg` you have to use macOS).
Tasks that are not compatible with the current OS are skipped by default.
* `packageDistributionForCurrentOS` is a [lifecycle](https://docs.gradle.org/current/userguide/more_about_tasks.html#sec:task_categories) task,
aggregating all package tasks for an application.
* `packageUberJarForCurrentOS` is used to create a single jar file, containing all dependencies for current OS.
The task is available starting from the M2 release.
The task expects `compose.desktop.currentOS` to be used as a `compile`/`implementation`/`runtime` dependency.
* `run` is used to run an app locally. You need to define a `mainClass` — an fq-name of a class,
containing the `main` function.
Note, that `run` starts a non-packaged JVM application with full runtime.
This is faster and easier to debug, than creating a compact binary image with minified runtime.
To run a final binary image, use `runDistributable` instead.
* `createDistributable` is used to create a prepackaged application image a final application image without creating an installer.
* `runDistributable` is used to run a prepackaged application image.
Note, that the tasks are created only if the `application` block/property is used in a script.
After a build, output binaries can be found in `${project.buildDir}/compose/binaries`.
## Configuring included JDK modules
The Gradle plugin uses [jlink](https://openjdk.java.net/jeps/282) to minimize a distributable size by
including only necessary JDK modules.
At this time, the Gradle plugin does not automatically determine necessary JDK Modules.
Failure to provide the necessary modules will not cause compilation issues,
but will lead to `ClassNotFoundException` at runtime.
If you encounter `ClassNotFoundException` when running a packaged application or
`runDistributable` task, you can include additional JDK modules using
`modules` DSL method (see example below).
You can determine, which modules are necessary either by hand or by running
`suggestModules` task. `suggestModules` uses the [jdeps](https://docs.oracle.com/javase/9/tools/jdeps.htm)
static analysis tool to determine possible missing modules. Note, that the output of the tool
might be incomplete or list unnecessary modules.
If a distributable size is not critical, you may simply include all runtime modules as an alternative
by using `includeAllModules` DSL property.
``` kotlin
compose.desktop {
application {
nativeDistributions {
modules("java.sql")
// alternatively: includeAllModules = true
}
}
}
```
## Available formats
The following formats available for the supported operating systems:
* macOS — `.dmg` (`TargetFormat.Dmg`), `.pkg` (`TargetFormat.Pkg`)
* Windows — `.exe` (`TargetFormat.Exe`), `.msi` (`TargetFormat.Msi`)
* Linux — `.deb` (`TargetFormat.Deb`), `.rpm` (`TargetFormat.Rpm`)
## Signing & notarization on macOS
By default, Apple does not allow users to execute unsigned applications downloaded from the internet. Users attempting
to run such applications will be faced with an error like this:
<img alt="" src="attrs-error.png" height="462" />
See [our tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md) on how to sign and notarize your application.
## Specifying package version
You must specify a package version for native distribution packages.
You can use the following DSL properties (in order of descending priority):
* `nativeDistributions.<os>.<packageFormat>PackageVersion` specifies a version for a single package format;
* `nativeDistributions.<os>.packageVersion` specifies a version for a single target OS;
* `nativeDistributions.packageVersion` specifies a version for all packages;
For macOS you can also specify the build version using the following DSL properties (in order of descending priority):
* `nativeDistributions.macOS.<packageFormat>PackageBuildVersion` specifies a build version for a single package format;
* `nativeDistributions.macOS.packageBuildVersion` specifies a build version for all macOS packages;
If the build version is not specified, the package version is used.
See [CFBundleShortVersionString](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring) (package version)
and [CFBundleVersion](https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleversion) (build version)
for more information about versions on macOS.
``` kotlin
compose.desktop {
application {
nativeDistributions {
// a version for all distributables
packageVersion = "..."
linux {
// a version for all Linux distributables
packageVersion = "..."
// a version only for the deb package
debPackageVersion = "..."
// a version only for the rpm package
rpmPackageVersion = "..."
}
macOS {
// a version for all macOS distributables
packageVersion = "..."
// a version only for the dmg package
dmgPackageVersion = "..."
// a version only for the pkg package
pkgPackageVersion = "..."
// a build version for all macOS distributables
packageBuildVersion = "..."
// a build version only for the dmg package
dmgPackageBuildVersion = "..."
// a build version only for the pkg package
pkgPackageBuildVersion = "..."
}
windows {
// a version for all Windows distributables
packageVersion = "..."
// a version only for the msi package
msiPackageVersion = "..."
// a version only for the exe package
exePackageVersion = "..."
}
}
}
}
```
Versions must follow the rules:
* For `dmg` and `pkg`:
* The format is `MAJOR[.MINOR][.PATCH]`, where:
* `MAJOR` is an integer > 0;
* `MINOR` is an optional non-negative integer;
* `PATCH` is an optional non-negative integer;
* For `msi` and `exe`:
* The format is `MAJOR.MINOR.BUILD`, where:
* `MAJOR` is a non-negative integer with a maximum value of 255;
* `MINOR` is a non-negative integer with a maximum value of 255;
* `BUILD` is a non-negative integer with a maximum value of 65535;
* For `deb`:
* The format is `[EPOCH:]UPSTREAM_VERSION[-DEBIAN_REVISION]`, where:
* `EPOCH` is an optional non-negative integer;
* `UPSTREAM_VERSION`
* may contain only alphanumerics and the characters `.`, `+`, `-`, `~`;
* must start with a digit;
* `DEBIAN_REVISION`
* is optional;
* may contain only alphanumerics and the characters `.`, `+`, `~`;
* See [Debian documentation](https://www.debian.org/doc/debian-policy/ch-controlfields.html#version) for more details;
* For `rpm`:
* A version must not contain the `-` (dash) character.
## Customizing JDK version
The plugin uses `jpackage`, for which you should be using at least [JDK 17](https://openjdk.java.net/projects/jdk/17/).
Make sure you meet at least one of the following requirements:
* `JAVA_HOME` environment variable points to the compatible JDK version.
* `javaHome` is set via DSL:
``` kotlin
compose.desktop {
application {
javaHome = System.getenv("JDK_17")
}
}
```
## Customizing output dir
``` kotlin
compose.desktop {
application {
nativeDistributions {
outputBaseDir.set(project.buildDir.resolve("customOutputDir"))
}
}
}
```
## Customizing launcher
The following properties are available for customizing the application startup:
* `mainClass` — a fully-qualified name of a class, containing the main method;
* `args` — arguments for the application's main method;
* `jvmArgs` — arguments for the application's JVM.
``` kotlin
compose.desktop {
application {
mainClass = "MainKt"
jvmArgs += listOf("-Xmx2G")
args += listOf("-customArgument")
}
}
```
## Customizing metadata
The following properties are available in the `nativeDistributions` DSL block:
* `packageName` — application's name (default value: Gradle project's [name](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#getName--));
* `packageVersion` — application's version (default value: Gradle project's [version](https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html#getVersion--));
* `description` — application's description (default value: none);
* `copyright` — application's copyright (default value: none);
* `vendor` — application's vendor (default value: none);
* `licenseFile` — application's license (default value: none).
``` kotlin
compose.desktop {
application {
nativeDistributions {
packageName = "ExampleApp"
packageVersion = "0.1-SNAPSHOT"
description = "Compose Example App"
copyright = "© 2020 My Name. All rights reserved."
vendor = "Example vendor"
licenseFile.set(project.file("LICENSE.txt"))
}
}
}
```
## Packaging resources
There are multiple ways to package and load resources with Compose for Desktop.
### JVM resource loading
Since Compose for Desktop uses JVM platform, you can load resources from a jar file using `java.lang.Class` API. Put a file under `src/main/resources`,
then access it using [Class::getResource](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Class.html#getResource(java.lang.String))
or [Class::getResourceAsStream](https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Class.html#getResourceAsStream(java.lang.String)).
### Adding files to packaged application
In some cases putting and reading resources from jar files might be inconvenient.
Or you may want to include a target specific asset (e.g. a file, that is included only
into a macOS package, but not into a Windows one).
Compose Gradle plugin can be configured to put additional
resource files under an installation directory.
To do so, specify a root resource directory via DSL:
```
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageVersion = "1.0.0"
appResourcesRootDir.set(project.layout.projectDirectory.dir("resources"))
}
}
}
```
In the example above a root resource directory is set to `<PROJECT_DIR>/resources`.
Compose Gradle plugin will include all files under the following subdirectories:
1. Files from `<RESOURCES_ROOT_DIR>/common` will be included into all packages.
2. Files from `<RESOURCES_ROOT_DIR>/<OS_NAME>` will be included only into packages for
a specific OS. Possible values for `<OS_NAME>` are: `windows`, `macos`, `linux`.
3. Files from `<RESOURCES_ROOT_DIR>/<OS_NAME>-<ARCH_NAME>` will be included only into packages for
a specific combination of OS and CPU architecture. Possible values for `<ARCH_NAME>` are: `x64` and `arm64`.
For example, files from `<RESOURCES_ROOT_DIR>/macos-arm64` will be included only into packages built for Apple Silicon
Macs.
Included resources can be accessed via `compose.application.resources.dir` system property:
```
import java.io.File
val resourcesDir = File(System.getProperty("compose.application.resources.dir"))
fun main() {
println(resourcesDir.resolve("resource.txt").readText())
}
```
## Customizing content
The plugin can configure itself, when either `org.jetbrains.kotlin.jvm` or `org.jetbrains.kotlin.multiplatform` plugins
are used.
* With `org.jetbrains.kotlin.jvm` the plugin includes content from the `main` [source set](https://docs.gradle.org/current/userguide/java_plugin.html#source_sets).
* With `org.jetbrains.kotlin.multiplatform` the plugin includes content a single [jvm target](https://kotlinlang.org/docs/reference/mpp-dsl-reference.html#targets).
The default configuration is disabled if multiple JVM targets are defined. In this case, the plugin should be configured
manually, or a single target should be specified (see below).
If the default configuration is ambiguous or not sufficient, the plugin can be configured:
* Using a Gradle [source set](https://docs.gradle.org/current/userguide/java_plugin.html#source_sets)
``` kotlin
plugins {
kotlin("jvm")
id("org.jetbrains.compose")
}
val customSourceSet = sourceSets.create("customSourceSet")
compose.desktop {
application {
from(customSourceSet)
}
}
```
* Using a Kotlin [JVM target](https://kotlinlang.org/docs/reference/mpp-dsl-reference.html#targets):
``` kotlin
plugins {
kotlin("multiplatform")
id("org.jetbrains.compose")
}
kotlin {
jvm("customJvmTarget") {}
}
compose.desktop {
application {
from(kotlin.targets["customJvmTarget"])
}
}
```
* manually:
* `disableDefaultConfiguration` can be used to disable the default configuration;
* `dependsOn` can be used to add task dependencies to all plugin's tasks;
* `fromFiles` can be used to specify files to include;
* `mainJar` file property can be specified to point to a jar, containing a main class.
``` kotlin
compose.desktop {
application {
disableDefaultConfiguration()
fromFiles(project.fileTree("libs/") { include("**/*.jar") })
mainJar.set(project.file("main.jar"))
dependsOn("mainJarTask")
}
}
```
## Platform-specific options
Platform-specific options should be set using the corresponding DSL blocks:
``` kotlin
compose.desktop {
application {
nativeDistributions {
macOS {
// macOS specific options
}
windows {
// Windows specific options
}
linux {
// Linux specific options
}
}
}
}
```
The following platform-specific options are available
(the usage of non-documented properties **is not recommended**):
* All platforms:
* `iconFile.set(File("PATH_TO_ICON"))` — a path to a platform-specific icon for the application.
(see the section `App icon` for details);
* `packageVersion = "1.0.0"` — a platform-specific package version
(see the section `Specifying package version` for details);
* `installationPath = "PATH_TO_INSTALL_DIR"` — an absolute or relative path to the default installation directory;
* On Windows `dirChooser = true` may be used to enable customizing the path during installation.
* Linux:
* `packageName = "custom-package-name"` overrides the default application name;
* `debMaintainer = "maintainer@example.com"` — an email of the deb package's maintainer;
* `menuGroup = "my-example-menu-group"` — a menu group for the application;
* `appRelease = "1"` — a release value for the rpm package, or a revision value for the deb package;
* `appCategory = "CATEGORY"` — a group value for the rpm package, or a section value for the deb package;
* `rpmLicenseType = "TYPE_OF_LICENSE"` — a type of license for the rpm package;
* `debPackageVersion = "DEB_VERSION"` — a deb-specific package version
(see the section `Specifying package version` for details);
* `rpmPackageVersion = "RPM_VERSION"` — a rpm-specific package version
(see the section `Specifying package version` for details);
* macOS:
* `bundleID` — a unique application identifier;
* May only contain alphanumeric characters (`A-Z`,`a-z`,`0-9`), hyphen (`-`) and period (`.`) characters;
* Use of a reverse DNS notation (e.g. `com.mycompany.myapp`) is recommended;
* `packageName` — a name of the application;
* `dockName` — a name of the application displayed in the menu bar, the "About <App>" menu item, in the dock, etc.
Equals to `packageName` by default.
* `minimumSystemVersion` — a minimum macOS version required to run the application.
See [LSMinimumSystemVersion](https://developer.apple.com/documentation/bundleresources/information_property_list/lsminimumsystemversion) for details;
* `signing`, `notarization`, `provisioningProfile`, and `runtimeProvisioningProfile` — see
[the corresponding tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md)
for details;
* `appStore = true` — build and sign for the Apple App Store. Requires at least JDK 17;
* `appCategory` — category of the app for the Apple App Store.
Default value is `public.app-category.utilities` when building for the App Store, `Unknown` otherwise.
See [LSApplicationCategoryType](https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype) for a list of valid categories;
* `entitlementsFile.set(File("PATH_TO_ENTITLEMENTS"))` — a path to file containing entitlements to use when signing.
When a custom file is provided, make sure to add the entitlements that are required for Java apps.
See [sandbox.plist](https://github.com/openjdk/jdk/blob/master/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/sandbox.plist) for the default file that is used when building for the App Store. It can be different depending on your JDK version.
If no file is provided the default entitlements provided by jpackage are used.
See [the corresponding tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md#configuring-entitlements)
* `runtimeEntitlementsFile.set(File("PATH_TO_RUNTIME_ENTITLEMENTS"))` — a path to file containing entitlements to use when signing the JVM runtime.
When a custom file is provided, make sure to add the entitlements that are required for Java apps.
See [sandbox.plist](https://github.com/openjdk/jdk/blob/master/src/jdk.jpackage/macosx/classes/jdk/jpackage/internal/resources/sandbox.plist) for the default file that is used when building for the App Store. It can be different depending on your JDK version.
If no file is provided then `entitlementsFile` is used. If that was also not provided, the default entitlements provided by jpackage are used.
See [the corresponding tutorial](/tutorials/Signing_and_notarization_on_macOS/README.md#configuring-entitlements)
* `dmgPackageVersion = "DMG_VERSION"` — a dmg-specific package version
(see the section `Specifying package version` for details);
* `pkgPackageVersion = "PKG_VERSION"` — a pkg-specific package version
(see the section `Specifying package version` for details);
* `packageBuildVersion = "DMG_VERSION"` — a package build version
(see the section `Specifying package version` for details);
* `dmgPackageBuildVersion = "DMG_VERSION"` — a dmg-specific package build version
(see the section `Specifying package version` for details);
* `pkgPackageBuildVersion = "PKG_VERSION"` — a pkg-specific package build version
(see the section `Specifying package version` for details);
* `infoPlist` — see the section `Customizing Info.plist on macOS` for details;
* Windows:
* `console = true` adds a console launcher for the application;
* `dirChooser = true` enables customizing the installation path during installation;
* `perUserInstall = true` enables installing the application on a per-user basis
* `menuGroup = "start-menu-group"` adds the application to the specified Start menu group;
* `upgradeUuid = "UUID"` — a unique ID, which enables users to update an app via installer,
when an updated version is newer, than an installed version. The value must remain constant for a single application.
See [the link](https://wixtoolset.org/documentation/manual/v3/howtos/general/generate_guids.html)
for details on generating a UUID.
* `msiPackageVersion = "MSI_VERSION"` — a msi-specific package version
(see the section `Specifying package version` for details);
* `exePackageVersion = "EXE_VERSION"` — a pkg-specific package version
(see the section `Specifying package version` for details);
## App icon
The app icon needs to be provided in OS-specific formats:
* `.icns` for macOS;
* `.ico` for Windows;
* `.png` for Linux.
``` kotlin
compose.desktop {
application {
nativeDistributions {
macOS {
iconFile.set(project.file("icon.icns"))
}
windows {
iconFile.set(project.file("icon.ico"))
}
linux {
iconFile.set(project.file("icon.png"))
}
}
}
}
```
## Customizing Info.plist on macOS
We aim to support important platform-specific customization use-cases via declarative DSL.
However, the provided DSL is not enough sometimes. If you need to specify `Info.plist`
values, that are not modeled in the DSL, you can work around by specifying a piece
of raw XML, that will be appended to the application's `Info.plist`.
### Example: deep linking into macOS apps
1. Specify a custom URL scheme:
``` kotlin
// build.gradle.kts
compose.desktop {
application {
mainClass = "MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg)
packageName = "Deep Linking Example App"
macOS {
bundleID = "org.jetbrains.compose.examples.deeplinking"
infoPlist {
extraKeysRawXml = macExtraPlistKeys
}
}
}
}
}
val macExtraPlistKeys: String
get() = """
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Example deep link</string>
<key>CFBundleURLSchemes</key>
<array>
<string>compose</string>
</array>
</dict>
</array>
"""
```
2. Use `java.awt.Desktop` to set up a URI handler:
``` kotlin
// src/main/main.kt
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.singleWindowApplication
import java.awt.Desktop
fun main() {
var text by mutableStateOf("Hello, World!")
try {
Desktop.getDesktop().setOpenURIHandler { event ->
text = "Open URI: " + event.uri
}
} catch (e: UnsupportedOperationException) {
println("setOpenURIHandler is unsupported")
}
singleWindowApplication {
MaterialTheme {
Text(text)
}
}
}
```
3. Run `./gradlew runDistributable`.
4. Links like `compose://foo/bar` are now redirected from a browser to your application.
## Minification & obfuscation
Starting from 1.2 the Compose Gradle plugin supports [ProGuard](https://www.guardsquare.com/proguard) out-of-the-box.
ProGuard is a well known [open source](https://github.com/Guardsquare/proguard) tool for minification and obfuscation,
that is developed by [Guardsquare](https://www.guardsquare.com/).
The Gradle plugin provides a *release* task for each corresponding *default* packaging task:
Default task (w/o ProGuard) | Release task (w. ProGuard) | Description
-----------------------------------|------------------------------------------|--------------------------------------------------------------------------
`createDistributable` | `createReleaseDistributable` | Creates an application image with bundled JDK & resources
`runDistributable` | `runReleaseDistributable` | Runs an application image with bundled JDK & resources
`run` | `runRelease` | Runs a non-packaged application `jar` using Gradle JDK
`package<FORMAT_NAME>` | `packageRelease<FORMAT_NAME>` | Packages an application image into a `<FORMAT_NAME>` file
`packageDistributionForCurrentOS` | `packageReleaseDistributionForCurrentOS` | Packages an application image into a format compatible with current OS
`packageUberJarForCurrentOS` | `packageReleaseUberJarForCurrentOS` | Packages an application image into an uber (fat) JAR
`notarize<FORMAT_NAME>` | `notarizeRelease<FORMAT_NAME>` | Uploads a `<FORMAT_NAME>` application image for notarization (macOS only)
`checkNotarizationStatus` | `checkReleaseNotarizationStatus` | Checks if notarization succeeded (macOS only)
The default configuration adds a few ProGuard rules:
* an application image is minified, i.e. non-used classes are removed;
* `compose.desktop.application.mainClass` is used as an entry point;
* a few `keep` rules to avoid breaking Compose runtime.
In many cases getting a minified Compose application will not require any additional configuration.
However, sometimes ProGuard might be unable to track certain usages in bytecode
(for example, this might happen if a class is used via reflection).
If you encounter an issue, which happens only after ProGuard processing,
you might want to add custom rules.
To do so, specify a configuration file via DSL:
```
compose.desktop {
application {
buildTypes.release.proguard {
configurationFiles.from(project.file("compose-desktop.pro"))
}
}
}
```
See the Guardsquare's [comprehensive manual](https://www.guardsquare.com/manual/configuration/usage)
on ProGuard's rules & configuration options.
Obfuscation is disabled by default. To enable it, set the following property via Gradle DSL:
```
compose.desktop {
application {
buildTypes.release.proguard {
obfuscate.set(true)
}
}
}
```
ProGuard's optimizations are enabled by default. To disable them, set the following property via Gradle DSL:
```
compose.desktop {
application {
buildTypes.release.proguard {
optimize.set(false)
}
}
}
```
Joining to the uber JAR is disabled by default - ProGuard produces the corresponding JAR for every input JAR.
To enable it, set the following property via Gradle DSL:
```
compose.desktop {
application {
buildTypes.release.proguard {
joinOutputJars.set(true)
}
}
}
```

BIN
tutorials/Native_distributions_and_local_execution/attrs-error.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

20
tutorials/README.md

@ -5,19 +5,19 @@
## Desktop
* [Image and icon manipulations](Image_And_Icons_Manipulations)
* [Mouse events and hover](Mouse_Events)
* [Scrolling and scrollbars](Desktop_Components#scrollbars)
* [Tooltips](Desktop_Components#tooltips)
* [Context Menu](Context_Menu/README.md)
* [Top level windows management](Window_API_new)
* [Mouse events and hover](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-mouse-events.html)
* [Scrolling and scrollbars](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-scrollbars.html)
* [Tooltips](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-tooltips.html)
* [Context Menu](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-context-menus.html)
* [Top level windows management](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-top-level-windows-management.html)
* [Menu, tray, notifications](Tray_Notifications_MenuBar_new)
* [Keyboard support](Keyboard)
* [Keyboard support](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-keyboard.html)
* [Tab focus navigation](Tab_Navigation)
* [Swing interoperability](Swing_Integration)
* [Swing interoperability](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-swing-interoperability.html)
* [Navigation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation-routing.html)
* [Accessibility](Accessibility)
* [Building a native distribution](Native_distributions_and_local_execution)
* [UI testing](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html)
* [Accessibility](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-accessibility.html)
* [Building a native distribution](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-native-distribution.html)
* [UI testing](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-desktop-ui-testing.html)
## Web (based on Wasm)
* [Getting started](https://kotl.in/wasm-compose-example)

504
tutorials/Swing_Integration/README.md

@ -1,504 +0,0 @@
# Integration of Compose Multiplatform and Swing
## What is covered
In this tutorial, we'll show you how to make the `Swing/Compose` interop work in your application, what its limitations are, what you can achieve with it, in which cases you may use it and when you shouldn't do that.
The main goals of the interoperability between Compose Multiplatform and Swing are
- make it easier and smoother to migrate Swing applications to to Compose
- allow to enhance Compose application with Swing components that don't have 'Compose' analogues
In many cases it is more efficient to implement a missing Component in Compose (and contribute it to community) rather than using a Swing component in a Compose Application.
## Swing interop use cases and limitations
Before combining Compose Multiplatform and Swing, it's important to keep in mind that these two technologies have different approaches to the content rendering. Compose Multiplatform uses one heavyweight Swing component to render all its content and has logical rendering layers, while Swing operates on both heavyweight and lightweight components (`Component/JComponent`). For Swing logic Compose Multiplatform is just one more heavyweight component and it interacts with it the same way as with all other Swing components.
The first use-case is addition of a Compose part into a Swing application. It could be done use `ComposePanel` Swing component to render the "Compose" part of the application. From Swing perspective it will be just another Swing component, that should be treated accordingly. Important point, that all Compose components will be rendered inside the `ComposePanel`, including popups, tooltips, context menus, etc. They will be positioned and resized inside the `ComposePanel`. So probably it would be better to replace them with a Swing based implementation.
Below you can find several cases where the use of `ComposePanel` is justified:
- you want to embed animated objects or a whole panel of animated objects into your application (selection of emoticons, toolbar with animated reaction to events, etc.)
- you want to implement an interactive rendering area in your application, which is easier and more convenient to implement using Compose (for example, any type of graphics or infographics)
- you want to embed a complex rendering area (perhaps even animated) into your application - this is easier and more convenient to do using Compose
- you want to replace complex parts of the user interface of your Swing-based application - Compose has a convenient component layout system, and Compose offers a wide range of built-in components and options for quickly creating your own components
If your case is somewhat similar to one of the above, then you should try to implement it using `ComposePanel`.
The second use case is situation when you want to use some component, that exists in Swing and there is no analogue in Compose. And creating it from scratch is too expensive. In this case, you can use `SwingPanel`. A `SwingPanel` is a wrapper that controls the size, position and rendering of a Swing component that is placed on top of a Compose Multiplatform component, meaning the component inside a `SwingPanel` will always be on top of the Compose in depth. Anything that is misplaced and rests on the `SwingPanel` will be clipped by the Swing component placed there, so try to think about these situations, and if there is such a risk, then it is better to either redesign the UI accordingly, or stop using the `SwingPanel` and still try to implement the missing component, thereby contributing to the development of technology and making life easier for other developers.
Below you can find several cases where the use of `SwingPanel` is justified:
- there are no popups, tooltips, context menus, etc. in your application. or they are not used inside your `SwingPanel`
- in your application, the `SwingPanel` will always be in the same position. This will reduce the risk of glitches and artifacts when changing the position of the Swing component (this condition is not mandatory and you need to test each such case separately)
If your case is somewhat similar to one of the above, then you should try to implement it using `SwingPanel`.
Since Compose Multiplatform and Swing can be combined in both directions, it is quite possible to place a `SwingPanel` into a `ComposePanel`, which in turn could be placed into another `SwingPanel`. In this case, you should be careful to minimize rendering glitches. At the end of this tutorial, you can find an example covering this case.
## Using ComposePanel
`ComposePanel` lets you create a UI using Compose Multiplatform in a Swing-based UI. To achieve this you need to create an instance of `ComposePanel`, add it to your Swing layout, and describe 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()
// creating 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
)
// addind ComposePanel on JFrame
window.contentPane.add(composePanel, BorderLayout.CENTER)
// setting 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<Int>) {
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)
}
}
}
}
}
```
<img alt="IntegrationWithSwing" src="screenshot.png" height="781" />
## Adding a Swing component to Compose Multiplatform composition using SwingPanel
SwingPanel lets you create a UI using Swing in a Compose-based UI. To achieve this you need to create Swing `JComponent` in the `factory` parameter of `SwingPanel`.
```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 {
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.White,
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
}
```
<img alt="IntegrationWithSwing" src="swing_panel.gif" height="523" />
## Updating Swing component when Сompose state changes
Example below shows how to update a Swing component in a `SwingPanel` when the composable state changes. To do this, you need to provide an `update: (T) -> Unit` callback that is called when the composable state changes or after the layout is inflated.
```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.Composable
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),
) {
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.setText("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")
}
}
}
}
}
```
<img alt="IntegrationWithSwing" src="swing_panel_update.gif" height="254" />
## Layouting with SwingPanel and ComposePanel
Example below shows how Compose for Desktop and Swing can be combined in both directions i.e. adding a `SwingPanel` to a `ComposePanel` which is in turn added to another `SwingPanel`.
```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.Canvas
import java.awt.Dimension
import java.awt.Insets
import java.awt.event.*
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<Boolean> = 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),
) {
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<Boolean>
) {
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)
}
}
}
```
<img alt="IntegrationWithSwing" src="swing_compose_layouting.gif" height="600" />

BIN
tutorials/Swing_Integration/screenshot.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

BIN
tutorials/Swing_Integration/swing_compose_layouting.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 MiB

BIN
tutorials/Swing_Integration/swing_panel.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 MiB

BIN
tutorials/Swing_Integration/swing_panel_update.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

656
tutorials/Window_API_new/README.md

@ -1,656 +0,0 @@
# Top level windows management
## What is covered
In this tutorial, we will show you how to work with windows using Compose for Desktop.
We represent the window state in a shape suitable for Compose-style state manipulations and automatically map it to the operating system window state.
Top-level windows can be conditionally created in other composable functions and their window manager state can also be manipulated using states produced by the `rememberWindowState()` function.
## Open and close windows
The main function for creating windows is `Window`. This function should be used in a Composable scope. The easiest way to create a Composable scope is to use the `application` function:
```kotlin
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
// Content
}
}
```
`Window` is a Composable function, meaning you can change its properties in a declarative way:
```kotlin
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.window.Window
import androidx.compose.ui.window.application
fun main() = application {
var fileName by remember { mutableStateOf("Untitled") }
Window(onCloseRequest = ::exitApplication, title = "$fileName - Editor") {
Button(onClick = { fileName = "note.txt" }) {
Text("Save")
}
}
}
```
<img alt="Window properties" src="window_properties.gif" height="260" />
## Open and close windows (conditionally)
You can also close/open windows using a simple `if` statement.
When the `Window` leaves the composition (`isPerformingTask` becomes `false`) – the native window automatically closes.
```kotlin
import androidx.compose.material.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import kotlinx.coroutines.delay
fun main() = application {
var isPerformingTask by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
delay(2000) // Do some heavy lifting
isPerformingTask = false
}
if (isPerformingTask) {
Window(onCloseRequest = ::exitApplication) {
Text("Performing some tasks. Please wait!")
}
} else {
Window(onCloseRequest = ::exitApplication) {
Text("Hello, World!")
}
}
}
```
<img alt="Window splash" src="window_splash.gif" height="354" />
If the window requires some custom logic on close (for example, to show a dialog), you can override the close action using `onCloseRequest`.
Instead of the imperative approach to closing the window (`window.close()`) we use a declarative approach - closing the window in response to a change of the state (`isOpen = false`)..
```kotlin
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.window.DialogWindow
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
var isOpen by remember { mutableStateOf(true) }
var isAskingToClose by remember { mutableStateOf(false) }
if (isOpen) {
Window(
onCloseRequest = { isAskingToClose = true }
) {
if (isAskingToClose) {
DialogWindow(
onCloseRequest = { isAskingToClose = false },
title = "Close the document without saving?",
) {
Button(
onClick = { isOpen = false }
) {
Text("Yes")
}
}
}
}
}
}
```
<img alt="Ask to close" src="ask_to_close.gif" height="309" />
## Hide the window into the tray
If you don't need to close the window and just need to hide it (for example to the tray), you can change the `windowState.isVisible` state:
```kotlin
import androidx.compose.material.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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 kotlinx.coroutines.delay
fun main() = application {
var isVisible by remember { mutableStateOf(true) }
Window(
onCloseRequest = { isVisible = false },
visible = isVisible,
title = "Counter",
) {
var counter by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
counter++
delay(1000)
}
}
Text(counter.toString())
}
if (!isVisible) {
Tray(
TrayIcon,
tooltip = "Counter",
onAction = { isVisible = true },
menu = {
Item("Exit", onClick = ::exitApplication)
},
)
}
}
object TrayIcon : Painter() {
override val intrinsicSize = Size(256f, 256f)
override fun DrawScope.onDraw() {
drawOval(Color(0xFFFFA500))
}
}
```
<img alt="Hide instead of closing" src="hide_instead_of_close.gif" height="308" />
## Open and close multiple windows
If an application has multiple windows, then it is better to put its state into a separate class and open/close window in response to `mutableStateListOf` changes (see [notepad example](https://github.com/JetBrains/compose-multiplatform/tree/master/examples/notepad) for more complex use cases):
```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.window.ApplicationScope
import androidx.compose.ui.window.MenuBar
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
val applicationState = remember { MyApplicationState() }
for (window in applicationState.windows) {
key(window) {
MyWindow(window)
}
}
}
@Composable
private fun ApplicationScope.MyWindow(
state: MyWindowState
) = Window(onCloseRequest = state::close, title = state.title) {
MenuBar {
Menu("File") {
Item("New window", onClick = state.openNewWindow)
Item("Exit", onClick = state.exit)
}
}
}
private class MyApplicationState {
val windows = mutableStateListOf<MyWindowState>()
init {
windows += MyWindowState("Initial window")
}
fun openNewWindow() {
windows += MyWindowState("Window ${windows.size}")
}
fun exit() {
windows.clear()
}
private fun MyWindowState(
title: String
) = MyWindowState(
title,
openNewWindow = ::openNewWindow,
exit = ::exit,
windows::remove
)
}
private class MyWindowState(
val title: String,
val openNewWindow: () -> Unit,
val exit: () -> Unit,
private val close: (MyWindowState) -> Unit
) {
fun close() = close(this)
}
```
<img alt="Multiple windows" src="multiple_windows.gif" height="280" />
## Function `singleWindowApplication`
There is a simplified function for creating a single window application:
```kotlin
import androidx.compose.ui.window.singleWindowApplication
fun main() = singleWindowApplication {
// Content
}
```
Use it if:
- your application has only one window
- you don't need custom closing logic
- you don't need to change the window parameters after it is already created
## Adaptive window size
Sometimes we want to show some content as a whole without knowing in advance what exactly will be shown, meaning that we don’t know the optimal window dimensions for it. By setting one or both dimensions of your window’s WindowSize to Dp.Unspecified, Compose for Desktop will automatically adjust the initial size of your window in that dimension to accommodate its content:
```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
state = rememberWindowState(width = Dp.Unspecified, height = Dp.Unspecified),
title = "Adaptive",
resizable = false
) {
Column(Modifier.background(Color(0xFFEEEEEE))) {
Row {
Text("label 1", Modifier.size(100.dp, 100.dp).padding(10.dp).background(Color.White))
Text("label 2", Modifier.size(150.dp, 200.dp).padding(5.dp).background(Color.White))
Text("label 3", Modifier.size(200.dp, 300.dp).padding(25.dp).background(Color.White))
}
}
}
}
```
<img alt="Adaptive window size" src="adaptive.png" height="327" />
## Changing the state (maximized, minimized, fullscreen, size, position) of the window.
Some states of the native window are moved into a separate API class, `WindowState`. You can change its properties in callbacks or observe it in Composable's.
When some state is changed (window size or position), Composable function will be automatically recomposed.
```kotlin
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPlacement
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
fun main() = application {
val state = rememberWindowState(placement = WindowPlacement.Maximized)
Window(onCloseRequest = ::exitApplication, state) {
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
state.placement == WindowPlacement.Fullscreen,
{
state.placement = if (it) {
WindowPlacement.Fullscreen
} else {
WindowPlacement.Floating
}
}
)
Text("isFullscreen")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
state.placement == WindowPlacement.Maximized,
{
state.placement = if (it) {
WindowPlacement.Maximized
} else {
WindowPlacement.Floating
}
}
)
Text("isMaximized")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(state.isMinimized, { state.isMinimized = !state.isMinimized })
Text("isMinimized")
}
Text(
"Position ${state.position}",
Modifier.clickable {
val position = state.position
if (position is WindowPosition.Absolute) {
state.position = position.copy(x = state.position.x + 10.dp)
}
}
)
Text(
"Size ${state.size}",
Modifier.clickable {
state.size = state.size.copy(width = state.size.width + 10.dp)
}
)
}
}
}
```
<img alt="Changing the state" src="state.gif" height="231" />
## Listening the state of the window
Reading the state in composition is useful when you need to update UI, but there are cases when you need to react to the state changes and send a value to another non-composable level of your application (write it to the database, for example):
```kotlin
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
fun main() = application {
val state = rememberWindowState()
Window(onCloseRequest = ::exitApplication, state) {
// Content
LaunchedEffect(state) {
snapshotFlow { state.size }
.onEach(::onWindowResize)
.launchIn(this)
snapshotFlow { state.position }
.filter { it.isSpecified }
.onEach(::onWindowRelocate)
.launchIn(this)
}
}
}
private fun onWindowResize(size: DpSize) {
println("onWindowResize $size")
}
private fun onWindowRelocate(position: WindowPosition) {
println("onWindowRelocate $position")
}
```
## Dialogs
There are two types of window – modal and regular. Below are the functions for creating each:
1. `Window` – regular window type.
2. `DialogWindow` – modal window type. This type locks its parent window until the user is finished working with it and closes the modal window.
You can see an example of both types of window below.
```kotlin
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
}
}
}
}
```
## Swing interoperability
Because Compose for Desktop uses Swing under the hood, it is possible to create a window using Swing directly:
```kotlin
import androidx.compose.ui.awt.ComposeWindow
import java.awt.Dimension
import javax.swing.JFrame
import javax.swing.SwingUtilities
fun main() = SwingUtilities.invokeLater {
ComposeWindow().apply {
size = Dimension(300, 300)
defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
setContent {
// Content
}
isVisible = true
}
}
```
You can also access ComposeWindow in the Composable `Window` scope:
```kotlin
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.window.singleWindowApplication
import java.awt.datatransfer.DataFlavor
import java.awt.dnd.DnDConstants
import java.awt.dnd.DropTarget
import java.awt.dnd.DropTargetAdapter
import java.awt.dnd.DropTargetDropEvent
fun main() = singleWindowApplication {
LaunchedEffect(Unit) {
window.dropTarget = DropTarget().apply {
addDropTargetListener(object : DropTargetAdapter() {
override fun drop(event: DropTargetDropEvent) {
event.acceptDrop(DnDConstants.ACTION_COPY);
val fileName = event.transferable.getTransferData(DataFlavor.javaFileListFlavor)
println(fileName)
}
})
}
}
}
```
If you need a dialog that is implemented in Swing, you can wrap it into a Composable function:
```kotlin
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.AwtWindow
import androidx.compose.ui.window.application
import java.awt.FileDialog
import java.awt.Frame
fun main() = application {
var isOpen by remember { mutableStateOf(true) }
if (isOpen) {
FileDialog(
onCloseRequest = {
isOpen = false
println("Result $it")
}
)
}
}
@Composable
private fun FileDialog(
parent: Frame? = null,
onCloseRequest: (result: String?) -> Unit
) = AwtWindow(
create = {
object : FileDialog(parent, "Choose a file", LOAD) {
override fun setVisible(value: Boolean) {
super.setVisible(value)
if (value) {
onCloseRequest(file)
}
}
}
},
dispose = FileDialog::dispose
)
```
## Draggable window area
If you window is undecorated and you want to add a custom draggable titlebar to it (or make the whole window draggable), you can use `WindowDraggableArea`:
```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.window.WindowDraggableArea
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
Window(onCloseRequest = ::exitApplication, undecorated = true) {
WindowDraggableArea {
Box(Modifier.fillMaxWidth().height(48.dp).background(Color.DarkGray))
}
}
}
```
Note that `WindowDraggableArea` can be used only inside `singleWindowApplication`, `Window` and `DialogWindow`. If you need to use it in another Composable function, pass `WindowScope` as a receiver there:
```kotlin
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.window.WindowDraggableArea
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowScope
import androidx.compose.ui.window.application
fun main() = application {
Window(onCloseRequest = ::exitApplication, undecorated = true) {
AppWindowTitleBar()
}
}
@Composable
private fun WindowScope.AppWindowTitleBar() = WindowDraggableArea {
Box(Modifier.fillMaxWidth().height(48.dp).background(Color.DarkGray))
}
```
<img alt="Draggable area" src="draggable_area.gif" height="239" />
## Transparent windows (e.g. allows to make windows of a custom form)
To create a transparent window it is enough to pass two parameners to the Window function: `transparent=true` and `undecorate=true` (it is not possible to decorate a transparent Window). Common scenario is to combine transparent window with a Surface of a custom form. Below is an example of a round-cornered Window.
```kotlin
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.material.Text
import androidx.compose.runtime.*
fun main() = application {
var isOpen by remember { mutableStateOf(true) }
if (isOpen) {
Window(
onCloseRequest = { isOpen = false },
title = "Transparent Window Example",
transparent = true,
undecorated = true, //transparent window must be undecorated
) {
Surface(
modifier = Modifier.fillMaxSize().padding(5.dp).shadow(3.dp, RoundedCornerShape(20.dp)),
color = Color(55, 55, 55),
shape = RoundedCornerShape(20.dp) //window has round corners now
) {
Text("Hello World!", color = Color.White)
}
}
}
}
```
_**Important note:** Window transparency is implemented based on JDK implementation, that contains **known issue on Linux** in case of moving a Window between two monitors with different density. So when you move an App, the Window stops being transparent. And it seems nothing can be done with this situation on Compose side.
[An issue about it](https://github.com/JetBrains/compose-multiplatform/issues/1339)_

BIN
tutorials/Window_API_new/adaptive.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

BIN
tutorials/Window_API_new/ask_to_close.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 KiB

BIN
tutorials/Window_API_new/draggable_area.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

BIN
tutorials/Window_API_new/hide_instead_of_close.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 KiB

BIN
tutorials/Window_API_new/multiple_windows.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

BIN
tutorials/Window_API_new/state.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

BIN
tutorials/Window_API_new/window_properties.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

BIN
tutorials/Window_API_new/window_splash.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 568 KiB

Loading…
Cancel
Save