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
35 changes: 28 additions & 7 deletions Sources/SidebarVisuals.swift
Original file line number Diff line number Diff line change
Expand Up @@ -573,13 +573,34 @@ struct SidebarEmptyArea: View {
Color.clear
.contentShape(Rectangle())
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture(count: 2) {
tabManager.addWorkspace(placementOverride: .end)
if let selectedId = tabManager.selectedTabId {
selectedTabIds = [selectedId]
lastSidebarSelectionIndex = tabManager.tabs.firstIndex { $0.id == selectedId }
}
selection = .tabs
// Drag view must be an .overlay (frontmost), not .background: as a background,
// the Color.clear + .contentShape content above it is the real hit-test target,
// so AppKit never even asks the background NSView's hitTest — that's why the
// titlebar.dragHandle.* logs never fired here. Once frontmost, this view's own
// hitTest bails out for non-mouseDown event types, so SwiftUI's onDrop target
// underneath is unaffected.
//
// Double-click is handled by `onDoubleClick` below rather than a sibling
// `.onTapGesture(count: 2)`: once this view is frontmost, it claims the hit-test
// for the *first* click of any double-click, so a sibling gesture recognizer on
// the content beneath never observes that first click and can't reliably count
// to two. Owning the whole click sequence here (mirroring Bonsplit's
// TabBarDragZoneView.onDoubleClick pattern) avoids that race entirely.
.overlay {
WindowDragHandleView(
handlesDoubleClick: false,
onDoubleClick: {
#if DEBUG
dlog("sidebar.dragHandle.doubleClick action=addWorkspace")
#endif
tabManager.addWorkspace(placementOverride: .end)
if let selectedId = tabManager.selectedTabId {
selectedTabIds = [selectedId]
lastSidebarSelectionIndex = tabManager.tabs.firstIndex { $0.id == selectedId }
}
selection = .tabs
}
)
}
.onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: SidebarTabDropDelegate(
targetTabId: nil,
Expand Down
46 changes: 44 additions & 2 deletions Sources/WindowDragHandleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,15 +309,36 @@ func windowDragHandleShouldCaptureHit(
/// This lets us keep `window.isMovableByWindowBackground = false` so drags in the app content
/// (e.g. sidebar tab reordering) don't move the whole window.
struct WindowDragHandleView: NSViewRepresentable {
/// When `false` (and `onDoubleClick` is `nil`), double-clicks are left untouched
/// (no titlebar zoom/minimize, no drag capture) so an underlying SwiftUI gesture
/// can still fire. Defaults to `true` to preserve existing callers.
var handlesDoubleClick: Bool = true

/// When set, this view owns double-click handling itself and invokes this closure
/// instead of the standard titlebar zoom/minimize action or the passthrough behavior.
/// Use this (rather than relying on a sibling SwiftUI `.onTapGesture(count: 2)`) when
/// this drag view is mounted in front of the gesture's content: a sibling gesture
/// recognizer never sees the first click of a double-click once this view has
/// claimed the hit-test for it, so passthrough-based double-click detection is
/// unreliable when this view is frontmost (see sidebar empty-area drag fix).
var onDoubleClick: (() -> Void)?

func makeNSView(context: Context) -> NSView {
DraggableView()
let view = DraggableView()
view.handlesDoubleClick = handlesDoubleClick
view.onDoubleClick = onDoubleClick
return view
}

func updateNSView(_ nsView: NSView, context: Context) {
// No-op
(nsView as? DraggableView)?.handlesDoubleClick = handlesDoubleClick
(nsView as? DraggableView)?.onDoubleClick = onDoubleClick
}

private final class DraggableView: NSView {
var handlesDoubleClick = true
var onDoubleClick: (() -> Void)?

override var mouseDownCanMoveWindow: Bool { false }

override func hitTest(_ point: NSPoint) -> NSView? {
Expand All @@ -329,6 +350,13 @@ struct WindowDragHandleView: NSViewRepresentable {
guard currentEvent?.type == .leftMouseDown else {
return nil
}
// Let double-clicks pass through to whatever is underneath when this
// instance doesn't own double-click handling in any form (no zoom, no
// custom action). When `onDoubleClick` is set, this view owns the double
// click itself (see mouseDown) and must keep capturing the hit here too.
if !handlesDoubleClick, onDoubleClick == nil, (currentEvent?.clickCount ?? 1) >= 2 {
return nil
}
let shouldCapture = windowDragHandleShouldCaptureHit(
point,
in: self,
Expand All @@ -353,6 +381,20 @@ struct WindowDragHandleView: NSViewRepresentable {
#endif

if event.clickCount >= 2 {
if let onDoubleClick {
#if DEBUG
dlog("titlebar.dragHandle.mouseDownDoubleClick action=custom")
#endif
onDoubleClick()
return
}
guard handlesDoubleClick else {
#if DEBUG
dlog("titlebar.dragHandle.mouseDownDoubleClick skipped=handlesDoubleClickFalse")
#endif
super.mouseDown(with: event)
return
}
let action = performStandardTitlebarDoubleClick(window: window)
#if DEBUG
dlog("titlebar.dragHandle.mouseDownDoubleClick action=\(String(describing: action))")
Expand Down
Loading