1 /*
<lambda>null2  * Copyright 2024 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.foundation.text.contextmenu.modifier
18 
19 import androidx.compose.foundation.text.contextmenu.builder.TextContextMenuBuilderScope
20 import androidx.compose.foundation.text.contextmenu.data.TextContextMenuComponent
21 import androidx.compose.foundation.text.contextmenu.data.TextContextMenuData
22 import androidx.compose.foundation.text.contextmenu.data.TextContextMenuSeparator
23 import androidx.compose.ui.Modifier
24 import androidx.compose.ui.node.DelegatableNode
25 import androidx.compose.ui.node.ModifierNodeElement
26 import androidx.compose.ui.node.TraversableNode
27 import androidx.compose.ui.node.traverseAncestors
28 import androidx.compose.ui.platform.InspectorInfo
29 
30 // TODO(grantapher-cm-api-publicize) add AddComponentsToTextContextMenu sample
31 /**
32  * Adds a [builder] to be run when the text context menu is shown within this hierarchy.
33  *
34  * When there are multiple instances of this modifier in a layout hierarchy, the [builder]s are
35  * applied in order from bottom to top. They are then filtered by every
36  * [Modifier.filterTextContextMenuComponents][filterTextContextMenuComponents] in the hierarchy.
37  *
38  * @param builder a snapshot-aware builder function for adding components to the context menu. In
39  *   this function you can use member functions from the receiver [TextContextMenuBuilderScope],
40  *   such as [item][TextContextMenuBuilderScope.item], to add components.
41  */
42 // TODO(grantapher-cm-api-publicize) Make function public
43 internal fun Modifier.addTextContextMenuComponents(
44     builder: TextContextMenuBuilderScope.() -> Unit,
45 ): Modifier = this then AddTextContextMenuDataComponentsElement(builder)
46 
47 // TODO(grantapher-cm-api-publicize) add AddFilterToTextContextMenu sample
48 /**
49  * Adds a [filter] to be run when the text context menu is shown within this hierarchy.
50  *
51  * [filter] will not be passed [TextContextMenuSeparator], as they pass by default.
52  *
53  * [filter]s added via this modifier will always run after every `builder` added via
54  * [Modifier.addTextContextMenuComponents][addTextContextMenuComponents]. When there are multiple
55  * instances of this modifier in a layout hierarchy, every [filter] must pass in order for a context
56  * menu to be shown. They are always applied after all
57  * [Modifier.addTextContextMenuComponents][addTextContextMenuComponents] have been applied, but the
58  * order in which they run should not be depended on.
59  *
60  * @param filter a snapshot-aware lambda that determines whether a [TextContextMenuComponent] should
61  *   be included in the context menu.
62  */
63 // TODO(grantapher-cm-api-publicize) Make function public
64 internal fun Modifier.filterTextContextMenuComponents(
65     filter: (TextContextMenuComponent) -> Boolean,
66 ): Modifier = this then FilterTextContextMenuDataComponentsElement(filter)
67 
68 private class AddTextContextMenuDataComponentsElement(
69     private val builder: TextContextMenuBuilderScope.() -> Unit,
70 ) : ModifierNodeElement<AddTextContextMenuDataComponentsNode>() {
71     override fun create(): AddTextContextMenuDataComponentsNode =
72         AddTextContextMenuDataComponentsNode(builder)
73 
74     override fun update(node: AddTextContextMenuDataComponentsNode) {
75         node.builder = builder
76     }
77 
78     override fun InspectorInfo.inspectableProperties() {
79         name = "addTextContextMenuDataComponents"
80         properties["builder"] = builder
81     }
82 
83     override fun equals(other: Any?): Boolean {
84         if (this === other) return true
85         if (other !is AddTextContextMenuDataComponentsElement) return false
86 
87         if (builder !== other.builder) return false
88 
89         return true
90     }
91 
92     override fun hashCode(): Int = builder.hashCode()
93 }
94 
95 private class FilterTextContextMenuDataComponentsElement(
96     private val filter: (TextContextMenuComponent) -> Boolean,
97 ) : ModifierNodeElement<FilterTextContextMenuDataComponentsNode>() {
createnull98     override fun create(): FilterTextContextMenuDataComponentsNode =
99         FilterTextContextMenuDataComponentsNode(filter)
100 
101     override fun update(node: FilterTextContextMenuDataComponentsNode) {
102         node.filter = filter
103     }
104 
inspectablePropertiesnull105     override fun InspectorInfo.inspectableProperties() {
106         name = "filterTextContextMenuDataComponents"
107         properties["filter"] = filter
108     }
109 
equalsnull110     override fun equals(other: Any?): Boolean {
111         if (this === other) return true
112         if (other !is FilterTextContextMenuDataComponentsElement) return false
113 
114         if (filter !== other.filter) return false
115 
116         return true
117     }
118 
hashCodenull119     override fun hashCode(): Int = filter.hashCode()
120 }
121 
122 private data object TextContextMenuDataTraverseKey
123 
124 internal class AddTextContextMenuDataComponentsNode(
125     var builder: TextContextMenuBuilderScope.() -> Unit,
126 ) : Modifier.Node(), TraversableNode {
127     override val traverseKey: Any
128         get() = TextContextMenuDataTraverseKey
129 }
130 
131 private class FilterTextContextMenuDataComponentsNode(
132     var filter: (TextContextMenuComponent) -> Boolean,
133 ) : Modifier.Node(), TraversableNode {
134     override val traverseKey: Any
135         get() = TextContextMenuDataTraverseKey
136 }
137 
138 private const val continueTraversal = true
139 private const val wrongNodeTypeErrorMessage =
140     "TextContextMenuDataNode.TraverseKey key must only be attached to instances of " +
141         "TextContextMenuDataNode."
142 
143 /**
144  * Traverses ancestors to find all
145  * [Modifier.addTextContextMenuComponents][addTextContextMenuComponents] and
146  * [Modifier.filterTextContextMenuComponents][filterTextContextMenuComponents] modifiers and runs
147  * [builderBlock] and [filterBlock] for each respectively. Each block allows the caller to make use
148  * of the `filter` and `builder` parameters of each of the related modifiers.
149  */
traverseTextContextMenuDataNodesnull150 private fun DelegatableNode.traverseTextContextMenuDataNodes(
151     filterBlock: ((TextContextMenuComponent) -> Boolean) -> Unit,
152     builderBlock: (TextContextMenuBuilderScope.() -> Unit) -> Unit,
153 ) {
154     traverseAncestors(TextContextMenuDataTraverseKey) { node ->
155         when (node) {
156             is AddTextContextMenuDataComponentsNode -> builderBlock(node.builder)
157             is FilterTextContextMenuDataComponentsNode -> filterBlock(node.filter)
158             else -> throw IllegalStateException(wrongNodeTypeErrorMessage)
159         }
160         continueTraversal
161     }
162 }
163 
collectTextContextMenuDatanull164 internal fun DelegatableNode.collectTextContextMenuData(): TextContextMenuData =
165     TextContextMenuBuilderScope()
166         .apply {
167             traverseTextContextMenuDataNodes(
168                 filterBlock = ::addFilter,
169                 builderBlock = { builder -> this.builder() }
170             )
171         }
172         .build()
173