1 /*
<lambda>null2  * Copyright 2020 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
18 
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.Composer
21 import androidx.compose.runtime.CompositionLocalMap
22 import androidx.compose.runtime.Stable
23 import androidx.compose.ui.node.ModifierNodeElement
24 import androidx.compose.ui.node.requireLayoutNode
25 import androidx.compose.ui.platform.InspectorInfo
26 import androidx.compose.ui.platform.InspectorValueInfo
27 import androidx.compose.ui.platform.NoInspectorInfo
28 import kotlin.jvm.JvmName
29 
30 /**
31  * Declare a just-in-time composition of a [Modifier] that will be composed for each element it
32  * modifies. [composed] may be used to implement **stateful modifiers** that have instance-specific
33  * state for each modified element, allowing the same [Modifier] instance to be safely reused for
34  * multiple elements while maintaining element-specific state.
35  *
36  * If [inspectorInfo] is specified this modifier will be visible to tools during development.
37  * Specify the name and arguments of the original modifier.
38  *
39  * Example usage:
40  *
41  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierSample
42  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierWithArgumentsSample
43  *
44  * [materialize] must be called to create instance-specific modifiers if you are directly applying a
45  * [Modifier] to an element tree node.
46  */
47 fun Modifier.composed(
48     inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
49     factory: @Composable Modifier.() -> Modifier
50 ): Modifier = this.then(ComposedModifier(inspectorInfo, factory))
51 
52 /**
53  * Declare a just-in-time composition of a [Modifier] that will be composed for each element it
54  * modifies. [composed] may be used to implement **stateful modifiers** that have instance-specific
55  * state for each modified element, allowing the same [Modifier] instance to be safely reused for
56  * multiple elements while maintaining element-specific state.
57  *
58  * When keys are provided, [composed] produces a [Modifier] that will compare [equals] to another
59  * modifier constructed with the same keys in order to take advantage of caching and skipping
60  * optimizations. [fullyQualifiedName] should be the fully-qualified `import` name for your modifier
61  * factory function, e.g. `com.example.myapp.ui.fancyPadding`.
62  *
63  * If [inspectorInfo] is specified this modifier will be visible to tools during development.
64  * Specify the name and arguments of the original modifier.
65  *
66  * Example usage:
67  *
68  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierSample
69  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierWithArgumentsSample
70  *
71  * [materialize] must be called to create instance-specific modifiers if you are directly applying a
72  * [Modifier] to an element tree node.
73  */
74 fun Modifier.composed(
75     fullyQualifiedName: String,
76     key1: Any?,
77     inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
78     factory: @Composable Modifier.() -> Modifier
79 ): Modifier = this.then(KeyedComposedModifier1(fullyQualifiedName, key1, inspectorInfo, factory))
80 
81 /**
82  * Declare a just-in-time composition of a [Modifier] that will be composed for each element it
83  * modifies. [composed] may be used to implement **stateful modifiers** that have instance-specific
84  * state for each modified element, allowing the same [Modifier] instance to be safely reused for
85  * multiple elements while maintaining element-specific state.
86  *
87  * When keys are provided, [composed] produces a [Modifier] that will compare [equals] to another
88  * modifier constructed with the same keys in order to take advantage of caching and skipping
89  * optimizations. [fullyQualifiedName] should be the fully-qualified `import` name for your modifier
90  * factory function, e.g. `com.example.myapp.ui.fancyPadding`.
91  *
92  * If [inspectorInfo] is specified this modifier will be visible to tools during development.
93  * Specify the name and arguments of the original modifier.
94  *
95  * Example usage:
96  *
97  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierSample
98  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierWithArgumentsSample
99  *
100  * [materialize] must be called to create instance-specific modifiers if you are directly applying a
101  * [Modifier] to an element tree node.
102  */
103 fun Modifier.composed(
104     fullyQualifiedName: String,
105     key1: Any?,
106     key2: Any?,
107     inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
108     factory: @Composable Modifier.() -> Modifier
109 ): Modifier =
110     this.then(KeyedComposedModifier2(fullyQualifiedName, key1, key2, inspectorInfo, factory))
111 
112 /**
113  * Declare a just-in-time composition of a [Modifier] that will be composed for each element it
114  * modifies. [composed] may be used to implement **stateful modifiers** that have instance-specific
115  * state for each modified element, allowing the same [Modifier] instance to be safely reused for
116  * multiple elements while maintaining element-specific state.
117  *
118  * When keys are provided, [composed] produces a [Modifier] that will compare [equals] to another
119  * modifier constructed with the same keys in order to take advantage of caching and skipping
120  * optimizations. [fullyQualifiedName] should be the fully-qualified `import` name for your modifier
121  * factory function, e.g. `com.example.myapp.ui.fancyPadding`.
122  *
123  * If [inspectorInfo] is specified this modifier will be visible to tools during development.
124  * Specify the name and arguments of the original modifier.
125  *
126  * Example usage:
127  *
128  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierSample
129  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierWithArgumentsSample
130  *
131  * [materialize] must be called to create instance-specific modifiers if you are directly applying a
132  * [Modifier] to an element tree node.
133  */
134 fun Modifier.composed(
135     fullyQualifiedName: String,
136     key1: Any?,
137     key2: Any?,
138     key3: Any?,
139     inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
140     factory: @Composable Modifier.() -> Modifier
141 ): Modifier =
142     this.then(KeyedComposedModifier3(fullyQualifiedName, key1, key2, key3, inspectorInfo, factory))
143 
144 /**
145  * Declare a just-in-time composition of a [Modifier] that will be composed for each element it
146  * modifies. [composed] may be used to implement **stateful modifiers** that have instance-specific
147  * state for each modified element, allowing the same [Modifier] instance to be safely reused for
148  * multiple elements while maintaining element-specific state.
149  *
150  * When keys are provided, [composed] produces a [Modifier] that will compare [equals] to another
151  * modifier constructed with the same keys in order to take advantage of caching and skipping
152  * optimizations. [fullyQualifiedName] should be the fully-qualified `import` name for your modifier
153  * factory function, e.g. `com.example.myapp.ui.fancyPadding`.
154  *
155  * If [inspectorInfo] is specified this modifier will be visible to tools during development.
156  * Specify the name and arguments of the original modifier.
157  *
158  * Example usage:
159  *
160  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierSample
161  * @sample androidx.compose.ui.samples.InspectorInfoInComposedModifierWithArgumentsSample
162  *
163  * [materialize] must be called to create instance-specific modifiers if you are directly applying a
164  * [Modifier] to an element tree node.
165  */
166 fun Modifier.composed(
167     fullyQualifiedName: String,
168     vararg keys: Any?,
169     inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
170     factory: @Composable Modifier.() -> Modifier
171 ): Modifier = this.then(KeyedComposedModifierN(fullyQualifiedName, keys, inspectorInfo, factory))
172 
173 private open class ComposedModifier(
174     inspectorInfo: InspectorInfo.() -> Unit,
175     val factory: @Composable Modifier.() -> Modifier
176 ) : Modifier.Element, InspectorValueInfo(inspectorInfo)
177 
178 @Stable
179 private class KeyedComposedModifier1(
180     val fqName: String,
181     val key1: Any?,
182     inspectorInfo: InspectorInfo.() -> Unit,
183     factory: @Composable Modifier.() -> Modifier
184 ) : ComposedModifier(inspectorInfo, factory) {
185     override fun equals(other: Any?) =
186         other is KeyedComposedModifier1 && fqName == other.fqName && key1 == other.key1
187 
188     override fun hashCode(): Int = 31 * fqName.hashCode() + key1.hashCode()
189 }
190 
191 @Stable
192 private class KeyedComposedModifier2(
193     val fqName: String,
194     val key1: Any?,
195     val key2: Any?,
196     inspectorInfo: InspectorInfo.() -> Unit,
197     factory: @Composable Modifier.() -> Modifier
198 ) : ComposedModifier(inspectorInfo, factory) {
equalsnull199     override fun equals(other: Any?) =
200         other is KeyedComposedModifier2 &&
201             fqName == other.fqName &&
202             key1 == other.key1 &&
203             key2 == other.key2
204 
205     override fun hashCode(): Int {
206         var result = fqName.hashCode()
207         result = 31 * result + key1.hashCode()
208         result = 31 * result + key2.hashCode()
209         return result
210     }
211 }
212 
213 @Stable
214 private class KeyedComposedModifier3(
215     val fqName: String,
216     val key1: Any?,
217     val key2: Any?,
218     val key3: Any?,
219     inspectorInfo: InspectorInfo.() -> Unit,
220     factory: @Composable Modifier.() -> Modifier
221 ) : ComposedModifier(inspectorInfo, factory) {
equalsnull222     override fun equals(other: Any?) =
223         other is KeyedComposedModifier3 &&
224             fqName == other.fqName &&
225             key1 == other.key1 &&
226             key2 == other.key2 &&
227             key3 == other.key3
228 
229     override fun hashCode(): Int {
230         var result = fqName.hashCode()
231         result = 31 * result + key1.hashCode()
232         result = 31 * result + key2.hashCode()
233         result = 31 * result + key3.hashCode()
234         return result
235     }
236 }
237 
238 @Stable
239 private class KeyedComposedModifierN(
240     val fqName: String,
241     val keys: Array<out Any?>,
242     inspectorInfo: InspectorInfo.() -> Unit,
243     factory: @Composable Modifier.() -> Modifier
244 ) : ComposedModifier(inspectorInfo, factory) {
equalsnull245     override fun equals(other: Any?) =
246         other is KeyedComposedModifierN && fqName == other.fqName && keys.contentEquals(other.keys)
247 
248     override fun hashCode() = 31 * fqName.hashCode() + keys.contentHashCode()
249 }
250 
251 /**
252  * Materialize any instance-specific [composed modifiers][composed] for applying to a raw tree node.
253  * Call right before setting the returned modifier on an emitted node. You almost certainly do not
254  * need to call this function directly.
255  */
256 @Suppress("ModifierFactoryExtensionFunction")
257 // "materialize" JVM name is taken below to solve a backwards-incompatibility
258 @JvmName("materializeModifier")
259 fun Composer.materialize(modifier: Modifier): Modifier {
260     // A group is required here so the number of slot added to the caller's group
261     // is unconditionally the same (in this case, none) as is now required by the runtime.
262     startReplaceGroup(0x1a365f2c) // Random number for fake group key. Chosen by fair die roll.
263     val result = materializeImpl(modifier)
264     endReplaceGroup()
265     return result
266 }
267 
268 @Suppress("ModifierFactoryExtensionFunction")
Composernull269 private fun Composer.materializeImpl(modifier: Modifier): Modifier {
270     if (modifier.all { it !is ComposedModifier }) {
271         return modifier
272     }
273 
274     // This is a fake composable function that invokes the compose runtime directly so that it
275     // can call the element factory functions from the non-@Composable lambda of Modifier.foldIn.
276     // It would be more efficient to redefine the Modifier type hierarchy such that the fold
277     // operations could be inlined or otherwise made cheaper, which could make this unnecessary.
278 
279     // Random number for fake group key. Chosen by fair die roll.
280     startReplaceableGroup(0x48ae8da7)
281 
282     val result =
283         modifier.foldIn<Modifier>(Modifier) { acc, element ->
284             acc.then(
285                 if (element is ComposedModifier) {
286                     @Suppress("UNCHECKED_CAST")
287                     val factory = element.factory as Modifier.(Composer, Int) -> Modifier
288                     val composedMod = factory(Modifier, this, 0)
289                     materializeImpl(composedMod)
290                 } else {
291                     element
292                 }
293             )
294         }
295 
296     endReplaceableGroup()
297     return result
298 }
299 
300 /**
301  * This class is only used for backwards compatibility purposes to inject the CompositionLocalMap
302  * into LayoutNodes that were created by inlined code of older versions of the Layout composable.
303  * More details can be found at https://issuetracker.google.com/275067189
304  */
305 internal class CompositionLocalMapInjectionNode(map: CompositionLocalMap) : Modifier.Node() {
306     var map: CompositionLocalMap = map
307         set(value) {
308             field = value
309             requireLayoutNode().compositionLocalMap = value
310         }
311 
onAttachnull312     override fun onAttach() {
313         requireLayoutNode().compositionLocalMap = map
314     }
315 }
316 
317 /**
318  * This class is only used for backwards compatibility purposes to inject the CompositionLocalMap
319  * into LayoutNodes that were created by inlined code of older versions of the Layout composable.
320  * More details can be found at https://issuetracker.google.com/275067189
321  */
322 internal class CompositionLocalMapInjectionElement(val map: CompositionLocalMap) :
323     ModifierNodeElement<CompositionLocalMapInjectionNode>() {
createnull324     override fun create() = CompositionLocalMapInjectionNode(map)
325 
326     override fun update(node: CompositionLocalMapInjectionNode) {
327         node.map = map
328     }
329 
hashCodenull330     override fun hashCode(): Int = map.hashCode()
331 
332     override fun equals(other: Any?): Boolean {
333         return other is CompositionLocalMapInjectionElement && other.map == map
334     }
335 
inspectablePropertiesnull336     override fun InspectorInfo.inspectableProperties() {
337         name = "<Injected CompositionLocalMap>"
338     }
339 }
340 
341 /**
342  * This function exists solely for solving a backwards-incompatibility with older compilations that
343  * used an older version of the `Layout` composable. New code paths should not call this. More
344  * details can be found at https://issuetracker.google.com/275067189
345  */
346 @Suppress("ModifierFactoryExtensionFunction")
347 @JvmName("materialize")
348 @Deprecated(
349     "Kept for backwards compatibility only. If you are recompiling, use materialize.",
350     ReplaceWith("materialize"),
351     DeprecationLevel.HIDDEN
352 )
Composernull353 fun Composer.materializeWithCompositionLocalInjection(modifier: Modifier): Modifier =
354     materializeWithCompositionLocalInjectionInternal(modifier)
355 
356 // This method is here to be called from tests since the deprecated hidden API cannot be.
357 @Suppress("ModifierFactoryExtensionFunction")
358 internal fun Composer.materializeWithCompositionLocalInjectionInternal(
359     modifier: Modifier
360 ): Modifier {
361     return if (modifier === Modifier) modifier
362     else materialize(CompositionLocalMapInjectionElement(currentCompositionLocalMap).then(modifier))
363 }
364