1 /*
2  * 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.xr.compose.platform
18 
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.Composition
21 import androidx.compose.runtime.CompositionContext
22 import androidx.compose.runtime.mutableStateOf
23 import androidx.compose.ui.InternalComposeUiApi
24 import androidx.xr.compose.subspace.SubspaceComposable
25 import androidx.xr.compose.subspace.layout.CoreEntity
26 import androidx.xr.compose.unit.VolumeConstraints
27 
28 /**
29  * Base class for custom [SpatialElement]s implemented using Jetpack Compose UI.
30  *
31  * Subclasses should implement the [Content] function with the appropriate content.
32  *
33  * Attempts to call [addChild] or its variants/overloads will result in an [IllegalStateException].
34  *
35  * This class is based on the existing `AbstractComposeView` class in
36  * [androidx.compose.ui.platform].
37  *
38  * @property compositionContext The [CompositionContext] used for compositions of this element's
39  *   hierarchy.
40  *
41  *   If this composition was created as the top level composition in the hierarchy, then the
42  *   recomposer will be cancelled if this element is detached from the subspace. Therefore, this
43  *   instance should not be shared or reused in other trees.
44  *
45  * @property rootCoreEntity The [CoreEntity] associated with the root of this composition. This root
46  *   CoreEntity will be the parent entity of the entire composition.
47  *
48  *   It is not necessary for a composition to have a root entity; however, it may be provided to
49  *   ensure that the composition is properly parented when it is a sub-composition of another
50  *   composition.
51  */
52 internal abstract class AbstractComposeElement(
53     internal var compositionContext: CompositionContext? = null,
54     internal val rootCoreEntity: CoreEntity? = null,
55 ) : SpatialElement() {
56 
57     /**
58      * Whether the composition should be created when the element is attached to a
59      * [SpatialComposeScene].
60      *
61      * If `true`, this [SpatialElement]'s composition will be created when it is attached to a
62      * [SpatialComposeScene] for the first time. Defaults to `true`.
63      *
64      * Subclasses may override this property to prevent eager initial composition if the element's
65      * content is not yet ready.
66      */
67     @get:Suppress("GetterSetterNames")
68     protected open val shouldCreateCompositionOnAttachedToSpatialComposeScene: Boolean
69         get() = true
70 
71     private var creatingComposition = false
72 
73     private var composition: Composition? = null
74 
75     /**
76      * The [AndroidComposeSpatialElement] that will be used to host the composition for this
77      * element.
78      */
79     internal val compositionOwner: AndroidComposeSpatialElement = AndroidComposeSpatialElement()
80 
81     /**
82      * The Jetpack Compose [SubspaceComposable] UI content for this element.
83      *
84      * Subclasses must implement this method to provide content. Initial composition will occur when
85      * the element is attached to a [SpatialComposeScene] or when [createComposition] is called,
86      * whichever comes first.
87      */
Contentnull88     @Composable @SubspaceComposable protected abstract fun Content()
89 
90     override fun addChild(element: SpatialElement) {
91         if (!creatingComposition) {
92             throw UnsupportedOperationException(
93                 "May only add $element to $this during composition."
94             )
95         }
96 
97         super.addChild(element)
98     }
99 
onAttachedToSubspacenull100     override fun onAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {
101         super.onAttachedToSubspace(spatialComposeScene)
102 
103         if (shouldCreateCompositionOnAttachedToSpatialComposeScene) {
104             createComposition()
105         }
106     }
107 
108     /**
109      * Performs the initial composition for this element.
110      *
111      * This method has no effect if the composition has already been created.
112      *
113      * This method should only be called if this element is attached to a [SpatialComposeScene] or
114      * if a parent [CompositionContext] has been set explicitly.
115      */
116     @OptIn(InternalComposeUiApi::class)
createCompositionnull117     protected fun createComposition() {
118         if (composition != null) return
119 
120         check(isAttachedToSpatialComposeScene) {
121             "Element.createComposition() requires the Element to be attached to the subspace."
122         }
123         check(!hasAncestorWithCompositionContext()) {
124             "Cannot construct a composition for $this element. The tree it is currently attached " +
125                 "to has another composition context."
126         }
127         check(children.isEmpty()) {
128             "Cannot set the composable content. $parent element already contains a subtree."
129         }
130 
131         creatingComposition = true
132 
133         GlobalSnapshotManager.ensureStarted()
134         addChild(compositionOwner)
135         compositionOwner.root.coreEntity = rootCoreEntity
136         composition =
137             WrappedComposition(
138                     compositionOwner,
139                     compositionContext
140                         ?: SubspaceRecomposerPolicy.createAndInstallSubspaceRecomposer(this),
141                 )
142                 .also {
143                     compositionOwner.wrappedComposition = it
144                     it.setContent { Content() }
145                 }
146 
147         creatingComposition = false
148     }
149 
150     /** Whether any of the ancestor elements have a [CompositionContext]. */
hasAncestorWithCompositionContextnull151     private fun hasAncestorWithCompositionContext(): Boolean =
152         generateSequence(parent) { it.parent }
<lambda>null153             .any { it is AbstractComposeElement && it.compositionContext != null }
154 
155     /**
156      * Disposes the composition for this element.
157      *
158      * This method has no effect if the composition has already been disposed.
159      */
disposeCompositionnull160     public fun disposeComposition() {
161         composition?.dispose()
162         composition = null
163     }
164 }
165 
166 /**
167  * An [SpatialElement] that can host Jetpack Compose [SubspaceComposable] content.
168  *
169  * Use [setContent] to provide the content composable function for the element.
170  *
171  * This class is based on the existing `ComposeView` class in [androidx.compose.ui.platform].
172  *
173  * @param scene The [SpatialComposeScene] that this element is attached to.
174  * @param compositionContext the [CompositionContext] from a parent composition to propagate
175  *   composition state. Should be `null` when this instance is the top-level composition context, in
176  *   which case a new [CompositionContext] will be created. This value should be provided when this
177  *   instance is a sub-composition of another composition.
178  * @param rootCoreEntity The [CoreEntity] associated with the root layout of this composition (see
179  *   [AbstractComposeElement.rootCoreEntity])
180  */
181 internal class SpatialComposeElement(
182     scene: SpatialComposeScene,
183     compositionContext: CompositionContext? = null,
184     rootCoreEntity: CoreEntity? = null,
185     rootVolumeConstraints: VolumeConstraints,
186 ) : AbstractComposeElement(compositionContext, rootCoreEntity) {
<lambda>null187     init {
188         spatialComposeScene = scene
189         compositionOwner.rootVolumeConstraints = rootVolumeConstraints
190     }
191 
192     private val content = mutableStateOf<(@Composable @SubspaceComposable () -> Unit)?>(null)
193 
194     @get:Suppress("GetterSetterNames")
195     override var shouldCreateCompositionOnAttachedToSpatialComposeScene: Boolean = false
196         private set
197 
198     @Composable
199     @SubspaceComposable
Contentnull200     override fun Content() {
201         content.value?.invoke()
202     }
203 
204     /**
205      * Sets the Jetpack Compose [SubspaceComposable] UI content for this element.
206      *
207      * Initial composition will occur when the element is attached to a [SpatialComposeScene] or
208      * when [createComposition] is called, whichever comes first.
209      *
210      * @param content the composable content to display in this element.
211      */
212     public fun setContent(content: @Composable @SubspaceComposable () -> Unit) {
213         shouldCreateCompositionOnAttachedToSpatialComposeScene = true
214 
215         this.content.value = content
216 
217         if (isAttachedToSpatialComposeScene) {
218             createComposition()
219         }
220     }
221 }
222