I/O menus: the search finds the hardware inputs, the main output and no route, not just buses (#1491)

Co-Authored-By: Patrick Istefanidis <smallroomproductionsfl@gmail.com>
This commit is contained in:
istefp
2026-11-11 05:21:27 +00:00
co-authored by Patrick Istefanidis
parent dc4f82ea9f
commit 2c042c599f
3 changed files with 57 additions and 24 deletions
+30 -19
View File
@@ -25,34 +25,45 @@ pub(crate) fn route_popup(app: &mut SoundApp, resp: &egui::Response, id: TrackId
egui::Popup::menu(resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| route_menu(app, ui, id, input));
}
/// Input or output menu: no route and buses on both, plus the main output (outputs) or inputs 1-8 (inputs).
/// Input or output menu: no route and buses on both, plus the main output (outputs) or inputs 1-8
/// (inputs). Its search field takes typing at once and finds any of them, not just the buses.
fn route_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId, input: bool) {
let key = if input { "input" } else { "output" };
let cmd = if input { "track.input" } else { "track.output" };
if ui.button(if input { "no input" } else { "no output" }).clicked() {
let _ = app.run(cmd, json!({"track": id.0, key: "none"}));
ui.close();
}
let hint = if input { "Search inputs…" } else { "Search outputs…" };
let search = MenuSearch::show(ui, egui::Id::unique(("route-search", id.0, input)), hint);
let busses: Vec<String> = app.engine.session().busses.iter().map(|b| b.name.clone()).collect();
let main_name = app.engine.session().outputs.first().map_or_else(|| "Out 1-2".to_string(), |o| o.name.clone());
if !input && ui.button(main_name).clicked() {
let _ = app.run(cmd, json!({"track": id.0, key: "main"}));
ui.close();
}
// (label, the route the command is given)
let mut fixed = vec![(if input { "no input" } else { "no output" }.to_string(), "none".to_string())];
if input {
for i in 1..=8 {
if ui.button(format!("In {i}")).clicked() {
let _ = app.run(cmd, json!({"track": id.0, key: format!("In {i}")}));
ui.close();
}
fixed.extend((1..=8).map(|i| (format!("In {i}"), format!("In {i}"))));
} else {
fixed.push((main_name, "main".to_string()));
}
if search.typing() {
let visible: Vec<(&String, &String)> =
fixed.iter().map(|(l, v)| (l, v)).chain(busses.iter().map(|b| (b, b))).filter(|(l, _)| search.matches(l)).collect();
let picked = search.rows(ui, visible.len(), 420.0, |ui, i, highlighted| visible.get(i).is_some_and(|(l, _)| menu_row(ui, highlighted, l)));
if let Some((_, value)) = picked.and_then(|i| visible.get(i)) {
let _ = app.run(cmd, json!({"track": id.0, key: value}));
ui.close();
}
return;
}
for (label, value) in &fixed {
if ui.button(label).clicked() {
let _ = app.run(cmd, json!({"track": id.0, key: value}));
ui.close();
}
}
ui.separator();
let busses: Vec<String> = app.engine.session().busses.iter().map(|b| b.name.clone()).collect();
ui.strong("Bus");
let search = MenuSearch::show(ui, egui::Id::unique(("route-bus-search", id.0, input)), "Search buses…");
let visible: Vec<&String> = busses.iter().filter(|b| search.matches(b)).collect();
if let Some(i) = search.rows(ui, visible.len(), 420.0, |ui, i, highlighted| menu_row(ui, highlighted, visible[i])) {
let _ = app.run(cmd, json!({"track": id.0, key: visible[i]}));
if let Some(bus) = search
.rows(ui, busses.len(), 420.0, |ui, i, highlighted| busses.get(i).is_some_and(|b| menu_row(ui, highlighted, b)))
.and_then(|i| busses.get(i))
{
let _ = app.run(cmd, json!({"track": id.0, key: bus}));
ui.close();
}
if ui.button("new bus...").clicked() {
@@ -151,19 +151,19 @@ fn the_views_menu_stays_open_until_a_click_elsewhere() {
}
#[test]
fn the_io_columns_route_menus_stay_open_while_searching_buses() {
fn the_io_columns_route_menus_stay_open_while_searching() {
let (mut h, _, row) = harness("Kick", "medium", "view.edit_io");
click(&mut h, pos2(row[0] + HEADER_W + 40.0, row[1] + 29.5));
let bus = h.get_by_label("Bus").rect();
let first = h.get_by_label("no output").rect();
let search = h
.query_all_by_role(egui::accesskit::Role::TextInput)
.map(|node| node.rect())
.filter(|r| r.min.y >= bus.max.y)
.min_by(|a, b| a.min.y.total_cmp(&b.min.y))
.filter(|r| r.max.y <= first.min.y && r.x_range().contains(first.center().x))
.max_by(|a, b| a.min.y.total_cmp(&b.min.y))
.unwrap()
.center();
click(&mut h, search);
assert!(h.query_by_label("no output").is_some(), "a click in the bus search closed the Output menu");
assert!(h.query_by_label("no output").is_some(), "a click in the menu's search closed the Output menu");
}
#[test]
+22
View File
@@ -169,6 +169,28 @@ fn send_and_output_menus_search_buses_as_soon_as_they_open() {
assert_eq!(h.state().engine.session().tracks[0].mixer.output, Route::Bus(bus(&h, "Verb")));
}
/// The I/O menus' search finds the hardware inputs, the main output and "no input/output", not just buses.
#[test]
fn io_menus_search_every_route_not_just_buses() {
let mut h = harness(vox());
open(&mut h, "Input for Vox");
type_text(&mut h, "in 3");
assert!(h.query_by_label("In 4").is_none(), "only the matches are listed");
press(&mut h, Key::Enter);
assert_eq!(h.state().engine.session().tracks[0].mixer.input, Route::Hardware("In 3".into()));
open(&mut h, "Output for Vox");
type_text(&mut h, "no out");
press(&mut h, Key::Enter);
assert_eq!(h.state().engine.session().tracks[0].mixer.output, Route::None);
let main = h.state().engine.session().outputs.first().map_or_else(|| "Out 1-2".to_string(), |o| o.name.clone());
open(&mut h, "Output for Vox");
type_text(&mut h, &main.to_lowercase());
press(&mut h, Key::Enter);
assert_eq!(h.state().engine.session().tracks[0].mixer.output, Route::Main);
}
/// Scripted typing and Enter reach a menu search even when the host window is unfocused.
#[test]
fn scripted_keys_reach_the_search_with_the_window_in_the_background() {