Replace UserView with StackViews

Bug: 426102764
Test: Manual
Flag: android.multiuser.widget_current_user_view
Change-Id: Ia62b51abf02a943c313ce3275ead7ae0ee072409
diff --git a/Widget/src/main/java/com/android/multiuser/widget/ui/view/AdaptiveUserView.kt b/Widget/src/main/java/com/android/multiuser/widget/ui/view/AdaptiveUserView.kt
deleted file mode 100644
index 6b45648..0000000
--- a/Widget/src/main/java/com/android/multiuser/widget/ui/view/AdaptiveUserView.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.multiuser.widget.ui.view
-
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.unit.sp
-import androidx.glance.GlanceModifier
-import androidx.glance.Image
-import androidx.glance.ImageProvider
-import androidx.glance.layout.size
-import androidx.glance.text.Text
-import androidx.glance.text.TextStyle
-import com.android.multiuser.widget.R
-import com.android.multiuser.widget.ui.view.layout.AdaptivePane
-import com.android.multiuser.widget.viewmodel.UiState
-import com.android.multiuser.widget.viewmodel.UserStack
-import com.android.multiuser.widget.viewmodel.UserViewModel
-import kotlinx.coroutines.launch
-
-/**
- * Displays user data in the widget.
- */
-@Composable
-fun AdaptiveUserView (
-    viewModel: UserViewModel,
-    userStack: UserStack,
-    modifier: GlanceModifier,
-) {
-    val bitmap by viewModel.bitmap.collectAsState()
-    val scope = rememberCoroutineScope()
-    val uiState by viewModel.uiState.collectAsState()
-    scope.launch {
-        if (uiState != UiState.Loading) {
-            viewModel.loadAvatar()
-        }
-    }
-    AdaptivePane(modifier = modifier,
-        arrangement = userStack.arrangement,
-        startPane = {
-            Image(
-            provider = bitmap?.let { ImageProvider(it) }
-                ?: ImageProvider(R.drawable.account_circle),
-            contentDescription = viewModel.contentDescription,
-            modifier = GlanceModifier.size(userStack.imageSize)
-        )},
-        endPane = { if (userStack.textMetric.fontSize > 0.sp) {
-            Text(
-                text = viewModel.name,
-                maxLines = 1,
-                style = TextStyle(fontSize = userStack.textMetric.fontSize),
-            )
-        } else { null }
-        })
-}
\ No newline at end of file
diff --git a/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt b/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt
index 8e25023..4b9ca8c 100644
--- a/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt
+++ b/Widget/src/main/java/com/android/multiuser/widget/ui/view/LayoutView.kt
@@ -70,7 +70,7 @@
                     arrangement = layout.arrangement,
                     startPane = {
                         layout.userStack?.let { userStack ->
-                            AdaptiveUserView(
+                            UserStackView(
                                 viewModel = userViewModel,
                                 userStack,
                                 modifier = GlanceModifier
diff --git a/Widget/src/main/java/com/android/multiuser/widget/ui/view/UserStackView.kt b/Widget/src/main/java/com/android/multiuser/widget/ui/view/UserStackView.kt
new file mode 100644
index 0000000..5d0c6b8
--- /dev/null
+++ b/Widget/src/main/java/com/android/multiuser/widget/ui/view/UserStackView.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.multiuser.widget.ui.view
+
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.glance.GlanceModifier
+import androidx.glance.Image
+import androidx.glance.ImageProvider
+import androidx.glance.text.Text
+import androidx.glance.text.TextStyle
+import com.android.multiuser.stacks.Stack
+import com.android.multiuser.stacks.ui.StackView
+import com.android.multiuser.stacks.ui.size
+import com.android.multiuser.widget.R
+import com.android.multiuser.widget.viewmodel.UiState
+import com.android.multiuser.widget.viewmodel.stacks.UserStacks
+import com.android.multiuser.widget.viewmodel.UserViewModel
+import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
+import kotlinx.coroutines.launch
+
+/**
+ * Displays user data in the widget.
+ */
+@Composable
+fun UserStackView (
+    viewModel: UserViewModel,
+    stack: Stack,
+    modifier: GlanceModifier,
+) {
+    val bitmap by viewModel.bitmap.collectAsState()
+    val scope = rememberCoroutineScope()
+    val uiState by viewModel.uiState.collectAsState()
+    scope.launch {
+        if (uiState != UiState.Loading) {
+            viewModel.loadAvatar()
+        }
+    }
+    StackView(
+        items = stack.list.mapNotNull { measurable ->
+            // Create a composable lambda for each item by wrapping in {}
+            when (measurable) {
+                is UserStacks.UserImageSquare -> {
+                    // Return a lambda: @Composable () -> Unit
+                    {
+                        Image(
+                            provider = bitmap?.let { ImageProvider(it) }
+                                ?: ImageProvider(R.drawable.account_circle),
+                            contentDescription = viewModel.contentDescription,
+                            modifier = GlanceModifier.size(measurable.vertically.min)
+                        )
+                    }
+                }
+                is TextMeasurable -> {
+                    // Return another lambda: @Composable () -> Unit
+                    {
+                        Text(
+                            text = measurable.text,
+                            maxLines = 1,
+                            style = TextStyle(fontSize = measurable.fontSize),
+                        )
+                    }
+                }
+                // mapNotNull will automatically filter out the nulls
+                else -> null
+            }
+        },
+        stack = stack,
+        modifier = modifier
+    )
+}
\ No newline at end of file
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt
index 476271f..cba34eb 100644
--- a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt
+++ b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/LayoutViewModel.kt
@@ -21,9 +21,11 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import com.android.multiuser.widget.R
+import com.android.multiuser.widget.viewmodel.stacks.ActionStacks
+import com.android.multiuser.stacks.Stack
 import com.android.multiuser.stacks.ui.dp
 import com.android.multiuser.stacks.ui.inDp
-import com.android.multiuser.widget.viewmodel.stacks.ActionStacks
+import com.android.multiuser.widget.viewmodel.stacks.UserStacks
 
 class LayoutViewModel(
     var size: DpSize, val res: Resources, val userViewModel: UserViewModel,
@@ -35,12 +37,12 @@
     } else {
         Arrangement.Horizontal()
     }
-    var userStack: UserStack? = null
+    var userStack: Stack? = null
     val widgetPadding = res.dp(R.dimen.content_padding) * 2// top+bottom or left+right
 
     // with title bar visible we do not set top padding
     val titleBarHeight = res.dp(R.dimen.title_bar_height) - res.dp(R.dimen.content_padding)
-    val userStacks = UserStacks(res, userViewModel.name)
+    val userStacks = UserStacks(res, userViewModel)
 
     // TODO: replace with more stacks when user stacks are measurable
     val actionStack =  ActionStacks(res, actionViewModelList).TALL_HORIZONTAL
@@ -59,8 +61,8 @@
                 listOf(
                     userStacks.SMALL_VERTICAL,
                     userStacks.LARGE_VERTICAL
-                ).filter { it.height + verticalHeight + titleBarHeight <= (size.height) }
-                    .maxByOrNull { it.height }
+                ).filter { it.vertically.min.inDp(res.displayMetrics) + verticalHeight + titleBarHeight <= (size.height) }
+                    .maxByOrNull { it.vertically.min.inDp(res.displayMetrics) }
             if (userStack == null) { // title bar does not fit, check without title bar
                 (titleBarVisible as MutableStateFlow<Boolean>).value = false
                 // |   🙃    |
@@ -70,8 +72,8 @@
                     userStacks.SMALL_IMAGE_ONLY,
                     userStacks.SMALL_VERTICAL,
                     userStacks.LARGE_VERTICAL
-                ).filter { it.height + verticalHeight <= (size.height) }
-                    .maxByOrNull { it.height }
+                ).filter { it.vertically.min.inDp(res.displayMetrics) + verticalHeight <= (size.height) }
+                    .maxByOrNull { it.vertically.min.inDp(res.displayMetrics) }
                 if (userStack == null) { // user name does not fit, use small image only
                     // |  🔄 ➕  |
                 }
@@ -84,8 +86,8 @@
             // |   🙃   | 🔄 |
             // |  Name  | âž• |
             var horizontalWidth = actionStackWidth + widgetPadding
-            if (userStacks.LARGE_VERTICAL.height + titleBarHeight + widgetPadding <= (size.height)
-                && (userStacks.LARGE_VERTICAL.width + horizontalWidth <= size.width)) {
+            if (userStacks.LARGE_VERTICAL.vertically.min.inDp(res.displayMetrics) + titleBarHeight + widgetPadding <= (size.height)
+                && (userStacks.LARGE_VERTICAL.horizontally.min.inDp(res.displayMetrics) + horizontalWidth <= size.width)) {
                 (titleBarVisible as MutableStateFlow<Boolean>).value = true
                 userStack = userStacks.LARGE_VERTICAL
             } else { // Vertical layout does not fit, try all others
@@ -98,9 +100,9 @@
                     userStacks.SMALL_VERTICAL,
                     userStacks.SMALL_IMAGE_ONLY
                 ).filter {
-                        (it.height + titleBarHeight + widgetPadding <= (size.height))
-                                && (it.width + horizontalWidth <= size.width)
-                    }.maxByOrNull { it.width }
+                        (it.vertically.min.inDp(res.displayMetrics) + titleBarHeight + widgetPadding <= (size.height))
+                                && (it.horizontally.min.inDp(res.displayMetrics) + horizontalWidth <= size.width)
+                    }.maxByOrNull { it.horizontally.min.inDp(res.displayMetrics) }
                 (titleBarVisible as MutableStateFlow<Boolean>).value = true
                 if (userStack == null) {// Title bar does not fit, try without
                     // | 🙃 Name | 🔄  ➕ |        | 🙃 | 🔄  ➕ |
@@ -109,9 +111,9 @@
                         userStacks.LARGE_HORIZONTAL,
                         userStacks.SMALL_IMAGE_ONLY
                     ).filter {
-                            (it.height + widgetPadding <= (size.height))
-                                    && (it.width + horizontalWidth <= size.width)
-                        }.maxByOrNull { it.width }
+                            (it.vertically.min.inDp(res.displayMetrics) + widgetPadding <= (size.height))
+                                    && (it.horizontally.min.inDp(res.displayMetrics) + horizontalWidth <= size.width)
+                        }.maxByOrNull { it.horizontally.min.inDp(res.displayMetrics) }
                     (titleBarVisible as MutableStateFlow<Boolean>).value = false
                 }
                 if (userStack == null) {
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStack.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStack.kt
deleted file mode 100644
index dc0a58e..0000000
--- a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStack.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.multiuser.widget.viewmodel
-
-import androidx.compose.ui.unit.Dp
-import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
-
-/**
- * Single User Stack = person image + person name
- */
-class UserStack(val arrangement: Arrangement, val imageSize: Dp, val textMetric: TextMeasurable) {
-    val width = if(arrangement is Arrangement.Horizontal) {
-        imageSize + textMetric.width + arrangement.gutter + arrangement.padding
-    } else {
-        imageSize + arrangement.padding
-    }
-
-    val height = if(arrangement is Arrangement.Vertical) {
-        imageSize + textMetric.height + arrangement.gutter + arrangement.padding
-    } else {
-        imageSize + arrangement.padding
-    }
-
-    override fun toString(): String {
-        return if(arrangement is Arrangement.Vertical) {"V"} else {"H"} + " ${width} x ${height} imageSize: $imageSize gutter: ${arrangement.gutter}\ntext: ${textMetric.width}x${textMetric.height}"
-    }
-}
\ No newline at end of file
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStacks.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStacks.kt
deleted file mode 100644
index b200830..0000000
--- a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/UserStacks.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.multiuser.widget.viewmodel
-
-import android.content.res.Resources
-import androidx.compose.ui.unit.Dp
-import com.android.multiuser.widget.R
-import com.android.multiuser.stacks.ui.dp
-import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
-
-class UserStacks(val res: Resources,val text:String) {
-    private val NONE_TEXT = TextMeasurable(res)
-    private val MEDIUM_TEXT = TextMeasurable(res, R.dimen.user_text_min_size)
-    private val LARGE_TEXT = TextMeasurable(res, R.dimen.user_text_max_size)
-
-    val mediumImageSize: Dp = res.dp(R.dimen.user_image_min_size)
-    val largeImageSize = res.dp(R.dimen.user_image_max_size)
-    val verticalGutter = res.dp(R.dimen.user_vertical_gutter)
-    val verticalArrangement = Arrangement.Vertical(verticalGutter)
-    val horizontalGutter = res.dp(R.dimen.user_horizontal_gutter)
-    val horizontalArrangement = Arrangement.Horizontal(horizontalGutter)
-
-    val SMALL_IMAGE_ONLY =
-        UserStack(Arrangement.Vertical(verticalGutter), mediumImageSize, NONE_TEXT)
-    val SMALL_VERTICAL =
-        UserStack(Arrangement.Vertical(verticalGutter), mediumImageSize, MEDIUM_TEXT)
-    val LARGE_VERTICAL = UserStack(Arrangement.Vertical(verticalGutter), largeImageSize, LARGE_TEXT)
-    val SMALL_HORIZONTAL =
-        UserStack(Arrangement.Horizontal(horizontalGutter), mediumImageSize, MEDIUM_TEXT)
-    val LARGE_HORIZONTAL =
-        UserStack(Arrangement.Horizontal(horizontalGutter), largeImageSize, LARGE_TEXT)
-}
-
diff --git a/Widget/src/main/java/com/android/multiuser/widget/viewmodel/stacks/UserStacks.kt b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/stacks/UserStacks.kt
new file mode 100644
index 0000000..c270b99
--- /dev/null
+++ b/Widget/src/main/java/com/android/multiuser/widget/viewmodel/stacks/UserStacks.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.multiuser.widget.viewmodel.stacks
+
+import android.content.res.Resources
+import com.android.multiuser.stacks.FixedSquare
+import com.android.multiuser.stacks.Stack
+import com.android.multiuser.stacks.ui.px
+import com.android.multiuser.widget.R
+import com.android.multiuser.widget.viewmodel.UserViewModel
+import com.android.multiuser.widget.viewmodel.measurables.TextMeasurable
+
+class UserStacks(val res: Resources, val viewModel: UserViewModel) {
+    private val MEDIUM_TEXT = TextMeasurable(res, R.dimen.user_text_min_size, viewModel.name)
+    private val LARGE_TEXT = TextMeasurable(res, R.dimen.user_text_max_size, viewModel.name)
+
+    val mediumImageSize: Float = res.px(R.dimen.user_image_min_size)
+    val largeImageSize = res.px(R.dimen.user_image_max_size)
+    val verticalGutter = res.px(R.dimen.user_vertical_gutter)
+    val horizontalGutter = res.px(R.dimen.user_horizontal_gutter)
+
+    class UserImageSquare(val viewModel: UserViewModel, imageSize: Float) :FixedSquare (imageSize)
+
+    val mediumImageMeasurable = UserImageSquare(viewModel, mediumImageSize)
+    val largeImageMeasurable = UserImageSquare(viewModel, largeImageSize)
+
+    val SMALL_IMAGE_ONLY = Stack.Vertical(0f, list = listOf(mediumImageMeasurable))
+        .apply {
+            TAG = "USER Small Image Only"
+            horizontally.stretchToFill = true
+        }
+    val SMALL_VERTICAL = Stack.Vertical(verticalGutter, list = listOf(mediumImageMeasurable, MEDIUM_TEXT))
+        .apply {
+            horizontally.stretchToFill = true
+            TAG = "USER Small Vertical"
+        }
+    val LARGE_VERTICAL = Stack.Vertical(verticalGutter, list = listOf(largeImageMeasurable, LARGE_TEXT))
+        .apply {
+            TAG = "USER Large Vertical"
+            horizontally.stretchToFill = true
+        }
+    val SMALL_HORIZONTAL = Stack.Horizontal(horizontalGutter, list = listOf(mediumImageMeasurable, MEDIUM_TEXT)).apply { TAG = "USER Small Horizontal" }
+    val LARGE_HORIZONTAL = Stack.Horizontal(horizontalGutter, list = listOf(mediumImageMeasurable, LARGE_TEXT))
+        .apply {
+            TAG = "USER Large Horizontal"
+            horizontally.stretchToFill = true
+            vertically.stretchToFill = true
+        }
+}
+