//! The universal `FocusedPttBackend` (SDD-087). PTT works only //! while the Chanora window has input focus; the actual press and //! release events come from the Flutter side through the existing //! bridge `set_ptt` command, which the audio engine routes through //! the [`AudioTransmitGate`]. //! //! This backend therefore does not hook the OS itself — it merely //! exposes a privacy-safe descriptor and stores the active binding //! for diagnostics. Reports `PttCapabilityLevel::L0Focused` and //! `backend_id = "focused"`. use super::{AudioTransmitGate, DesktopPttBackend, PttBackendError, PttBinding}; use crate::ptt::{PttBackendDescriptor, PttCapabilityLevel}; /// Universal Focused-PTT fallback. Reports /// `PttCapabilityLevel::L0Focused` and `backend_id = "focused"`. /// Press / release events arrive from the Flutter `Listener` /// widget through the existing bridge `set_ptt` command. pub struct FocusedPttBackend { binding: PttBinding, gate: Option, } impl FocusedPttBackend { /// Construct an idle Focused backend. The audio engine arms it /// during `start_audio`. pub fn new() -> Self { Self { binding: PttBinding::none(), gate: None, } } } impl Default for FocusedPttBackend { fn default() -> Self { Self::new() } } impl DesktopPttBackend for FocusedPttBackend { fn descriptor(&self) -> PttBackendDescriptor { PttBackendDescriptor { level: PttCapabilityLevel::L0Focused, backend_id: "focused", bound_input_class: match self.binding.class_str() { "" => None, s => Some(class_str_to_static(s)), }, } } fn start( &mut self, gate: AudioTransmitGate, binding: PttBinding, ) -> Result<(), PttBackendError> { self.gate = Some(gate); self.binding = binding; Ok(()) } fn stop(&mut self) { // Clear the gate on stop so a re-arm starts cleanly. if let Some(g) = self.gate.take() { g.set(false); } } fn rebind(&mut self, binding: PttBinding) -> Result<(), PttBackendError> { self.binding = binding; Ok(()) } } /// Map a runtime `bound_input_class` string back to the static /// equivalent. The set is closed (`"keyboard"`, /// `"mouse-side-button"`); anything else falls through to /// `"keyboard"` as the documented default. fn class_str_to_static(s: &str) -> &'static str { match s { "mouse-side-button" => "mouse-side-button", _ => "keyboard", } }