diff --git a/crates/ui-egui/src/mix_window/routing.rs b/crates/ui-egui/src/mix_window/routing.rs index a343a021..53a5ba10 100644 --- a/crates/ui-egui/src/mix_window/routing.rs +++ b/crates/ui-egui/src/mix_window/routing.rs @@ -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 = 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 = 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() { diff --git a/crates/ui-egui/tests/it/edit_window_headers.rs b/crates/ui-egui/tests/it/edit_window_headers.rs index fa5ae057..a455dd10 100644 --- a/crates/ui-egui/tests/it/edit_window_headers.rs +++ b/crates/ui-egui/tests/it/edit_window_headers.rs @@ -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] diff --git a/crates/ui-egui/tests/it/menu_search.rs b/crates/ui-egui/tests/it/menu_search.rs index c193ad18..854b69ef 100644 --- a/crates/ui-egui/tests/it/menu_search.rs +++ b/crates/ui-egui/tests/it/menu_search.rs @@ -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() {