PointerIcon.kt
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.ui.input.pointer
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
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.composed
import androidx.compose.ui.input.pointer.PointerEventPass.Main
import androidx.compose.ui.modifier.ModifierLocalConsumer
import androidx.compose.ui.modifier.ModifierLocalProvider
import androidx.compose.ui.modifier.ModifierLocalReadScope
import androidx.compose.ui.modifier.ProvidableModifierLocal
import androidx.compose.ui.modifier.modifierLocalOf
import androidx.compose.ui.platform.LocalPointerIconService
import androidx.compose.ui.platform.debugInspectorInfo
/**
* Represents a pointer icon to use in [Modifier.pointerHoverIcon]
*/
@Stable
interface PointerIcon {
/**
* A collection of common pointer icons used for the mouse cursor. These icons will be used to
* assign default pointer icons for various widgets.
*/
companion object {
/** The default arrow icon that is commonly used for cursor icons. */
val Default = pointerIconDefault
/** Commonly used when selecting precise portions of the screen. */
val Crosshair = pointerIconCrosshair
/** Also called an I-beam cursor, this is commonly used on selectable or editable text. */
val Text = pointerIconText
/** Commonly used to indicate to a user that an element is clickable. */
val Hand = pointerIconHand
}
}
internal expect val pointerIconDefault: PointerIcon
internal expect val pointerIconCrosshair: PointerIcon
internal expect val pointerIconText: PointerIcon
internal expect val pointerIconHand: PointerIcon
internal interface PointerIconService {
fun getIcon(): PointerIcon
fun setIcon(value: PointerIcon?)
}
/**
* Modifier that lets a developer define a pointer icon to display when the cursor is hovered over
* the element. When [overrideDescendants] is set to true, children cannot override the pointer icon
* using this modifier.
*
* @sample androidx.compose.ui.samples.PointerIconSample
*
* @param icon The icon to set
* @param overrideDescendants when false (by default) descendants are able to set their own pointer
* icon. If true, all children under this parent will receive the requested pointer [icon] and are
* no longer allowed to override their own pointer icon.
*/
@Stable
fun Modifier.pointerHoverIcon(icon: PointerIcon, overrideDescendants: Boolean = false) =
composed(inspectorInfo = debugInspectorInfo {
name = "pointerHoverIcon"
properties["icon"] = icon
properties["overrideDescendants"] = overrideDescendants
}) {
val pointerIconService = LocalPointerIconService.current
if (pointerIconService == null) {
Modifier
} else {
val onSetIcon = { pointerIcon: PointerIcon? ->
pointerIconService.setIcon(pointerIcon)
}
val pointerIconModifierLocal = remember {
PointerIconModifierLocal(icon, overrideDescendants, onSetIcon)
}
SideEffect {
pointerIconModifierLocal.updateValues(
icon = icon,
overrideDescendants = overrideDescendants,
onSetIcon = onSetIcon
)
}
val pointerInputModifier = if (pointerIconModifierLocal.shouldUpdatePointerIcon()) {
pointerInput(pointerIconModifierLocal) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(Main)
if (event.type == PointerEventType.Enter) {
pointerIconModifierLocal.enter()
} else if (event.type == PointerEventType.Exit) {
pointerIconModifierLocal.exit()
}
}
}
}
} else {
Modifier
}
pointerIconModifierLocal.then(pointerInputModifier)
}
}
/**
* Handles storing all pointer icon information that needs to be passed between Modifiers to
* determine which icon needs to be set in the hierarchy.
*
* @property icon the stored current icon we are keeping track of.
* @property overrideDescendants value indicating whether the stored icon should always be
* respected by its children. If true, the stored icon will be considered the source of truth for
* all children. If false, the stored icon can be overwritten by a child.
* @property onSetIcon is a lambda that will handle the process of physically setting the user
* facing pointer icon. This allows the [PointerIconModifierLocal] to be solely responsible for
* determining what the state of the icon should be, but removes the responsibility of needing to
* actually set the icon for the user.
*/
private class PointerIconModifierLocal(
private var icon: PointerIcon,
private var overrideDescendants: Boolean,
private var onSetIcon: (PointerIcon?) -> Unit,
) : PointerIcon, ModifierLocalProvider<PointerIconModifierLocal?>, ModifierLocalConsumer {
// TODO: (b/266976920) Remove making this a mutable state once we fully support a dynamic
// overrideDescendants param.
private var parentInfo: PointerIconModifierLocal? by mutableStateOf(null)
// TODO: (b/267170292) Properly reset isPaused upon PointerIconModifierLocal disposal.
var isPaused: Boolean = false
/* True if the cursor is within the surface area of this element's bounds. Otherwise, false. */
var isHovered: Boolean = false
override val key: ProvidableModifierLocal<PointerIconModifierLocal?> = ModifierLocalPointerIcon
override val value: PointerIconModifierLocal = this
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
val oldParentInfo = parentInfo
parentInfo = ModifierLocalPointerIcon.current
if (oldParentInfo != null && parentInfo == null) {
// When the old parentInfo for this element is reassigned to null, we assume this
// element is being alienated for disposal. Exit out of our pointer icon logic for this
// element and then update onSetIcon to null so it will not change the icon any further.
exit(oldParentInfo)
onSetIcon = {}
}
}
fun shouldUpdatePointerIcon(): Boolean {
val parentPointerInfo = parentInfo
return parentPointerInfo == null || !parentPointerInfo.hasOverride()
}
private fun hasOverride(): Boolean {
return overrideDescendants || parentInfo?.hasOverride() == true
}
fun enter() {
isHovered = true
if (!isPaused) {
parentInfo?.pause()
onSetIcon(icon)
}
}
fun exit() {
exit(parentInfo)
}
private fun exit(parent: PointerIconModifierLocal?) {
if (isHovered) {
if (parent == null) {
// Notify that oldest ancestor in hierarchy exited by passing null to onSetIcon().
onSetIcon(null)
} else {
parent.reassignIcon()
}
}
isHovered = false
}
private fun reassignIcon() {
isPaused = false
if (isHovered) {
onSetIcon(icon)
} else if (parentInfo == null) {
// Reassign the icon back to the default arrow by passing in a null PointerIcon
onSetIcon(null)
} else {
parentInfo?.reassignIcon()
}
}
private fun pause() {
isPaused = true
parentInfo?.pause()
}
fun updateValues(
icon: PointerIcon,
overrideDescendants: Boolean,
onSetIcon: (PointerIcon?) -> Unit
) {
if (this.icon != icon && isHovered && !isPaused) {
// Hovered element's icon has dynamically changed so we need to set the user facing icon
onSetIcon(icon)
}
this.icon = icon
this.overrideDescendants = overrideDescendants
this.onSetIcon = onSetIcon
}
}
/**
* The unique identifier used as the key for the custom [ModifierLocalProvider] created to tell us
* the current [PointerIcon].
*/
private val ModifierLocalPointerIcon = modifierLocalOf<PointerIconModifierLocal?> { null }