diff --git a/MCT/UI/OverlayView.swift b/MCT/UI/OverlayView.swift index d4110b4..1714815 100644 --- a/MCT/UI/OverlayView.swift +++ b/MCT/UI/OverlayView.swift @@ -61,15 +61,16 @@ struct OverlayView: View { .scaleEffect(0.9 + 0.1 * itemProgress) .offset(y: 30 * (1.0 - itemProgress)) .opacity(itemProgress) - .position( - x: item.frame.midX - screenBounds.origin.x, - y: item.frame.midY - screenBounds.origin.y - ) .contentShape(Rectangle()) .onHover { hovering in if isInteractive { hoveredIndex = hovering ? index : nil - if hovering { selectedIndex = nil } + if hovering { + selectedIndex = nil + NSCursor.pointingHand.push() + } else { + NSCursor.pop() + } } } .onTapGesture { @@ -77,10 +78,19 @@ struct OverlayView: View { onSelect(item.windowInfo) } } + .position( + x: item.frame.midX - screenBounds.origin.x, + y: item.frame.midY - screenBounds.origin.y + ) } } } .frame(width: screenBounds.width, height: screenBounds.height) + .onChange(of: isInteractive) { _, interactive in + if interactive { + updateHoverFromMousePosition() + } + } .background( KeyEventHandlingView( onEscape: { @@ -99,6 +109,20 @@ struct OverlayView: View { onSelect(layout.items[idx].windowInfo) } } + }, + onTab: { shiftHeld in + if isInteractive, let layout = layout { + hoveredIndex = nil + let count = layout.items.count + guard count > 0 else { return } + if let idx = selectedIndex { + selectedIndex = shiftHeld + ? (idx - 1 + count) % count + : (idx + 1) % count + } else { + selectedIndex = shiftHeld ? count - 1 : 0 + } + } } ) ) @@ -163,6 +187,27 @@ struct OverlayView: View { } } } + + /// One-time hit-test: if the cursor is already over a thumbnail when the + /// overlay becomes interactive, highlight it immediately. + private func updateHoverFromMousePosition() { + guard let layout = layout else { return } + let mouse = NSEvent.mouseLocation + // Convert AppKit screen coords (bottom-left origin) to overlay SwiftUI coords (top-left origin) + let mx = mouse.x - screenBounds.origin.x + let my = screenBounds.origin.y + screenBounds.height - mouse.y + + for (index, item) in layout.items.enumerated() { + let ix = item.frame.origin.x - screenBounds.origin.x + let iy = item.frame.origin.y - screenBounds.origin.y + if mx >= ix && mx <= ix + item.frame.width && my >= iy && my <= iy + item.frame.height { + hoveredIndex = index + selectedIndex = nil + NSCursor.pointingHand.push() + return + } + } + } } enum ArrowDirection { @@ -174,12 +219,14 @@ struct KeyEventHandlingView: NSViewRepresentable { let onEscape: () -> Void let onArrow: (ArrowDirection) -> Void let onReturn: () -> Void + let onTab: (Bool) -> Void // Bool = shift held func makeNSView(context: Context) -> KeyCaptureNSView { let view = KeyCaptureNSView() view.onEscape = onEscape view.onArrow = onArrow view.onReturn = onReturn + view.onTab = onTab // Retry making first responder until the view is in a key-capable window func attemptFirstResponder(retries: Int = 5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { @@ -198,6 +245,7 @@ struct KeyEventHandlingView: NSViewRepresentable { nsView.onEscape = onEscape nsView.onArrow = onArrow nsView.onReturn = onReturn + nsView.onTab = onTab } } @@ -205,6 +253,7 @@ final class KeyCaptureNSView: NSView { var onEscape: (() -> Void)? var onArrow: ((ArrowDirection) -> Void)? var onReturn: (() -> Void)? + var onTab: ((Bool) -> Void)? override var acceptsFirstResponder: Bool { true } @@ -222,6 +271,8 @@ final class KeyCaptureNSView: NSView { onArrow?(.up) case 36: // Return onReturn?() + case 48: // Tab + onTab?(event.modifierFlags.contains(.shift)) default: super.keyDown(with: event) }