1 /*
<lambda>null2  * 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.subspace.layout
18 
19 import android.view.View
20 import androidx.compose.runtime.mutableStateOf
21 import androidx.compose.ui.unit.Density
22 import androidx.xr.compose.subspace.SpatialPanelDefaults
23 import androidx.xr.compose.subspace.node.SubspaceLayoutNode
24 import androidx.xr.compose.unit.IntVolumeSize
25 import androidx.xr.compose.unit.Meter
26 import androidx.xr.runtime.Session
27 import androidx.xr.runtime.math.Pose
28 import androidx.xr.scenecore.BasePanelEntity
29 import androidx.xr.scenecore.Component
30 import androidx.xr.scenecore.ContentlessEntity
31 import androidx.xr.scenecore.Entity
32 import androidx.xr.scenecore.PanelEntity
33 import androidx.xr.scenecore.PixelDimensions
34 import androidx.xr.scenecore.SurfaceEntity
35 import androidx.xr.scenecore.scene
36 
37 /**
38  * Wrapper class for Entities from SceneCore to provide convenience methods for working with
39  * Entities from SceneCore.
40  */
41 @PublishedApi
42 internal sealed class CoreEntity(public val entity: Entity) : OpaqueEntity {
43 
44     internal var layout: SubspaceLayoutNode? = null
45         set(value) {
46             field = value
47             updateEntityPose()
48         }
49 
50     private val density: Density
51         get() = layout?.density ?: error { "CoreEntity is not attached to a layout." }
52 
53     internal fun updateEntityPose() {
54         // Compose XR uses pixels, SceneCore uses meters.
55         val corePose =
56             layout?.measurableLayout?.poseInParentEntity?.convertPixelsToMeters(density)
57                 ?: Pose.Identity
58         if (entity.getPose() != corePose) {
59             entity.setPose(corePose)
60         }
61     }
62 
63     public open fun dispose() {
64         entity.dispose()
65     }
66 
67     /**
68      * The backing value for the size of the [CoreEntity] in pixels. It uses a MutableState object
69      * so that recompositions can be triggered on size changes.
70      */
71     protected val mutableSize = mutableStateOf(IntVolumeSize.Zero)
72 
73     /** The volume size of the [CoreEntity] in pixels. */
74     public open var size: IntVolumeSize
75         get() = mutableSize.value
76         set(value) {
77             if (mutableSize.value == value) {
78                 return
79             }
80             mutableSize.value = value
81         }
82 
83     /**
84      * The scale of this entity relative to its parent. This value will affect the rendering of this
85      * Entity's children. As the scale increases, this will uniformly stretch the content of the
86      * Entity. This does not affect layout and other content will be laid out according to the
87      * original scale of the entity.
88      */
89     internal var scale = 1f
90         set(value) {
91             if (field != value) {
92                 entity.setScale(value)
93             }
94             field = value
95         }
96 
97     /**
98      * The opacity of this entity (and its children) as a value between [0..1]. An alpha value of
99      * 0.0f means fully transparent while the value of 1.0f means fully opaque.
100      */
101     internal var alpha = 1f
102         set(value) {
103             if (field != value) {
104                 entity.setAlpha(value)
105             }
106             field = value
107         }
108 
109     public var parent: CoreEntity? = null
110         set(value) {
111             field = value
112 
113             // Leave SceneCore's parent as-is if we're trying to clear it out. SceneCore
114             // parents all
115             // newly-created non-Anchor entities under a world space point of reference for the
116             // activity
117             // space, but we don't have access to it. To maintain this parent-is-not-null property,
118             // we use
119             // this hack to keep the original parent, even if it's not technically correct when
120             // we're
121             // trying to reparent a node. The correct parent will be set on the "set" part of the
122             // reparent.
123             //
124             // TODO(b/356952297): Remove this hack once we can save and restore the original parent.
125             if (value == null) return
126 
127             entity.setParent(value.entity)
128         }
129 
130     /**
131      * Add a SceneCore [Component] to this entity.
132      *
133      * @param component The [Component] to add.
134      * @return true if the component was added successfully, false otherwise.
135      */
136     public fun addComponent(component: Component): Boolean {
137         return entity.addComponent(component)
138     }
139 
140     /**
141      * Remove a SceneCore [Component] from this entity.
142      *
143      * @param component The [Component] to remove.
144      */
145     public fun removeComponent(component: Component) {
146         entity.removeComponent(component)
147     }
148 }
149 
150 /** Wrapper class for contentless entities from SceneCore. */
151 @PublishedApi
152 internal class CoreContentlessEntity(entity: Entity) : CoreEntity(entity) {
153     init {
<lambda>null154         require(entity is ContentlessEntity) {
155             "Entity passed to CoreContentlessEntity should be a ContentlessEntity."
156         }
157     }
158 }
159 
160 /**
161  * Wrapper class for [BasePanelEntity] to provide convenience methods for working with panel
162  * entities from SceneCore.
163  */
164 internal sealed class CoreBasePanelEntity(
165     private val panelEntity: BasePanelEntity<*>,
166     private val density: Density,
167 ) : CoreEntity(panelEntity), MovableCoreEntity, ResizableCoreEntity {
168     override var overrideSize: IntVolumeSize? = null
169 
170     override var size: IntVolumeSize
171         get() = super.size
172         set(value) {
173             val nextSize = overrideSize ?: value
174             if (super.size != nextSize) {
175                 super.size = nextSize
176                 panelEntity.setSizeInPixels(PixelDimensions(size.width, size.height))
177                 updateShape()
178             }
179         }
180 
181     /** The [SpatialShape] of this [CoreBasePanelEntity]. */
182     public var shape: SpatialShape = SpatialPanelDefaults.shape
183         set(value) {
184             if (field != value) {
185                 field = value
186                 updateShape()
187             }
188         }
189 
190     /** Apply shape changes to the SceneCore [Entity]. */
updateShapenull191     private fun updateShape() {
192         val shape = shape
193         if (shape is SpatialRoundedCornerShape) {
194             val radius =
195                 shape.computeCornerRadius(size.width.toFloat(), size.height.toFloat(), density)
196             panelEntity.setCornerRadius(Meter.fromPixel(radius, density).toM())
197         }
198     }
199 }
200 
201 /**
202  * Wrapper class for [PanelEntity] to provide convenience methods for working with panel entities
203  * from SceneCore.
204  */
205 internal class CorePanelEntity(entity: PanelEntity, density: Density) :
206     CoreBasePanelEntity(entity, density)
207 
208 /**
209  * Wrapper class for [Session.mainPanelEntity] to provide convenience methods for working with the
210  * main panel from SceneCore.
211  */
212 internal class CoreMainPanelEntity(session: Session, density: Density) :
213     CoreBasePanelEntity(session.scene.mainPanelEntity, density) {
214     private val mainView = session.activity.window.decorView
215     private val listener =
_null216         View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
217             mutableSize.value =
218                 session.scene.mainPanelEntity.getSizeInPixels().run {
219                     IntVolumeSize(width, height, 0)
220                 }
221         }
222 
223     init {
224         mainView.addOnLayoutChangeListener(listener)
225     }
226 
227     /**
228      * Whether this entity or any of its ancestors is marked as hidden.
229      *
230      * Note that a non-hidden entity may still not be visible if its alpha is 0.
231      */
232     public var hidden
233         get() = entity.isHidden(includeParents = true)
234         set(value) {
235             entity.setHidden(value)
236         }
237 
disposenull238     override fun dispose() {
239         // Do not call super.dispose() because we don't want to dispose the main panel entity.
240         mainView.removeOnLayoutChangeListener(listener)
241     }
242 }
243 
244 /** Wrapper class for surface entities from SceneCore. */
245 internal class CoreSurfaceEntity(
246     internal val surfaceEntity: SurfaceEntity,
247     private val density: Density,
248 ) : CoreEntity(surfaceEntity), ResizableCoreEntity, MovableCoreEntity {
249     internal var stereoMode: Int
250         get() = surfaceEntity.stereoMode
251         set(value) {
252             if (value != surfaceEntity.stereoMode) {
253                 surfaceEntity.stereoMode = value
254             }
255         }
256 
257     private var currentFeatheringEffect: SpatialFeatheringEffect =
258         SpatialSmoothFeatheringEffect(ZeroFeatheringSize)
259 
260     override var size: IntVolumeSize
261         get() = super.size
262         set(value) {
263             val nextSize = overrideSize ?: value
264             if (super.size != nextSize) {
265                 super.size = nextSize
266                 surfaceEntity.canvasShape =
267                     SurfaceEntity.CanvasShape.Quad(
268                         Meter.fromPixel(size.width.toFloat(), density).value,
269                         Meter.fromPixel(size.height.toFloat(), density).value,
270                     )
271                 updateFeathering()
272             }
273         }
274 
275     override var overrideSize: IntVolumeSize? = null
276 
setFeatheringEffectnull277     internal fun setFeatheringEffect(featheringEffect: SpatialFeatheringEffect) {
278         currentFeatheringEffect = featheringEffect
279         updateFeathering()
280     }
281 
updateFeatheringnull282     private fun updateFeathering() {
283         (currentFeatheringEffect as? SpatialSmoothFeatheringEffect)?.let {
284             surfaceEntity.featherRadiusY = it.size.toWidthPercent(size.width.toFloat(), density)
285             surfaceEntity.featherRadiusX = it.size.toHeightPercent(size.height.toFloat(), density)
286         }
287     }
288 }
289 
290 /** [CoreEntity] types that implement this interface may have the ResizableComponent attached. */
291 internal interface ResizableCoreEntity {
292     /**
293      * The size of the [CoreEntity] in pixels.
294      *
295      * This value is used to override the layout size of the [CoreEntity] when it is resizable. When
296      * this value is null, the layout size of the [CoreEntity] is used.
297      */
298     public var overrideSize: IntVolumeSize?
299 }
300 
301 /** [CoreEntity] types that implement this interface may have the MovableComponent attached. */
302 internal interface MovableCoreEntity
303