diff --git a/Sources/Bonsplit/Internal/Styling/TabBarColors.swift b/Sources/Bonsplit/Internal/Styling/TabBarColors.swift index 854d9a83..08704972 100644 --- a/Sources/Bonsplit/Internal/Styling/TabBarColors.swift +++ b/Sources/Bonsplit/Internal/Styling/TabBarColors.swift @@ -168,11 +168,7 @@ enum TabBarColors { return activeTabBackground } if appearance.usesSharedBackdrop { - let semanticBackground = semanticTabBarBackgroundColor(for: appearance) ?? custom - let overlayColor = semanticBackground.isBonsplitLightColor - ? NSColor.black.withAlphaComponent(0.06) - : NSColor.white.withAlphaComponent(0.08) - return Color(nsColor: overlayColor) + return .clear } let adjusted = custom.isBonsplitLightColor ? custom.bonsplitDarken(by: 0.065) diff --git a/Sources/Bonsplit/Internal/Views/TabBarView.swift b/Sources/Bonsplit/Internal/Views/TabBarView.swift index b4aa5ec2..c96a3eee 100644 --- a/Sources/Bonsplit/Internal/Views/TabBarView.swift +++ b/Sources/Bonsplit/Internal/Views/TabBarView.swift @@ -97,6 +97,25 @@ public enum BonsplitTabItemHitRegionRegistry { } } +enum BonsplitTabItemHitTesting { + static let horizontalSlop: CGFloat = 2 + static let verticalSlop: CGFloat = 6 + + static func containsTabLaneHit( + localPoint: NSPoint, + tabFrames: [CGRect], + bounds: NSRect + ) -> Bool { + guard bounds.insetBy(dx: 0, dy: -verticalSlop).contains(localPoint) else { + return false + } + return tabFrames.contains { frame in + localPoint.x >= frame.minX - horizontalSlop + && localPoint.x <= frame.maxX + horizontalSlop + } + } +} + private struct TabItemHitRegionView: NSViewRepresentable { func makeNSView(context: Context) -> RegionNSView { RegionNSView() @@ -150,7 +169,12 @@ private struct TabItemHitRegionView: NSViewRepresentable { } nonisolated func containsBonsplitTabItemHit(localPoint: NSPoint) -> Bool { - hitBounds.insetBy(dx: -2, dy: -2).contains(localPoint) + hitBounds + .insetBy( + dx: -BonsplitTabItemHitTesting.horizontalSlop, + dy: -BonsplitTabItemHitTesting.verticalSlop + ) + .contains(localPoint) } override func hitTest(_ point: NSPoint) -> NSView? { @@ -2464,8 +2488,11 @@ private struct TabBarDragAndHoverView: NSViewRepresentable { } nonisolated func containsBonsplitTabItemHit(localPoint: NSPoint) -> Bool { - let paddedFrames = tabFrames.map { $0.insetBy(dx: -2, dy: -2) } - return paddedFrames.contains { $0.contains(localPoint) } + BonsplitTabItemHitTesting.containsTabLaneHit( + localPoint: localPoint, + tabFrames: tabFrames, + bounds: bounds + ) } override func updateTrackingAreas() { diff --git a/Tests/BonsplitTests/BonsplitTests.swift b/Tests/BonsplitTests/BonsplitTests.swift index 69a3bdff..3b7eb7a6 100644 --- a/Tests/BonsplitTests/BonsplitTests.swift +++ b/Tests/BonsplitTests/BonsplitTests.swift @@ -1264,6 +1264,28 @@ final class BonsplitTests: XCTestCase { XCTAssertLessThan(hoverAlpha, 0.12) } + func testSharedBackdropActiveTabBackgroundIsClear() { + let appearance = BonsplitConfiguration.Appearance( + chromeColors: .init( + backgroundHex: "#272822", + tabBarBackgroundHex: "#00000000", + splitButtonBackdropHex: "#00000000", + paneBackgroundHex: "#00000000" + ), + usesSharedBackdrop: true + ) + let active = NSColor(TabBarColors.activeTabBackground(for: appearance)).usingColorSpace(.sRGB)! + + var alpha: CGFloat = 1 + active.getRed(nil, green: nil, blue: nil, alpha: &alpha) + + XCTAssertLessThan( + alpha, + 0.01, + "Shared-backdrop selected tabs should rely on the active indicator instead of a hover-like fill" + ) + } + func testSplitActionPressedStateUsesHigherContrast() { let appearance = BonsplitConfiguration.Appearance( chromeColors: .init(backgroundHex: "#272822") @@ -1714,6 +1736,15 @@ final class BonsplitTests: XCTestCase { "A short-titled tab must own the full configured visible tab width so minimal-mode drags do not become window drags" ) + let topEdgeTabPoint = hostingView.convert( + NSPoint(x: appearance.tabMinWidth - 20, y: appearance.tabBarHeight + 3), + to: nil + ) + XCTAssertTrue( + BonsplitTabItemHitRegionRegistry.containsWindowPoint(topEdgeTabPoint, in: window), + "A tab's horizontal lane must own near-titlebar-edge drags so minimal-mode top-edge tab drags do not become window drags" + ) + let emptyChromePoint = hostingView.convert( NSPoint(x: appearance.tabMinWidth + 30, y: verticalCenter), to: nil @@ -1722,6 +1753,15 @@ final class BonsplitTests: XCTestCase { BonsplitTabItemHitRegionRegistry.containsWindowPoint(emptyChromePoint, in: window), "Empty tab-strip chrome after the configured tab remains available for app-window dragging" ) + + let topEdgeEmptyChromePoint = hostingView.convert( + NSPoint(x: appearance.tabMinWidth + 30, y: appearance.tabBarHeight + 3), + to: nil + ) + XCTAssertFalse( + BonsplitTabItemHitRegionRegistry.containsWindowPoint(topEdgeEmptyChromePoint, in: window), + "Near-titlebar-edge empty chrome after the tab should remain available for app-window dragging" + ) } @MainActor