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