1 /*
2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.input.pointer
18 
19 import androidx.compose.runtime.Stable
20 import androidx.compose.ui.Modifier
21 import androidx.compose.ui.input.pointer.PointerEventPass.Main
22 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
23 import androidx.compose.ui.node.DpTouchBoundsExpansion
24 import androidx.compose.ui.node.ModifierNodeElement
25 import androidx.compose.ui.node.PointerInputModifierNode
26 import androidx.compose.ui.node.TouchBoundsExpansion
27 import androidx.compose.ui.node.TraversableNode
28 import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction
29 import androidx.compose.ui.node.currentValueOf
30 import androidx.compose.ui.node.requireDensity
31 import androidx.compose.ui.node.traverseAncestors
32 import androidx.compose.ui.node.traverseDescendants
33 import androidx.compose.ui.platform.InspectorInfo
34 import androidx.compose.ui.platform.LocalPointerIconService
35 import androidx.compose.ui.unit.IntSize
36 import androidx.compose.ui.util.fastAny
37 
38 /**
39  * Represents a pointer icon to use in [Modifier.pointerHoverIcon] or [Modifier.stylusHoverIcon].
40  */
41 @Stable
42 interface PointerIcon {
43 
44     /**
45      * A collection of common pointer icons used for the mouse cursor. These icons will be used to
46      * assign default pointer icons for various widgets.
47      */
48     companion object {
49 
50         /** The default arrow icon that is commonly used for cursor icons. */
51         val Default = pointerIconDefault
52 
53         /** Commonly used when selecting precise portions of the screen. */
54         val Crosshair = pointerIconCrosshair
55 
56         /** Also called an I-beam cursor, this is commonly used on selectable or editable text. */
57         val Text = pointerIconText
58 
59         /** Commonly used to indicate to a user that an element is clickable. */
60         val Hand = pointerIconHand
61     }
62 }
63 
64 internal expect val pointerIconDefault: PointerIcon
65 internal expect val pointerIconCrosshair: PointerIcon
66 internal expect val pointerIconText: PointerIcon
67 internal expect val pointerIconHand: PointerIcon
68 
69 internal interface PointerIconService {
getIconnull70     fun getIcon(): PointerIcon
71 
72     fun setIcon(value: PointerIcon?)
73 
74     fun getStylusHoverIcon(): PointerIcon?
75 
76     fun setStylusHoverIcon(value: PointerIcon?)
77 }
78 
79 /**
80  * Modifier that lets a developer define a pointer icon to display when the cursor is hovered over
81  * the element. When [overrideDescendants] is set to true, descendants cannot override the pointer
82  * icon using this modifier.
83  *
84  * @sample androidx.compose.ui.samples.PointerIconSample
85  * @param icon the icon to set
86  * @param overrideDescendants when false (by default), descendants are able to set their own pointer
87  *   icon. If true, no descendants under this parent are eligible to change the icon (it will be set
88  *   to the this (the parent's) icon).
89  */
90 @Stable
91 fun Modifier.pointerHoverIcon(icon: PointerIcon, overrideDescendants: Boolean = false) =
92     this then
93         PointerHoverIconModifierElement(icon = icon, overrideDescendants = overrideDescendants)
94 
95 internal data class PointerHoverIconModifierElement(
96     val icon: PointerIcon,
97     val overrideDescendants: Boolean = false
98 ) : ModifierNodeElement<PointerHoverIconModifierNode>() {
99     override fun create() = PointerHoverIconModifierNode(icon, overrideDescendants)
100 
101     override fun update(node: PointerHoverIconModifierNode) {
102         node.icon = icon
103         node.overrideDescendants = overrideDescendants
104     }
105 
106     override fun InspectorInfo.inspectableProperties() {
107         name = "pointerHoverIcon"
108         properties["icon"] = icon
109         properties["overrideDescendants"] = overrideDescendants
110     }
111 }
112 
113 /*
114  * Changes the pointer hover icon if the node is in bounds and if the node is not overridden
115  * by a parent pointer hover icon node. This node implements [PointerInputModifierNode] so it can
116  * listen to pointer input events and determine if the pointer has entered or exited the bounds of
117  * the modifier itself.
118  *
119  * If the icon or overrideDescendants values are changed, this node will determine if it needs to
120  * walk down and/or up the modifier chain to update those pointer hover icon modifier nodes as well.
121  */
122 internal class PointerHoverIconModifierNode(
123     icon: PointerIcon,
124     overrideDescendants: Boolean = false
125 ) : HoverIconModifierNode(icon, overrideDescendants) {
126     /* Traversal key used with the [TraversableNode] interface to enable all the traversing
127      * functions (ancestor, child, subtree, and subtreeIf).
128      */
129     override val traverseKey = "androidx.compose.ui.input.pointer.PointerHoverIcon"
130 
isRelevantPointerTypenull131     override fun isRelevantPointerType(pointerType: PointerType) =
132         pointerType != PointerType.Stylus && pointerType != PointerType.Eraser
133 
134     override fun displayIcon(icon: PointerIcon?) {
135         pointerIconService?.setIcon(icon)
136     }
137 }
138 
139 /**
140  * Modifier that lets a developer define a pointer icon to display when a stylus is hovered over the
141  * element. When [overrideDescendants] is set to true, descendants cannot override the pointer icon
142  * using this modifier.
143  *
144  * @param icon the icon to set
145  * @param overrideDescendants when false (by default), descendants are able to set their own pointer
146  *   icon. If true, no descendants under this parent are eligible to change the icon (it will be set
147  *   to the this (the parent's) icon).
148  * @param touchBoundsExpansion amount by which the element's bounds is expanded
149  * @sample androidx.compose.ui.samples.StylusHoverIconSample
150  */
stylusHoverIconnull151 fun Modifier.stylusHoverIcon(
152     icon: PointerIcon,
153     overrideDescendants: Boolean = false,
154     touchBoundsExpansion: DpTouchBoundsExpansion? = null
155 ) =
156     this then
157         StylusHoverIconModifierElement(
158             icon = icon,
159             overrideDescendants = overrideDescendants,
160             touchBoundsExpansion = touchBoundsExpansion
161         )
162 
163 internal data class StylusHoverIconModifierElement(
164     val icon: PointerIcon,
165     val overrideDescendants: Boolean = false,
166     val touchBoundsExpansion: DpTouchBoundsExpansion? = null
167 ) : ModifierNodeElement<StylusHoverIconModifierNode>() {
168     override fun create() =
169         StylusHoverIconModifierNode(icon, overrideDescendants, touchBoundsExpansion)
170 
171     override fun update(node: StylusHoverIconModifierNode) {
172         node.icon = icon
173         node.overrideDescendants = overrideDescendants
174         node.dpTouchBoundsExpansion = touchBoundsExpansion
175     }
176 
177     override fun InspectorInfo.inspectableProperties() {
178         name = "stylusHoverIcon"
179         properties["icon"] = icon
180         properties["overrideDescendants"] = overrideDescendants
181         properties["touchBoundsExpansion"] = touchBoundsExpansion
182     }
183 }
184 
185 internal class StylusHoverIconModifierNode(
186     icon: PointerIcon,
187     overrideDescendants: Boolean = false,
188     touchBoundsExpansion: DpTouchBoundsExpansion? = null
189 ) : HoverIconModifierNode(icon, overrideDescendants, touchBoundsExpansion) {
190     /* Traversal key used with the [TraversableNode] interface to enable all the traversing
191      * functions (ancestor, child, subtree, and subtreeIf).
192      */
193     override val traverseKey = "androidx.compose.ui.input.pointer.StylusHoverIcon"
194 
isRelevantPointerTypenull195     override fun isRelevantPointerType(pointerType: PointerType) =
196         pointerType == PointerType.Stylus || pointerType == PointerType.Eraser
197 
198     override fun displayIcon(icon: PointerIcon?) {
199         pointerIconService?.setStylusHoverIcon(icon)
200     }
201 }
202 
203 internal abstract class HoverIconModifierNode(
204     icon: PointerIcon,
205     overrideDescendants: Boolean = false,
206     var dpTouchBoundsExpansion: DpTouchBoundsExpansion? = null
207 ) :
208     Modifier.Node(),
209     TraversableNode,
210     PointerInputModifierNode,
211     CompositionLocalConsumerModifierNode {
212 
213     var icon = icon
214         set(value) {
215             if (field != value) {
216                 field = value
217                 if (cursorInBoundsOfNode) {
218                     displayIconIfDescendantsDoNotHavePriority()
219                 }
220             }
221         }
222 
223     var overrideDescendants = overrideDescendants
224         set(value) {
225             if (field != value) {
226                 field = value
227 
228                 if (overrideDescendants) { // overrideDescendants changed from false -> true
229                     // If this node or any descendants have the cursor in bounds, change the icon.
230                     if (cursorInBoundsOfNode) {
231                         displayIcon()
232                     }
233                 } else { // overrideDescendants changed from true -> false
234                     if (cursorInBoundsOfNode) {
235                         displayIconFromCurrentNodeOrDescendantsWithCursorInBounds()
236                     }
237                 }
238             }
239         }
240 
241     // Service used to actually update the icon with the system when needed.
242     protected val pointerIconService: PointerIconService?
243         get() = currentValueOf(LocalPointerIconService)
244 
245     private var cursorInBoundsOfNode = false
246 
247     // Pointer Input callback for determining if a Pointer has Entered or Exited this node.
onPointerEventnull248     override fun onPointerEvent(
249         pointerEvent: PointerEvent,
250         pass: PointerEventPass,
251         bounds: IntSize
252     ) {
253         if (pass == Main && pointerEvent.changes.fastAny { isRelevantPointerType(it.type) }) {
254             // Cursor within the surface area of this node's bounds
255             if (pointerEvent.type == PointerEventType.Enter) {
256                 onEnter()
257             } else if (pointerEvent.type == PointerEventType.Exit) {
258                 onExit()
259             }
260         }
261     }
262 
onEnternull263     private fun onEnter() {
264         cursorInBoundsOfNode = true
265         displayIconIfDescendantsDoNotHavePriority()
266     }
267 
onExitnull268     private fun onExit() {
269         if (cursorInBoundsOfNode) {
270             cursorInBoundsOfNode = false
271 
272             if (isAttached) {
273                 displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
274             }
275         }
276     }
277 
onCancelPointerInputnull278     override fun onCancelPointerInput() {
279         // While pointer icon only really cares about enter/exit, there are some cases (dynamically
280         // adding Modifier Nodes) where a modifier might be cancelled but hasn't been detached or
281         // exited, so we need to cover that case.
282         onExit()
283     }
284 
onDetachnull285     override fun onDetach() {
286         onExit()
287         super.onDetach()
288     }
289 
290     override val touchBoundsExpansion: TouchBoundsExpansion
291         get() =
292             dpTouchBoundsExpansion?.roundToTouchBoundsExpansion(requireDensity())
293                 ?: TouchBoundsExpansion.None
294 
isRelevantPointerTypenull295     abstract fun isRelevantPointerType(pointerType: PointerType): Boolean
296 
297     abstract fun displayIcon(icon: PointerIcon?)
298 
299     private fun displayIcon() {
300         // If there are any ancestor that override this node, we must use that icon. Otherwise, we
301         // use the current node's icon
302         val iconToUse = findOverridingAncestorNode()?.icon ?: icon
303         displayIcon(iconToUse)
304     }
305 
displayIconIfDescendantsDoNotHavePrioritynull306     private fun displayIconIfDescendantsDoNotHavePriority() {
307         var hasIconRightsOverDescendants = true
308 
309         if (!overrideDescendants) {
310             traverseDescendants {
311                 // Descendant in bounds has rights to the icon (and has already set it),
312                 // so we ignore.
313                 val continueTraversal =
314                     if (it.cursorInBoundsOfNode) {
315                         hasIconRightsOverDescendants = false
316                         TraverseDescendantsAction.CancelTraversal
317                     } else {
318                         TraverseDescendantsAction.ContinueTraversal
319                     }
320                 continueTraversal
321             }
322         }
323 
324         if (hasIconRightsOverDescendants) {
325             displayIcon()
326         }
327     }
328 
329     /*
330      * Finds and returns the lowest descendant node with the cursor within its bounds (true node
331      * that gets to decide the icon).
332      *
333      * Note: Multiple descendant nodes may have `cursorInBoundsOfNode` set to true (for when the
334      * cursor enters their bounds). The lowest one is the one that is the correct node for the
335      * pointer (see example for explanation).
336      *
337      * Example: Parent node contains a child node within its visual border (both are pointer icon
338      * nodes).
339      * - Pointer moves over the PARENT node triggers the pointer input handler ENTER event which
340      * sets `cursorInBoundsOfNode` = `true`.
341      * - Pointer moves over CHILD node triggers the pointer input handler ENTER event which sets
342      * `cursorInBoundsOfNode` = `true`.
343      *
344      * They are both true now because the pointer input event's exit is not triggered (which would
345      * set cursorInBoundsOfNode` = `false`) unless the pointer moves outside the parent node.
346      * Because the child node is contained visually within the parent node, it is not triggered.
347      * That is why we need to get the lowest node with `cursorInBoundsOfNode` set to true.
348      */
findDescendantNodeWithCursorInBoundsnull349     private fun findDescendantNodeWithCursorInBounds(): HoverIconModifierNode? {
350         var descendantNodeWithCursorInBounds: HoverIconModifierNode? = null
351 
352         traverseDescendants {
353             var actionForSubtreeOfCurrentNode = TraverseDescendantsAction.ContinueTraversal
354 
355             if (it.cursorInBoundsOfNode) {
356                 descendantNodeWithCursorInBounds = it
357 
358                 // No descendant nodes below this one are eligible to set the icon.
359                 if (it.overrideDescendants) {
360                     actionForSubtreeOfCurrentNode =
361                         TraverseDescendantsAction.SkipSubtreeAndContinueTraversal
362                 }
363             }
364             actionForSubtreeOfCurrentNode
365         }
366 
367         return descendantNodeWithCursorInBounds
368     }
369 
displayIconFromCurrentNodeOrDescendantsWithCursorInBoundsnull370     private fun displayIconFromCurrentNodeOrDescendantsWithCursorInBounds() {
371         if (!cursorInBoundsOfNode) return
372 
373         var hoverIconModifierNode: HoverIconModifierNode = this
374 
375         if (!overrideDescendants) {
376             findDescendantNodeWithCursorInBounds()?.let { hoverIconModifierNode = it }
377         }
378 
379         hoverIconModifierNode.displayIcon()
380     }
381 
findOverridingAncestorNodenull382     private fun findOverridingAncestorNode(): HoverIconModifierNode? {
383         var hoverIconModifierNode: HoverIconModifierNode? = null
384 
385         traverseAncestors {
386             if (it.overrideDescendants && it.cursorInBoundsOfNode) {
387                 hoverIconModifierNode = it
388             }
389             // continue traversal
390             true
391         }
392 
393         return hoverIconModifierNode
394     }
395 
396     /*
397      * Sets the icon to either the ancestor where the pointer is in its bounds (or to its
398      * ancestors if one overrides it) or to a default icon.
399      */
displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIconnull400     private fun displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon() {
401         var hoverIconModifierNode: HoverIconModifierNode? = null
402 
403         traverseAncestors {
404             if (hoverIconModifierNode == null && it.cursorInBoundsOfNode) {
405                 hoverIconModifierNode = it
406 
407                 // We should only assign a node that override its descendants if there was a node
408                 // below it where the pointer was in bounds meaning the hoverIconModifierNode will
409                 // not be null.
410             } else if (
411                 hoverIconModifierNode != null && it.overrideDescendants && it.cursorInBoundsOfNode
412             ) {
413                 hoverIconModifierNode = it
414             }
415 
416             // continue traversal
417             true
418         }
419         hoverIconModifierNode?.displayIcon() ?: displayIcon(null)
420     }
421 }
422