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