Listless/Helpers/ItemListTypes.swift (8000B)
1import Foundation
2
3enum FocusField: Hashable {
4 case item(UUID)
5 case scrollView
6}
7
8let pageNavigationSize = 10
9
10enum DragState: Equatable {
11 case idle
12 case dragging(id: UUID, order: [UUID])
13}
14
15enum DraftItemPlacement: Equatable {
16 case prepend
17 case append
18}
19
20struct FocusStateData {
21 var focusedField: FocusField?
22 var pendingFocus: FocusField?
23
24
25
26
27 private(set) var selectedItemIDs: Set<UUID> = []
28
29
30
31 var anchorItemID: UUID?
32
33
34
35 private(set) var cursorItemID: UUID?
36
37
38
39 private(set) var inactiveSelections: Set<UUID> = []
40
41
42
43
44
45 var selectedItemID: UUID? {
46 get { cursorItemID }
47 set {
48 anchorItemID = newValue
49 cursorItemID = newValue
50 selectedItemIDs = newValue.map { Set([$0]) } ?? []
51 inactiveSelections = []
52 }
53 }
54
55 func isItemSelected(_ id: UUID) -> Bool {
56 selectedItemIDs.contains(id)
57 }
58
59 var hasMultipleSelection: Bool {
60 selectedItemIDs.count > 1
61 }
62
63
64
65 mutating func selectAll(displayOrder: [UUID]) {
66 guard !displayOrder.isEmpty else { return }
67 anchorItemID = displayOrder.first
68 cursorItemID = displayOrder.last
69 selectedItemIDs = Set(displayOrder)
70 inactiveSelections = []
71 }
72
73
74
75
76
77
78
79 mutating func toggleSelection(itemID: UUID, displayOrder: [UUID]) {
80 guard let toggledIndex = displayOrder.firstIndex(of: itemID) else { return }
81
82 let wasSelected = selectedItemIDs.contains(itemID)
83 if wasSelected {
84 selectedItemIDs.remove(itemID)
85 } else {
86 selectedItemIDs.insert(itemID)
87 }
88
89 guard !selectedItemIDs.isEmpty else {
90 anchorItemID = nil
91 cursorItemID = nil
92 inactiveSelections = []
93 return
94 }
95
96
97 anchorItemID =
98 toggledIndex + 1 < displayOrder.count
99 ? displayOrder[toggledIndex + 1]
100 : displayOrder[toggledIndex]
101
102 if wasSelected {
103
104
105 if cursorItemID == nil {
106 cursorItemID = anchorItemID
107 }
108 } else {
109
110
111 cursorItemID = anchorItemID
112 }
113
114 recomputeInactiveSelections(displayOrder: displayOrder)
115 }
116
117
118
119
120
121 mutating func extendSelection(to targetID: UUID, displayOrder: [UUID]) {
122 guard let anchorID = anchorItemID,
123 let anchorIndex = displayOrder.firstIndex(of: anchorID),
124 let targetIndex = displayOrder.firstIndex(of: targetID)
125 else {
126 return
127 }
128 let lo = min(anchorIndex, targetIndex)
129 let hi = max(anchorIndex, targetIndex)
130 let activeRange = Set(displayOrder[lo...hi])
131 selectedItemIDs = inactiveSelections.union(activeRange)
132 cursorItemID = targetID
133 mergeAdjacentInactiveSelections(displayOrder: displayOrder)
134 }
135
136
137
138 mutating func pruneDeletedItems(displayOrder: [UUID]) {
139 let valid = Set(displayOrder)
140 selectedItemIDs.formIntersection(valid)
141 inactiveSelections.formIntersection(valid)
142 if let id = anchorItemID, !valid.contains(id) {
143 anchorItemID = nil
144 }
145 if let id = cursorItemID, !valid.contains(id) {
146 cursorItemID = nil
147 }
148
149 if cursorItemID == nil, !selectedItemIDs.isEmpty {
150 cursorItemID = anchorItemID ?? selectedItemIDs.first
151 }
152 if selectedItemIDs.isEmpty {
153 anchorItemID = nil
154 cursorItemID = nil
155 inactiveSelections = []
156 }
157 }
158
159
160
161
162
163 private mutating func recomputeInactiveSelections(displayOrder: [UUID]) {
164 guard let anchorID = anchorItemID, let cursorID = cursorItemID,
165 let anchorIndex = displayOrder.firstIndex(of: anchorID),
166 let cursorIndex = displayOrder.firstIndex(of: cursorID)
167 else {
168 inactiveSelections = []
169 return
170 }
171 let lo = min(anchorIndex, cursorIndex)
172 let hi = max(anchorIndex, cursorIndex)
173 let activeRange = Set(displayOrder[lo...hi])
174 inactiveSelections = selectedItemIDs.subtracting(activeRange)
175 }
176
177
178
179
180
181 private mutating func mergeAdjacentInactiveSelections(displayOrder: [UUID]) {
182 guard !inactiveSelections.isEmpty,
183 let anchorID = anchorItemID,
184 let anchorIndex = displayOrder.firstIndex(of: anchorID),
185 let cursorID = cursorItemID,
186 let cursorIndex = displayOrder.firstIndex(of: cursorID)
187 else {
188 return
189 }
190
191 var lo = min(anchorIndex, cursorIndex)
192 var hi = max(anchorIndex, cursorIndex)
193 var mergedIDs: Set<UUID> = []
194 var changed = true
195
196 while changed {
197 changed = false
198 for inactiveID in inactiveSelections where !mergedIDs.contains(inactiveID) {
199 guard let idx = displayOrder.firstIndex(of: inactiveID) else { continue }
200 if idx == lo - 1 || idx == hi + 1 || (idx >= lo && idx <= hi) {
201 if idx < lo { lo = idx }
202 if idx > hi { hi = idx }
203 mergedIDs.insert(inactiveID)
204 changed = true
205 }
206 }
207 }
208
209 guard !mergedIDs.isEmpty else { return }
210
211 inactiveSelections.subtract(mergedIDs)
212
213 if cursorIndex <= anchorIndex {
214 cursorItemID = displayOrder[lo]
215 } else {
216 cursorItemID = displayOrder[hi]
217 }
218
219 selectedItemIDs = inactiveSelections.union(Set(displayOrder[lo...hi]))
220 }
221}
222
223let draftPrependRowID = UUID()
224let draftAppendRowID = UUID()