Files
Termind/TFM/Platform/UserInputModality.swift
T

69 lines
2.1 KiB
Swift

import AppKit
import Observation
import SwiftUI
nonisolated enum UserInputModality: Equatable, Sendable {
case keyboard
case pointer
func updated(for eventType: NSEvent.EventType) -> Self {
switch eventType {
case .keyDown:
.keyboard
case .leftMouseDown, .rightMouseDown, .otherMouseDown:
.pointer
default:
self
}
}
}
/// The most recent input family used inside Termind.
///
/// AppKit makes the first focusable control in a newly mounted hosting view the
/// first responder. With full keyboard navigation enabled that programmatic
/// focus draws exactly like keyboard navigation, even when the user opened the
/// surface with the mouse. Observing the input event without consuming it lets
/// custom controls keep the useful keyboard ring while suppressing that
/// pointer-triggered false signal.
@MainActor
@Observable
final class UserInputModalityTracker {
static let shared = UserInputModalityTracker()
private(set) var modality: UserInputModality = .pointer
@ObservationIgnored private var eventMonitor: Any?
private init() {
eventMonitor = NSEvent.addLocalMonitorForEvents(
matching: [.keyDown, .leftMouseDown, .rightMouseDown, .otherMouseDown]
) { [weak self] event in
self?.record(event.type)
return event
}
}
/// Status-item clicks can arrive from Control Center without a local mouse event.
func notePointerInteraction() { record(.leftMouseDown) }
private func record(_ eventType: NSEvent.EventType) {
let updated = modality.updated(for: eventType)
if updated != modality { modality = updated }
}
}
private struct InputAwareFocusEffect: ViewModifier {
@State private var input = UserInputModalityTracker.shared
func body(content: Content) -> some View {
content.focusEffectDisabled(input.modality != .keyboard)
}
}
extension View {
/// Shows the native focus ring only after actual keyboard input.
func termindInputAwareFocusEffect() -> some View {
modifier(InputAwareFocusEffect())
}
}