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