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