Merge "Shape tab" into main
diff --git a/res/drawable/ic_apps_filled_24px.xml b/res/drawable/ic_apps_filled_24px.xml
new file mode 100644
index 0000000..af6fcef
--- /dev/null
+++ b/res/drawable/ic_apps_filled_24px.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 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.
+  ~
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+    <path android:fillColor="@android:color/white" android:pathData="M240,800Q207,800 183.5,776.5Q160,753 160,720Q160,687 183.5,663.5Q207,640 240,640Q273,640 296.5,663.5Q320,687 320,720Q320,753 296.5,776.5Q273,800 240,800ZM480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM720,800Q687,800 663.5,776.5Q640,753 640,720Q640,687 663.5,663.5Q687,640 720,640Q753,640 776.5,663.5Q800,687 800,720Q800,753 776.5,776.5Q753,800 720,800ZM240,560Q207,560 183.5,536.5Q160,513 160,480Q160,447 183.5,423.5Q207,400 240,400Q273,400 296.5,423.5Q320,447 320,480Q320,513 296.5,536.5Q273,560 240,560ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM720,560Q687,560 663.5,536.5Q640,513 640,480Q640,447 663.5,423.5Q687,400 720,400Q753,400 776.5,423.5Q800,447 800,480Q800,513 776.5,536.5Q753,560 720,560ZM240,320Q207,320 183.5,296.5Q160,273 160,240Q160,207 183.5,183.5Q207,160 240,160Q273,160 296.5,183.5Q320,207 320,240Q320,273 296.5,296.5Q273,320 240,320ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320ZM720,320Q687,320 663.5,296.5Q640,273 640,240Q640,207 663.5,183.5Q687,160 720,160Q753,160 776.5,183.5Q800,207 800,240Q800,273 776.5,296.5Q753,320 720,320Z"/>
+</vector>
\ No newline at end of file
diff --git a/res/drawable/ic_category_filled_24px.xml b/res/drawable/ic_category_filled_24px.xml
new file mode 100644
index 0000000..ae87e03
--- /dev/null
+++ b/res/drawable/ic_category_filled_24px.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 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.
+  ~
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" android:tint="?attr/colorControlNormal">
+    <path android:fillColor="@android:color/white" android:pathData="M260,440L480,80L700,440L260,440ZM700,880Q625,880 572.5,827.5Q520,775 520,700Q520,625 572.5,572.5Q625,520 700,520Q775,520 827.5,572.5Q880,625 880,700Q880,775 827.5,827.5Q775,880 700,880ZM120,860L120,540L440,540L440,860L120,860Z"/>
+</vector>
\ No newline at end of file
diff --git a/res/layout/floating_sheet_shape_grid.xml b/res/layout/floating_sheet_shape_grid.xml
index 01a7a89..4e2409b 100644
--- a/res/layout/floating_sheet_shape_grid.xml
+++ b/res/layout/floating_sheet_shape_grid.xml
@@ -21,34 +21,83 @@
     android:orientation="vertical">
 
     <FrameLayout
+        android:id="@+id/shape_grid_floating_sheet_content_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:paddingVertical="@dimen/floating_sheet_content_vertical_padding"
         android:background="@drawable/floating_sheet_content_background"
+        android:paddingVertical="@dimen/floating_sheet_content_vertical_padding"
+        android:orientation="vertical"
         android:clipToPadding="false"
         android:clipChildren="false">
 
-        <!--
-        This is just an invisible placeholder put in place so that the parent keeps its height
-        stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
-        layout logic to keep the size of the preview container stable as well, which bodes well
-        for setting up the SurfaceView for remote rendering without changing its size after the
-        content is loaded into the RecyclerView.
-
-        It's critical for any TextViews inside the included layout to have text.
-        -->
-        <include
-            layout="@layout/grid_option"
-            android:layout_width="wrap_content"
+        <FrameLayout
+            android:id="@+id/app_shape_container"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:visibility="invisible" />
-
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@id/options"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center_horizontal"
             android:clipToPadding="false"
-            android:clipChildren="false" />
+            android:clipChildren="false">
+
+            <!--
+            This is just an invisible placeholder put in place so that the parent keeps its height
+            stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
+            layout logic to keep the size of the preview container stable as well, which bodes well
+            for setting up the SurfaceView for remote rendering without changing its size after the
+            content is loaded into the RecyclerView.
+
+            It's critical for any TextViews inside the included layout to have text.
+            -->
+            <include
+                layout="@layout/shape_option"
+                android:layout_width="64dp"
+                android:layout_height="64dp"
+                android:visibility="invisible" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/shape_options"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:clipToPadding="false"
+                android:clipChildren="false" />
+        </FrameLayout>
+
+        <FrameLayout
+            android:id="@+id/app_grid_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:clipToPadding="false"
+            android:clipChildren="false">
+
+            <!--
+            This is just an invisible placeholder put in place so that the parent keeps its height
+            stable as the RecyclerView updates from 0 items to N items. Keeping it stable allows the
+            layout logic to keep the size of the preview container stable as well, which bodes well
+            for setting up the SurfaceView for remote rendering without changing its size after the
+            content is loaded into the RecyclerView.
+
+            It's critical for any TextViews inside the included layout to have text.
+            -->
+            <include
+                layout="@layout/grid_option"
+                android:id="@+id/invisible_grid_option"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:visibility="invisible"/>
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/grid_options"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:clipToPadding="false"
+                android:clipChildren="false"
+                android:layout_gravity="center_horizontal" />
+        </FrameLayout>
     </FrameLayout>
+
+    <com.android.wallpaper.picker.customization.ui.view.FloatingToolbar
+        android:id="@+id/floating_toolbar"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:layout_marginVertical="@dimen/floating_sheet_tab_toolbar_vertical_margin" />
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout/shape_option.xml b/res/layout/shape_option.xml
new file mode 100644
index 0000000..78201d8
--- /dev/null
+++ b/res/layout/shape_option.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  ~
+  -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="64dp"
+    android:layout_height="64dp"
+    android:clipChildren="false">
+
+    <ImageView
+        android:id="@id/background"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/option_item_background"
+        android:importantForAccessibility="no" />
+
+    <ImageView
+        android:id="@id/foreground"
+        android:layout_width="40dp"
+        android:layout_height="40dp"
+        android:layout_gravity="center" />
+</FrameLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index cc6ce20..07b2800 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -104,6 +104,10 @@
         grid layouts for the home screen. [CHAR LIMIT=15] -->
     <string name="shape_and_grid_title">App shape &amp; layout</string>
 
+    <!-- Tab title that switch to app grid customization section, where people can customization
+        the grid layout of the apps -->
+    <string name="grid_layout">Layout</string>
+
     <!-- Label for a button that allows the user to apply the currently selected Theme.
         [CHAR LIMIT=20] -->
     <string name="apply_theme_btn">Apply</string>
diff --git a/src/com/android/wallpaper/customization/ui/binder/ShapeGridFloatingSheetBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ShapeGridFloatingSheetBinder.kt
index 2fafd41..4f199f8 100644
--- a/src/com/android/wallpaper/customization/ui/binder/ShapeGridFloatingSheetBinder.kt
+++ b/src/com/android/wallpaper/customization/ui/binder/ShapeGridFloatingSheetBinder.kt
@@ -16,9 +16,13 @@
 
 package com.android.wallpaper.customization.ui.binder
 
+import android.animation.ValueAnimator
 import android.content.Context
 import android.view.View
+import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
 import android.widget.ImageView
+import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
@@ -28,29 +32,141 @@
 import com.android.customization.picker.common.ui.view.SingleRowListItemSpacing
 import com.android.customization.picker.grid.ui.binder.GridIconViewBinder
 import com.android.customization.picker.grid.ui.viewmodel.GridIconViewModel
-import com.android.wallpaper.R
-import com.android.wallpaper.customization.ui.viewmodel.ShapeGridPickerViewModel
+import com.android.themepicker.R
+import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.APP_SHAPE_GRID
+import com.android.wallpaper.customization.ui.viewmodel.ShapeGridFloatingSheetHeightsViewModel
+import com.android.wallpaper.customization.ui.viewmodel.ShapeGridPickerViewModel.Tab.GRID
+import com.android.wallpaper.customization.ui.viewmodel.ShapeGridPickerViewModel.Tab.SHAPE
+import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel
+import com.android.wallpaper.picker.customization.ui.view.FloatingToolbar
+import com.android.wallpaper.picker.customization.ui.view.adapter.FloatingToolbarTabAdapter
+import com.android.wallpaper.picker.customization.ui.viewmodel.ColorUpdateViewModel
 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
 import com.android.wallpaper.picker.option.ui.binder.OptionItemBinder
+import java.lang.ref.WeakReference
 import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.launch
 
 object ShapeGridFloatingSheetBinder {
+    private const val ANIMATION_DURATION = 200L
+
+    private val _shapeGridFloatingSheetHeights:
+        MutableStateFlow<ShapeGridFloatingSheetHeightsViewModel?> =
+        MutableStateFlow(null)
+    private val shapeGridFloatingSheetHeights: Flow<ShapeGridFloatingSheetHeightsViewModel> =
+        _shapeGridFloatingSheetHeights.asStateFlow().filterNotNull().filter {
+            it.shapeContentHeight != null && it.gridContentHeight != null
+        }
 
     fun bind(
         view: View,
-        viewModel: ShapeGridPickerViewModel,
+        optionsViewModel: ThemePickerCustomizationOptionsViewModel,
+        colorUpdateViewModel: ColorUpdateViewModel,
         lifecycleOwner: LifecycleOwner,
         backgroundDispatcher: CoroutineDispatcher,
     ) {
+        val floatingSheetContentVerticalPadding =
+            view.resources.getDimensionPixelSize(R.dimen.floating_sheet_content_vertical_padding)
+        val viewModel = optionsViewModel.shapeGridPickerViewModel
+
+        val tabs = view.requireViewById<FloatingToolbar>(R.id.floating_toolbar)
+        val tabAdapter =
+            FloatingToolbarTabAdapter(
+                    colorUpdateViewModel = WeakReference(colorUpdateViewModel),
+                    shouldAnimateColor = { optionsViewModel.selectedOption.value == APP_SHAPE_GRID },
+                )
+                .also { tabs.setAdapter(it) }
+        val floatingSheetContainer =
+            view.requireViewById<ViewGroup>(R.id.shape_grid_floating_sheet_content_container)
+
+        val shapeContent = view.requireViewById<View>(R.id.app_shape_container)
+
+        val gridContent = view.requireViewById<View>(R.id.app_grid_container)
         val adapter = createOptionItemAdapter(view.context, lifecycleOwner, backgroundDispatcher)
         val gridOptionList =
-            view.requireViewById<RecyclerView>(R.id.options).also {
+            view.requireViewById<RecyclerView>(R.id.grid_options).also {
                 it.initGridOptionList(view.context, adapter)
             }
 
+        // Get the shape content height when it is ready
+        shapeContent.viewTreeObserver.addOnGlobalLayoutListener(
+            object : OnGlobalLayoutListener {
+                override fun onGlobalLayout() {
+                    _shapeGridFloatingSheetHeights.value =
+                        _shapeGridFloatingSheetHeights.value?.copy(
+                            shapeContentHeight = shapeContent.height
+                        )
+                            ?: ShapeGridFloatingSheetHeightsViewModel(
+                                shapeContentHeight = shapeContent.height
+                            )
+                    shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
+                }
+            }
+        )
+        // Get the grid content height when it is ready
+        gridContent.viewTreeObserver.addOnGlobalLayoutListener(
+            object : OnGlobalLayoutListener {
+                override fun onGlobalLayout() {
+                    // Make sure the recycler view height is the same as its parent. It's possible
+                    // that the recycler view is shorter than expected.
+                    gridOptionList.layoutParams =
+                        gridOptionList.layoutParams.apply { height = gridContent.height }
+                    _shapeGridFloatingSheetHeights.value =
+                        _shapeGridFloatingSheetHeights.value?.copy(
+                            gridContentHeight = gridContent.height
+                        )
+                            ?: ShapeGridFloatingSheetHeightsViewModel(
+                                gridContentHeight = shapeContent.height
+                            )
+                    shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this)
+                }
+            }
+        )
+
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch { viewModel.tabs.collect { tabAdapter.submitList(it) } }
+
+                launch {
+                    combine(shapeGridFloatingSheetHeights, viewModel.selectedTab) {
+                            heights,
+                            selectedTab ->
+                            heights to selectedTab
+                        }
+                        .collect { (heights, selectedTab) ->
+                            val (shapeContentHeight, gridContentHeight) = heights
+                            shapeContentHeight ?: return@collect
+                            gridContentHeight ?: return@collect
+                            val targetHeight =
+                                when (selectedTab) {
+                                    SHAPE -> shapeContentHeight
+                                    GRID -> gridContentHeight
+                                } + floatingSheetContentVerticalPadding * 2
+
+                            ValueAnimator.ofInt(floatingSheetContainer.height, targetHeight)
+                                .apply {
+                                    addUpdateListener { valueAnimator ->
+                                        val value = valueAnimator.animatedValue as Int
+                                        floatingSheetContainer.layoutParams =
+                                            floatingSheetContainer.layoutParams.apply {
+                                                height = value
+                                            }
+                                    }
+                                    duration = ANIMATION_DURATION
+                                }
+                                .start()
+
+                            shapeContent.isVisible = selectedTab == SHAPE
+                            gridContent.isVisible = selectedTab == GRID
+                        }
+                }
+
                 launch {
                     viewModel.optionItems.collect { options ->
                         adapter.setItems(options) {
@@ -72,13 +188,15 @@
         backgroundDispatcher: CoroutineDispatcher,
     ): OptionItemAdapter<GridIconViewModel> =
         OptionItemAdapter(
-            layoutResourceId = com.android.themepicker.R.layout.grid_option,
+            layoutResourceId = R.layout.grid_option,
             lifecycleOwner = lifecycleOwner,
             backgroundDispatcher = backgroundDispatcher,
             foregroundTintSpec =
                 OptionItemBinder.TintSpec(
-                    selectedColor = context.getColor(R.color.system_on_surface),
-                    unselectedColor = context.getColor(R.color.system_on_surface),
+                    selectedColor =
+                        context.getColor(com.android.wallpaper.R.color.system_on_surface),
+                    unselectedColor =
+                        context.getColor(com.android.wallpaper.R.color.system_on_surface),
                 ),
             bindIcon = { foregroundView: View, gridIcon: GridIconViewModel ->
                 val imageView = foregroundView as? ImageView
diff --git a/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
index 7792734..a92471c 100644
--- a/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
+++ b/src/com/android/wallpaper/customization/ui/binder/ThemePickerCustomizationOptionBinder.kt
@@ -222,7 +222,8 @@
             ?.let {
                 ShapeGridFloatingSheetBinder.bind(
                     it,
-                    optionsViewModel.shapeGridPickerViewModel,
+                    optionsViewModel,
+                    colorUpdateViewModel,
                     lifecycleOwner,
                     Dispatchers.IO,
                 )
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/ShapeGridFloatingSheetHeightsViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/ShapeGridFloatingSheetHeightsViewModel.kt
new file mode 100644
index 0000000..237ab36
--- /dev/null
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/ShapeGridFloatingSheetHeightsViewModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.wallpaper.customization.ui.viewmodel
+
+data class ShapeGridFloatingSheetHeightsViewModel(
+    val shapeContentHeight: Int? = null,
+    val gridContentHeight: Int? = null,
+)
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/ShapeGridPickerViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/ShapeGridPickerViewModel.kt
index 78b01af..b6f48a1 100644
--- a/src/com/android/wallpaper/customization/ui/viewmodel/ShapeGridPickerViewModel.kt
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/ShapeGridPickerViewModel.kt
@@ -22,7 +22,11 @@
 import com.android.customization.model.grid.GridOptionModel
 import com.android.customization.picker.grid.domain.interactor.GridInteractor2
 import com.android.customization.picker.grid.ui.viewmodel.GridIconViewModel
+import com.android.themepicker.R
+import com.android.wallpaper.customization.ui.viewmodel.ClockPickerViewModel.Tab
+import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
+import com.android.wallpaper.picker.customization.ui.viewmodel.FloatingToolbarTabViewModel
 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
@@ -33,6 +37,8 @@
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
@@ -46,6 +52,41 @@
     interactor: GridInteractor2,
     @Assisted private val viewModelScope: CoroutineScope,
 ) {
+
+    enum class Tab {
+        SHAPE,
+        GRID,
+    }
+
+    // Tabs
+    private val _selectedTab = MutableStateFlow(Tab.SHAPE)
+    val selectedTab: StateFlow<Tab> = _selectedTab.asStateFlow()
+    val tabs: Flow<List<FloatingToolbarTabViewModel>> =
+        _selectedTab.map {
+            listOf(
+                FloatingToolbarTabViewModel(
+                    Icon.Resource(
+                        res = R.drawable.ic_category_filled_24px,
+                        contentDescription = Text.Resource(R.string.preview_name_shape),
+                    ),
+                    context.getString(R.string.preview_name_shape),
+                    it == Tab.SHAPE,
+                ) {
+                    _selectedTab.value = Tab.SHAPE
+                },
+                FloatingToolbarTabViewModel(
+                    Icon.Resource(
+                        res = R.drawable.ic_apps_filled_24px,
+                        contentDescription = Text.Resource(R.string.grid_layout),
+                    ),
+                    context.getString(R.string.grid_layout),
+                    it == Tab.GRID,
+                ) {
+                    _selectedTab.value = Tab.GRID
+                },
+            )
+        }
+
     // The currently-set system grid option
     val selectedGridOption =
         interactor.selectedGridOption
@@ -63,6 +104,7 @@
 
     fun resetPreview() {
         overridingGridOptionKey.value = null
+        _selectedTab.value = Tab.SHAPE
     }
 
     val optionItems: Flow<List<OptionItemViewModel<GridIconViewModel>>> =