• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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
18 
19 import androidx.compose.runtime.Composable
20 import androidx.compose.runtime.getValue
21 import androidx.compose.runtime.mutableStateOf
22 import androidx.compose.runtime.remember
23 import androidx.compose.runtime.setValue
24 import androidx.compose.runtime.snapshots.SnapshotStateList
25 import androidx.compose.runtime.toMutableStateList
26 import androidx.compose.ui.geometry.Offset
27 import com.android.systemui.qs.panels.shared.model.SizedTile
28 import com.android.systemui.qs.panels.ui.compose.selection.PlacementEvent
29 import com.android.systemui.qs.panels.ui.model.GridCell
30 import com.android.systemui.qs.panels.ui.model.TileGridCell
31 import com.android.systemui.qs.panels.ui.model.toGridCells
32 import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
33 import com.android.systemui.qs.pipeline.shared.TileSpec
34 
35 /**
36  * Creates the edit tile list state that is remembered across compositions.
37  *
38  * Changes to the tiles or columns will recreate the state.
39  */
40 @Composable
41 fun rememberEditListState(
42     tiles: List<SizedTile<EditTileViewModel>>,
43     columns: Int,
44     largeTilesSpan: Int,
45 ): EditTileListState {
46     return remember(tiles, columns) { EditTileListState(tiles, columns, largeTilesSpan) }
47 }
48 
49 /** Holds the temporary state of the tile list during a drag movement where we move tiles around. */
50 class EditTileListState(
51     tiles: List<SizedTile<EditTileViewModel>>,
52     private val columns: Int,
53     private val largeTilesSpan: Int,
54 ) : DragAndDropState {
55     override var draggedCell by mutableStateOf<SizedTile<EditTileViewModel>?>(null)
56         private set
57 
58     override var draggedPosition by mutableStateOf(Offset.Unspecified)
59         private set
60 
61     override var dragType by mutableStateOf<DragType?>(null)
62         private set
63 
64     // A dragged cell can be removed if it was added in the drag movement OR if it's marked as
65     // removable
66     override val isDraggedCellRemovable: Boolean
67         get() = dragType == DragType.Add || draggedCell?.tile?.isRemovable ?: false
68 
69     override val dragInProgress: Boolean
70         get() = draggedCell != null
71 
72     private val _tiles: SnapshotStateList<GridCell> =
73         tiles.toGridCells(columns).toMutableStateList()
74     val tiles: List<GridCell>
75         get() = _tiles.toList()
76 
tileSpecsnull77     fun tileSpecs(): List<TileSpec> {
78         return _tiles.filterIsInstance<TileGridCell>().map { it.tile.tileSpec }
79     }
80 
indexOfnull81     private fun indexOf(tileSpec: TileSpec): Int {
82         return _tiles.indexOfFirst { it is TileGridCell && it.tile.tileSpec == tileSpec }
83     }
84 
isRemovablenull85     fun isRemovable(tileSpec: TileSpec): Boolean {
86         return _tiles.find {
87             it is TileGridCell && it.tile.tileSpec == tileSpec && it.tile.isRemovable
88         } != null
89     }
90 
91     /** Resize the tile corresponding to the [TileSpec] to [toIcon] */
resizeTilenull92     fun resizeTile(tileSpec: TileSpec, toIcon: Boolean) {
93         val fromIndex = indexOf(tileSpec)
94         if (fromIndex != INVALID_INDEX) {
95             val cell = _tiles[fromIndex] as TileGridCell
96 
97             if (cell.isIcon == toIcon) return
98 
99             _tiles.removeAt(fromIndex)
100             _tiles.add(fromIndex, cell.copy(width = if (toIcon) 1 else largeTilesSpan))
101             regenerateGrid(fromIndex)
102         }
103     }
104 
isMovingnull105     override fun isMoving(tileSpec: TileSpec): Boolean {
106         return draggedCell?.let { it.tile.tileSpec == tileSpec } ?: false
107     }
108 
onStartednull109     override fun onStarted(cell: SizedTile<EditTileViewModel>, dragType: DragType) {
110         draggedCell = cell
111         this.dragType = dragType
112     }
113 
onTargetingnull114     override fun onTargeting(target: Int, insertAfter: Boolean) {
115         val draggedTile = draggedCell ?: return
116 
117         val fromIndex = indexOf(draggedTile.tile.tileSpec)
118         if (fromIndex == target) {
119             return
120         }
121 
122         val insertionIndex = if (insertAfter) target + 1 else target
123         if (fromIndex != INVALID_INDEX) {
124             val cell = _tiles.removeAt(fromIndex)
125             regenerateGrid()
126             _tiles.add(insertionIndex.coerceIn(0, _tiles.size), cell)
127         } else {
128             // Add the tile with a temporary row/col which will get reassigned when
129             // regenerating spacers
130             _tiles.add(insertionIndex.coerceIn(0, _tiles.size), TileGridCell(draggedTile, 0, 0))
131         }
132 
133         regenerateGrid()
134     }
135 
onMovednull136     override fun onMoved(offset: Offset) {
137         draggedPosition = offset
138     }
139 
movedOutOfBoundsnull140     override fun movedOutOfBounds() {
141         val draggedTile = draggedCell ?: return
142 
143         _tiles.removeIf { cell ->
144             cell is TileGridCell && cell.tile.tileSpec == draggedTile.tile.tileSpec
145         }
146         draggedPosition = Offset.Unspecified
147 
148         // Regenerate spacers without the dragged tile
149         regenerateGrid()
150     }
151 
onDropnull152     override fun onDrop() {
153         draggedCell = null
154         draggedPosition = Offset.Unspecified
155         dragType = null
156 
157         // Remove the spacers
158         regenerateGrid()
159     }
160 
161     /**
162      * Return the appropriate index to move the tile to for the placement [event]
163      *
164      * The grid includes spacers. As a result, indexes from the grid need to be translated to the
165      * corresponding index from [currentTileSpecs].
166      */
targetIndexForPlacementnull167     fun targetIndexForPlacement(event: PlacementEvent): Int {
168         val currentTileSpecs = tileSpecs()
169         return when (event) {
170             is PlacementEvent.PlaceToTileSpec -> {
171                 currentTileSpecs.indexOf(event.targetSpec)
172             }
173             is PlacementEvent.PlaceToIndex -> {
174                 if (event.targetIndex >= _tiles.size) {
175                     currentTileSpecs.size
176                 } else if (event.targetIndex <= 0) {
177                     0
178                 } else {
179                     // The index may point to a spacer, so first find the first tile located
180                     // after index, then use its position as a target
181                     val targetTile =
182                         _tiles.subList(event.targetIndex, _tiles.size).firstOrNull {
183                             it is TileGridCell
184                         } as? TileGridCell
185 
186                     if (targetTile == null) {
187                         currentTileSpecs.size
188                     } else {
189                         val targetIndex = currentTileSpecs.indexOf(targetTile.tile.tileSpec)
190                         val fromIndex = currentTileSpecs.indexOf(event.movingSpec)
191                         if (fromIndex < targetIndex) targetIndex - 1 else targetIndex
192                     }
193                 }
194             }
195         }
196     }
197 
198     /** Regenerate the list of [GridCell] with their new potential rows */
regenerateGridnull199     private fun regenerateGrid() {
200         _tiles.filterIsInstance<TileGridCell>().toGridCells(columns).let {
201             _tiles.clear()
202             _tiles.addAll(it)
203         }
204     }
205 
206     /**
207      * Regenerate the list of [GridCell] with their new potential rows from [fromIndex], leaving
208      * cells before that untouched.
209      */
regenerateGridnull210     private fun regenerateGrid(fromIndex: Int) {
211         val fromRow = _tiles[fromIndex].row
212         val (pre, post) = _tiles.partition { it.row < fromRow }
213         post.filterIsInstance<TileGridCell>().toGridCells(columns, startingRow = fromRow).let {
214             _tiles.clear()
215             _tiles.addAll(pre)
216             _tiles.addAll(it)
217         }
218     }
219 
220     companion object {
221         const val INVALID_INDEX = -1
222     }
223 }
224