Compose Multiplatform UI library for iOS Simulator ARM64 target providing declarative UI framework based on Jetpack Compose for multiplatform development
—
Multi-platform input handling system supporting touch, mouse, keyboard input, and gesture recognition with comprehensive focus management and accessibility features.
Low-level pointer input handling supporting touch, mouse, and stylus input with gesture recognition capabilities.
/**
* Modifier that provides access to pointer input events.
*/
fun Modifier.pointerInput(
key1: Any?,
block: suspend PointerInputScope.() -> Unit
): Modifier
/**
* Modifier that provides access to pointer input events with multiple keys.
*/
fun Modifier.pointerInput(
key1: Any?,
key2: Any?,
block: suspend PointerInputScope.() -> Unit
): Modifier
/**
* Modifier that provides access to pointer input events with arbitrary keys.
*/
fun Modifier.pointerInput(
vararg keys: Any?,
block: suspend PointerInputScope.() -> Unit
): Modifier
/**
* Scope for handling pointer input events.
*/
interface PointerInputScope : Density {
/**
* The size of the pointer input region.
*/
val size: IntSize
/**
* The extended touch slop for gestures.
*/
val extendedTouchSlop: Float
/**
* Configuration for pointer input behavior.
*/
val viewConfiguration: ViewConfiguration
/**
* Wait for a pointer event to occur.
*/
suspend fun awaitPointerEventScope(
pass: PointerEventPass = PointerEventPass.Main,
block: suspend AwaitPointerEventScope.() -> Unit
)
}
/**
* Scope for awaiting and handling pointer events.
*/
interface AwaitPointerEventScope : Density {
/**
* The size of the pointer input region.
*/
val size: IntSize
/**
* The current pointer event.
*/
val currentEvent: PointerEvent
/**
* Extended touch slop configuration.
*/
val extendedTouchSlop: Float
/**
* View configuration for pointer input.
*/
val viewConfiguration: ViewConfiguration
/**
* Wait for the next pointer event.
*/
suspend fun awaitPointerEvent(
pass: PointerEventPass = PointerEventPass.Main
): PointerEvent
/**
* Attempt to drag one pointer.
*/
suspend fun drag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
): Boolean
/**
* Wait for all pointers to be up.
*/
suspend fun waitForUpOrCancellation(
pass: PointerEventPass = PointerEventPass.Main
): PointerInputChange?
}
/**
* Represents a pointer event containing information about pointer changes.
*/
class PointerEvent(
val changes: List<PointerInputChange>,
val buttons: PointerButtons = PointerButtons(),
val keyboardModifiers: PointerKeyboardModifiers = PointerKeyboardModifiers(),
val type: PointerEventType = PointerEventType.Unknown
) {
/**
* The number of pointers in this event.
*/
val pointerCount: Int
/**
* Get a pointer change by its ID.
*/
fun getPointerById(pointerId: PointerId): PointerInputChange?
}
/**
* Information about a single pointer's state changes.
*/
@Immutable
data class PointerInputChange(
val id: PointerId,
val uptimeMillis: Long,
val position: Offset,
val pressed: Boolean,
val pressure: Float = 1.0f,
val previousUptimeMillis: Long = uptimeMillis,
val previousPosition: Offset = position,
val previousPressed: Boolean = pressed,
val isConsumed: Boolean = false,
val type: PointerType = PointerType.Unknown,
val scrollDelta: Offset = Offset.Zero
) {
/**
* Whether this pointer change represents a press event.
*/
val changedToDown: Boolean
/**
* Whether this pointer change represents a release event.
*/
val changedToUp: Boolean
/**
* Whether this pointer change represents movement.
*/
val changedToDownIgnoreConsumed: Boolean
/**
* The change in position.
*/
val positionChange: Offset
/**
* The change in position, ignoring consumption.
*/
val positionChangeIgnoreConsumed: Offset
/**
* Consume this pointer change.
*/
fun consume()
/**
* Copy this change with consumption applied.
*/
fun consumeAllChanges(): PointerInputChange
/**
* Copy this change with position change consumed.
*/
fun consumePositionChange(): PointerInputChange
}
/**
* Unique identifier for a pointer.
*/
@JvmInline
value class PointerId(val value: Long)
/**
* Types of pointer events.
*/
enum class PointerEventType {
Unknown, Press, Release, Move, Enter, Exit, Scroll
}
/**
* Types of pointers.
*/
enum class PointerType {
Unknown, Touch, Mouse, Stylus, Eraser
}
/**
* Pointer event pass for event handling phases.
*/
enum class PointerEventPass {
Initial, Main, Final
}Usage Examples:
// Basic pointer input handling
@Composable
fun PointerInputExample() {
var position by remember { mutableStateOf(Offset.Zero) }
var isPressed by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.size(200.dp)
.background(if (isPressed) Color.Red else Color.Blue)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
val change = event.changes.first()
position = change.position
isPressed = change.pressed
if (change.changedToDown || change.changedToUp) {
change.consume()
}
}
}
}
) {
Text(
text = "Position: (${position.x.toInt()}, ${position.y.toInt()})\nPressed: $isPressed",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
// Multi-touch handling
@Composable
fun MultiTouchExample() {
var pointers by remember { mutableStateOf(mapOf<PointerId, Offset>()) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
pointers = event.changes.associate { change ->
if (change.pressed) {
change.id to change.position
} else {
change.id to Offset.Unspecified
}
}.filterValues { it != Offset.Unspecified }
event.changes.forEach { it.consume() }
}
}
}
) {
pointers.forEach { (id, position) ->
Box(
modifier = Modifier
.offset(position.x.dp - 25.dp, position.y.dp - 25.dp)
.size(50.dp)
.background(Color.Red, CircleShape)
)
}
}
}Pre-built gesture detectors for common interaction patterns like taps, drags, and swipes.
/**
* Detects tap gestures.
*/
suspend fun PointerInputScope.detectTapGestures(
onDoubleTap: ((Offset) -> Unit)? = null,
onLongPress: ((Offset) -> Unit)? = null,
onPress: (suspend PressGestureScope.(Offset) -> Unit)? = null,
onTap: ((Offset) -> Unit)? = null
)
/**
* Detects drag gestures.
*/
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)
/**
* Detects drag gestures after a long press.
*/
suspend fun PointerInputScope.detectDragGesturesAfterLongPress(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)
/**
* Detects horizontal drag gestures.
*/
suspend fun PointerInputScope.detectHorizontalDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
)
/**
* Detects vertical drag gestures.
*/
suspend fun PointerInputScope.detectVerticalDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
)
/**
* Detects rotation and zoom gestures.
*/
suspend fun PointerInputScope.detectTransformGestures(
panZoomLock: Boolean = false,
onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
)
/**
* Scope for press gestures.
*/
interface PressGestureScope : Density {
/**
* Try to await release of the press.
*/
suspend fun tryAwaitRelease(): Boolean
/**
* Await release of the press.
*/
suspend fun awaitRelease()
}
/**
* Configuration for drag gestures.
*/
data class DragGestureDetectorConfig(
val canDrag: (PointerInputChange) -> Boolean = { true }
)Usage Examples:
// Tap gesture detection
@Composable
fun TapGestureExample() {
var tapCount by remember { mutableStateOf(0) }
var lastTapPosition by remember { mutableStateOf(Offset.Zero) }
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Green)
.pointerInput(Unit) {
detectTapGestures(
onTap = { offset ->
tapCount++
lastTapPosition = offset
},
onDoubleTap = { offset ->
tapCount += 2
lastTapPosition = offset
},
onLongPress = { offset ->
tapCount = 0
lastTapPosition = offset
}
)
}
) {
Text(
text = "Taps: $tapCount\nLast: (${lastTapPosition.x.toInt()}, ${lastTapPosition.y.toInt()})",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
// Drag gesture detection
@Composable
fun DragGestureExample() {
var offset by remember { mutableStateOf(Offset.Zero) }
var isDragging by remember { mutableStateOf(false) }
Box(
modifier = Modifier.fillMaxSize()
) {
Box(
modifier = Modifier
.offset(offset.x.dp, offset.y.dp)
.size(100.dp)
.background(if (isDragging) Color.Red else Color.Blue)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = {
isDragging = true
},
onDragEnd = {
isDragging = false
},
onDrag = { change, dragAmount ->
offset += dragAmount
}
)
}
)
}
}
// Transform gestures (zoom, rotate, pan)
@Composable
fun TransformGestureExample() {
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotationChange ->
scale *= zoom
rotation += rotationChange
offset += pan
}
}
) {
Box(
modifier = Modifier
.size(200.dp)
.offset(offset.x.dp, offset.y.dp)
.scale(scale)
.rotate(rotation)
.background(Color.Yellow)
.align(Alignment.Center)
) {
Text(
text = "Transform me!",
modifier = Modifier.align(Alignment.Center)
)
}
}
}High-level modifiers for common interactive behaviors like clicks, selections, and toggles.
/**
* Modifier that makes a component clickable.
*/
fun Modifier.clickable(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier
/**
* Modifier that makes a component clickable with custom indication.
*/
fun Modifier.clickable(
interactionSource: MutableInteractionSource,
indication: Indication?,
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier
/**
* Modifier that makes a component selectable.
*/
fun Modifier.selectable(
selected: Boolean,
enabled: Boolean = true,
role: Role? = null,
onValueChange: (Boolean) -> Unit
): Modifier
/**
* Modifier that makes a component toggleable.
*/
fun Modifier.toggleable(
value: Boolean,
enabled: Boolean = true,
role: Role? = null,
onValueChange: (Boolean) -> Unit
): Modifier
/**
* Modifier that makes a component tristate toggleable.
*/
fun Modifier.triStateToggleable(
state: ToggleableState,
enabled: Boolean = true,
role: Role? = null,
onClick: () -> Unit
): Modifier
/**
* Represents the state of a toggleable component.
*/
enum class ToggleableState {
On, Off, Indeterminate
}
/**
* Semantic roles for accessibility.
*/
enum class Role {
Button, Checkbox, Switch, RadioButton, Tab, Image, DropdownList
}Usage Examples:
// Basic clickable component
@Composable
fun ClickableExample() {
var clickCount by remember { mutableStateOf(0) }
Box(
modifier = Modifier
.size(150.dp)
.background(Color.Blue)
.clickable {
clickCount++
}
) {
Text(
text = "Clicked: $clickCount",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
// Selectable component
@Composable
fun SelectableExample() {
var selected by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = selected,
onClick = { selected = !selected },
role = Role.Checkbox
)
.padding(16.dp)
) {
Box(
modifier = Modifier
.size(24.dp)
.background(
if (selected) Color.Blue else Color.Gray,
RoundedCornerShape(4.dp)
)
) {
if (selected) {
Text(
text = "✓",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}
Spacer(modifier = Modifier.width(16.dp))
Text(text = "Select this option")
}
}
// Toggleable component
@Composable
fun ToggleableExample() {
var isOn by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.toggleable(
value = isOn,
onValueChange = { isOn = it },
role = Role.Switch
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Enable feature")
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.size(50.dp, 30.dp)
.background(
if (isOn) Color.Green else Color.Gray,
RoundedCornerShape(15.dp)
)
) {
Box(
modifier = Modifier
.size(26.dp)
.background(Color.White, CircleShape)
.align(if (isOn) Alignment.CenterEnd else Alignment.CenterStart)
)
}
}
}
// Custom interaction source and indication
@Composable
fun CustomInteractionExample() {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val isHovered by interactionSource.collectIsHoveredAsState()
Box(
modifier = Modifier
.size(150.dp)
.background(
when {
isPressed -> Color.Red
isHovered -> Color.Yellow
else -> Color.Blue
}
)
.clickable(
interactionSource = interactionSource,
indication = null // Custom indication handling
) {
// Handle click
}
) {
Text(
text = when {
isPressed -> "Pressed"
isHovered -> "Hovered"
else -> "Normal"
},
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
}Comprehensive focus handling system for keyboard navigation and accessibility.
/**
* Modifier that makes a component focusable.
*/
fun Modifier.focusable(
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null
): Modifier
/**
* Modifier that specifies focus behavior.
*/
fun Modifier.focusTarget(): Modifier
/**
* Modifier that specifies focus properties.
*/
fun Modifier.focusProperties(scope: FocusProperties.() -> Unit): Modifier
/**
* Modifier for observing focus changes.
*/
fun Modifier.onFocusChanged(onFocusChanged: (FocusState) -> Unit): Modifier
/**
* Modifier for observing focus events.
*/
fun Modifier.onFocusEvent(onFocusEvent: (FocusState) -> Unit): Modifier
/**
* Represents the focus state of a component.
*/
interface FocusState {
/**
* Whether this component has focus.
*/
val hasFocus: Boolean
/**
* Whether this component is focused or contains a focused descendant.
*/
val isFocused: Boolean
/**
* Whether focus is captured by this component.
*/
val isCaptured: Boolean
}
/**
* Properties for configuring focus behavior.
*/
interface FocusProperties {
/**
* Whether this component can be focused.
*/
var canFocus: Boolean
/**
* Custom focus enter behavior.
*/
var enter: (FocusDirection) -> FocusRequester?
/**
* Custom focus exit behavior.
*/
var exit: (FocusDirection) -> FocusRequester?
/**
* Focus order within the parent.
*/
var next: FocusRequester?
var previous: FocusRequester?
var up: FocusRequester?
var down: FocusRequester?
var left: FocusRequester?
var right: FocusRequester?
var start: FocusRequester?
var end: FocusRequester?
}
/**
* Used to request focus programmatically.
*/
class FocusRequester {
/**
* Request focus for this component.
*/
fun requestFocus()
/**
* Capture focus for this component.
*/
fun captureFocus(): Boolean
/**
* Free captured focus.
*/
fun freeFocus(): Boolean
companion object {
/**
* Default focus requester.
*/
val Default: FocusRequester
/**
* Focus requester that cancels focus.
*/
val Cancel: FocusRequester
}
}
/**
* Directions for focus movement.
*/
enum class FocusDirection {
Next, Previous, Up, Down, Left, Right, In, Out, Enter, Exit
}
/**
* CompositionLocal for accessing the focus manager.
*/
val LocalFocusManager: ProvidableCompositionLocal<FocusManager>
/**
* Interface for managing focus.
*/
interface FocusManager {
/**
* Clear focus from the currently focused component.
*/
fun clearFocus(force: Boolean = false)
/**
* Move focus in the specified direction.
*/
fun moveFocus(focusDirection: FocusDirection): Boolean
}Usage Examples:
// Basic focus handling
@Composable
fun FocusExample() {
val focusRequester = remember { FocusRequester() }
var isFocused by remember { mutableStateOf(false) }
Column {
Box(
modifier = Modifier
.size(100.dp)
.background(if (isFocused) Color.Blue else Color.Gray)
.focusRequester(focusRequester)
.focusable()
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
}
) {
Text(
text = if (isFocused) "Focused" else "Not Focused",
color = Color.White,
modifier = Modifier.align(Alignment.Center)
)
}
Button(
onClick = { focusRequester.requestFocus() }
) {
Text("Request Focus")
}
}
}
// Custom focus navigation
@Composable
fun FocusNavigationExample() {
val focusRequesters = remember { List(4) { FocusRequester() } }
val focusManager = LocalFocusManager.current
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.padding(16.dp)
) {
items(4) { index ->
var isFocused by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.size(100.dp)
.padding(4.dp)
.background(
if (isFocused) Color.Blue else Color.LightGray,
RoundedCornerShape(8.dp)
)
.focusRequester(focusRequesters[index])
.focusProperties {
// Custom focus navigation
next = if (index < 3) focusRequesters[index + 1] else FocusRequester.Default
previous = if (index > 0) focusRequesters[index - 1] else FocusRequester.Default
// Grid-like navigation
down = if (index < 2) focusRequesters[index + 2] else FocusRequester.Default
up = if (index >= 2) focusRequesters[index - 2] else FocusRequester.Default
}
.focusable()
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
}
.clickable {
focusRequesters[index].requestFocus()
}
) {
Text(
text = "Item ${index + 1}",
color = if (isFocused) Color.White else Color.Black,
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
// Focus capture for modal behavior
@Composable
fun FocusCaptureExample() {
var showModal by remember { mutableStateOf(false) }
val modalFocusRequester = remember { FocusRequester() }
Box(modifier = Modifier.fillMaxSize()) {
Column {
Button(onClick = { showModal = true }) {
Text("Show Modal")
}
Text("This content becomes unfocusable when modal is shown")
Button(onClick = { /* Regular button */ }) {
Text("Regular Button")
}
}
if (showModal) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f))
.clickable { showModal = false }
) {
Box(
modifier = Modifier
.size(300.dp, 200.dp)
.background(Color.White, RoundedCornerShape(8.dp))
.align(Alignment.Center)
.focusRequester(modalFocusRequester)
.focusProperties {
// Capture focus to prevent navigation outside modal
canFocus = true
}
.focusable()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.SpaceAround,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Modal Dialog")
Button(onClick = { showModal = false }) {
Text("Close")
}
}
}
}
LaunchedEffect(showModal) {
modalFocusRequester.requestFocus()
}
}
}
}Keyboard event handling for text input, shortcuts, and navigation keys.
/**
* Modifier for handling key events.
*/
fun Modifier.onKeyEvent(
onKeyEvent: (KeyEvent) -> Boolean
): Modifier
/**
* Modifier for handling pre-view key events.
*/
fun Modifier.onPreviewKeyEvent(
onPreviewKeyEvent: (KeyEvent) -> Boolean
): Modifier
/**
* Represents a keyboard event.
*/
expect class KeyEvent {
/**
* The key that was pressed or released.
*/
val key: Key
/**
* The type of key event.
*/
val type: KeyEventType
/**
* Whether the Alt key is pressed.
*/
val isAltPressed: Boolean
/**
* Whether the Ctrl key is pressed.
*/
val isCtrlPressed: Boolean
/**
* Whether the Meta key is pressed.
*/
val isMetaPressed: Boolean
/**
* Whether the Shift key is pressed.
*/
val isShiftPressed: Boolean
}
/**
* Types of key events.
*/
enum class KeyEventType {
KeyDown, KeyUp, Unknown
}
/**
* Represents keyboard keys.
*/
expect class Key {
companion object {
val A: Key
val B: Key
// ... other alphabet keys
val Zero: Key
val One: Key
// ... other number keys
val Enter: Key
val Escape: Key
val Backspace: Key
val Delete: Key
val Tab: Key
val Spacebar: Key
val DirectionUp: Key
val DirectionDown: Key
val DirectionLeft: Key
val DirectionRight: Key
val PageUp: Key
val PageDown: Key
val Home: Key
val MoveEnd: Key
val F1: Key
val F2: Key
// ... other function keys
}
}Usage Examples:
// Keyboard event handling
@Composable
fun KeyboardHandlingExample() {
var lastKey by remember { mutableStateOf("None") }
var keyCount by remember { mutableStateOf(0) }
Box(
modifier = Modifier
.size(300.dp, 200.dp)
.background(Color.LightGray, RoundedCornerShape(8.dp))
.focusable()
.onKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown) {
lastKey = keyEvent.key.toString()
keyCount++
// Handle specific keys
when (keyEvent.key) {
Key.Escape -> {
lastKey = "Escape pressed"
true // Consume event
}
Key.Enter -> {
lastKey = "Enter pressed"
true
}
else -> false // Don't consume
}
} else {
false
}
}
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Focus this box and press keys")
Text("Last key: $lastKey")
Text("Key count: $keyCount")
}
}
}
// Keyboard shortcuts
@Composable
fun KeyboardShortcutsExample() {
var content by remember { mutableStateOf("Type here...") }
var saved by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown && keyEvent.isCtrlPressed) {
when (keyEvent.key) {
Key.S -> {
// Ctrl+S to save
saved = true
true
}
Key.N -> {
// Ctrl+N for new
content = ""
saved = false
true
}
else -> false
}
} else {
false
}
}
) {
if (saved) {
Text(
text = "Saved!",
color = Color.Green,
fontWeight = FontWeight.Bold
)
} else {
Text(
text = "Unsaved changes",
color = Color.Orange
)
}
Spacer(modifier = Modifier.height(16.dp))
TextField(
value = content,
onValueChange = {
content = it
saved = false
},
label = { Text("Content (Ctrl+S to save, Ctrl+N for new)") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Text("Keyboard shortcuts:")
Text("• Ctrl+S: Save")
Text("• Ctrl+N: New document")
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-jetbrains-compose-ui--ui-uikitsimarm64