Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions Sources/Bonsplit/Internal/Views/SplitContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -737,10 +737,12 @@ struct SplitContainerView<Content: View, EmptyContent: View>: NSViewRepresentabl
let firstSubview = splitView.arrangedSubviews[0]
return splitState.orientation == .horizontal ? firstSubview.frame.width : firstSubview.frame.height
}()
let currentNormalized = max(
stateBounds.lowerBound,
min(stateBounds.upperBound, currentDividerPixels / availableSize)
)
// Compare the RAW ratio, not a pre-clamped one: clamping the
// current position into stateBounds before the equality check
// would let a divider that physically drifted outside the
// configured range (window resize, range narrowed on a reused
// split view) satisfy the early return and stay out of range.
let currentNormalized = currentDividerPixels / availableSize

if abs(clampedStatePosition - lastAppliedPosition) <= 0.01 &&
abs(currentNormalized - clampedStatePosition) <= 0.01 {
Expand Down
15 changes: 8 additions & 7 deletions Sources/Bonsplit/Public/BonsplitController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -598,13 +598,14 @@ public final class BonsplitController {
return newPaneId
}

private func normalizedInitialDividerPosition(_ position: CGFloat?) -> CGFloat? {
position.map {
min(
max($0, configuration.dividerPositionRange.lowerBound),
configuration.dividerPositionRange.upperBound
)
}
private func normalizedInitialDividerPosition(_ position: CGFloat?) -> CGFloat {
// A nil position must not bypass the configured range: the internal
// controller's 0.5 fallback can sit outside a narrowed range, so the
// default is clamped here exactly like an explicit position.
min(
max(position ?? 0.5, configuration.dividerPositionRange.lowerBound),
configuration.dividerPositionRange.upperBound
)
Comment thread
austinywang marked this conversation as resolved.
}

/// Split a pane by moving an existing tab into the new pane.
Expand Down
74 changes: 74 additions & 0 deletions Tests/BonsplitTests/BonsplitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3432,6 +3432,80 @@ final class BonsplitTests: XCTestCase {
)
}

@MainActor
func testSyncRestoresDividerThatDriftedOutsideConfiguredRange() throws {
let controller = BonsplitController(configuration: BonsplitConfiguration(
dividerPositionRange: 0.4...0.6,
appearance: .init(enableAnimations: false)
))
_ = controller.createTab(title: "Base")
let sourcePane = try XCTUnwrap(controller.focusedPaneId)
XCTAssertNotNil(controller.splitPane(
sourcePane,
orientation: .horizontal,
initialDividerPosition: 0.4
))
guard case .split = controller.treeSnapshot() else {
XCTFail("Expected split root")
return
}

let hostingView = NSHostingView(
rootView: BonsplitView(controller: controller) { _, _ in
Color.clear
} emptyPane: { _ in
Color.clear
}
)
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
defer { window.orderOut(nil) }
let contentView = try XCTUnwrap(window.contentView)
hostingView.frame = contentView.bounds
hostingView.autoresizingMask = [.width, .height]
contentView.addSubview(hostingView)
window.makeKeyAndOrderFront(nil)
contentView.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
contentView.layoutSubtreeIfNeeded()

let splitView = try XCTUnwrap(firstDescendant(ofType: NSSplitView.self, in: hostingView))
XCTAssertEqual(splitView.arrangedSubviews.count, 2)
let available = max(splitView.frame.width - splitView.dividerThickness, 1)

// Push the divider physically below the configured range without
// touching the model — the delegate constrain hooks enforce only the
// minimum pane size, mirroring an AppKit-driven (non-drag) resize.
// The non-drag resize path re-asserts the model position through
// syncPosition; that re-assert must compare the RAW pixel ratio, not a
// pre-clamped value that masks the out-of-range divider and
// early-returns.
splitView.setPosition(available * 0.2, ofDividerAt: 0)
splitView.layoutSubtreeIfNeeded()
RunLoop.current.run(until: Date().addingTimeInterval(0.05))
contentView.layoutSubtreeIfNeeded()
XCTAssertEqual(
splitView.arrangedSubviews[0].frame.width / available,
0.4,
accuracy: 0.02,
"Sync should restore an out-of-range divider to the model position"
)
guard case .split(let finalSplit) = controller.treeSnapshot() else {
XCTFail("Expected split root")
return
}
XCTAssertEqual(
finalSplit.dividerPosition,
0.4,
accuracy: 0.0001,
"A non-drag resize must not move the model divider position"
)
}

@MainActor
func testTranslucentSplitWrappersStayClear() {
let appearance = BonsplitConfiguration.Appearance(
Expand Down
15 changes: 15 additions & 0 deletions Tests/BonsplitTests/EmbeddedConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ final class EmbeddedConfigurationTests: XCTestCase {
XCTAssertEqual(updated.dividerPosition, 0.98, accuracy: 0.0001)
}

func testDefaultSplitPositionRespectsConfiguredDividerRange() throws {
let controller = BonsplitController(configuration: BonsplitConfiguration(
dividerPositionRange: 0.6...0.9
))
let rootPane = try XCTUnwrap(controller.allPaneIds.first)
XCTAssertNotNil(controller.splitPane(rootPane, orientation: .horizontal))
guard case .split(let split) = controller.treeSnapshot() else {
XCTFail("Expected split root")
return
}
// The implicit 0.5 default must be clamped into the configured range,
// exactly like an explicit initialDividerPosition.
XCTAssertEqual(split.dividerPosition, 0.6, accuracy: 0.0001)
}

func testDisabledTabMovesRejectBothReorderAndCrossPaneMutation() throws {
let controller = BonsplitController(configuration: BonsplitConfiguration(
allowTabReordering: false,
Expand Down
Loading