-
Notifications
You must be signed in to change notification settings - Fork 85
Expand file tree
/
Copy pathTabIndex.swift
More file actions
382 lines (340 loc) · 14.1 KB
/
Copy pathTabIndex.swift
File metadata and controls
382 lines (340 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
//
// TabIndex.swift
//
// Copyright © 2022 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/**
* Represents a tab position in one of the 2 sections
* of the tab bar view (pinned or unpinned tabs).
*
* The associated value represents the position in a respective tab bar section.
*/
enum TabIndex: Equatable, Comparable {
case pinned(Int), unpinned(Int)
/**
* Returns tab position within its respective section.
*
* - Note: the name follows `IndexPath.item` pattern.
*/
var item: Int {
switch self {
case .pinned(let index), .unpinned(let index):
return index
}
}
var isPinnedTab: Bool {
if case .pinned = self {
return true
}
return false
}
var isUnpinnedTab: Bool {
!isPinnedTab
}
/**
* Creates a new tab index by incrementing position by 1.
*
* No bounds checking is performed.
*/
func makeNext() -> TabIndex {
switch self {
case let .pinned(index):
return .pinned(index + 1)
case let .unpinned(index):
return .unpinned(index + 1)
}
}
func makeNextUnpinned() -> TabIndex {
switch self {
case .pinned:
return .unpinned(0)
case let .unpinned(index):
return .unpinned(index + 1)
}
}
func isInSameSection(as other: TabIndex) -> Bool {
switch (self, other) {
case (.pinned, .unpinned), (.unpinned, .pinned):
return false
default:
return true
}
}
static func < (_ lhs: TabIndex, _ rhs: TabIndex) -> Bool {
switch (lhs, rhs) {
case (.pinned, .unpinned):
return true
case (.unpinned, .pinned):
return false
default:
return lhs.item < rhs.item
}
}
}
// MARK: - Tab Collection View Model index manipulation
extension TabIndex {
@MainActor
static func first(in viewModel: TabCollectionViewModel) -> TabIndex {
if viewModel.pinnedTabsCount > 0 {
return .pinned(0)
}
assert(viewModel.tabsCount > 0, "There must be at least 1 tab, pinned or unpinned")
return .unpinned(0)
}
@MainActor
static func last(in viewModel: TabCollectionViewModel) -> TabIndex {
if viewModel.tabsCount > 0 {
return .unpinned(viewModel.tabsCount - 1)
}
assert(viewModel.pinnedTabsCount > 0, "There must be at least 1 tab, pinned or unpinned")
return .pinned(viewModel.pinnedTabsCount - 1)
}
@MainActor
static func at(_ position: Int, in viewModel: TabCollectionViewModel) -> TabIndex {
.pinned(position).sanitized(for: viewModel)
}
@MainActor
func next(in viewModel: TabCollectionViewModel) -> TabIndex {
switch self {
case .pinned(let index):
if index >= viewModel.pinnedTabsCount - 1 {
return viewModel.tabsCount > 0 ? .unpinned(0) : .first(in: viewModel)
}
return .pinned(index + 1)
case .unpinned(let index):
if index >= viewModel.tabCollection.tabs.count - 1 {
return .first(in: viewModel)
}
return .unpinned(index + 1)
}
}
@MainActor
func previous(in viewModel: TabCollectionViewModel) -> TabIndex {
switch self {
case .pinned(let index):
if index == 0 {
return viewModel.tabsCount > 0 ? .unpinned(viewModel.tabsCount - 1) : .pinned(viewModel.pinnedTabsCount - 1)
}
return .pinned(index - 1)
case .unpinned(let index):
if index == 0 {
return viewModel.pinnedTabsCount > 0 ? .pinned(viewModel.pinnedTabsCount - 1) : .unpinned(viewModel.tabsCount - 1)
}
return .unpinned(index - 1)
}
}
@MainActor
func sanitized(for viewModel: TabCollectionViewModel) -> TabIndex {
switch self {
case .pinned(let index):
if index >= viewModel.pinnedTabsCount && viewModel.tabsCount > 0 {
return .unpinned(min(index - viewModel.pinnedTabsCount, viewModel.tabsCount - 1))
}
if index < 0 {
return viewModel.pinnedTabsCount > 0 ? .pinned(0) : .unpinned(0)
}
return .pinned(max(0, min(index, viewModel.pinnedTabsCount - 1)))
case .unpinned(let index):
if index >= 0 && viewModel.tabsCount == 0 {
return .pinned(viewModel.pinnedTabsCount - 1)
}
return .unpinned(max(0, min(index, viewModel.tabsCount - 1)))
}
}
// MARK: - Logic when closing a tab
/// When closing an active tab, the following rules will be used to find the appropriate tab to activate. Rules will be evaluated top to bottom and evaluation stops once a tab has been found:
/// 1. If this tab has a parent (i.e. has been opened via "Open In New Tab"):
/// a. Try to find the next tab with the same parent tab
/// b. Try to find the previous tab with the same parent tab
/// c. Try to find the parent tab
/// 2. Try to find the next tab that has the closing tab as it's parent
/// 3. Try to find the previous tab that has the closing tab as it's parent
/// 4. Try to find the previously active tab.
/// a. The previously active tab is only remembered until the user moves to an existing tab or creates new tabs.
/// 5. Try to find the next tab
/// 6. Try to find the previous tab
@MainActor
func calculateSelectedTabIndexAfterClosing(for viewModel: TabCollectionViewModel, removedTab: AnyTab) -> TabIndex? {
if let parentTabId = removedTab.parentTabID {
if let nextTabWithSameParent = findNextTabWithSameParent(for: viewModel, parentTabId: parentTabId) {
return nextTabWithSameParent
}
if let previousTabWithSameParent = findPreviousTabWithSameParent(for: viewModel, parentTabId: parentTabId) {
return previousTabWithSameParent
}
if let parentTab = removedTab.parentTab, let parentTabIndex = viewModel.indexInAllTabs(of: parentTab) {
return parentTabIndex
}
}
return findNewSelectionIndexWithoutParent(for: viewModel, removedTab: removedTab)
}
private enum SearchDirection {
case next, previous
}
/// - Parameters:
/// - viewModel: The `TabCollectionViewModel` to search within
/// - parentTabId: The ID of the parent tab to find the next tab for
/// - Returns: The `TabIndex` of the next tab with the same parent, if found
@MainActor
private func findNextTabWithSameParent(for viewModel: TabCollectionViewModel, parentTabId: String) -> TabIndex? {
return findTabWithParent(for: viewModel, parentTabId: parentTabId, direction: .next)
}
/// - Parameters:
/// - viewModel: The `TabCollectionViewModel` to search within
/// - parentTabId: The ID of the parent tab to find the next tab for
/// - Returns: The `TabIndex` of the previous tab with the same parent, if found
@MainActor
private func findPreviousTabWithSameParent(for viewModel: TabCollectionViewModel, parentTabId: String) -> TabIndex? {
return findTabWithParent(for: viewModel, parentTabId: parentTabId, direction: .previous)
}
/// Finds the next or previous tab that has the given parent tab
///
/// - Parameters:
/// - viewModel: The `TabCollectionViewModel` to search within
/// - parentTabId: The ID of the parent tab to find the next tab for
/// - direction: The direction to search in (.next or .previous)
/// - Returns: The `TabIndex` of the first tab found with the given parent, if any
@MainActor
private func findTabWithParent(for viewModel: TabCollectionViewModel, parentTabId: String, direction: SearchDirection) -> TabIndex? {
var currentIndex = self
if let viewModelTab = viewModel.tabViewModel(at: currentIndex), viewModelTab.tab.parentTabID == parentTabId {
return currentIndex
}
while let nextIndex = direction == .next ? currentIndex.getRighteousTab(for: viewModel) : currentIndex.getLeftTab(for: viewModel) {
if let viewModelTab = viewModel.tabViewModel(at: nextIndex), viewModelTab.tab.parentTabID == parentTabId {
return nextIndex
}
currentIndex = nextIndex
}
return nil
}
/// Finds the new tab index to select after closing a tab that doesn't have a parent
///
/// The rules are:
/// 1. Try to find the next tab that has the closed tab as its parent
/// 2. Try to find the previous tab that has the closed tab as its parent
/// 3. Try to find the recently active tab
/// a. The previously active tab is only remembered until the user moves to an existing tab or creates new tabs.
/// 4. Try to find the current tab index (if it still exists)
/// 5. Try to find the next tab
/// 6. Try to find the previous tab
@MainActor
private func findNewSelectionIndexWithoutParent(for viewModel: TabCollectionViewModel, removedTab: AnyTab) -> TabIndex? {
if let nextTabWithRemovedTabAsParent = findNextTabWithRemovedTabAsParent(for: viewModel, removedTabID: removedTab.uuid) {
return nextTabWithRemovedTabAsParent
}
if let previousTabWithRemovedTabAsParent = findPreviousTabWithRemovedTabAsParent(for: viewModel, removedTabID: removedTab.uuid) {
return previousTabWithRemovedTabAsParent
}
if let recentlyClosedTabIndex = viewModel.getPreviouslyActiveTab() {
return recentlyClosedTabIndex
}
/// Given of the nature of when this method is called, the tab index being manipulated (self) could be the tab to the right.
/// So we need to check for self to see if it exists, if it exists, we return it.
/// Use `tabBarViewModel(at:)` so unloaded tabs are not skipped — they materialize on selection.
if viewModel.tabBarViewModel(at: self) != nil {
return self
}
if let nextIndex = getRighteousTab(for: viewModel) {
return nextIndex
}
if let previousIndex = getLeftTab(for: viewModel) {
return previousIndex
}
return nil
}
/// Finds the next tab that has the given removed tab as its parent
///
/// - Parameters:
/// - viewModel: The `TabCollectionViewModel` to search within
/// - removedTab: The tab that was removed
/// - Returns: The `TabIndex` of the next tab with the given removed tab as its parent, if found
@MainActor
private func findNextTabWithRemovedTabAsParent(for viewModel: TabCollectionViewModel, removedTabID: TabIdentifier) -> TabIndex? {
var currentIndex = self
if let viewModelTab = viewModel.tabViewModel(at: currentIndex), viewModelTab.tab.parentTabID == removedTabID {
return currentIndex
}
while let nextIndex = currentIndex.getRighteousTab(for: viewModel) {
if let viewModelTab = viewModel.tabViewModel(at: nextIndex), viewModelTab.tab.parentTabID == removedTabID {
return nextIndex
}
currentIndex = nextIndex
}
return nil
}
/// Finds the previous tab that has the given removed tab as its parent
///
/// - Parameters:
/// - viewModel: The `TabCollectionViewModel` to search within
/// - removedTab: The tab that was removed
/// - Returns: The `TabIndex` of the previous tab with the given removed tab as its parent, if found
@MainActor
private func findPreviousTabWithRemovedTabAsParent(for viewModel: TabCollectionViewModel, removedTabID: TabIdentifier) -> TabIndex? {
var currentIndex = self
while let previousIndex = currentIndex.getLeftTab(for: viewModel) {
if let viewModelTab = viewModel.tabViewModel(at: previousIndex), viewModelTab.tab.parentTabID == removedTabID {
return previousIndex
}
currentIndex = previousIndex
}
return nil
}
/// Gets the tab to the right of the current tab index
///
/// If the current tab index is the last one, returns `nil`
@MainActor
private func getRighteousTab(for viewModel: TabCollectionViewModel) -> TabIndex? {
switch self {
case .pinned(let index):
if index >= viewModel.pinnedTabsCount - 1 {
return viewModel.tabsCount > 0 ? .unpinned(0) : nil
}
return .pinned(index + 1)
case .unpinned(let index):
if index >= viewModel.tabCollection.tabs.count - 1 {
return nil
}
return .unpinned(index + 1)
}
}
/// Gets the tab to the left of the current tab index
///
/// If the current tab index is the first one, returns `nil`
@MainActor
private func getLeftTab(for viewModel: TabCollectionViewModel) -> TabIndex? {
switch self {
case .pinned(let index):
if index == 0 {
return nil
}
return .pinned(index - 1)
case .unpinned(let index):
if index == 0 {
return viewModel.pinnedTabsCount > 0 ? .pinned(viewModel.pinnedTabsCount - 1) : nil
}
return .unpinned(index - 1)
}
}
}
private extension TabCollectionViewModel {
var tabsCount: Int {
tabCollection.tabs.count
}
var pinnedTabsCount: Int {
pinnedTabsCollection?.tabs.count ?? 0
}
}