fix: enable window dragging outside interactive controls

Use background-only native drag gestures so scroll content and overlays remain draggable without stealing control, text, or queue-row input. Compact mode uses the same explicit drag surfaces rather than broad native background movement.
This commit is contained in:
2026-10-19 17:41:55 +02:00
parent 997eb144d2
commit 6d653e8875
8 changed files with 249 additions and 13 deletions
+2 -1
View File
@@ -334,7 +334,8 @@ final class CompactWindowController: NSObject {
window.collectionBehavior = behavior
window.level = .floating
window.hidesOnDeactivate = false
window.isMovableByWindowBackground = true
// Explicit drag surfaces preserve controls' mouse tracking in compact mode too.
window.isMovableByWindowBackground = false
fitCompactWindow(window, initial: true, animated: animated)
}
+23
View File
@@ -2101,6 +2101,7 @@ private struct PlayerContent: View {
@ViewState private var queueFrames: [UUID: CGRect] = [:]
@ViewState private var queueDrag: PlaybackQueueDrag?
@GestureState private var queueDragging = false
@GestureState private var windowDragging = false
@ViewState private var queueScroll = ScrollPosition(y: 0)
@ViewState private var queueScrollOffset: CGFloat = 0
@ViewState private var queueViewportHeight: CGFloat = 0
@@ -2228,6 +2229,8 @@ private struct PlayerContent: View {
.padding(.horizontal, size.width < 600 ? 14 : 30)
.padding(.top, 64)
.padding(.bottom, 24)
.frame(maxWidth: .infinity, minHeight: size.height, alignment: .topLeading)
.background(windowDragBackground)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@@ -2337,6 +2340,7 @@ private struct PlayerContent: View {
.padding(.trailing, 92 - 88 * model.queueWidth / 260)
.frame(height: 32)
.foregroundStyle(Color(SunsetPalette.text))
.background(windowDragBackground)
.onHover(perform: model.setTitleHovered)
.animation(controlsVisibilityAnimation) { content in
content.opacity(model.controlsVisible ? 1 : 0)
@@ -2357,6 +2361,7 @@ private struct PlayerContent: View {
}
.padding(.horizontal, 4 * (1 - model.queueWidth / 260))
.frame(height: 24)
.background(windowDragBackground)
.background {
PlayerGlass(cornerRadius: 8)
.clipShape(RoundedRectangle(cornerRadius: 8))
@@ -2434,6 +2439,7 @@ private struct PlayerContent: View {
.padding(.horizontal, 12)
.padding(.vertical, showsBufferReadout ? 6 : 8)
.foregroundStyle(Color(SunsetPalette.text))
.background(windowDragBackground)
.background(PlayerGlass(cornerRadius: 10).clipShape(RoundedRectangle(cornerRadius: 10)))
.onHover(perform: model.setControlsHovered)
.animation(controlsVisibilityAnimation) { content in
@@ -2673,6 +2679,8 @@ private struct PlayerContent: View {
.highPriorityGesture(queueRowDrag(item))
}
}
.frame(maxWidth: .infinity, minHeight: queueViewportHeight, alignment: .top)
.background(windowDragBackground)
.coordinateSpace(name: "queueContent")
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { queueContentHeight = $0 }
}
@@ -2743,6 +2751,7 @@ private struct PlayerContent: View {
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(windowDragBackground)
.background { queueChromeBackground(top: false) }
}
.frame(width: 260)
@@ -2794,6 +2803,14 @@ private struct PlayerContent: View {
.foregroundStyle(Color(SunsetPalette.text))
.tint(Color(SunsetPalette.accent))
.background(WindowChrome(model: model))
.onChange(of: windowDragging) { _, active in model.setControlsDragging(active) }
}
private var windowDragBackground: some View {
Color.clear.contentShape(Rectangle())
.gesture(WindowDragGesture()
.updating($windowDragging) { _, active, _ in active = true })
.padding(5) // Preserve the native resize border.
}
private var videoContent: some View {
@@ -2837,6 +2854,7 @@ private struct PlayerContent: View {
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 12)).allowsHitTesting(false))
.padding(.top, 44).padding(.trailing, 16)
.frame(maxWidth: .infinity, alignment: .trailing)
.allowsHitTesting(false)
}
if let error = model.errorText {
VStack(spacing: 12) {
@@ -2849,6 +2867,7 @@ private struct PlayerContent: View {
.frame(maxWidth: 420)
.background(PlayerGlass().clipShape(RoundedRectangle(cornerRadius: 12)).allowsHitTesting(false))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(windowDragBackground)
} else if model.isBusy {
VStack(spacing: 12) {
ProgressView().controlSize(.large)
@@ -2857,6 +2876,7 @@ private struct PlayerContent: View {
.font(.callout).foregroundStyle(Color(SunsetPalette.secondaryText))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
} else if model.fileName == nil {
startScreen(in: geometry.size)
} else if let device = model.castDevice {
@@ -2867,6 +2887,7 @@ private struct PlayerContent: View {
}
.foregroundStyle(Color(SunsetPalette.text))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(windowDragBackground)
} else if !model.state.has_video && model.state.has_audio {
VStack(spacing: 16) {
Image(systemName: "waveform").font(.system(size: 64, weight: .ultraLight))
@@ -2875,6 +2896,7 @@ private struct PlayerContent: View {
.foregroundStyle(Color(SunsetPalette.secondaryText))
.padding(40)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.allowsHitTesting(false)
}
if model.state.buffering && !model.isBusy && model.errorText == nil {
ProgressView("Buffering…")
@@ -2928,6 +2950,7 @@ private struct PlayerContent: View {
if model.canTogglePlayback {
playbackControls
.frame(width: 440, height: controlHeight)
.background(windowDragBackground)
.background {
PlayerGlass()
.clipShape(RoundedRectangle(cornerRadius: 12))