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