ui: add eye health chips
This commit is contained in:
parent
628b506b64
commit
5a5990d593
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.23"
|
||||
version = "0.22.24"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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('_', " ");
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 [
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.23"
|
||||
version = "0.22.24"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.23"
|
||||
version = "0.22.24"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user