import Foundation /// What has the keyboard in a window when Space is pressed. public enum KeyboardFocus: Equatable, Sendable { /// Nothing in particular: the window itself, or its root view. case none /// A text field or text view, where Space is a space. case text /// A button, picker or any other control, where Space is the /// control's own activation. Anything not known to be one of the /// other two counts as this: an unrecognised focused thing keeps its /// Space rather than losing it to the microphone. case control } /// Space toggles the microphone during a call, the way it does in most /// calling apps — but only when Space means nothing else. The rules are /// here, apart from the key monitor that applies them, so they can be /// held by tests without a window or a keyboard. public enum SpaceToMute { public enum Action: Equatable, Sendable { /// Toggle mute, and consume the key. case toggle /// Consume the key and do nothing: a held Space's key repeats, which /// would otherwise flicker the microphone or beep. case swallow /// Not ours: let the key go on to whatever has focus. case pass } /// The virtual key code of the Space bar on every Mac keyboard layout. public static let keyCode: UInt16 = 49 /// - Parameters: /// - isSpace: the key is the Space bar. /// - isRepeat: the event is the key repeating while held. /// - modified: ⌘, ⌃ or ⌥ (or ⇧) is held. ⇧Space and the rest are /// shortcuts of their own, never the bare Space this is. /// - focus: what has the keyboard. public static func action( isSpace: Bool, isRepeat: Bool, modified: Bool, focus: KeyboardFocus ) -> Action { guard isSpace, !modified, focus == .none else { return .pass } return isRepeat ? .swallow : .toggle } } #if os(macOS) import AppKit import SwiftUI extension SpaceToMute { /// What has the keyboard, from AppKit's first responder. A SwiftUI /// TextField edits through the window's field editor, an NSText; a /// SwiftUI Button focused by Full Keyboard Access is a private proxy /// view inside the hosting view, which is why anything unrecognised /// is a control. The window, nothing, or the root view is nobody. public static func focus(of responder: NSResponder?, contentView: NSView?) -> KeyboardFocus { switch responder { case nil: return .none case is NSWindow: return .none case let view as NSView where view === contentView: return .none case is NSText: return .text case let field as NSTextField where field.isEditable: return .text default: return .control } } /// The decision for a key event arriving in `window`. @MainActor static func action(for event: NSEvent, in window: NSWindow) -> Action { guard event.type == .keyDown else { return .pass } let modifiers = event.modifierFlags.intersection(.deviceIndependentFlagsMask) .subtracting([.capsLock, .numericPad, .function]) return action( isSpace: event.keyCode == keyCode, isRepeat: event.isARepeat, modified: !modifiers.isEmpty, focus: focus(of: window.firstResponder, contentView: window.contentView)) } } extension View { /// Space toggles `call`'s microphone while this view's window — or the /// mini window standing in for it — is the key window. Apply it to the /// call's own view. func spaceToMute(_ call: Call) -> some View { background(SpaceToMuteMonitor(call: call).frame(width: 0, height: 0).accessibilityHidden(true)) } } /// Holds a local key monitor for as long as the call's view is in a /// window. Local monitors see every key event the app receives, so the /// monitor checks which window the event is for before anything else. private struct SpaceToMuteMonitor: NSViewRepresentable { let call: Call func makeNSView(context: Context) -> MonitorView { MonitorView() } func updateNSView(_ view: MonitorView, context: Context) { view.call = call } final class MonitorView: NSView { weak var call: Call? private var monitor: Any? override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if let monitor { NSEvent.removeMonitor(monitor) } monitor = nil guard window != nil else { return } monitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in // Local monitors run on the main thread, with the event the app // is about to dispatch there. nonisolated(unsafe) let event = event let consumed = MainActor.assumeIsolated { () -> Bool in guard let self, let host = self.window, let call = self.call, let target = event.window, target === host || target is MiniWindowPanel else { return false } switch SpaceToMute.action(for: event, in: target) { case .pass: return false case .swallow: return true case .toggle: call.mute() return true } } return consumed ? nil : event } } deinit { if let monitor { NSEvent.removeMonitor(monitor) } } } } #endif