• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 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 com.android.systemui.qs.panels.ui.compose.selection
18 
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.Stable
21 import androidx.compose.runtime.getValue
22 import androidx.compose.runtime.mutableStateOf
23 import androidx.compose.runtime.remember
24 import androidx.compose.runtime.setValue
25 import androidx.compose.ui.Modifier
26 import androidx.compose.ui.input.pointer.pointerInput
27 import com.android.systemui.common.ui.compose.gestures.detectEagerTapGestures
28 import com.android.systemui.qs.pipeline.shared.TileSpec
29 import kotlinx.coroutines.delay
30 
31 /** Creates the state of the current selected tile that is remembered across compositions. */
32 @Composable
rememberSelectionStatenull33 fun rememberSelectionState(): MutableSelectionState {
34     return remember { MutableSelectionState() }
35 }
36 
37 /** Holds the state of the current selection. */
38 class MutableSelectionState {
39     /** The [TileSpec] of a tile is selected, null if not. */
40     var selection by mutableStateOf<TileSpec?>(null)
41         private set
42 
43     /**
44      * Whether the current selection is in placement mode or not.
45      *
46      * A tile in placement mode can be positioned by tapping at the desired location in the grid.
47      */
48     var placementEnabled by mutableStateOf(false)
49         private set
50 
51     /** Latest event from coming from placement mode. */
52     var placementEvent by mutableStateOf<PlacementEvent?>(null)
53 
54     val selected: Boolean
55         get() = selection != null
56 
selectnull57     fun select(tileSpec: TileSpec) {
58         selection = tileSpec
59     }
60 
unSelectnull61     fun unSelect() {
62         selection = null
63         exitPlacementMode()
64     }
65 
66     /** Selects [tileSpec] and enable placement mode. */
enterPlacementModenull67     fun enterPlacementMode(tileSpec: TileSpec) {
68         selection = tileSpec
69         placementEnabled = true
70     }
71 
72     /** Disable placement mode but maintains current selection. */
exitPlacementModenull73     private fun exitPlacementMode() {
74         placementEnabled = false
75     }
76 
togglePlacementModenull77     fun togglePlacementMode(tileSpec: TileSpec) {
78         if (placementEnabled) exitPlacementMode() else enterPlacementMode(tileSpec)
79     }
80 
tileStateFornull81     suspend fun tileStateFor(
82         tileSpec: TileSpec,
83         previousState: TileState,
84         canShowRemovalBadge: Boolean,
85     ): TileState {
86         return when {
87             placementEnabled && selection == tileSpec -> TileState.Placeable
88             placementEnabled -> TileState.GreyedOut
89             selection == tileSpec -> {
90                 if (previousState == TileState.None && canShowRemovalBadge) {
91                     // The tile decoration is None if a tile is newly composed OR the removal
92                     // badge can't be shown.
93                     // For newly composed and selected tiles, such as dragged tiles or moved
94                     // tiles from resizing, introduce a short delay. This avoids clipping issues
95                     // on the border and resizing handle, as well as letting the selection
96                     // animation play correctly.
97                     delay(250)
98                 }
99                 TileState.Selected
100             }
101             canShowRemovalBadge -> TileState.Removable
102             else -> TileState.None
103         }
104     }
105 
106     /**
107      * Tap callback on a tile.
108      *
109      * Tiles can be selected and placed using placement mode.
110      */
onTapnull111     fun onTap(tileSpec: TileSpec) {
112         when {
113             placementEnabled && selection == tileSpec -> {
114                 exitPlacementMode()
115             }
116             placementEnabled -> {
117                 selection?.let { placementEvent = PlacementEvent.PlaceToTileSpec(it, tileSpec) }
118                 exitPlacementMode()
119             }
120             selection == tileSpec -> {
121                 unSelect()
122             }
123             else -> {
124                 select(tileSpec)
125             }
126         }
127     }
128 
129     /**
130      * Tap on a position.
131      *
132      * Use on grid items not associated with a [TileSpec], such as a spacer. Spacers can't be
133      * selected, but selections can be moved to their position.
134      */
onTapnull135     fun onTap(index: Int) {
136         when {
137             placementEnabled -> {
138                 selection?.let { placementEvent = PlacementEvent.PlaceToIndex(it, index) }
139                 exitPlacementMode()
140             }
141             selected -> {
142                 unSelect()
143             }
144         }
145     }
146 }
147 
148 // Not using data classes here as distinct placement events may have the same moving spec and target
149 @Stable
150 sealed interface PlacementEvent {
151     val movingSpec: TileSpec
152 
153     /** Placement event corresponding to [movingSpec] moving to [targetSpec]'s position */
154     class PlaceToTileSpec(override val movingSpec: TileSpec, val targetSpec: TileSpec) :
155         PlacementEvent
156 
157     /** Placement event corresponding to [movingSpec] moving to [targetIndex] */
158     class PlaceToIndex(override val movingSpec: TileSpec, val targetIndex: Int) : PlacementEvent
159 }
160 
161 /**
162  * Listens for click events on selectable tiles.
163  *
164  * Use this on current tiles as they can be selected.
165  *
166  * @param tileSpec the [TileSpec] of the tile this modifier is applied to
167  * @param selectionState the [MutableSelectionState] representing the grid's selection
168  */
169 @Composable
selectableTilenull170 fun Modifier.selectableTile(tileSpec: TileSpec, selectionState: MutableSelectionState): Modifier {
171     return pointerInput(Unit) {
172         detectEagerTapGestures(
173             doubleTapEnabled = {
174                 // Double tap enabled if where not in placement mode already
175                 !selectionState.placementEnabled
176             },
177             onDoubleTap = { selectionState.enterPlacementMode(tileSpec) },
178             onTap = { selectionState.onTap(tileSpec) },
179         )
180     }
181 }
182