Files
OSGKeyboard/OSGKeyboardShared/DesignSystem/RecordButtonGesturePolicy.swift
T
Rocky 5d7fcdf24e feat(keyboard): harden polish/clipboard guards and ship 1.6.6 (build 59)
Keep marketing version at 1.6.6 and bump build to 59. Strengthen never-answer
polish safeguards, clipboard reply-intent continuity, voice undo UX, device
UITests harness, eval fixtures, and What's New assets.
2026-08-09 12:45:33 +08:00

61 lines
2.1 KiB
Swift

// RecordButtonGesturePolicy.swift
// OSGKeyboard · Shared
//
// Pure tap / hold routing for the mic button. Kept out of the view so the
// "one press produces at most one action" invariant is unit-testable: the
// clipboard long-press flips the phase while the finger is still down, and
// regressions there let a single press both open and close a round.
import Foundation
public enum RecordButtonGestureAction: Equatable, Sendable {
case none
/// Start dictation, cancel a preparing clipboard intent, or stop recording —
/// all of which the coordinator resolves from its own phase.
case toggle
case beginClipboardCommand
}
public enum RecordButtonGesturePolicy {
/// Action for a press released before the hold threshold.
public static func tapAction(
phase: RecordButton.Phase,
isEnabled: Bool
) -> RecordButtonGestureAction {
switch phase {
case .idleUnavailable:
return .toggle
case .idleReady, .error, .preparing, .recording:
return isEnabled ? .toggle : .none
case .processing:
return .none
}
}
/// Action for a press held past the threshold. A `.none` result means the
/// hold did not consume the press, so its release still counts as a tap and
/// no gesture becomes a dead key.
public static func holdAction(
phase: RecordButton.Phase,
isEnabled: Bool,
supportsClipboardLongPress: Bool
) -> RecordButtonGestureAction {
switch phase {
case .idleReady, .idleUnavailable, .error:
guard supportsClipboardLongPress else { return .none }
guard isEnabled || phase == .idleUnavailable else { return .none }
return .beginClipboardCommand
case .recording:
return isEnabled ? .toggle : .none
case .preparing, .processing:
return .none
}
}
/// A hold that produced an action owns the press; its release must be
/// swallowed rather than replayed as a tap.
public static func consumesPress(_ action: RecordButtonGestureAction) -> Bool {
action != .none
}
}