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
18 
19 import android.content.Intent
20 import android.graphics.Color
21 import android.graphics.Rect
22 import android.view.View
23 import android.view.View.MeasureSpec
24 import android.view.ViewGroup
25 import android.widget.FrameLayout
26 import androidx.annotation.RestrictTo
27 import androidx.compose.foundation.background
28 import androidx.compose.foundation.gestures.detectTapGestures
29 import androidx.compose.foundation.layout.Box
30 import androidx.compose.foundation.layout.fillMaxSize
31 import androidx.compose.foundation.shape.CornerSize
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.CompositionLocalProvider
34 import androidx.compose.runtime.DisposableEffect
35 import androidx.compose.runtime.LaunchedEffect
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.mutableIntStateOf
38 import androidx.compose.runtime.remember
39 import androidx.compose.runtime.setValue
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.UiComposable
42 import androidx.compose.ui.graphics.Color as UiColor
43 import androidx.compose.ui.input.pointer.pointerInput
44 import androidx.compose.ui.layout.Layout
45 import androidx.compose.ui.platform.LocalContext
46 import androidx.compose.ui.platform.LocalView
47 import androidx.compose.ui.unit.dp
48 import androidx.compose.ui.util.fastForEach
49 import androidx.compose.ui.util.fastMaxOfOrNull
50 import androidx.xr.compose.platform.LocalDialogManager
51 import androidx.xr.compose.platform.LocalOpaqueEntity
52 import androidx.xr.compose.platform.LocalSession
53 import androidx.xr.compose.platform.getActivity
54 import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
55 import androidx.xr.compose.subspace.layout.SpatialShape
56 import androidx.xr.compose.subspace.layout.SubspaceLayout
57 import androidx.xr.compose.subspace.layout.SubspaceModifier
58 import androidx.xr.compose.unit.Meter.Companion.millimeters
59 import androidx.xr.runtime.math.Pose
60 import androidx.xr.runtime.math.Vector3
61 import androidx.xr.scenecore.ActivityPanelEntity
62 import androidx.xr.scenecore.Dimensions
63 import androidx.xr.scenecore.PanelEntity
64 
65 private const val DEFAULT_SIZE_PX = 400
66 
67 /** Contains default values used by spatial panels. */
68 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
69 public object SpatialPanelDefaults {
70 
71     /** Default shape for a Spatial Panel. */
72     public val shape: SpatialShape = SpatialRoundedCornerShape(CornerSize(32.dp))
73 }
74 
75 /**
76  * Creates a [SpatialPanel] representing a 2D plane in 3D space in which an application can fill
77  * content.
78  *
79  * @param view Content view to be displayed within the SpatialPanel.
80  * @param modifier SubspaceModifiers to apply to the SpatialPanel.
81  * @param shape The shape of this Spatial Panel.
82  */
83 @Composable
84 @SubspaceComposable
85 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
SpatialPanelnull86 public fun SpatialPanel(
87     view: View,
88     modifier: SubspaceModifier = SubspaceModifier,
89     shape: SpatialShape = SpatialPanelDefaults.shape,
90 ) {
91     val minimumPanelDimension = Dimensions(10f, 10f, 10f)
92     val frameLayout = remember {
93         FrameLayout(view.context).also {
94             if (view.parent != it) {
95                 val parent = view.parent as? ViewGroup
96                 parent?.removeView(view)
97                 it.addView(view)
98             }
99         }
100     }
101     val scrim = remember { View(view.context) }
102     val dialogManager = LocalDialogManager.current
103     val corePanelEntity =
104         rememberCorePanelEntity(shape = shape) {
105             PanelEntity.create(
106                 session = this,
107                 view = frameLayout,
108                 dimensions = minimumPanelDimension,
109                 name = entityName("SpatialPanel"),
110                 pose = Pose.Identity,
111             )
112         }
113 
114     LaunchedEffect(dialogManager.isSpatialDialogActive.value) {
115         if (dialogManager.isSpatialDialogActive.value) {
116             scrim.setBackgroundColor(Color.argb(90, 0, 0, 0))
117             val scrimLayoutParams =
118                 FrameLayout.LayoutParams(
119                     FrameLayout.LayoutParams.MATCH_PARENT,
120                     FrameLayout.LayoutParams.MATCH_PARENT,
121                 )
122 
123             if (scrim.parent == null) {
124                 frameLayout.addView(scrim, scrimLayoutParams)
125             }
126 
127             scrim.setOnClickListener { dialogManager.isSpatialDialogActive.value = false }
128         } else {
129             frameLayout.removeView(scrim)
130         }
131     }
132 
133     SubspaceLayout(modifier = modifier, coreEntity = corePanelEntity) { _, constraints ->
134         view.measure(
135             MeasureSpec.makeMeasureSpec(constraints.maxWidth, MeasureSpec.AT_MOST),
136             MeasureSpec.makeMeasureSpec(constraints.maxHeight, MeasureSpec.AT_MOST),
137         )
138         val width = view.measuredWidth.coerceIn(constraints.minWidth, constraints.maxWidth)
139         val height = view.measuredHeight.coerceIn(constraints.minHeight, constraints.maxHeight)
140         val depth = constraints.minDepth.coerceAtLeast(0)
141         layout(width, height, depth) {}
142     }
143 }
144 
145 /**
146  * Creates a [SpatialPanel] representing a 2D plane in 3D space in which an application can fill
147  * content.
148  *
149  * @param modifier SubspaceModifiers to apply to the SpatialPanel.
150  * @param shape The shape of this Spatial Panel.
151  * @param content The composable content to render within the SpatialPanel.
152  */
153 @Composable
154 @SubspaceComposable
155 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
156 public fun SpatialPanel(
157     modifier: SubspaceModifier = SubspaceModifier,
158     shape: SpatialShape = SpatialPanelDefaults.shape,
159     content: @Composable @UiComposable () -> Unit,
160 ) {
161     val view = rememberComposeView()
162     val minimumPanelDimension = Dimensions(10f, 10f, 10f)
163     val dialogManager = LocalDialogManager.current
164     val corePanelEntity =
<lambda>null165         rememberCorePanelEntity(shape = shape) {
166             PanelEntity.create(
167                 session = this,
168                 view = view,
169                 dimensions = minimumPanelDimension,
170                 name = entityName("SpatialPanel"),
171                 pose = Pose.Identity,
172             )
173         }
<lambda>null174     var intrinsicWidth by remember { mutableIntStateOf(DEFAULT_SIZE_PX) }
<lambda>null175     var intrinsicHeight by remember { mutableIntStateOf(DEFAULT_SIZE_PX) }
176 
volumeConstraintsnull177     SubspaceLayout(modifier = modifier, coreEntity = corePanelEntity) { _, volumeConstraints ->
178         view.setContent {
179             CompositionLocalProvider(LocalOpaqueEntity provides corePanelEntity) {
180                 Layout(content = content, modifier = Modifier) { measurables, constraints ->
181                     intrinsicWidth =
182                         measurables.fastMaxOfOrNull {
183                             try {
184                                 it.maxIntrinsicWidth(volumeConstraints.maxHeight)
185                             } catch (e: IllegalStateException) {
186                                 0
187                             }
188                         } ?: DEFAULT_SIZE_PX
189                     intrinsicHeight =
190                         measurables.fastMaxOfOrNull {
191                             try {
192                                 it.maxIntrinsicHeight(volumeConstraints.maxWidth)
193                             } catch (e: IllegalStateException) {
194                                 0
195                             }
196                         } ?: DEFAULT_SIZE_PX
197                     val placeables = measurables.map { it.measure(constraints) }
198                     layout(
199                         placeables.fastMaxOfOrNull { it.measuredWidth } ?: DEFAULT_SIZE_PX,
200                         placeables.fastMaxOfOrNull { it.measuredHeight } ?: DEFAULT_SIZE_PX,
201                     ) {
202                         placeables.fastForEach { placeable -> placeable.place(0, 0) }
203                     }
204                 }
205             }
206 
207             if (dialogManager.isSpatialDialogActive.value) {
208                 Box(
209                     modifier =
210                         Modifier.fillMaxSize()
211                             .background(UiColor.Black.copy(alpha = 0.5f))
212                             .pointerInput(Unit) {
213                                 detectTapGestures {
214                                     dialogManager.isSpatialDialogActive.value = false
215                                 }
216                             }
217                 ) {}
218             }
219         }
220         val width = intrinsicWidth.coerceIn(volumeConstraints.minWidth, volumeConstraints.maxWidth)
221         val height =
222             intrinsicHeight.coerceIn(volumeConstraints.minHeight, volumeConstraints.maxHeight)
223         val depth = volumeConstraints.minDepth.coerceAtLeast(0)
224         layout(width, height, depth) {}
225     }
226 }
227 
228 /**
229  * Creates a [SpatialPanel] backed by the main Window content.
230  *
231  * This panel requires the following specific configuration in the Android Manifest for proper
232  * sizing/resizing behavior:
233  * ```
234  * <activity
235  * android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize>
236  * <!--suppress AndroidElementNotAllowed -->
237  * <layout android:defaultWidth="50dp" android:defaultHeight="50dp" android:minHeight="50dp"
238  * android:minWidth="50dp"/>
239  * </activity>
240  * ```
241  *
242  * To disable Title Bars in the activity add the following to Activity#onCreate:
243  * ```
244  * requestWindowFeature(Window.FEATURE_NO_TITLE)
245  * ```
246  *
247  * Or disable Title Bars in the theme by setting:
248  * ```
249  * <item name="windowNoTitle">true</item>
250  * ```
251  *
252  * @param modifier SubspaceModifier to apply to the MainPanel.
253  * @param shape The shape of this Spatial Panel.
254  */
255 @Composable
256 @SubspaceComposable
257 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
MainPanelnull258 public fun MainPanel(
259     modifier: SubspaceModifier = SubspaceModifier,
260     shape: SpatialShape = SpatialPanelDefaults.shape,
261 ) {
262     val mainPanel = rememberCoreMainPanelEntity(shape = shape)
263     val view = LocalContext.current.getActivity().window?.decorView ?: LocalView.current
264 
265     DisposableEffect(Unit) {
266         mainPanel.hidden = false
267         onDispose { mainPanel.hidden = true }
268     }
269 
270     SubspaceLayout(modifier = modifier, coreEntity = mainPanel) { _, constraints ->
271         val width = view.measuredWidth.coerceIn(constraints.minWidth, constraints.maxWidth)
272         val height = view.measuredHeight.coerceIn(constraints.minHeight, constraints.maxHeight)
273         val depth = constraints.minDepth.coerceAtLeast(0)
274         layout(width, height, depth) {}
275     }
276 }
277 
278 /**
279  * Creates a [SpatialPanel] and launches an Activity within it.
280  *
281  * To disable Title Bars in the activity add the following to Activity#onCreate:
282  * ```
283  * requestWindowFeature(Window.FEATURE_NO_TITLE)
284  * ```
285  *
286  * Or disable Title Bars in the theme by setting:
287  * ```
288  * <item name="windowNoTitle">true</item>
289  * ```
290  *
291  * @param intent The intent of an Activity to launch within this panel.
292  * @param modifier SubspaceModifiers to apply to the SpatialPanel.
293  * @param shape The shape of this Spatial Panel.
294  */
295 @Composable
296 @SubspaceComposable
297 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
SpatialPanelnull298 public fun SpatialPanel(
299     intent: Intent,
300     modifier: SubspaceModifier = SubspaceModifier,
301     shape: SpatialShape = SpatialPanelDefaults.shape,
302 ) {
303     val session = checkNotNull(LocalSession.current) { "session must be initialized" }
304     val dialogManager = LocalDialogManager.current
305 
306     val minimumPanelDimension = Dimensions(10f, 10f, 10f)
307     val rect = Rect(0, 0, DEFAULT_SIZE_PX, DEFAULT_SIZE_PX)
308     val activityPanelEntity = rememberCorePanelEntity {
309         ActivityPanelEntity.create(session, rect, entityName("ActivityPanel-${intent.action}"))
310             .also { it.launchActivity(intent) }
311     }
312 
313     SpatialBox {
314         SubspaceLayout(modifier = modifier, coreEntity = activityPanelEntity) { _, constraints ->
315             val width = DEFAULT_SIZE_PX.coerceIn(constraints.minWidth, constraints.maxWidth)
316             val height = DEFAULT_SIZE_PX.coerceIn(constraints.minHeight, constraints.maxHeight)
317             val depth = constraints.minDepth.coerceAtLeast(0)
318             layout(width, height, depth) {}
319         }
320 
321         if (dialogManager.isSpatialDialogActive.value) {
322             val scrimView = rememberComposeView()
323 
324             scrimView.setContent {
325                 Box(
326                     modifier =
327                         Modifier.fillMaxSize()
328                             .background(UiColor.Black.copy(alpha = 0.5f))
329                             .pointerInput(Unit) {
330                                 detectTapGestures {
331                                     dialogManager.isSpatialDialogActive.value = false
332                                 }
333                             }
334                 ) {}
335             }
336 
337             val scrimPanelEntity =
338                 rememberCorePanelEntity(shape = shape) {
339                     PanelEntity.create(
340                             session = session,
341                             view = scrimView,
342                             dimensions = minimumPanelDimension,
343                             name = entityName("ScrimPanel"),
344                             pose = Pose.Identity,
345                         )
346                         .also {
347                             it.setParent(activityPanelEntity.entity)
348                             it.setPose(Pose(translation = Vector3(0f, 0f, 3.millimeters.toM())))
349                         }
350                 }
351 
352             LaunchedEffect(activityPanelEntity.size) {
353                 val size = activityPanelEntity.size
354                 scrimPanelEntity.size = size
355             }
356         }
357     }
358 }
359