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