@ -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" /> |
|
Before Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 2.7 MiB |
Before Width: | Height: | Size: 2.4 MiB |
@ -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)) |
|
||||||
} |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
Before Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 257 KiB |