@@ -183,18 +183,40 @@ struct TabBarView: View {
183183 }
184184 . frame ( height: TabBarMetrics . barHeight)
185185 . mask ( combinedMask)
186- // Buttons float on top. No backdrop color needed because
187- // the mask hides scroll content and the tab bar's own
188- // background shows through naturally.
186+ // Split buttons sit on top of the tab strip in their own opaque backdrop.
187+ // The backdrop visually obscures any tabs that scroll under the buttons,
188+ // and (critically) does not break hit testing on tabs outside the backdrop —
189+ // unlike the prior approach of using a `Color.clear` region in `combinedMask`,
190+ // which silently blocked SwiftUI hit tests in the masked-out area and let
191+ // tab clicks fall through to `TabBarDragAndHoverView` (which performs a
192+ // window drag in minimal mode).
189193 . overlay ( alignment: . trailing) {
190194 if showSplitButtons {
191195 let shouldShow = presentationMode != " minimal " || isHoveringTabBar
192- splitButtons
193- . saturation ( tabBarSaturation)
194- . padding ( . bottom, 1 )
195- . opacity ( shouldShow ? 1 : 0 )
196- . allowsHitTesting ( shouldShow)
197- . animation ( . easeInOut( duration: 0.14 ) , value: shouldShow)
196+ let backdropColor = Color ( nsColor: Self . buttonBackdropColor (
197+ for: appearance,
198+ focused: isFocused,
199+ style: fadeColorStyle
200+ ) )
201+ ZStack ( alignment: . trailing) {
202+ HStack ( spacing: 0 ) {
203+ LinearGradient (
204+ colors: [ backdropColor. opacity ( 0 ) , backdropColor] ,
205+ startPoint: . leading,
206+ endPoint: . trailing
207+ )
208+ . frame ( width: 24 )
209+ Rectangle ( ) . fill ( backdropColor)
210+ }
211+ . frame ( width: 114 )
212+
213+ splitButtons
214+ . saturation ( tabBarSaturation)
215+ }
216+ . padding ( . bottom, 1 )
217+ . opacity ( shouldShow ? 1 : 0 )
218+ . allowsHitTesting ( shouldShow)
219+ . animation ( . easeInOut( duration: 0.14 ) , value: shouldShow)
198220 }
199221 }
200222 }
@@ -571,32 +593,31 @@ struct TabBarView: View {
571593 }
572594
573595 // MARK: - Combined Mask (scroll fades + button area)
596+ //
597+ // IMPORTANT: SwiftUI's `.mask()` with `Color.clear` regions blocks hit testing on the
598+ // masked content in those regions. Previously this mask used a 90pt clear region at the
599+ // trailing edge to hide tabs under the split buttons; that caused clicks on tabs in that
600+ // 90pt area to fall through the masked ScrollView to the `TabBarDragAndHoverView`
601+ // background, which (in minimal mode) interpreted the click as a window drag instead
602+ // of a tab tap. Keep the entire mask opaque so hit testing works on every tab; the split
603+ // buttons' opaque backdrop (rendered in the splitButtons overlay) handles the visual
604+ // obscuring of tabs underneath.
574605
575606 @ViewBuilder
576607 private var combinedMask : some View {
577608 let fadeWidth : CGFloat = 24
578- let shouldShowButtons = showSplitButtons && ( presentationMode != " minimal " || isHoveringTabBar)
579- let buttonClearWidth : CGFloat = shouldShowButtons ? 90 : 0
580- let buttonFadeWidth : CGFloat = shouldShowButtons ? fadeWidth : 0
581-
582609 HStack ( spacing: 0 ) {
583610 // Left scroll fade
584611 LinearGradient ( colors: [ . clear, . black] , startPoint: . leading, endPoint: . trailing)
585612 . frame ( width: canScrollLeft ? fadeWidth : 0 )
586613
587- // Visible content area
614+ // Visible content area (always opaque so hit testing reaches the tabs)
588615 Rectangle ( ) . fill ( Color . black)
589616
590- // Right: either scroll fade or button area fade
617+ // Right scroll fade only when scroll content actually overflows.
591618 LinearGradient ( colors: [ . black, . clear] , startPoint: . leading, endPoint: . trailing)
592- . frame ( width: canScrollRight || shouldShowButtons ? fadeWidth : 0 )
593-
594- // Button clear area (content hidden here)
595- if shouldShowButtons {
596- Color . clear. frame ( width: buttonClearWidth)
597- }
619+ . frame ( width: canScrollRight ? fadeWidth : 0 )
598620 }
599- . animation ( . easeInOut( duration: 0.14 ) , value: shouldShowButtons)
600621 }
601622
602623 // MARK: - Fade Overlays
@@ -713,6 +734,9 @@ private struct TabBarDragAndHoverView: NSViewRepresentable {
713734 }
714735
715736 override func mouseDown( with event: NSEvent ) {
737+ #if DEBUG
738+ dlog ( " tab.bar.bg.mouseDown isMinimal= \( isMinimalMode ? 1 : 0 ) clickCount= \( event. clickCount) " )
739+ #endif
716740 guard isMinimalMode, let window else {
717741 super. mouseDown ( with: event)
718742 return
@@ -760,6 +784,10 @@ private struct TabBarDragZoneView: NSViewRepresentable {
760784 }
761785
762786 override func mouseDown( with event: NSEvent ) {
787+ #if DEBUG
788+ let isMinimal = UserDefaults . standard. string ( forKey: " workspacePresentationMode " ) == " minimal "
789+ dlog ( " tab.bar.dragZone.mouseDown isMinimal= \( isMinimal ? 1 : 0 ) clickCount= \( event. clickCount) " )
790+ #endif
763791 guard let window = self . window else {
764792 super. mouseDown ( with: event)
765793 return
0 commit comments