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.annotation.CallSuper
20 
21 /**
22  * Represents the basic building block for a [SpatialElement] that can contain other
23  * [SpatialElement] instances.
24  *
25  * It provides functionality for managing child SpatialElements and for attaching and detaching from
26  * a [SpatialComposeScene].
27  */
28 internal open class SpatialElement {
29 
30     /**
31      * Interface definition for callbacks that are invoked when this element's attachment state
32      * changes.
33      */
34     public interface OnAttachStateChangeListener {
35         /**
36          * Called when the [SpatialElement] is attached to a [SpatialComposeScene].
37          *
38          * @param spatialComposeScene The [SpatialComposeScene] that the element was attached to.
39          */
onElementAttachedToSubspacenull40         public fun onElementAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {}
41 
42         /**
43          * Called when the [SpatialElement] is detached from a [SpatialComposeScene].
44          *
45          * @param spatialComposeScene The [SpatialComposeScene] that the element was detached from.
46          */
onElementDetachedFromSubspacenull47         public fun onElementDetachedFromSubspace(spatialComposeScene: SpatialComposeScene) {}
48     }
49 
50     /**
51      * The parent of this [SpatialElement].
52      *
53      * This property is `null` if the element is not attached to a parent [SpatialElement].
54      */
55     public var parent: SpatialElement? = null
56         internal set(parentEl) {
57             field = parentEl
58             spatialComposeScene = parentEl?.spatialComposeScene
59         }
60 
61     /**
62      * The [SpatialComposeScene] that this element is attached to, or `null` if it is not attached
63      * to any subspace.
64      */
65     public var spatialComposeScene: SpatialComposeScene? = null
66         set(value) {
67             if (field == value) {
68                 return
69             }
70 
71             if (value == null) {
<lambda>null72                 val oldScene = checkNotNull(field) { "Scene must be non-null before clearing." }
73                 onDetachedFromSubspace(oldScene)
<lambda>null74                 onAttachStateChangeListeners?.forEach { it.onElementDetachedFromSubspace(oldScene) }
75             } else {
76                 onAttachedToSubspace(value)
<lambda>null77                 onAttachStateChangeListeners?.forEach { it.onElementAttachedToSubspace(value) }
78             }
79             field = value
80         }
81 
82     /** Whether the element is attached to a [SpatialComposeScene]. */
83     protected val isAttachedToSpatialComposeScene: Boolean
84         get() = spatialComposeScene != null
85 
86     private var onAttachStateChangeListeners: MutableList<OnAttachStateChangeListener>? = null
87 
88     /**
89      * Detaches this element from its parent [SpatialElement], setting the [parent] to `null` and
90      * removing from parent's [SpatialElement.children] list.
91      */
detachFromParentnull92     private fun detachFromParent() {
93         @Suppress("UNUSED_VARIABLE") val unused = parent?.removeChild(this)
94         parent = null
95     }
96 
97     /**
98      * Registers the [listener] whose callbacks are invoked when this [SpatialElement]'s attachment
99      * state to [SpatialComposeScene] changes.
100      *
101      * Use [removeOnAttachStateChangeListener] to unregister the [listener].
102      */
103     @Suppress("ExecutorRegistration")
addOnAttachStateChangeListenernull104     public fun addOnAttachStateChangeListener(listener: OnAttachStateChangeListener) {
105         val listeners = onAttachStateChangeListeners
106 
107         if (listeners == null) {
108             onAttachStateChangeListeners = mutableListOf(listener)
109         } else {
110             listeners.add(listener)
111         }
112     }
113 
114     /**
115      * Registers a one-time callback to be invoked when this [SpatialElement] is detached from the
116      * [SpatialComposeScene].
117      *
118      * @param listener the callback to be invoked when the element is detached.
119      */
onDetachedFromSubspaceOncenull120     public fun onDetachedFromSubspaceOnce(listener: (SpatialComposeScene) -> Unit) {
121         addOnAttachStateChangeListener(
122             object : OnAttachStateChangeListener {
123                 override fun onElementDetachedFromSubspace(
124                     spatialComposeScene: SpatialComposeScene
125                 ) {
126                     removeOnAttachStateChangeListener(this)
127                     listener(spatialComposeScene)
128                 }
129             }
130         )
131     }
132 
133     /**
134      * Removes and unregisters the previously registered [listener].
135      *
136      * The [listener] will no longer receive any further notification whenever [spatialComposeScene]
137      * attachment changes.
138      */
removeOnAttachStateChangeListenernull139     public fun removeOnAttachStateChangeListener(listener: OnAttachStateChangeListener) {
140         onAttachStateChangeListeners?.remove(listener)
141     }
142 
143     private val _children = mutableListOf<SpatialElement>()
144 
145     /** Immediate children of this [SpatialElement]. */
146     public val children: List<SpatialElement>
147         get() = _children
148 
149     /**
150      * Appends the given [element] to the [children] of this element.
151      *
152      * @param element the element to add as a child.
153      */
154     @CallSuper
addChildnull155     public open fun addChild(element: SpatialElement) {
156         element.parent = this
157         _children.add(element)
158     }
159 
160     /**
161      * Removes the given [element] from the [children] of this element.
162      *
163      * @param element the element to remove as a child.
164      * @return `true` if element was successfully removed from the list, `false` otherwise.
165      */
166     @CallSuper
removeChildnull167     public open fun removeChild(element: SpatialElement): Boolean {
168         if (_children.remove(element)) {
169             element.parent = null
170             return true
171         }
172 
173         return false
174     }
175 
176     /** Detaches all of its children and clears the [children] list. */
177     @CallSuper
removeChildrennull178     public open fun removeChildren() {
179         _children.forEach { it.parent = null }
180         _children.clear()
181     }
182 
183     /**
184      * Update children's spatialComposeScene to [SpatialComposeScene] when this element is attached
185      * to it.
186      *
187      * @param spatialComposeScene the [SpatialComposeScene] this element is attached to.
188      */
189     @CallSuper
onAttachedToSubspacenull190     public open fun onAttachedToSubspace(spatialComposeScene: SpatialComposeScene) {
191         // Make sure all children have the same `spatialComposeScene` reference too.
192         _children.forEach { it.spatialComposeScene = spatialComposeScene }
193     }
194 
195     /**
196      * Called when this element is detached from a [SpatialComposeScene].
197      *
198      * @param spatialComposeScene the [SpatialComposeScene] this element is detached from.
199      */
200     @CallSuper
onDetachedFromSubspacenull201     public open fun onDetachedFromSubspace(spatialComposeScene: SpatialComposeScene) {
202         // make sure `spatialComposeScene` references of all children are cleaned up too.
203         _children.forEach { it.spatialComposeScene = null }
204     }
205 }
206