use serde::{Deserialize, Serialize}; use super::devices::DeviceCatalog; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum InputRouting { Local, Remote, } impl InputRouting { pub fn as_env(self) -> &'static str { match self { Self::Local => "0", Self::Remote => "1", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ViewMode { Unified, Breakout, } impl ViewMode { pub fn as_env(self) -> &'static str { match self { Self::Unified => "unified", Self::Breakout => "breakout", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum DisplaySurface { Preview, Window, } impl DisplaySurface { pub fn label(self) -> &'static str { match self { Self::Preview => "preview", Self::Window => "window", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum BreakoutSizePreset { P360, P540, P720, P900, P1080, P1440, Source, FillDisplay, } impl BreakoutSizePreset { pub fn as_id(self) -> &'static str { match self { Self::P360 => "360p", Self::P540 => "540p", Self::P720 => "720p", Self::P900 => "900p", Self::P1080 => "1080p", Self::P1440 => "1440p", Self::Source => "source", Self::FillDisplay => "fill", } } pub fn from_id(raw: &str) -> Option { match raw { "360p" => Some(Self::P360), "540p" => Some(Self::P540), "720p" => Some(Self::P720), "900p" => Some(Self::P900), "1080p" => Some(Self::P1080), "1440p" => Some(Self::P1440), "source" => Some(Self::Source), "fill" => Some(Self::FillDisplay), _ => None, } } pub fn label(self) -> &'static str { match self { Self::P360 => "360p", Self::P540 => "540p", Self::P720 => "720p", Self::P900 => "900p", Self::P1080 => "1080p", Self::P1440 => "1440p", Self::Source => "Source", Self::FillDisplay => "Display", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum CaptureSizePreset { P360, P540, P720, P900, P1080, P1440, Source, } impl CaptureSizePreset { pub fn as_id(self) -> &'static str { match self { Self::P360 => "360p", Self::P540 => "540p", Self::P720 => "720p", Self::P900 => "900p", Self::P1080 => "1080p", Self::P1440 => "1440p", Self::Source => "source", } } pub fn from_id(raw: &str) -> Option { match raw { "360p" => Some(Self::P360), "540p" => Some(Self::P540), "720p" => Some(Self::P720), "900p" => Some(Self::P900), "1080p" => Some(Self::P1080), "1440p" => Some(Self::P1440), "source" => Some(Self::Source), _ => None, } } pub fn label(self) -> &'static str { match self { Self::P360 => "360p", Self::P540 => "540p", Self::P720 => "720p", Self::P900 => "900p", Self::P1080 => "1080p", Self::P1440 => "1440p", Self::Source => "Source", } } pub fn transport_label(self) -> &'static str { match self { Self::Source => "source pass-through", _ => "server re-encode", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct PreviewSourceSize { pub width: u32, pub height: u32, pub fps: u32, } impl Default for PreviewSourceSize { fn default() -> Self { Self { width: 1920, height: 1080, fps: 30, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BreakoutSizeChoice { pub preset: BreakoutSizePreset, pub width: i32, pub height: i32, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct CaptureSizeChoice { pub preset: CaptureSizePreset, pub width: i32, pub height: i32, pub fps: u32, pub max_bitrate_kbit: u32, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CapturePowerStatus { pub available: bool, pub enabled: bool, pub unit: String, pub detail: String, pub active_leases: u32, pub mode: String, } impl Default for CapturePowerStatus { fn default() -> Self { Self { available: false, enabled: false, unit: "relay.service".to_string(), detail: "unknown".to_string(), active_leases: 0, mode: "auto".to_string(), } } } #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct DeviceSelection { pub camera: Option, pub microphone: Option, pub speaker: Option, pub keyboard: Option, pub mouse: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LauncherState { pub server_available: bool, pub server_version: Option, pub routing: InputRouting, pub view_mode: ViewMode, pub displays: [DisplaySurface; 2], pub preview_source: PreviewSourceSize, pub breakout_limit: PreviewSourceSize, pub breakout_display: PreviewSourceSize, pub capture_sizes: [CaptureSizePreset; 2], pub breakout_sizes: [BreakoutSizePreset; 2], pub devices: DeviceSelection, pub swap_key: String, pub swap_key_binding: bool, pub swap_key_binding_token: u64, pub capture_power: CapturePowerStatus, pub remote_active: bool, pub notes: Vec, } impl Default for LauncherState { fn default() -> Self { Self { server_available: false, server_version: None, routing: InputRouting::Remote, view_mode: ViewMode::Unified, displays: [DisplaySurface::Preview, DisplaySurface::Preview], preview_source: PreviewSourceSize::default(), breakout_limit: PreviewSourceSize::default(), breakout_display: PreviewSourceSize::default(), capture_sizes: [CaptureSizePreset::Source, CaptureSizePreset::Source], breakout_sizes: [BreakoutSizePreset::Source, BreakoutSizePreset::Source], devices: DeviceSelection::default(), swap_key: "pause".to_string(), swap_key_binding: false, swap_key_binding_token: 0, capture_power: CapturePowerStatus::default(), remote_active: false, notes: Vec::new(), } } } impl LauncherState { pub fn new() -> Self { Self::default() } pub fn set_routing(&mut self, routing: InputRouting) { self.routing = routing; } pub fn set_server_available(&mut self, available: bool) { self.server_available = available; } pub fn set_server_version(&mut self, version: Option) { self.server_version = version.and_then(|value| { let trimmed = value.trim(); if trimmed.is_empty() { None } else { Some(trimmed.to_string()) } }); } pub fn set_view_mode(&mut self, view_mode: ViewMode) { self.view_mode = view_mode; self.displays = match view_mode { ViewMode::Unified => [DisplaySurface::Preview, DisplaySurface::Preview], ViewMode::Breakout => [DisplaySurface::Window, DisplaySurface::Window], }; } pub fn display_surface(&self, monitor_id: usize) -> DisplaySurface { self.displays .get(monitor_id) .copied() .unwrap_or(DisplaySurface::Preview) } pub fn set_display_surface(&mut self, monitor_id: usize, surface: DisplaySurface) { if let Some(slot) = self.displays.get_mut(monitor_id) { *slot = surface; self.view_mode = if self .displays .iter() .any(|display| matches!(display, DisplaySurface::Window)) { ViewMode::Breakout } else { ViewMode::Unified }; } } pub fn breakout_count(&self) -> usize { self.displays .iter() .filter(|surface| matches!(surface, DisplaySurface::Window)) .count() } pub fn preview_source_size(&self) -> PreviewSourceSize { self.preview_source } pub fn set_preview_source_profile(&mut self, width: u32, height: u32, fps: u32) { if width == 0 || height == 0 { return; } self.preview_source = PreviewSourceSize { width, height, fps: fps.max(1), }; } pub fn breakout_limit_size(&self) -> PreviewSourceSize { self.breakout_limit } pub fn set_breakout_limit_size(&mut self, width: u32, height: u32) { if width == 0 || height == 0 { return; } self.breakout_limit = PreviewSourceSize { width, height, fps: self.breakout_limit.fps.max(1), }; } pub fn breakout_display_size(&self) -> PreviewSourceSize { self.breakout_display } pub fn set_breakout_display_size(&mut self, width: u32, height: u32) { if width == 0 || height == 0 { return; } self.breakout_display = PreviewSourceSize { width, height, fps: self.breakout_display.fps.max(1), }; } pub fn capture_size_preset(&self, monitor_id: usize) -> CaptureSizePreset { self.capture_sizes .get(monitor_id) .copied() .unwrap_or(CaptureSizePreset::Source) } pub fn set_capture_size_preset(&mut self, monitor_id: usize, preset: CaptureSizePreset) { if let Some(slot) = self.capture_sizes.get_mut(monitor_id) { *slot = preset; } } pub fn capture_size_choice(&self, monitor_id: usize) -> CaptureSizeChoice { capture_size_choice(self.preview_source, self.capture_size_preset(monitor_id)) } pub fn capture_size_options(&self) -> Vec { capture_size_options(self.preview_source) } pub fn breakout_size_preset(&self, monitor_id: usize) -> BreakoutSizePreset { self.breakout_sizes .get(monitor_id) .copied() .unwrap_or(BreakoutSizePreset::Source) } pub fn set_breakout_size_preset(&mut self, monitor_id: usize, preset: BreakoutSizePreset) { if let Some(slot) = self.breakout_sizes.get_mut(monitor_id) { *slot = preset; } } pub fn breakout_size_choice(&self, monitor_id: usize) -> BreakoutSizeChoice { breakout_size_choice( self.breakout_limit, self.breakout_display, self.preview_source, self.breakout_size_preset(monitor_id), ) } pub fn breakout_size_options(&self) -> Vec { breakout_size_options( self.breakout_limit, self.breakout_display, self.preview_source, ) } pub fn select_camera(&mut self, camera: Option) { self.devices.camera = normalize_selection(camera); } pub fn select_microphone(&mut self, microphone: Option) { self.devices.microphone = normalize_selection(microphone); } pub fn select_speaker(&mut self, speaker: Option) { self.devices.speaker = normalize_selection(speaker); } pub fn select_keyboard(&mut self, keyboard: Option) { self.devices.keyboard = normalize_selection(keyboard); } pub fn select_mouse(&mut self, mouse: Option) { self.devices.mouse = normalize_selection(mouse); } pub fn apply_catalog_defaults(&mut self, catalog: &DeviceCatalog) { if self.devices.camera.is_none() { self.devices.camera = catalog.cameras.first().cloned(); } if self.devices.microphone.is_none() { self.devices.microphone = catalog.microphones.first().cloned(); } if self.devices.speaker.is_none() { self.devices.speaker = catalog.speakers.first().cloned(); } } pub fn set_swap_key(&mut self, swap_key: impl Into) { self.swap_key = normalize_swap_key(swap_key.into()); } pub fn begin_swap_key_binding(&mut self) -> u64 { self.swap_key_binding = true; self.swap_key_binding_token = self.swap_key_binding_token.wrapping_add(1); self.swap_key_binding_token } pub fn finish_swap_key_binding(&mut self) { self.swap_key_binding = false; } pub fn cancel_swap_key_binding(&mut self, token: u64) -> bool { if self.swap_key_binding && self.swap_key_binding_token == token { self.swap_key_binding = false; true } else { false } } pub fn complete_swap_key_binding(&mut self, swap_key: impl Into) { self.set_swap_key(swap_key); self.finish_swap_key_binding(); } pub fn start_remote(&mut self) -> bool { if self.remote_active { return false; } self.remote_active = true; true } pub fn stop_remote(&mut self) -> bool { if !self.remote_active { return false; } self.remote_active = false; true } pub fn push_note(&mut self, note: impl Into) { self.notes.push(note.into()); } pub fn set_capture_power(&mut self, power: CapturePowerStatus) { self.capture_power = power; } pub fn status_line(&self) -> String { format!( "server={} mode={} view={} active={} power={} source={}x{} d1={} d2={} camera={} mic={} speaker={} kbd={} mouse={} swap={}", self.server_available, match self.routing { InputRouting::Local => "local", InputRouting::Remote => "remote", }, match self.view_mode { ViewMode::Unified => "unified", ViewMode::Breakout => "breakout", }, self.remote_active, if self.capture_power.enabled { "on" } else { "off" }, self.preview_source.width, self.preview_source.height, self.displays[0].label(), self.displays[1].label(), self.devices.camera.as_deref().unwrap_or("auto"), self.devices.microphone.as_deref().unwrap_or("auto"), self.devices.speaker.as_deref().unwrap_or("auto"), self.devices.keyboard.as_deref().unwrap_or("all"), self.devices.mouse.as_deref().unwrap_or("all"), self.swap_key, ) } } fn breakout_size_choice( physical_limit: PreviewSourceSize, display_fill: PreviewSourceSize, source: PreviewSourceSize, preset: BreakoutSizePreset, ) -> BreakoutSizeChoice { let physical_width = physical_limit.width.max(1) as i32; let physical_height = physical_limit.height.max(1) as i32; let display_width = display_fill.width.max(1) as i32; let display_height = display_fill.height.max(1) as i32; let (width, height) = match preset { BreakoutSizePreset::P360 => { fit_standard_dimensions(physical_width, physical_height, 640, 360) } BreakoutSizePreset::P540 => { fit_standard_dimensions(physical_width, physical_height, 960, 540) } BreakoutSizePreset::P720 => { fit_standard_dimensions(physical_width, physical_height, 1280, 720) } BreakoutSizePreset::P900 => { fit_standard_dimensions(physical_width, physical_height, 1600, 900) } BreakoutSizePreset::P1080 => { fit_standard_dimensions(physical_width, physical_height, 1920, 1080) } BreakoutSizePreset::P1440 => { fit_standard_dimensions(physical_width, physical_height, 2560, 1440) } BreakoutSizePreset::Source => fit_standard_dimensions( physical_width, physical_height, source.width.max(1) as i32, source.height.max(1) as i32, ), BreakoutSizePreset::FillDisplay => (display_width, display_height), }; BreakoutSizeChoice { preset, width, height, } } fn breakout_size_options( physical_limit: PreviewSourceSize, display_fill: PreviewSourceSize, source: PreviewSourceSize, ) -> Vec { let mut options = Vec::new(); for preset in [ BreakoutSizePreset::Source, BreakoutSizePreset::P360, BreakoutSizePreset::P540, BreakoutSizePreset::P720, BreakoutSizePreset::P900, BreakoutSizePreset::P1080, BreakoutSizePreset::P1440, BreakoutSizePreset::FillDisplay, ] { let choice = breakout_size_choice(physical_limit, display_fill, source, preset); let allow_duplicate_label = matches!( preset, BreakoutSizePreset::Source | BreakoutSizePreset::FillDisplay ); if !allow_duplicate_label && options.iter().any(|existing: &BreakoutSizeChoice| { existing.width == choice.width && existing.height == choice.height }) { continue; } options.push(choice); } options } fn capture_size_choice(source: PreviewSourceSize, preset: CaptureSizePreset) -> CaptureSizeChoice { let source_width = source.width.max(1) as i32; let source_height = source.height.max(1) as i32; let source_fps = source.fps.max(1); let (width, height, fps, max_bitrate_kbit) = match preset { CaptureSizePreset::P360 => { let (width, height) = fit_standard_dimensions(source_width, source_height, 640, 360); (width, height, source_fps.min(15), 2_500) } CaptureSizePreset::P540 => { let (width, height) = fit_standard_dimensions(source_width, source_height, 960, 540); (width, height, source_fps.min(20), 4_000) } CaptureSizePreset::P720 => { let (width, height) = fit_standard_dimensions(source_width, source_height, 1280, 720); (width, height, source_fps.min(24), 6_000) } CaptureSizePreset::P900 => { let (width, height) = fit_standard_dimensions(source_width, source_height, 1600, 900); (width, height, source_fps.min(30), 8_500) } CaptureSizePreset::P1080 => { let (width, height) = fit_standard_dimensions(source_width, source_height, 1920, 1080); (width, height, source_fps.min(30), 12_000) } CaptureSizePreset::P1440 => { let (width, height) = fit_standard_dimensions(source_width, source_height, 2560, 1440); (width, height, source_fps.min(30), 18_000) } CaptureSizePreset::Source => ( source_width, source_height, source_fps, estimate_source_bitrate_kbit(source_width, source_height, source_fps), ), }; CaptureSizeChoice { preset, width, height, fps, max_bitrate_kbit, } } fn estimate_source_bitrate_kbit(width: i32, height: i32, fps: u32) -> u32 { let pixels_per_second = width.max(1) as u64 * height.max(1) as u64 * fps.max(1) as u64; match pixels_per_second { p if p >= 1920_u64 * 1080 * 50 => 18_000, p if p >= 1920_u64 * 1080 * 24 => 12_000, p if p >= 1280_u64 * 720 * 24 => 6_000, _ => 2_500, } } fn capture_size_options(source: PreviewSourceSize) -> Vec { let mut options = Vec::new(); for preset in [ CaptureSizePreset::Source, CaptureSizePreset::P360, CaptureSizePreset::P540, CaptureSizePreset::P720, CaptureSizePreset::P900, CaptureSizePreset::P1080, CaptureSizePreset::P1440, ] { let choice = capture_size_choice(source, preset); if options.iter().any(|existing: &CaptureSizeChoice| { let existing_transport = existing.preset.transport_label(); let current_transport = choice.preset.transport_label(); existing_transport == current_transport && existing.width == choice.width && existing.height == choice.height && existing.fps == choice.fps && existing.max_bitrate_kbit == choice.max_bitrate_kbit }) { continue; } options.push(choice); } options } fn fit_standard_dimensions( limit_width: i32, limit_height: i32, wanted_width: i32, wanted_height: i32, ) -> (i32, i32) { let width = wanted_width.min(limit_width).max(2); let height = wanted_height.min(limit_height).max(2); if width == limit_width && height == limit_height { return (width, height); } let width_from_height = round_down_even((height * 16) / 9); if width_from_height <= limit_width { (round_down_even(width_from_height), round_down_even(height)) } else { let height_from_width = round_down_even((width * 9) / 16); (round_down_even(width), round_down_even(height_from_width)) } } fn round_down_even(value: i32) -> i32 { let rounded = value.max(2); rounded - (rounded % 2) } fn normalize_selection(value: Option) -> Option { value.and_then(|v| { let trimmed = v.trim(); if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("auto") { None } else { Some(trimmed.to_string()) } }) } fn normalize_swap_key(value: String) -> String { let trimmed = value.trim(); if trimmed.is_empty() { "off".to_string() } else { trimmed.to_ascii_lowercase() } } #[cfg(test)] mod tests { use super::*; #[test] fn routing_and_view_env_values_are_stable() { assert_eq!(InputRouting::Local.as_env(), "0"); assert_eq!(InputRouting::Remote.as_env(), "1"); assert_eq!(ViewMode::Unified.as_env(), "unified"); assert_eq!(ViewMode::Breakout.as_env(), "breakout"); } #[test] fn defaults_pick_remote_unified_and_inactive_session() { let state = LauncherState::new(); assert_eq!(state.routing, InputRouting::Remote); assert_eq!(state.view_mode, ViewMode::Unified); assert_eq!(state.display_surface(0), DisplaySurface::Preview); assert_eq!(state.display_surface(1), DisplaySurface::Preview); assert_eq!(state.preview_source_size(), PreviewSourceSize::default()); assert_eq!(state.breakout_limit_size(), PreviewSourceSize::default()); assert_eq!(state.capture_size_preset(0), CaptureSizePreset::Source); assert_eq!(state.breakout_size_preset(0), BreakoutSizePreset::Source); assert!(!state.server_available); assert!(!state.remote_active); assert!(state.devices.camera.is_none()); assert!(state.devices.microphone.is_none()); assert!(state.devices.speaker.is_none()); assert!(state.devices.keyboard.is_none()); assert!(state.devices.mouse.is_none()); assert_eq!(state.capture_power.unit, "relay.service"); assert_eq!(state.capture_power.mode, "auto"); } #[test] fn display_surface_updates_global_view_summary() { let mut state = LauncherState::new(); state.set_display_surface(1, DisplaySurface::Window); assert_eq!(state.view_mode, ViewMode::Breakout); assert_eq!(state.breakout_count(), 1); state.set_display_surface(1, DisplaySurface::Preview); assert_eq!(state.view_mode, ViewMode::Unified); assert_eq!(state.breakout_count(), 0); state.set_view_mode(ViewMode::Breakout); assert_eq!(state.display_surface(0), DisplaySurface::Window); assert_eq!(state.display_surface(1), DisplaySurface::Window); } #[test] fn selecting_auto_or_blank_clears_explicit_device() { let mut state = LauncherState::new(); state.select_camera(Some("/dev/video0".to_string())); assert_eq!(state.devices.camera.as_deref(), Some("/dev/video0")); state.select_camera(Some("auto".to_string())); assert!(state.devices.camera.is_none()); state.select_microphone(Some(" ".to_string())); assert!(state.devices.microphone.is_none()); } #[test] fn catalog_defaults_fill_only_missing_values() { let mut state = LauncherState::new(); state.select_camera(Some("/dev/video-special".to_string())); let catalog = DeviceCatalog { cameras: vec!["/dev/video0".to_string()], microphones: vec!["alsa_input.usb".to_string()], speakers: vec!["alsa_output.usb".to_string()], keyboards: vec!["/dev/input/event10".to_string()], mice: vec!["/dev/input/event11".to_string()], }; state.apply_catalog_defaults(&catalog); assert_eq!(state.devices.camera.as_deref(), Some("/dev/video-special")); assert_eq!(state.devices.microphone.as_deref(), Some("alsa_input.usb")); assert_eq!(state.devices.speaker.as_deref(), Some("alsa_output.usb")); } #[test] fn start_and_stop_remote_only_report_changes_once() { let mut state = LauncherState::new(); assert!(state.start_remote()); assert!(!state.start_remote()); assert!(state.remote_active); assert!(state.stop_remote()); assert!(!state.stop_remote()); assert!(!state.remote_active); } #[test] fn status_line_mentions_all_user_visible_controls() { let mut state = LauncherState::new(); state.set_server_available(true); state.set_routing(InputRouting::Local); state.set_view_mode(ViewMode::Unified); state.select_camera(Some("/dev/video0".to_string())); state.select_microphone(Some("alsa_input.usb".to_string())); state.select_speaker(Some("alsa_output.usb".to_string())); state.select_keyboard(Some("/dev/input/event-kbd".to_string())); state.select_mouse(Some("/dev/input/event-mouse".to_string())); state.set_preview_source_profile(1920, 1080, 30); state.start_remote(); let status = state.status_line(); assert!(status.contains("mode=local")); assert!(status.contains("server=true")); assert!(status.contains("view=unified")); assert!(status.contains("active=true")); assert!(status.contains("source=1920x1080")); assert!(status.contains("d1=preview")); assert!(status.contains("d2=preview")); assert!(status.contains("camera=/dev/video0")); assert!(status.contains("mic=alsa_input.usb")); assert!(status.contains("speaker=alsa_output.usb")); assert!(status.contains("kbd=/dev/input/event-kbd")); assert!(status.contains("mouse=/dev/input/event-mouse")); } #[test] fn capture_power_status_updates_snapshot_state() { let mut state = LauncherState::new(); state.set_capture_power(CapturePowerStatus { available: true, enabled: true, unit: "relay.service".to_string(), detail: "active/running".to_string(), active_leases: 2, mode: "forced-on".to_string(), }); assert!(state.capture_power.available); assert!(state.capture_power.enabled); assert_eq!(state.capture_power.active_leases, 2); assert!(state.status_line().contains("power=on")); } #[test] fn server_availability_tracks_reachability() { let mut state = LauncherState::new(); assert!(!state.server_available); state.set_server_available(true); assert!(state.server_available); } #[test] fn breakout_size_choices_track_the_negotiated_source_size() { let mut state = LauncherState::new(); state.set_preview_source_profile(1920, 1080, 30); state.set_breakout_limit_size(2560, 1440); let source = state.capture_size_choice(0); assert_eq!(source.width, 1920); assert_eq!(source.height, 1080); assert_eq!(source.fps, 30); assert_eq!(source.max_bitrate_kbit, 12_000); state.set_capture_size_preset(0, CaptureSizePreset::P540); let compact_capture = state.capture_size_choice(0); assert_eq!(compact_capture.width, 960); assert_eq!(compact_capture.height, 540); assert_eq!(compact_capture.fps, 20); assert_eq!(compact_capture.max_bitrate_kbit, 4_000); let display = state.breakout_size_choice(0); assert_eq!(display.width, 1920); assert_eq!(display.height, 1080); state.set_breakout_size_preset(0, BreakoutSizePreset::P360); let smaller = state.breakout_size_choice(0); assert_eq!(smaller.width, 640); assert_eq!(smaller.height, 360); state.set_breakout_size_preset(0, BreakoutSizePreset::P540); let compact = state.breakout_size_choice(0); assert_eq!(compact.width, 960); assert_eq!(compact.height, 540); let capture_options = state.capture_size_options(); assert!(capture_options.len() >= 5); assert!(capture_options.iter().any(|choice| { choice.preset == CaptureSizePreset::Source && choice.width == 1920 && choice.height == 1080 })); assert!(capture_options.iter().any(|choice| { choice.preset == CaptureSizePreset::P1080 && choice.width == 1920 && choice.height == 1080 })); let breakout_options = state.breakout_size_options(); assert!(breakout_options.len() >= 5); assert!(breakout_options.iter().any(|choice| { choice.preset == BreakoutSizePreset::Source && choice.width == 1920 && choice.height == 1080 })); } #[test] fn swap_key_binding_tracks_selected_key_and_binding_mode() { let mut state = LauncherState::new(); assert_eq!(state.swap_key, "pause"); assert!(!state.swap_key_binding); let token = state.begin_swap_key_binding(); assert!(state.swap_key_binding); assert_eq!(token, state.swap_key_binding_token); state.set_swap_key("F8"); assert_eq!(state.swap_key, "f8"); state.set_swap_key(" "); assert_eq!(state.swap_key, "off"); state.finish_swap_key_binding(); assert!(!state.swap_key_binding); } #[test] fn swap_key_binding_timeout_only_cancels_matching_attempt() { let mut state = LauncherState::new(); let first = state.begin_swap_key_binding(); let second = state.begin_swap_key_binding(); assert!(!state.cancel_swap_key_binding(first)); assert!(state.swap_key_binding); assert!(state.cancel_swap_key_binding(second)); assert!(!state.swap_key_binding); } #[test] fn complete_swap_key_binding_updates_value_and_ends_binding() { let mut state = LauncherState::new(); state.begin_swap_key_binding(); state.complete_swap_key_binding("F12"); assert_eq!(state.swap_key, "f12"); assert!(!state.swap_key_binding); } #[test] fn push_note_accumulates_operator_context() { let mut state = LauncherState::new(); state.push_note("preview warm"); state.push_note("relay linked"); assert_eq!(state.notes, vec!["preview warm", "relay linked"]); } #[test] fn source_capture_profile_uses_source_fps_and_scaled_profiles_cap_it() { let mut state = LauncherState::new(); state.set_preview_source_profile(1920, 1080, 60); let source = state.capture_size_choice(0); assert_eq!(source.width, 1920); assert_eq!(source.height, 1080); assert_eq!(source.fps, 60); assert!(source.max_bitrate_kbit >= 12_000); state.set_capture_size_preset(0, CaptureSizePreset::P720); let hd = state.capture_size_choice(0); assert_eq!(hd.fps, 24); state.set_capture_size_preset(0, CaptureSizePreset::P540); let compact = state.capture_size_choice(0); assert_eq!(compact.fps, 20); state.set_capture_size_preset(0, CaptureSizePreset::P360); let small = state.capture_size_choice(0); assert_eq!(small.fps, 15); } }