diff --git a/tutorials/Mouse_Events/README.md b/tutorials/Mouse_Events/README.md
index 04d0ab5584..336d491d30 100644
--- a/tutorials/Mouse_Events/README.md
+++ b/tutorials/Mouse_Events/README.md
@@ -61,7 +61,7 @@ fun main() = singleWindowApplication {
-Please note, that advanced click events processing is available on Desktop via AWT interop. See **Advanced click events processing** section below.
+`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
@@ -185,100 +185,6 @@ fun main() = singleWindowApplication {
```
*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)*.
-### Mouse right/middle clicks and keyboard modifiers
-
-Compose for Desktop contains desktop-only `Modifier.mouseClickable`, where data about pressed mouse buttons and keyboard modifiers is available. This is an experimental API, which means that it's likely to be changed before release.
-
-```kotlin
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.mouseClickable
-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.Modifier
-import androidx.compose.ui.input.pointer.isAltPressed
-import androidx.compose.ui.input.pointer.isCtrlPressed
-import androidx.compose.ui.input.pointer.isMetaPressed
-import androidx.compose.ui.input.pointer.isPrimaryPressed
-import androidx.compose.ui.input.pointer.isSecondaryPressed
-import androidx.compose.ui.input.pointer.isShiftPressed
-import androidx.compose.ui.input.pointer.isTertiaryPressed
-import androidx.compose.ui.window.singleWindowApplication
-
-@OptIn(ExperimentalFoundationApi::class)
-fun main() = singleWindowApplication {
- var clickableText by remember { mutableStateOf("Click me!") }
-
- Text(
- modifier = Modifier.mouseClickable(
- onClick = {
- clickableText = buildString {
- append("Buttons pressed:\n")
- append("primary: ${buttons.isPrimaryPressed}\t")
- append("secondary: ${buttons.isSecondaryPressed}\t")
- append("tertiary: ${buttons.isTertiaryPressed}\t")
-
- append("\n\nKeyboard modifiers pressed:\n")
-
- append("alt: ${keyboardModifiers.isAltPressed}\t")
- append("ctrl: ${keyboardModifiers.isCtrlPressed}\t")
- append("meta: ${keyboardModifiers.isMetaPressed}\t")
- append("shift: ${keyboardModifiers.isShiftPressed}\t")
- }
- }
- ),
- text = clickableText
- )
-}
-```
-
-
-If you need to listen left/right clicks simultaneously, you should listen for raw 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.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.isPrimaryPressed
-import androidx.compose.ui.input.pointer.isSecondaryPressed
-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 text by remember { mutableStateOf("Press me") }
-
- Box(
- Modifier
- .fillMaxSize()
- .onPointerEvent(PointerEventType.Press) {
- val position = it.changes.first().position
- text = when {
- it.buttons.isPrimaryPressed &&
- it.buttons.isSecondaryPressed -> "Left+Right click $position"
- it.buttons.isSecondaryPressed -> "Right click $position"
- it.buttons.isPrimaryPressed -> "Left click $position"
- else -> text
- }
- },
- contentAlignment = Alignment.Center
- ) {
- Text(text, 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:
@@ -357,61 +263,230 @@ fun main() = singleWindowApplication {
}
```
-### Advanced click events processing (only for Desktop-JVM platform)
-_NB: Please note, that approach described below is temporary and is to be replaced by Compose API in future!_
+### 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.
-It is possible to get additional information about mouse event, like number of clicks or state of other mouse buttons at the click time, via awt event.
+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.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
+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.unit.sp
+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
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.onPointerEvent
-import androidx.compose.ui.awt.awtEventOrNull
-import androidx.compose.ui.input.pointer.isPrimaryPressed
-import java.awt.event.MouseEvent
-@androidx.compose.ui.ExperimentalComposeUiApi
+@OptIn(ExperimentalFoundationApi::class)
fun main() = singleWindowApplication {
- 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)
- .onPointerEvent(PointerEventType.Press) {
- when(it.awtEventOrNull?.button) {
- MouseEvent.BUTTON1 ->
- when (it.awtEventOrNull?.clickCount) {
- 1 -> { text = "Single click"}
- 2 -> { text = "Double click"}
- }
- MouseEvent.BUTTON3 -> { //BUTTON3 is right button
- if (it.buttons.isPrimaryPressed) { text = "Right + left click" }
- else { text = "Right click" }
- }
- }
- }
- )
- Text(text = text, fontSize = 40.sp)
+ 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))
}
}
```
+