fix(client): harden audio gain control
This commit is contained in:
parent
3b685415ed
commit
e33ff7e42d
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.11.45"
|
||||
version = "0.11.46"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -166,6 +166,52 @@ fn network_spread_ms(samples: &VecDeque<(Instant, f32)>) -> f32 {
|
||||
deviations[deviations.len() / 2]
|
||||
}
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
/// Apply a remote-audio gain slider update without unwinding through GTK callbacks.
|
||||
fn apply_audio_gain_change(
|
||||
scale: >k::Scale,
|
||||
state: &Rc<RefCell<LauncherState>>,
|
||||
widgets: &super::ui_components::LauncherWidgets,
|
||||
child_proc: &Rc<RefCell<Option<RelayChild>>>,
|
||||
) -> bool {
|
||||
let percent = scale
|
||||
.value()
|
||||
.round()
|
||||
.clamp(0.0, MAX_AUDIO_GAIN_PERCENT as f64) as u32;
|
||||
let label = {
|
||||
let Ok(mut state) = state.try_borrow_mut() else {
|
||||
return false;
|
||||
};
|
||||
if state.audio_gain_percent == percent {
|
||||
widgets.audio_gain_value.set_text(&state.audio_gain_label());
|
||||
return true;
|
||||
}
|
||||
state.set_audio_gain_percent(percent);
|
||||
state.audio_gain_label()
|
||||
};
|
||||
widgets.audio_gain_value.set_text(&label);
|
||||
let relay_live = child_proc
|
||||
.try_borrow()
|
||||
.map(|child| child.is_some())
|
||||
.unwrap_or(false);
|
||||
if relay_live {
|
||||
let path = audio_gain_control_path();
|
||||
match write_audio_gain_request(&path, percent) {
|
||||
Ok(()) => widgets
|
||||
.status_label
|
||||
.set_text(&format!("Remote audio gain set to {label}.")),
|
||||
Err(err) => widgets.status_label.set_text(&format!(
|
||||
"Remote audio gain set to {label} for the next relay launch, but live gain control could not be written: {err}"
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
widgets.status_label.set_text(&format!(
|
||||
"Remote audio gain set to {label} for the next relay launch."
|
||||
));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
fn request_capture_power_refresh(
|
||||
power_tx: std::sync::mpsc::Sender<PowerMessage>,
|
||||
@ -1077,34 +1123,14 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
|
||||
let child_proc = Rc::clone(&child_proc);
|
||||
let audio_gain_scale = widgets.audio_gain_scale.clone();
|
||||
audio_gain_scale.connect_value_changed(move |scale| {
|
||||
let percent = scale
|
||||
.value()
|
||||
.round()
|
||||
.clamp(0.0, MAX_AUDIO_GAIN_PERCENT as f64)
|
||||
as u32;
|
||||
let label = {
|
||||
let mut state = state.borrow_mut();
|
||||
if state.audio_gain_percent == percent {
|
||||
return;
|
||||
}
|
||||
state.set_audio_gain_percent(percent);
|
||||
state.audio_gain_label()
|
||||
};
|
||||
widgets.audio_gain_value.set_text(&label);
|
||||
if child_proc.borrow().is_some() {
|
||||
let path = audio_gain_control_path();
|
||||
match write_audio_gain_request(&path, percent) {
|
||||
Ok(()) => widgets
|
||||
.status_label
|
||||
.set_text(&format!("Remote audio gain set to {label}.")),
|
||||
Err(err) => widgets.status_label.set_text(&format!(
|
||||
"Remote audio gain set to {label} for the next relay launch, but live gain control could not be written: {err}"
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
widgets.status_label.set_text(&format!(
|
||||
"Remote audio gain set to {label} for the next relay launch."
|
||||
));
|
||||
if !apply_audio_gain_change(scale, &state, &widgets, &child_proc) {
|
||||
let scale = scale.clone();
|
||||
let state = Rc::clone(&state);
|
||||
let widgets = widgets.clone();
|
||||
let child_proc = Rc::clone(&child_proc);
|
||||
glib::idle_add_local_once(move || {
|
||||
let _ = apply_audio_gain_change(&scale, &state, &widgets, &child_proc);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 158;
|
||||
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 280;
|
||||
const EYE_PREVIEW_MIN_HEIGHT: i32 = 258;
|
||||
const EYE_PREVIEW_MIN_WIDTH: i32 = 460;
|
||||
const SIDE_LOG_HEIGHT: i32 = 124;
|
||||
const SIDE_LOG_MIN_HEIGHT: i32 = 124;
|
||||
|
||||
pub fn build_launcher_view(
|
||||
app: >k::Application,
|
||||
@ -427,7 +427,8 @@ pub fn build_launcher_view(
|
||||
relay_row.append(&server_entry);
|
||||
let start_button = gtk::Button::with_label("Connect");
|
||||
start_button.add_css_class("suggested-action");
|
||||
stabilize_button(&start_button, 92);
|
||||
start_button.set_hexpand(false);
|
||||
stabilize_button(&start_button, 108);
|
||||
relay_row.append(&start_button);
|
||||
connection_body.append(&relay_row);
|
||||
|
||||
@ -457,7 +458,7 @@ pub fn build_launcher_view(
|
||||
connection_body.append(&live_actions_row);
|
||||
|
||||
connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
||||
let power_heading = gtk::Label::new(Some("Power"));
|
||||
let power_heading = gtk::Label::new(Some("GPIO Power"));
|
||||
power_heading.add_css_class("subgroup-title");
|
||||
power_heading.set_halign(gtk::Align::Start);
|
||||
|
||||
@ -465,31 +466,38 @@ pub fn build_launcher_view(
|
||||
power_shell.set_halign(gtk::Align::Fill);
|
||||
let power_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
power_row.set_hexpand(true);
|
||||
power_heading.set_width_chars(5);
|
||||
power_heading.set_width_chars(10);
|
||||
power_row.append(&power_heading);
|
||||
let power_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
power_buttons.set_hexpand(true);
|
||||
power_buttons.set_homogeneous(true);
|
||||
let power_on_button = gtk::Button::with_label("On");
|
||||
power_on_button.set_hexpand(true);
|
||||
stabilize_button(&power_on_button, 52);
|
||||
power_on_button.add_css_class("pill-toggle");
|
||||
let power_auto_button = gtk::Button::with_label("Auto");
|
||||
power_auto_button.set_hexpand(true);
|
||||
stabilize_button(&power_auto_button, 52);
|
||||
power_auto_button.add_css_class("pill-toggle");
|
||||
let power_off_button = gtk::Button::with_label("Off");
|
||||
power_off_button.set_hexpand(true);
|
||||
stabilize_button(&power_off_button, 52);
|
||||
power_off_button.add_css_class("pill-toggle");
|
||||
let power_detail = gtk::Label::new(Some("Capture power status is loading..."));
|
||||
power_detail.add_css_class("dim-label");
|
||||
power_detail.set_wrap(true);
|
||||
power_detail.set_xalign(0.0);
|
||||
power_row.append(&power_on_button);
|
||||
power_row.append(&power_auto_button);
|
||||
power_row.append(&power_off_button);
|
||||
power_buttons.append(&power_on_button);
|
||||
power_buttons.append(&power_auto_button);
|
||||
power_buttons.append(&power_off_button);
|
||||
power_row.append(&power_buttons);
|
||||
let audio_gain_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
audio_gain_row.set_size_request(220, -1);
|
||||
audio_gain_row.set_hexpand(true);
|
||||
let audio_gain_label = gtk::Label::new(Some("Audio"));
|
||||
audio_gain_label.add_css_class("dim-label");
|
||||
audio_gain_label.set_halign(gtk::Align::Start);
|
||||
audio_gain_label.set_width_chars(5);
|
||||
audio_gain_label.set_width_chars(10);
|
||||
let audio_gain_adjustment = gtk::Adjustment::new(
|
||||
state.audio_gain_percent as f64,
|
||||
0.0,
|
||||
@ -515,15 +523,18 @@ pub fn build_launcher_view(
|
||||
power_shell.append(&power_row);
|
||||
power_shell.append(&audio_gain_row);
|
||||
connection_body.append(&power_shell);
|
||||
let routing_heading = gtk::Label::new(Some("Input"));
|
||||
let routing_heading = gtk::Label::new(Some("Inputs"));
|
||||
routing_heading.add_css_class("subgroup-title");
|
||||
routing_heading.set_halign(gtk::Align::Start);
|
||||
connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
||||
|
||||
let routing_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
routing_row.set_hexpand(true);
|
||||
routing_heading.set_width_chars(5);
|
||||
routing_heading.set_width_chars(10);
|
||||
routing_row.append(&routing_heading);
|
||||
let routing_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
routing_buttons.set_hexpand(true);
|
||||
routing_buttons.set_homogeneous(true);
|
||||
let input_toggle_button = gtk::Button::with_label("Route");
|
||||
input_toggle_button.set_hexpand(true);
|
||||
stabilize_button(&input_toggle_button, 106);
|
||||
@ -531,13 +542,18 @@ pub fn build_launcher_view(
|
||||
"Change live keyboard and mouse ownership between this machine and the remote target.",
|
||||
));
|
||||
let swap_key_button = gtk::Button::with_label("Set Swap Key");
|
||||
swap_key_button.set_hexpand(true);
|
||||
stabilize_button(&swap_key_button, 106);
|
||||
routing_row.append(&input_toggle_button);
|
||||
routing_row.append(&swap_key_button);
|
||||
routing_buttons.append(&input_toggle_button);
|
||||
routing_buttons.append(&swap_key_button);
|
||||
routing_row.append(&routing_buttons);
|
||||
connection_body.append(&routing_row);
|
||||
operations.append(&connection_panel);
|
||||
|
||||
let (diagnostics_panel, diagnostics_body) = build_panel("Diagnostics");
|
||||
diagnostics_panel.set_vexpand(true);
|
||||
diagnostics_panel.set_valign(gtk::Align::Fill);
|
||||
diagnostics_body.set_vexpand(true);
|
||||
let diagnostics_toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
diagnostics_toolbar.set_homogeneous(true);
|
||||
let diagnostics_copy_button = gtk::Button::with_label("Copy Report");
|
||||
@ -562,9 +578,8 @@ pub fn build_launcher_view(
|
||||
diagnostics_shell.append(&diagnostics_label);
|
||||
let diagnostics_scroll = gtk::ScrolledWindow::builder()
|
||||
.hexpand(true)
|
||||
.vexpand(false)
|
||||
.min_content_height(SIDE_LOG_HEIGHT)
|
||||
.max_content_height(SIDE_LOG_HEIGHT)
|
||||
.vexpand(true)
|
||||
.min_content_height(SIDE_LOG_MIN_HEIGHT)
|
||||
.child(&diagnostics_shell)
|
||||
.build();
|
||||
diagnostics_body.append(&diagnostics_toolbar);
|
||||
@ -572,7 +587,9 @@ pub fn build_launcher_view(
|
||||
operations.append(&diagnostics_panel);
|
||||
|
||||
let (console_panel, console_body) = build_panel("Session Console");
|
||||
console_panel.set_vexpand(false);
|
||||
console_panel.set_vexpand(true);
|
||||
console_panel.set_valign(gtk::Align::Fill);
|
||||
console_body.set_vexpand(true);
|
||||
let console_toolbar = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
console_toolbar.set_homogeneous(true);
|
||||
let console_copy_button = gtk::Button::with_label("Copy Log");
|
||||
@ -602,9 +619,8 @@ pub fn build_launcher_view(
|
||||
session_log_view.set_wrap_mode(gtk::WrapMode::WordChar);
|
||||
let log_scroll = gtk::ScrolledWindow::builder()
|
||||
.hexpand(true)
|
||||
.vexpand(false)
|
||||
.min_content_height(SIDE_LOG_HEIGHT)
|
||||
.max_content_height(SIDE_LOG_HEIGHT)
|
||||
.vexpand(true)
|
||||
.min_content_height(SIDE_LOG_MIN_HEIGHT)
|
||||
.child(&session_log_view)
|
||||
.build();
|
||||
console_body.append(&console_toolbar);
|
||||
@ -884,6 +900,10 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
||||
label.status-line {
|
||||
opacity: 0.9;
|
||||
}
|
||||
label.eye-inline-status {
|
||||
font-size: 0.86rem;
|
||||
opacity: 0.82;
|
||||
}
|
||||
textview.status-log,
|
||||
label.status-log {
|
||||
font-family: monospace;
|
||||
@ -1280,15 +1300,6 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
stack.set_visible_child_name("preview");
|
||||
root.append(&stack);
|
||||
|
||||
let stream_status = gtk::Label::new(Some("Connect relay to preview."));
|
||||
stream_status.add_css_class("status-line");
|
||||
stream_status.set_halign(gtk::Align::Start);
|
||||
stream_status.set_hexpand(true);
|
||||
stream_status.set_ellipsize(pango::EllipsizeMode::End);
|
||||
stream_status.set_single_line_mode(true);
|
||||
stream_status.set_max_width_chars(24);
|
||||
stream_status.set_tooltip_text(Some("Connect relay to preview."));
|
||||
root.append(&stream_status);
|
||||
let feed_source_combo = gtk::ComboBoxText::new();
|
||||
feed_source_combo.set_tooltip_text(Some(
|
||||
"Choose which physical eye feed appears in this pane. Off disables the pane; the opposite-eye option mirrors the other physical feed while preserving a separate stream load for realistic validation.",
|
||||
@ -1310,6 +1321,17 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
let action_button = gtk::Button::with_label("Break Out");
|
||||
stabilize_button(&action_button, 104);
|
||||
action_button.set_halign(gtk::Align::End);
|
||||
let stream_status = gtk::Label::new(Some("Connect relay to preview."));
|
||||
stream_status.add_css_class("status-line");
|
||||
stream_status.add_css_class("eye-inline-status");
|
||||
stream_status.set_halign(gtk::Align::Fill);
|
||||
stream_status.set_valign(gtk::Align::Center);
|
||||
stream_status.set_hexpand(true);
|
||||
stream_status.set_ellipsize(pango::EllipsizeMode::End);
|
||||
stream_status.set_single_line_mode(true);
|
||||
stream_status.set_width_chars(12);
|
||||
stream_status.set_max_width_chars(18);
|
||||
stream_status.set_tooltip_text(Some("Connect relay to preview."));
|
||||
let footer_shell = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
let controls_grid = gtk::Grid::new();
|
||||
controls_grid.set_column_spacing(8);
|
||||
@ -1322,9 +1344,10 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
capture_row.set_hexpand(true);
|
||||
breakout_row.set_hexpand(true);
|
||||
controls_grid.attach(&feed_row, 0, 0, 1, 1);
|
||||
controls_grid.attach(&capture_row, 1, 0, 1, 1);
|
||||
controls_grid.attach(&capture_row, 1, 0, 2, 1);
|
||||
controls_grid.attach(&breakout_row, 0, 1, 1, 1);
|
||||
controls_grid.attach(&action_button, 1, 1, 1, 1);
|
||||
controls_grid.attach(&stream_status, 1, 1, 1, 1);
|
||||
controls_grid.attach(&action_button, 2, 1, 1, 1);
|
||||
footer_shell.append(&controls_grid);
|
||||
root.append(&footer_shell);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.11.45"
|
||||
version = "0.11.46"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -98,12 +98,12 @@
|
||||
"client/src/launcher/ui.rs": {
|
||||
"clippy_warnings": 62,
|
||||
"doc_debt": 23,
|
||||
"loc": 2341
|
||||
"loc": 2367
|
||||
},
|
||||
"client/src/launcher/ui_components.rs": {
|
||||
"clippy_warnings": 16,
|
||||
"doc_debt": 15,
|
||||
"loc": 1349
|
||||
"loc": 1372
|
||||
},
|
||||
"client/src/launcher/ui_runtime.rs": {
|
||||
"clippy_warnings": 62,
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
},
|
||||
"client/src/launcher/ui.rs": {
|
||||
"line_percent": 100.0,
|
||||
"loc": 2341
|
||||
"loc": 2367
|
||||
},
|
||||
"client/src/layout.rs": {
|
||||
"line_percent": 97.73,
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.11.45"
|
||||
version = "0.11.46"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -44,6 +44,16 @@ fn eye_panes_keep_the_locked_larger_preview_footprint() {
|
||||
|| UI_SRC.contains("capture_label.set_halign(gtk::Align::End)")
|
||||
);
|
||||
assert!(UI_SRC.contains("capture_label.set_ellipsize(pango::EllipsizeMode::Start);"));
|
||||
assert!(!UI_SRC.contains("root.append(&stream_status);"));
|
||||
assert!(UI_SRC.contains("stream_status.add_css_class(\"eye-inline-status\");"));
|
||||
assert!(
|
||||
source_index("controls_grid.attach(&breakout_row, 0, 1, 1, 1);")
|
||||
< source_index("controls_grid.attach(&stream_status, 1, 1, 1, 1);")
|
||||
);
|
||||
assert!(
|
||||
source_index("controls_grid.attach(&stream_status, 1, 1, 1, 1);")
|
||||
< source_index("controls_grid.attach(&action_button, 2, 1, 1, 1);")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -74,20 +84,21 @@ fn device_testing_keeps_webcam_and_mic_playback_as_equal_bottom_columns() {
|
||||
|
||||
#[test]
|
||||
fn operations_column_fills_height_and_splits_extra_space_between_logs() {
|
||||
assert_eq!(const_i32("SIDE_LOG_HEIGHT"), 124);
|
||||
assert_eq!(const_i32("SIDE_LOG_MIN_HEIGHT"), 124);
|
||||
assert!(UI_SRC.contains("operations.set_vexpand(true);"));
|
||||
assert!(UI_SRC.contains("operations.set_valign(gtk::Align::Fill);"));
|
||||
assert!(UI_SRC.contains("diagnostics_panel.set_vexpand(true);"));
|
||||
assert!(UI_SRC.contains("console_panel.set_vexpand(true);"));
|
||||
assert_eq!(
|
||||
UI_SRC
|
||||
.matches(".min_content_height(SIDE_LOG_HEIGHT)")
|
||||
.matches(".min_content_height(SIDE_LOG_MIN_HEIGHT)")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
UI_SRC
|
||||
.matches(".max_content_height(SIDE_LOG_HEIGHT)")
|
||||
.count(),
|
||||
2
|
||||
UI_SRC.matches(".max_content_height(SIDE_LOG").count(),
|
||||
0,
|
||||
"the docked logs must be allowed to split extra right-rail height"
|
||||
);
|
||||
}
|
||||
|
||||
@ -97,6 +108,7 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
||||
assert!(UI_SRC.contains("let relay_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);"));
|
||||
assert!(UI_SRC.contains("relay_row.append(&server_entry);"));
|
||||
assert!(UI_SRC.contains("let start_button = gtk::Button::with_label(\"Connect\");"));
|
||||
assert!(UI_SRC.contains("stabilize_button(&start_button, 108);"));
|
||||
assert!(UI_SRC.contains("relay_row.append(&start_button);"));
|
||||
assert!(
|
||||
source_index("relay_row.append(&server_entry);")
|
||||
@ -108,8 +120,9 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
||||
fn remote_audio_gain_control_stays_in_the_operations_rail() {
|
||||
assert!(!UI_SRC.contains("Remote Audio"));
|
||||
assert!(UI_SRC.contains("let power_shell = gtk::Box::new(gtk::Orientation::Vertical, 6);"));
|
||||
assert!(UI_SRC.contains("let power_heading = gtk::Label::new(Some(\"Power\"));"));
|
||||
assert!(UI_SRC.contains("let power_heading = gtk::Label::new(Some(\"GPIO Power\"));"));
|
||||
assert!(UI_SRC.contains("power_row.append(&power_heading);"));
|
||||
assert!(UI_SRC.contains("power_buttons.set_homogeneous(true);"));
|
||||
assert!(UI_SRC.contains("let audio_gain_scale ="));
|
||||
assert!(UI_SRC.contains("audio_gain_scale.set_draw_value(false);"));
|
||||
assert!(UI_SRC.contains("audio_gain_value.set_width_chars(5);"));
|
||||
@ -123,11 +136,12 @@ fn remote_audio_gain_control_stays_in_the_operations_rail() {
|
||||
"the operations rail should not gain extra vertical sections that stretch the lower layout"
|
||||
);
|
||||
assert!(
|
||||
source_index("let power_heading = gtk::Label::new(Some(\"Power\"));")
|
||||
source_index("let power_heading = gtk::Label::new(Some(\"GPIO Power\"));")
|
||||
< source_index("let audio_gain_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);")
|
||||
);
|
||||
assert!(
|
||||
source_index("power_shell.append(&audio_gain_row);")
|
||||
< source_index("let routing_heading = gtk::Label::new(Some(\"Input\"));")
|
||||
< source_index("let routing_heading = gtk::Label::new(Some(\"Inputs\"));")
|
||||
);
|
||||
assert!(UI_SRC.contains("routing_buttons.set_homogeneous(true);"));
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
//! video, and input streams must not keep running as leaked child processes.
|
||||
|
||||
const UI_RUNTIME_SRC: &str = include_str!("../../client/src/launcher/ui_runtime.rs");
|
||||
const UI_SRC: &str = include_str!("../../client/src/launcher/ui.rs");
|
||||
const LAUNCHER_MOD_SRC: &str = include_str!("../../client/src/launcher/mod.rs");
|
||||
const MAIN_SRC: &str = include_str!("../../client/src/main.rs");
|
||||
|
||||
@ -32,3 +33,12 @@ fn relay_address_entry_is_locked_while_relay_is_live() {
|
||||
assert!(UI_RUNTIME_SRC.contains("\"Connect\""));
|
||||
assert!(UI_RUNTIME_SRC.contains("\"Disconnect\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_gain_slider_callback_never_panics_on_refresh_reentry() {
|
||||
assert!(UI_SRC.contains("fn apply_audio_gain_change("));
|
||||
assert!(UI_SRC.contains("state.try_borrow_mut()"));
|
||||
assert!(UI_SRC.contains("return false;"));
|
||||
assert!(UI_SRC.contains("glib::idle_add_local_once"));
|
||||
assert!(!UI_SRC.contains("let mut state = state.borrow_mut();\n if state.audio_gain_percent == percent"));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user