diff --git a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme
index 8720f4dbb..c6868c5f6 100644
--- a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme
+++ b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme
@@ -77,18 +77,6 @@
-
-
-
-
-
-
-
-
diff --git a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift
index 51cd6650b..1cc2cca5f 100644
--- a/CodeEdit/Features/ActivityViewer/ActivityViewer.swift
+++ b/CodeEdit/Features/ActivityViewer/ActivityViewer.swift
@@ -13,30 +13,25 @@ struct ActivityViewer: View {
var colorScheme
@ObservedObject var taskNotificationHandler: TaskNotificationHandler
+
var body: some View {
- HStack {
- HStack(spacing: 0) {
- // This is only a placeholder for the task popover(coming in the next pr)
- Rectangle()
- .frame(height: 22)
- .hidden()
+ HStack(spacing: 0) {
+ // This is only a placeholder for the task popover(coming in the next pr)
+ Rectangle()
+ .frame(height: 22)
+ .hidden()
+ .fixedSize()
- Spacer()
+ Spacer(minLength: 0)
- TaskNotificationView(taskNotificationHandler: taskNotificationHandler)
- }
- .padding(.horizontal, 10)
- .background {
- if colorScheme == .dark {
- RoundedRectangle(cornerRadius: 5)
- .opacity(0.10)
- } else {
- RoundedRectangle(cornerRadius: 5)
- .opacity(0.1)
- }
- }
- .frame(minWidth: 200, idealWidth: 680)
+ TaskNotificationView(taskNotificationHandler: taskNotificationHandler)
+ .fixedSize()
+ }
+ .fixedSize(horizontal: false, vertical: false)
+ .padding(.horizontal, 10)
+ .background {
+ RoundedRectangle(cornerRadius: 5)
+ .opacity(0.1)
}
- .frame(height: 22)
}
}
diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift
index 9f270f061..af83e56ac 100644
--- a/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift
+++ b/CodeEdit/Features/Documents/Controllers/CodeEditSplitViewController.swift
@@ -8,45 +8,29 @@
import Cocoa
import SwiftUI
-struct CodeEditSplitView: NSViewControllerRepresentable {
- let controller: NSSplitViewController
-
- func makeNSViewController(context: Context) -> NSSplitViewController {
- controller
- }
-
- func updateNSViewController(_ nsViewController: NSSplitViewController, context: Context) {}
-}
-
-private extension CGFloat {
+final class CodeEditSplitViewController: NSSplitViewController {
+ static let minSidebarWidth: CGFloat = 242
+ static let maxSnapWidth: CGFloat = snapWidth + 10
static let snapWidth: CGFloat = 272
-
static let minSnapWidth: CGFloat = snapWidth - 10
- static let maxSnapWidth: CGFloat = snapWidth + 10
-}
-final class CodeEditSplitViewController: NSSplitViewController {
private var workspace: WorkspaceDocument
- private var setWidthFromState = false
- private var viewIsReady = false
-
- // Properties
- private(set) var isSnapped: Bool = false {
- willSet {
- if newValue, newValue != isSnapped && viewIsReady {
- feedbackPerformer.perform(.alignment, performanceTime: .now)
- }
- }
- }
-
- // Dependencies
- private let feedbackPerformer: NSHapticFeedbackPerformer
+ private var navigatorViewModel: NavigatorSidebarViewModel
+ private weak var windowRef: NSWindow?
+ private unowned var hapticPerformer: NSHapticFeedbackPerformer
// MARK: - Initialization
- init(workspace: WorkspaceDocument, feedbackPerformer: NSHapticFeedbackPerformer) {
+ init(
+ workspace: WorkspaceDocument,
+ navigatorViewModel: NavigatorSidebarViewModel,
+ windowRef: NSWindow,
+ hapticPerformer: NSHapticFeedbackPerformer = NSHapticFeedbackManager.defaultPerformer
+ ) {
self.workspace = workspace
- self.feedbackPerformer = feedbackPerformer
+ self.navigatorViewModel = navigatorViewModel
+ self.windowRef = windowRef
+ self.hapticPerformer = hapticPerformer
super.init(nibName: nil, bundle: nil)
}
@@ -55,13 +39,67 @@ final class CodeEditSplitViewController: NSSplitViewController {
fatalError("init(coder:) has not been implemented")
}
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ guard let windowRef else {
+ // swiftlint:disable:next line_length
+ assertionFailure("No WindowRef found, not initialized properly or the window was dereferenced and the controller was not.")
+ return
+ }
+
+ splitView.translatesAutoresizingMaskIntoConstraints = false
+
+ let settingsView = SettingsInjector {
+ NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel)
+ .environmentObject(workspace)
+ .environmentObject(workspace.editorManager)
+ }
+
+ let navigator = NSSplitViewItem(sidebarWithViewController: NSHostingController(rootView: settingsView))
+ navigator.titlebarSeparatorStyle = .none
+ navigator.isSpringLoaded = true
+ navigator.minimumThickness = Self.minSidebarWidth
+ navigator.collapseBehavior = .useConstraints
+
+ addSplitViewItem(navigator)
+
+ let workspaceView = SettingsInjector {
+ WindowObserver(window: windowRef) {
+ WorkspaceView()
+ .environmentObject(workspace)
+ .environmentObject(workspace.editorManager)
+ .environmentObject(workspace.statusBarViewModel)
+ .environmentObject(workspace.utilityAreaModel)
+ }
+ }
+
+ let mainContent = NSSplitViewItem(viewController: NSHostingController(rootView: workspaceView))
+ mainContent.titlebarSeparatorStyle = .line
+ mainContent.minimumThickness = 200
+
+ addSplitViewItem(mainContent)
+
+ let inspectorView = SettingsInjector {
+ InspectorAreaView(viewModel: InspectorAreaViewModel())
+ .environmentObject(workspace)
+ .environmentObject(workspace.editorManager)
+ }
+
+ let inspector = NSSplitViewItem(inspectorWithViewController: NSHostingController(rootView: inspectorView))
+ inspector.titlebarSeparatorStyle = .none
+ inspector.minimumThickness = Self.minSidebarWidth
+ inspector.maximumThickness = .greatestFiniteMagnitude
+ inspector.collapseBehavior = .useConstraints
+ inspector.isSpringLoaded = true
+
+ addSplitViewItem(inspector)
+ }
+
override func viewWillAppear() {
super.viewWillAppear()
- viewIsReady = false
- let width = workspace.getFromWorkspaceState(.splitViewWidth) as? CGFloat
- splitView.setPosition(width ?? .snapWidth, ofDividerAt: .zero)
- setWidthFromState = true
+ let navigatorWidth = workspace.getFromWorkspaceState(.splitViewWidth) as? CGFloat
+ splitView.setPosition(navigatorWidth ?? Self.minSidebarWidth, ofDividerAt: 0)
if let firstSplitView = splitViewItems.first {
firstSplitView.isCollapsed = workspace.getFromWorkspaceState(
@@ -74,50 +112,64 @@ final class CodeEditSplitViewController: NSSplitViewController {
.inspectorCollapsed
) as? Bool ?? true
}
-
- self.insertToolbarItemIfNeeded()
- }
-
- override func viewDidAppear() {
- viewIsReady = true
- hideInspectorToolbarBackground()
}
// MARK: - NSSplitViewDelegate
+ /// Perform the spring loaded navigator splits.
+ /// - Note: This could be removed. The only additional functionality this provides over using just the
+ /// `NSSplitViewItem.isSpringLoaded` & `NSSplitViewItem.minimumThickness` is the haptic feedback we add.
+ /// - Parameters:
+ /// - splitView: The split view to use.
+ /// - proposedPosition: The proposed drag position.
+ /// - dividerIndex: The index of the divider being dragged.
+ /// - Returns: The position to move the divider to.
override func splitView(
_ splitView: NSSplitView,
constrainSplitPosition proposedPosition: CGFloat,
ofSubviewAt dividerIndex: Int
) -> CGFloat {
- if dividerIndex == 0 {
+ switch dividerIndex {
+ case 0:
// Navigator
- if (CGFloat.minSnapWidth...CGFloat.maxSnapWidth).contains(proposedPosition) {
- isSnapped = true
- return .snapWidth
+ if (Self.minSnapWidth...Self.maxSnapWidth).contains(proposedPosition) {
+ return Self.snapWidth
+ } else if proposedPosition <= Self.minSidebarWidth / 2 {
+ hapticCollapse(splitViewItems.first, collapseAction: true)
+ return 0
} else {
- isSnapped = false
- if proposedPosition <= CodeEditWindowController.minSidebarWidth / 2 {
- splitViewItems.first?.isCollapsed = true
- return 0
- }
- return max(CodeEditWindowController.minSidebarWidth, proposedPosition)
+ hapticCollapse(splitViewItems.first, collapseAction: false)
+ return max(Self.minSidebarWidth, proposedPosition)
}
- } else if dividerIndex == 1 {
+ case 1:
let proposedWidth = view.frame.width - proposedPosition
- if proposedWidth <= CodeEditWindowController.minSidebarWidth / 2 {
- splitViewItems.last?.isCollapsed = true
- removeToolbarItemIfNeeded()
+ if proposedWidth <= Self.minSidebarWidth / 2 {
+ hapticCollapse(splitViewItems.last, collapseAction: true)
return proposedPosition
+ } else {
+ hapticCollapse(splitViewItems.last, collapseAction: false)
+ return min(view.frame.width - Self.minSidebarWidth, proposedPosition)
}
- splitViewItems.last?.isCollapsed = false
- insertToolbarItemIfNeeded()
- return min(view.frame.width - CodeEditWindowController.minSidebarWidth, proposedPosition)
+ default:
+ return proposedPosition
+ }
+ }
+
+ /// Performs a haptic feedback while collapsing or revealing a split item.
+ /// If the item was not previously in the new intended state, a haptic `.alignment` feedback is sent.
+ /// - Parameters:
+ /// - item: The item to collapse or reveal
+ /// - collapseAction: Whether or not to collapse the item. Set to true to collapse it.
+ private func hapticCollapse(_ item: NSSplitViewItem?, collapseAction: Bool) {
+ if item?.isCollapsed == !collapseAction {
+ hapticPerformer.perform(.alignment, performanceTime: .now)
}
- return proposedPosition
+ item?.isCollapsed = collapseAction
}
+ /// Save the width of the inspector and navigator between sessions.
override func splitViewDidResizeSubviews(_ notification: Notification) {
+ super.splitViewDidResizeSubviews(notification)
guard let resizedDivider = notification.userInfo?["NSSplitViewDividerIndex"] as? Int else {
return
}
@@ -125,7 +177,7 @@ final class CodeEditSplitViewController: NSSplitViewController {
if resizedDivider == 0 {
let panel = splitView.subviews[0]
let width = panel.frame.size.width
- if width > 0 && setWidthFromState {
+ if width > 0 {
workspace.addToWorkspaceState(key: .splitViewWidth, value: width)
}
}
@@ -138,36 +190,4 @@ final class CodeEditSplitViewController: NSSplitViewController {
func saveInspectorCollapsedState(isCollapsed: Bool) {
workspace.addToWorkspaceState(key: .inspectorCollapsed, value: isCollapsed)
}
-
- /// Quick fix for list tracking separator needing to be added again after closing,
- /// then opening the inspector with a drag.
- private func insertToolbarItemIfNeeded() {
- guard !(
- view.window?.toolbar?.items.contains(where: { $0.itemIdentifier == .itemListTrackingSeparator }) ?? true
- ) else {
- return
- }
- view.window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4)
- }
-
- /// Quick fix for list tracking separator needing to be removed after closing the inspector with a drag
- private func removeToolbarItemIfNeeded() {
- guard let index = view.window?.toolbar?.items.firstIndex(
- where: { $0.itemIdentifier == .itemListTrackingSeparator }
- ) else {
- return
- }
- view.window?.toolbar?.removeItem(at: index)
- }
-
- func hideInspectorToolbarBackground() {
- let controller = self.view.window?.perform(Selector(("titlebarViewController"))).takeUnretainedValue()
- if let controller = controller as? NSViewController {
- let effectViewCount = controller.view.subviews.filter { $0 is NSVisualEffectView }.count
- guard effectViewCount > 2 else { return }
- if let view = controller.view.subviews[0] as? NSVisualEffectView {
- view.isHidden = true
- }
- }
- }
}
diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift
index ba7b06b3e..f9acda1f1 100644
--- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift
+++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController+Toolbar.swift
@@ -118,12 +118,23 @@ extension CodeEditWindowController {
case .activityViewer:
let toolbarItem = NSToolbarItem(itemIdentifier: NSToolbarItem.Identifier.activityViewer)
toolbarItem.visibilityPriority = .user
- toolbarItem.view = NSHostingView(
+ let view = NSHostingView(
rootView: ActivityViewer(
taskNotificationHandler: taskNotificationHandler
)
)
+ let weakWidth = view.widthAnchor.constraint(equalToConstant: 650)
+ weakWidth.priority = .defaultLow
+ let strongWidth = view.widthAnchor.constraint(greaterThanOrEqualToConstant: 200)
+ strongWidth.priority = .defaultHigh
+
+ NSLayoutConstraint.activate([
+ weakWidth,
+ strongWidth
+ ])
+
+ toolbarItem.view = view
return toolbarItem
default:
return NSToolbarItem(itemIdentifier: itemIdentifier)
diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift
index 7da694c2e..0b4066585 100644
--- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift
+++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowController.swift
@@ -10,8 +10,6 @@ import SwiftUI
import Combine
final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, ObservableObject {
- static let minSidebarWidth: CGFloat = 242
-
@Published var navigatorCollapsed = false
@Published var inspectorCollapsed = false
@Published var toolbarCollapsed = false
@@ -43,11 +41,20 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
self.workspaceSettings = CEWorkspaceSettings(workspaceDocument: workspace)
setupSplitView(with: workspace)
- let view = CodeEditSplitView(controller: splitViewController).ignoresSafeArea()
-
+ // Previous:
// An NSHostingController is used, so the root viewController of the window is a SwiftUI-managed one.
// This allows us to use some SwiftUI features, like focusedSceneObject.
- contentViewController = NSHostingController(rootView: view)
+ // -----
+ // let view = CodeEditSplitView(controller: splitViewController).ignoresSafeArea()
+ // contentViewController = NSHostingController(rootView: view)
+ // -----
+ //
+ // New:
+ // The previous decision led to a very jank split controller mechanism because SwiftUI's layout system is not
+ // very compatible with AppKit's when it comes to the inspector/navigator toolbar & split view system.
+ // -----
+ contentViewController = splitViewController
+ // -----
observers = [
splitViewController.splitViewItems.first!.observe(\.isCollapsed, changeHandler: { [weak self] item, _ in
@@ -70,60 +77,18 @@ final class CodeEditWindowController: NSWindowController, NSToolbarDelegate, Obs
}
private func setupSplitView(with workspace: WorkspaceDocument) {
- let feedbackPerformer = NSHapticFeedbackManager.defaultPerformer
- let splitVC = CodeEditSplitViewController(workspace: workspace, feedbackPerformer: feedbackPerformer)
-
- let navigatorViewModel = NavigatorSidebarViewModel()
- navigatorSidebarViewModel = navigatorViewModel
-
- let settingsView = SettingsInjector {
- NavigatorAreaView(workspace: workspace, viewModel: navigatorViewModel)
- .environmentObject(workspace)
- .environmentObject(workspace.editorManager)
+ guard let window else {
+ assertionFailure("No window found for this controller. Cannot set up content.")
+ return
}
- let navigator = NSSplitViewItem(
- sidebarWithViewController: NSHostingController(rootView: settingsView)
+ let navigatorModel = NavigatorSidebarViewModel()
+ navigatorSidebarViewModel = navigatorModel
+ self.splitViewController = CodeEditSplitViewController(
+ workspace: workspace,
+ navigatorViewModel: navigatorModel,
+ windowRef: window
)
- navigator.titlebarSeparatorStyle = .none
- navigator.minimumThickness = Self.minSidebarWidth
- navigator.collapseBehavior = .useConstraints
-
- splitVC.addSplitViewItem(navigator)
-
- let workspaceView = SettingsInjector {
- WindowObserver(window: window!) {
- WorkspaceView()
- .environmentObject(workspace)
- .environmentObject(workspace.editorManager)
- .environmentObject(workspace.statusBarViewModel)
- .environmentObject(workspace.utilityAreaModel)
- }
- }
-
- let mainContent = NSSplitViewItem(viewController: NSHostingController(rootView: workspaceView))
- mainContent.titlebarSeparatorStyle = .line
- mainContent.holdingPriority = .init(50)
-
- splitVC.addSplitViewItem(mainContent)
-
- let inspectorView = SettingsInjector {
- InspectorAreaView(viewModel: InspectorAreaViewModel())
- .environmentObject(workspace)
- .environmentObject(workspace.editorManager)
- }
-
- let inspector = NSSplitViewItem(viewController: NSHostingController(rootView: inspectorView))
- inspector.titlebarSeparatorStyle = .none
- inspector.minimumThickness = Self.minSidebarWidth
- inspector.isCollapsed = true
- inspector.canCollapse = true
- inspector.collapseBehavior = .useConstraints
- inspector.isSpringLoaded = true
-
- splitVC.addSplitViewItem(inspector)
-
- self.splitViewController = splitVC
self.listenToDocumentEdited(workspace: workspace)
}
diff --git a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift
index f3bea3c5f..f1a858615 100644
--- a/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift
+++ b/CodeEdit/Features/Documents/Controllers/CodeEditWindowControllerExtensions.swift
@@ -20,25 +20,16 @@ extension CodeEditWindowController {
@objc
func toggleLastPanel() {
- guard let lastSplitView = splitViewController.splitViewItems.last else { return }
-
- if let toolbar = window?.toolbar,
- lastSplitView.isCollapsed,
- !toolbar.items.map(\.itemIdentifier).contains(.itemListTrackingSeparator) {
- window?.toolbar?.insertItem(withItemIdentifier: .itemListTrackingSeparator, at: 4)
+ guard let lastSplitView = splitViewController.splitViewItems.last,
+ let codeEditSplitVC = splitViewController as? CodeEditSplitViewController else {
+ return
}
+
NSAnimationContext.runAnimationGroup { _ in
lastSplitView.animator().isCollapsed.toggle()
- } completionHandler: { [weak self] in
- if lastSplitView.isCollapsed {
- self?.window?.animator().toolbar?.removeItem(at: 4)
- }
}
- if let codeEditSplitVC = splitViewController as? CodeEditSplitViewController {
- codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed)
- codeEditSplitVC.hideInspectorToolbarBackground()
- }
+ codeEditSplitVC.saveInspectorCollapsedState(isCollapsed: lastSplitView.isCollapsed)
}
/// These are example items that added as commands to command palette
diff --git a/CodeEdit/Features/Documents/WorkspaceDocument.swift b/CodeEdit/Features/Documents/WorkspaceDocument.swift
index c9fccc70d..71a187b39 100644
--- a/CodeEdit/Features/Documents/WorkspaceDocument.swift
+++ b/CodeEdit/Features/Documents/WorkspaceDocument.swift
@@ -80,14 +80,16 @@ final class WorkspaceDocument: NSDocument, ObservableObject, NSToolbarDelegate {
backing: .buffered,
defer: false
)
+ // Note For anyone hoping to switch back to a Root-SwiftUI window:
+ // See Commit 0200c87 for more details and to see what was previously here.
+ // -----
// Setting the "min size" like this is hacky, but SwiftUI overrides the contentRect and
// any of the built-in window size functions & autosave stuff. So we have to set it like this.
// SwiftUI also ignores this value, so it just manages to set the initial window size. *Hopefully* this
// is fixed in the future.
+ // ----
if let rectString = getFromWorkspaceState(.workspaceWindowSize) as? String {
- window.minSize = NSRectFromString(rectString).size
- } else {
- window.minSize = .init(width: 1400, height: 900)
+ window.setContentSize(NSRectFromString(rectString).size)
}
let windowController = CodeEditWindowController(
window: window,
diff --git a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift
index 2708e784e..b46742f64 100644
--- a/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift
+++ b/CodeEdit/Features/InspectorArea/Views/InspectorAreaView.swift
@@ -51,14 +51,6 @@ struct InspectorAreaView: View {
NoSelectionInspectorView()
}
}
- .clipShape(Rectangle())
- .frame(
- minWidth: CodeEditWindowController.minSidebarWidth,
- idealWidth: 300,
- minHeight: 0,
- maxHeight: .infinity,
- alignment: .top
- )
.safeAreaInset(edge: .trailing, spacing: 0) {
if sidebarPosition == .side {
HStack(spacing: 0) {
diff --git a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift
index 6cce3e50f..c116d7a19 100644
--- a/CodeEditTests/Features/Documents/DocumentsUnitTests.swift
+++ b/CodeEditTests/Features/Documents/DocumentsUnitTests.swift
@@ -12,17 +12,26 @@ final class DocumentsUnitTests: XCTestCase {
// Properties
private var splitViewController: CodeEditSplitViewController!
private var hapticFeedbackPerformerMock: NSHapticFeedbackPerformerMock!
+ private var navigatorViewModel: NavigatorSidebarViewModel!
+ private var window: NSWindow!
// MARK: - Lifecycle
override func setUp() {
super.setUp()
- hapticFeedbackPerformerMock = .init()
- splitViewController = .init(workspace: WorkspaceDocument(), feedbackPerformer: hapticFeedbackPerformerMock)
+ hapticFeedbackPerformerMock = NSHapticFeedbackPerformerMock()
+ navigatorViewModel = .init()
+ window = NSWindow()
+ splitViewController = .init(
+ workspace: WorkspaceDocument(),
+ navigatorViewModel: navigatorViewModel,
+ windowRef: window,
+ hapticPerformer: hapticFeedbackPerformerMock
+ )
+ splitViewController.viewDidLoad()
}
override func tearDown() {
- hapticFeedbackPerformerMock = nil
splitViewController = nil
super.tearDown()
}
@@ -30,83 +39,103 @@ final class DocumentsUnitTests: XCTestCase {
// MARK: - Tests
func testSplitViewControllerSnappedWhenWidthInAppropriateRange() {
- // Given
- let position = (260...280).randomElement() ?? .zero
-
- // When
- let result = splitViewController.splitView(
- splitViewController.splitView,
- constrainSplitPosition: .init(position),
- ofSubviewAt: .zero
- )
+ for _ in 0..<10 {
+ // Given
+ let position = CGFloat.random(
+ in: (CodeEditSplitViewController.minSnapWidth...CodeEditSplitViewController.maxSnapWidth)
+ )
- // Then
- XCTAssertEqual(result, 272)
+ // When
+ let result = splitViewController.splitView(
+ splitViewController.splitView,
+ constrainSplitPosition: .init(position),
+ ofSubviewAt: .zero
+ )
+
+ // Then
+ XCTAssertEqual(result, CodeEditSplitViewController.snapWidth)
+ }
}
func testSplitViewControllerStopSnappedWhenWidthIsLowerAppropriateRange() {
- // Given
- // 242 is the minimum width of the sidebar
- let position = (242..<260).randomElement() ?? .zero
-
- // When
- let result = splitViewController.splitView(
- splitViewController.splitView,
- constrainSplitPosition: .init(position),
- ofSubviewAt: .zero
- )
+ for _ in 0..<10 {
+ // Given
+ let position = CGFloat.random(in: 0..<(CodeEditSplitViewController.minSidebarWidth / 2))
- // Then
- XCTAssertEqual(result, .init(position))
+ // When
+ let result = splitViewController.splitView(
+ splitViewController.splitView,
+ constrainSplitPosition: .init(position),
+ ofSubviewAt: .zero
+ )
+
+ // Then
+ XCTAssertEqual(result, .zero)
+ }
}
func testSplitViewControllerStopSnappedWhenWidthIsHigherAppropriateRange() {
- // Given
- let position = (281...500).randomElement() ?? .zero
-
- // When
- let result = splitViewController.splitView(
- splitViewController.splitView,
- constrainSplitPosition: .init(position),
- ofSubviewAt: .zero
- )
+ for _ in 0..<10 {
+ // Given
+ let position = CGFloat.random(in: (CodeEditSplitViewController.maxSnapWidth...500))
- // Then
- XCTAssertEqual(result, .init(position))
- }
-
- func testSplitViewControllerProducedHapticFeedback() {
- // Given
- let position = (260...280).randomElement() ?? .zero
-
- // When
- _ = splitViewController.splitView(
- splitViewController.splitView,
- constrainSplitPosition: .init(position),
- ofSubviewAt: .zero
- )
+ // When
+ let result = splitViewController.splitView(
+ splitViewController.splitView,
+ constrainSplitPosition: .init(position),
+ ofSubviewAt: .zero
+ )
- // Then
- XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform)
- XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1)
+ // Then
+ XCTAssertEqual(result, .init(position))
+ }
}
- func testSplitViewControllerProducedHapticFeedbackOnceWhenPlentyChangesOccur() {
- // Given
- let firstPosition = (260...280).randomElement() ?? .zero
- let secondPosition = 300
+ // Test moving from collapsed to uncollapsed makes a haptic.
+ func testSplitViewControllerProducedHapticFeedback() {
+ for _ in 0..<10 {
+ // Given
+ splitViewController.splitViewItems.first?.isCollapsed = true
+ let position = CGFloat.random(
+ in: (CodeEditSplitViewController.minSidebarWidth / 2)...CodeEditSplitViewController.minSidebarWidth
+ )
- // When
- [firstPosition, secondPosition].forEach { position in
+ // When
_ = splitViewController.splitView(
splitViewController.splitView,
constrainSplitPosition: .init(position),
ofSubviewAt: .zero
)
+
+ // Then
+ XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform)
+ XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1)
+ hapticFeedbackPerformerMock.reset()
}
+ }
- // Then
- XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform)
- XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1)
+ func testSplitViewControllerProducedHapticFeedbackOnceWhenPlentyChangesOccur() {
+ for _ in 0..<10 {
+ // Given
+ splitViewController.splitViewItems.first?.isCollapsed = true
+ let firstPosition = CGFloat.random(in: 0..<(CodeEditSplitViewController.minSidebarWidth / 2))
+ let secondPosition = CGFloat.random(
+ in: (CodeEditSplitViewController.minSidebarWidth / 2)...CodeEditSplitViewController.minSidebarWidth
+ )
+
+ // When
+ [firstPosition, secondPosition].forEach { position in
+ _ = splitViewController.splitView(
+ splitViewController.splitView,
+ constrainSplitPosition: .init(position),
+ ofSubviewAt: .zero
+ )
+ }
+
+ // Then
+ XCTAssertTrue(hapticFeedbackPerformerMock.invokedPerform)
+ XCTAssertEqual(hapticFeedbackPerformerMock.invokedPerformCount, 1)
+ hapticFeedbackPerformerMock.reset()
+ }
}
}
diff --git a/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift b/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift
index 7a8b12e27..5ee85dbf7 100644
--- a/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift
+++ b/CodeEditTests/Features/Documents/Mocks/NSHapticFeedbackPerformerMock.swift
@@ -9,14 +9,19 @@ import Cocoa
final class NSHapticFeedbackPerformerMock: NSObject, NSHapticFeedbackPerformer {
- var invokedPerform = false
+ var invokedPerform: Bool {
+ invokedPerformCount > 0
+ }
var invokedPerformCount = 0
func perform(
_ pattern: NSHapticFeedbackManager.FeedbackPattern,
performanceTime: NSHapticFeedbackManager.PerformanceTime
) {
- invokedPerform = true
invokedPerformCount += 1
}
+
+ func reset() {
+ invokedPerformCount = 0
+ }
}