From 5a5990d593b91922ba54a7bb4373170eb51bd3dd Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 13 May 2026 12:32:45 -0300 Subject: [PATCH] ui: add eye health chips --- Cargo.lock | 6 +- client/Cargo.toml | 2 +- client/src/launcher/tests/ui_runtime.rs | 99 +++++++++++++++++-- client/src/launcher/ui_components.rs | 4 + .../launcher/ui_components/assemble_view.rs | 4 + .../launcher/ui_components/build_contexts.rs | 4 + .../src/launcher/ui_components/build_shell.rs | 15 ++- .../launcher/ui_components/combo_helpers.rs | 2 +- .../src/launcher/ui_components/panel_chips.rs | 11 ++- client/src/launcher/ui_components/types.rs | 6 +- .../src/launcher/ui_runtime/status_details.rs | 96 ++++++++++++++---- .../src/launcher/ui_runtime/status_refresh.rs | 28 +++++- common/Cargo.toml | 2 +- server/Cargo.toml | 2 +- .../client_launcher_layout_contract.rs | 10 +- .../client_launcher_runtime_contract.rs | 8 +- 16 files changed, 246 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d36c51d..8f10e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.22.23" +version = "0.22.24" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.22.23" +version = "0.22.24" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.22.23" +version = "0.22.24" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 540d1be..a6c768b 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.22.23" +version = "0.22.24" edition = "2024" [dependencies] diff --git a/client/src/launcher/tests/ui_runtime.rs b/client/src/launcher/tests/ui_runtime.rs index 889c4dd..066490e 100644 --- a/client/src/launcher/tests/ui_runtime.rs +++ b/client/src/launcher/tests/ui_runtime.rs @@ -1,6 +1,7 @@ use super::*; use crate::launcher::{ devices::{CameraMode, DeviceCatalog}, + diagnostics::PerformanceSample, preview::PreviewBinding, state::{ BreakoutSizePreset, LauncherState, PreviewSourceSize, UpstreamAudioTransport, @@ -45,19 +46,101 @@ fn local_test_detail_mentions_idle_and_running_modes() { #[test] fn gpio_power_label_tracks_detected_devices() { let mut power = CapturePowerStatus::default(); - assert_eq!(gpio_power_label(&power), "Unavailable"); + assert_eq!(gpio_power_label(&power), "Off"); power.available = true; - assert_eq!(gpio_power_label(&power), "Power Off"); + assert_eq!(gpio_power_label(&power), "Off"); power.enabled = true; - assert_eq!(gpio_power_label(&power), "No Eyes"); + assert_eq!(gpio_power_label(&power), "On"); power.detected_devices = 1; - assert_eq!(gpio_power_label(&power), "1 Eye"); + assert_eq!(gpio_power_label(&power), "On"); power.detected_devices = 2; - assert_eq!(gpio_power_label(&power), "2 Eyes"); + assert_eq!(gpio_power_label(&power), "On"); +} + +fn eye_health_sample() -> PerformanceSample { + PerformanceSample { + rtt_ms: 20.0, + probe_spread_ms: 2.0, + input_latency_ms: 12.0, + probe_loss_pct: 0.0, + client_process_cpu_pct: 10.0, + server_process_cpu_pct: 8.0, + video_loss_pct: 0.0, + left_receive_fps: 30.0, + left_present_fps: 30.0, + left_server_fps: 30.0, + left_stream_spread_ms: 3.0, + left_packet_gap_peak_ms: 20.0, + left_present_gap_peak_ms: 20.0, + left_queue_depth: 0, + left_queue_peak: 0, + left_server_source_gap_peak_ms: 20.0, + left_server_send_gap_peak_ms: 20.0, + left_server_queue_peak: 0, + left_server_encoder_label: "source-pass-through".to_string(), + left_decoder_label: "nvidia-h264".to_string(), + left_stream_caps_label: "video/x-h264".to_string(), + left_decoded_caps_label: "video/x-raw".to_string(), + left_rendered_caps_label: "video/x-raw".to_string(), + right_receive_fps: 30.0, + right_present_fps: 30.0, + right_server_fps: 30.0, + right_stream_spread_ms: 3.0, + right_packet_gap_peak_ms: 20.0, + right_present_gap_peak_ms: 20.0, + right_queue_depth: 0, + right_queue_peak: 0, + right_server_source_gap_peak_ms: 20.0, + right_server_send_gap_peak_ms: 20.0, + right_server_queue_peak: 0, + right_server_encoder_label: "source-pass-through".to_string(), + right_decoder_label: "nvidia-h264".to_string(), + right_stream_caps_label: "video/x-h264".to_string(), + right_decoded_caps_label: "video/x-raw".to_string(), + right_rendered_caps_label: "video/x-raw".to_string(), + upstream_camera: UpstreamStreamTelemetry::default(), + upstream_microphone: UpstreamStreamTelemetry::default(), + upstream_mode: "bundled-webcam-media".to_string(), + dropped_frames: 0, + queue_depth: 0, + } +} + +#[test] +fn eye_stream_chip_reports_per_eye_fps_quality() { + let sample = eye_health_sample(); + assert_eq!( + eye_stream_health(Some(&sample), EyeChipSide::Left, false), + ( + StatusLightState::Idle, + "Off".to_string(), + "Relay is not connected.".to_string() + ) + ); + + let (state, label, tooltip) = eye_stream_health(Some(&sample), EyeChipSide::Left, true); + assert_eq!(state, StatusLightState::Connected); + assert_eq!(label, "30 fps"); + assert!(tooltip.contains("present=30.0 fps")); + + let mut degraded = sample.clone(); + degraded.right_present_fps = 20.0; + degraded.right_queue_peak = 3; + let (state, label, tooltip) = eye_stream_health(Some(°raded), EyeChipSide::Right, true); + assert_eq!(state, StatusLightState::Caution); + assert_eq!(label, "20 fps"); + assert!(tooltip.contains("queue=0/3")); + + let mut broken = sample; + broken.left_receive_fps = 0.0; + broken.left_present_fps = 0.0; + let (state, label, _) = eye_stream_health(Some(&broken), EyeChipSide::Left, true); + assert_eq!(state, StatusLightState::Warning); + assert_eq!(label, "0 fps"); } #[gtk::test] @@ -624,7 +707,7 @@ fn uac_chip_uses_live_microphone_flow_not_only_server_caps() { assert_eq!( recovery_uac_health(&state, false, None), - (StatusLightState::Live, "Opus".to_string()) + (StatusLightState::Live, "PCM".to_string()) ); assert_eq!( recovery_uac_health(&state, true, None), @@ -642,7 +725,7 @@ fn uac_chip_uses_live_microphone_flow_not_only_server_caps() { }; assert_eq!( recovery_uac_health(&state, true, Some(&healthy)), - (StatusLightState::Live, "Opus".to_string()) + (StatusLightState::Live, "PCM".to_string()) ); state.set_server_media_caps(None, None, None, None); @@ -652,7 +735,7 @@ fn uac_chip_uses_live_microphone_flow_not_only_server_caps() { ); assert_eq!( recovery_uac_health(&state, true, Some(&healthy)), - (StatusLightState::Live, "Opus".to_string()) + (StatusLightState::Live, "PCM".to_string()) ); state.select_upstream_audio_transport(UpstreamAudioTransport::Pcm); diff --git a/client/src/launcher/ui_components.rs b/client/src/launcher/ui_components.rs index 7387cee..c93b471 100644 --- a/client/src/launcher/ui_components.rs +++ b/client/src/launcher/ui_components.rs @@ -41,6 +41,10 @@ pub fn build_launcher_view( routing_value, gpio_light, gpio_value, + left_eye_light, + left_eye_value, + right_eye_light, + right_eye_value, usb_light, usb_value, uac_light, diff --git a/client/src/launcher/ui_components/assemble_view.rs b/client/src/launcher/ui_components/assemble_view.rs index 8e49cc5..821c462 100644 --- a/client/src/launcher/ui_components/assemble_view.rs +++ b/client/src/launcher/ui_components/assemble_view.rs @@ -110,6 +110,10 @@ routing_value, gpio_light, gpio_value, + left_eye_light, + left_eye_value, + right_eye_light, + right_eye_value, usb_light, usb_value, uac_light, diff --git a/client/src/launcher/ui_components/build_contexts.rs b/client/src/launcher/ui_components/build_contexts.rs index 462f81b..0f038ff 100644 --- a/client/src/launcher/ui_components/build_contexts.rs +++ b/client/src/launcher/ui_components/build_contexts.rs @@ -11,6 +11,10 @@ struct LauncherShellContext { routing_value: gtk::Label, gpio_light: gtk::Box, gpio_value: gtk::Label, + left_eye_light: gtk::Box, + left_eye_value: gtk::Label, + right_eye_light: gtk::Box, + right_eye_value: gtk::Label, usb_light: gtk::Box, usb_value: gtk::Label, uac_light: gtk::Box, diff --git a/client/src/launcher/ui_components/build_shell.rs b/client/src/launcher/ui_components/build_shell.rs index 4163de0..f0ecba4 100644 --- a/client/src/launcher/ui_components/build_shell.rs +++ b/client/src/launcher/ui_components/build_shell.rs @@ -49,14 +49,20 @@ let (relay_chip, relay_light, relay_value) = build_status_chip_with_light("Server", ""); let (routing_chip, routing_light, routing_value) = build_status_chip_with_light("Inputs", "Local"); - let (gpio_chip, gpio_light, gpio_value) = build_status_chip_with_light("GPIO", "Unknown"); + let (gpio_chip, gpio_light, gpio_value) = build_status_chip_with_light("GPIO", "Off"); + let (left_eye_chip, left_eye_light, left_eye_value) = + build_status_chip_with_light("Left", "Off"); + let (right_eye_chip, right_eye_light, right_eye_value) = + build_status_chip_with_light("Right", "Off"); let (usb_chip, usb_light, usb_value) = build_status_chip_with_light("HID", "Unknown"); let (uac_chip, uac_light, uac_value) = build_status_chip_with_light("UAC", "Unknown"); let (uvc_chip, uvc_light, uvc_value) = build_status_chip_with_light("UVC", "Unknown"); - let (shortcut_chip, shortcut_value) = build_status_chip("Swap Key", "Pause"); + let (shortcut_chip, shortcut_value) = build_status_chip("Key", "Pause"); chips.append(&relay_chip); chips.append(&routing_chip); chips.append(&gpio_chip); + chips.append(&left_eye_chip); + chips.append(&right_eye_chip); chips.append(&usb_chip); chips.append(&uac_chip); chips.append(&uvc_chip); @@ -70,6 +76,7 @@ chips_shell.set_has_frame(false); chips_shell.set_propagate_natural_width(false); chips_shell.set_min_content_width(0); + chips_shell.set_size_request(0, -1); hero.append(&chips_shell); root.append(&hero); @@ -121,6 +128,10 @@ routing_value, gpio_light, gpio_value, + left_eye_light, + left_eye_value, + right_eye_light, + right_eye_value, usb_light, usb_value, uac_light, diff --git a/client/src/launcher/ui_components/combo_helpers.rs b/client/src/launcher/ui_components/combo_helpers.rs index a308fe4..f90a47b 100644 --- a/client/src/launcher/ui_components/combo_helpers.rs +++ b/client/src/launcher/ui_components/combo_helpers.rs @@ -294,7 +294,7 @@ fn human_audio_node_label(value: &str) -> String { } } -const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 24; +const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 18; fn shorten_label_with_limit(value: &str, max: usize) -> String { let compact = value.replace('_', " "); diff --git a/client/src/launcher/ui_components/panel_chips.rs b/client/src/launcher/ui_components/panel_chips.rs index e2af709..7d344d6 100644 --- a/client/src/launcher/ui_components/panel_chips.rs +++ b/client/src/launcher/ui_components/panel_chips.rs @@ -47,12 +47,15 @@ fn build_subgroup_with_action(title: &str, action: Option<>k::Widget>) -> gtk: group } +const STATUS_CHIP_WIDTH: i32 = 84; +const STATUS_CHIP_VALUE_CHARS: i32 = 8; + /// Build a fixed-width status chip so changing labels do not move the header row. fn build_status_chip(label: &str, value: &str) -> (gtk::Box, gtk::Label) { let chip = gtk::Box::new(gtk::Orientation::Vertical, 4); chip.add_css_class("status-chip"); chip.set_hexpand(false); - chip.set_size_request(96, -1); + chip.set_size_request(STATUS_CHIP_WIDTH, -1); let label_widget = gtk::Label::new(Some(label)); label_widget.add_css_class("status-chip-label"); @@ -64,7 +67,7 @@ fn build_status_chip(label: &str, value: &str) -> (gtk::Box, gtk::Label) { value_widget.set_xalign(0.5); value_widget.set_ellipsize(pango::EllipsizeMode::End); value_widget.set_single_line_mode(true); - value_widget.set_width_chars(9); + value_widget.set_width_chars(STATUS_CHIP_VALUE_CHARS); chip.append(&label_widget); chip.append(&value_widget); (chip, value_widget) @@ -75,7 +78,7 @@ fn build_status_chip_with_light(label: &str, value: &str) -> (gtk::Box, gtk::Box let chip = gtk::Box::new(gtk::Orientation::Vertical, 4); chip.add_css_class("status-chip"); chip.set_hexpand(false); - chip.set_size_request(96, -1); + chip.set_size_request(STATUS_CHIP_WIDTH, -1); let meta = gtk::Box::new(gtk::Orientation::Horizontal, 6); meta.add_css_class("status-chip-meta"); @@ -95,7 +98,7 @@ fn build_status_chip_with_light(label: &str, value: &str) -> (gtk::Box, gtk::Box value_widget.set_xalign(0.5); value_widget.set_ellipsize(pango::EllipsizeMode::End); value_widget.set_single_line_mode(true); - value_widget.set_width_chars(9); + value_widget.set_width_chars(STATUS_CHIP_VALUE_CHARS); chip.append(&meta); chip.append(&value_widget); (chip, light, value_widget) diff --git a/client/src/launcher/ui_components/types.rs b/client/src/launcher/ui_components/types.rs index ec4df96..1aeccd8 100644 --- a/client/src/launcher/ui_components/types.rs +++ b/client/src/launcher/ui_components/types.rs @@ -6,6 +6,10 @@ pub struct SummaryWidgets { pub routing_value: gtk::Label, pub gpio_light: gtk::Box, pub gpio_value: gtk::Label, + pub left_eye_light: gtk::Box, + pub left_eye_value: gtk::Label, + pub right_eye_light: gtk::Box, + pub right_eye_value: gtk::Label, pub usb_light: gtk::Box, pub usb_value: gtk::Label, pub uac_light: gtk::Box, @@ -217,7 +221,7 @@ const LAUNCHER_DEFAULT_WIDTH: i32 = 1540; const LAUNCHER_DEFAULT_HEIGHT: i32 = 880; const OPERATIONS_RAIL_WIDTH: i32 = 276; const RELAY_SUBGROUP_LABEL_WIDTH: i32 = 11; -const DEVICE_STAGING_PANEL_WIDTH: i32 = 560; +const DEVICE_STAGING_PANEL_WIDTH: i32 = 512; const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 270; const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 480; const EYE_PREVIEW_MIN_HEIGHT: i32 = 299; diff --git a/client/src/launcher/ui_runtime/status_details.rs b/client/src/launcher/ui_runtime/status_details.rs index 3c06c97..0baca59 100644 --- a/client/src/launcher/ui_runtime/status_details.rs +++ b/client/src/launcher/ui_runtime/status_details.rs @@ -1,14 +1,8 @@ pub fn gpio_power_label(power: &CapturePowerStatus) -> String { - if !power.available { - return "Unavailable".to_string(); - } - if !power.enabled { - return "Power Off".to_string(); - } - match power.detected_devices { - 0 => "No Eyes".to_string(), - 1 => "1 Eye".to_string(), - count => format!("{count} Eyes"), + if power.available && power.enabled { + "On".to_string() + } else { + "Off".to_string() } } @@ -286,14 +280,7 @@ fn media_stream_health( } fn gpio_light_state(power: &CapturePowerStatus) -> StatusLightState { - if !power.available || !power.enabled { - return StatusLightState::Idle; - } - match power.detected_devices { - 0 => StatusLightState::Warning, - 1 => StatusLightState::Caution, - _ => StatusLightState::Live, - } + StatusLightState::from_active(power.available && power.enabled) } fn gpio_detection_detail(detected_devices: u32) -> String { @@ -304,6 +291,79 @@ fn gpio_detection_detail(detected_devices: u32) -> String { } } +#[derive(Clone, Copy)] +enum EyeChipSide { + Left, + Right, +} + +fn eye_stream_health( + sample: Option<&crate::launcher::diagnostics::PerformanceSample>, + side: EyeChipSide, + relay_live: bool, +) -> (StatusLightState, String, String) { + if !relay_live { + return ( + StatusLightState::Idle, + "Off".to_string(), + "Relay is not connected.".to_string(), + ); + } + let Some(sample) = sample else { + return ( + StatusLightState::Warning, + "0 fps".to_string(), + "No eye telemetry sample is available yet.".to_string(), + ); + }; + let (receive_fps, present_fps, server_fps, queue_depth, queue_peak, packet_gap, present_gap) = + match side { + EyeChipSide::Left => ( + sample.left_receive_fps, + sample.left_present_fps, + sample.left_server_fps, + sample.left_queue_depth, + sample.left_queue_peak, + sample.left_packet_gap_peak_ms, + sample.left_present_gap_peak_ms, + ), + EyeChipSide::Right => ( + sample.right_receive_fps, + sample.right_present_fps, + sample.right_server_fps, + sample.right_queue_depth, + sample.right_queue_peak, + sample.right_packet_gap_peak_ms, + sample.right_present_gap_peak_ms, + ), + }; + let label = format!("{:.0} fps", present_fps.max(0.0)); + let target_fps = server_fps.max(receive_fps).max(1.0); + let ratio = (present_fps / target_fps).clamp(0.0, 1.0); + let has_glitch = sample.video_loss_pct > 0.1 + || sample.dropped_frames > 0 + || queue_depth > 0 + || queue_peak > 2 + || packet_gap > 90.0 + || present_gap > 90.0; + let state = if present_fps < 1.0 || receive_fps < 1.0 { + StatusLightState::Warning + } else if ratio >= 0.98 && !has_glitch { + StatusLightState::Connected + } else if ratio >= 0.90 && sample.video_loss_pct < 1.0 { + StatusLightState::Live + } else if ratio >= 0.65 { + StatusLightState::Caution + } else { + StatusLightState::Warning + }; + let tooltip = format!( + "present={present_fps:.1} fps receive={receive_fps:.1} fps server={server_fps:.1} fps loss={:.1}% dropped={} queue={}/{} gaps={packet_gap:.0}/{present_gap:.0} ms", + sample.video_loss_pct, sample.dropped_frames, queue_depth, queue_peak + ); + (state, label, tooltip) +} + /// Highlights the currently active capture mode so it reads like a segmented control. fn sync_power_mode_button_styles(widgets: &LauncherWidgets, mode: &str) { for button in [ diff --git a/client/src/launcher/ui_runtime/status_refresh.rs b/client/src/launcher/ui_runtime/status_refresh.rs index 8aa074e..415c3ae 100644 --- a/client/src/launcher/ui_runtime/status_refresh.rs +++ b/client/src/launcher/ui_runtime/status_refresh.rs @@ -68,7 +68,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi let gpio_label = if state.server_available { gpio_power_label(&state.capture_power) } else { - "Offline".to_string() + "Off".to_string() }; set_status_light( &widgets.summary.gpio_light, @@ -79,12 +79,32 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi }, ); widgets.summary.gpio_value.set_text(&gpio_label); - widgets.summary.gpio_value.set_tooltip_text(Some(&gpio_label)); + widgets + .summary + .gpio_value + .set_tooltip_text(Some(&capture_power_detail(&state.capture_power))); widgets .summary .shortcut_value .set_text(&toggle_key_label(&state.swap_key)); + let (left_eye_state, left_eye_value, left_eye_tooltip) = + eye_stream_health(latest_sample.as_ref(), EyeChipSide::Left, relay_live); + set_status_light(&widgets.summary.left_eye_light, left_eye_state); + widgets.summary.left_eye_value.set_text(&left_eye_value); + widgets + .summary + .left_eye_value + .set_tooltip_text(Some(&left_eye_tooltip)); + let (right_eye_state, right_eye_value, right_eye_tooltip) = + eye_stream_health(latest_sample.as_ref(), EyeChipSide::Right, relay_live); + set_status_light(&widgets.summary.right_eye_light, right_eye_state); + widgets.summary.right_eye_value.set_text(&right_eye_value); + widgets + .summary + .right_eye_value + .set_tooltip_text(Some(&right_eye_tooltip)); + let (usb_state, usb_value) = recovery_usb_health(state, relay_live); set_status_light(&widgets.summary.usb_light, usb_state); widgets.summary.usb_value.set_text(&usb_value); @@ -285,9 +305,9 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi widgets .upstream_audio_transport_combo .set_tooltip_text(Some(if relay_live { - "Changing upstream audio transport restarts the microphone path; Opus compresses, PCM is the fallback." + "Changing upstream audio transport restarts the microphone path; PCM is stable, Opus is compressed and experimental." } else { - "Choose Opus for compressed upstream audio or PCM as the known-good fallback." + "Choose PCM for the known-good route or Opus for compressed upstream audio experiments." })); widgets.input_toggle_button.set_label(match state.routing { InputRouting::Remote => "Route Local", diff --git a/common/Cargo.toml b/common/Cargo.toml index 8450673..0e271e3 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.22.23" +version = "0.22.24" edition = "2024" build = "build.rs" diff --git a/server/Cargo.toml b/server/Cargo.toml index dd04156..dcff47a 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.22.23" +version = "0.22.24" edition = "2024" autobins = false diff --git a/tests/ui/client/launcher/client_launcher_layout_contract.rs b/tests/ui/client/launcher/client_launcher_layout_contract.rs index 4a4e1e8..29d9541 100644 --- a/tests/ui/client/launcher/client_launcher_layout_contract.rs +++ b/tests/ui/client/launcher/client_launcher_layout_contract.rs @@ -158,7 +158,7 @@ fn eye_and_device_combos_use_compact_labels_so_the_rail_stays_visible() { assert!(UI_LAYOUT_SRC.contains("combo.set_popup_fixed_width(false);")); assert!(UI_LAYOUT_SRC.contains("fn combo_active_full_label(")); assert!(UI_LAYOUT_SRC.contains("Selected: {full}. Open the dropdown for compact choices")); - assert!(UI_LAYOUT_SRC.contains("const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 24;")); + assert!(UI_LAYOUT_SRC.contains("const DEVICE_COMBO_BUTTON_LABEL_LIMIT: usize = 18;")); assert!(UI_LAYOUT_SRC.contains("append_input_choice(combo, value);")); assert!( UI_LAYOUT_SRC.contains("shorten_label_with_limit(&label, DEVICE_COMBO_BUTTON_LABEL_LIMIT)") @@ -189,7 +189,7 @@ fn eye_and_device_combos_use_compact_labels_so_the_rail_stays_visible() { fn device_staging_and_testing_stay_independent_so_preview_does_not_fill_dead_height() { assert!(UI_LAYOUT_SRC.contains("staging_row.set_homogeneous(false);")); assert!(UI_LAYOUT_SRC.contains("staging_row.set_vexpand(false);")); - assert_eq!(const_i32("DEVICE_STAGING_PANEL_WIDTH"), 560); + assert_eq!(const_i32("DEVICE_STAGING_PANEL_WIDTH"), 512); assert!(UI_LAYOUT_SRC.contains("devices_panel.set_hexpand(false);")); assert!( UI_LAYOUT_SRC.contains("devices_panel.set_size_request(DEVICE_STAGING_PANEL_WIDTH, -1);") @@ -324,13 +324,15 @@ fn status_chip_text_is_centered_inside_each_pill() { UI_LAYOUT_SRC .matches("set_halign(gtk::Align::Center);") .count() - >= 5, + >= 7, "chip labels and values should be horizontally centered" ); assert!( - UI_LAYOUT_SRC.matches("set_xalign(0.5);").count() >= 4, + UI_LAYOUT_SRC.matches("set_xalign(0.5);").count() >= 6, "chip text lines should center their own text, not only their widgets" ); + assert!(UI_LAYOUT_SRC.contains("build_status_chip_with_light(\"Left\", \"Off\")")); + assert!(UI_LAYOUT_SRC.contains("build_status_chip_with_light(\"Right\", \"Off\")")); } #[test] diff --git a/tests/ui/client/launcher/client_launcher_runtime_contract.rs b/tests/ui/client/launcher/client_launcher_runtime_contract.rs index 8a20162..a6f8494 100644 --- a/tests/ui/client/launcher/client_launcher_runtime_contract.rs +++ b/tests/ui/client/launcher/client_launcher_runtime_contract.rs @@ -288,11 +288,9 @@ fn launcher_audio_transport_selection_reaches_relay_env_and_chips() { assert!( UI_RUNTIME_SRC.contains("Changing upstream audio transport restarts the microphone path") ); - assert!( - UI_RUNTIME_SRC.contains( - "Choose Opus for compressed upstream audio or PCM as the known-good fallback." - ) - ); + assert!(UI_RUNTIME_SRC.contains( + "Choose PCM for the known-good route or Opus for compressed upstream audio experiments." + )); assert!(UI_SRC.contains("audio_combo.connect_changed")); assert!(UI_SRC.contains("toggle.connect_toggled")); }