diff --git a/crates/ui-egui/src/mix_window/sends.rs b/crates/ui-egui/src/mix_window/sends.rs index 87cac59d..90aca765 100644 --- a/crates/ui-egui/src/mix_window/sends.rs +++ b/crates/ui-egui/src/mix_window/sends.rs @@ -388,8 +388,17 @@ fn send_route_menu(app: &mut SoundApp, ui: &Ui, field: &egui::Response, id: Trac }, ); let Some((picked, footer)) = shown else { return }; - if let Some(bus) = picked { - let _ = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": bus})); + // As in Pro Tools, a new send opens its Send window (moving it from any other send), so its + // fader is on screen right away. A send that only changes destination leaves the window alone. + let show_new_send = |app: &mut SoundApp| { + if !has_send { + app.ui.send_window = Some((id, slot)); + } + }; + if let Some(bus) = picked + && app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": bus})).is_ok() + { + show_new_send(app); } match footer { Some(SendFooter::NewBus) => { @@ -399,8 +408,10 @@ fn send_route_menu(app: &mut SoundApp, ui: &Ui, field: &egui::Response, id: Trac { n += 1; } - if app.run("mix.new_bus", json!({"name": format!("Bus {n}")})).is_ok() { - let _ = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": format!("Bus {n}")})); + if app.run("mix.new_bus", json!({"name": format!("Bus {n}")})).is_ok() + && app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": format!("Bus {n}")})).is_ok() + { + show_new_send(app); } } Some(SendFooter::RemoveSend) => { @@ -464,4 +475,79 @@ mod tests { assert_eq!(field(&h), search, "the search field stays put"); assert!((footer(&h).top() - full.top()).abs() < 1.0, "cleared, the footer is back under the full list: {:?} vs {full:?}", footer(&h)); } + + /// Kick's send A slot in a test harness, with buses "Aux" and "Other"; with `existing_send`, the + /// slot already sends to "Other". + fn kick_send_harness(existing_send: bool) -> (Harness<'static, (SoundApp, TrackId)>, TrackId) { + let mut app = SoundApp::new(soundcraft_engine::demo::demo_engine(), None, Services::default()); + app.run("mix.new_bus", json!({"name": "Aux"})).unwrap(); + app.run("mix.new_bus", json!({"name": "Other"})).unwrap(); + let kick = app.engine.session().track_by_name("Kick").unwrap().id; + if existing_send { + app.run("mix.send", json!({"track": kick.0, "slot": 0, "bus": "Other"})).unwrap(); + } + let mut ready = false; + let h = Harness::builder().with_size(vec2(600.0, 800.0)).build_ui_state( + move |ui, (app, kick): &mut (SoundApp, TrackId)| { + if !ready { + crate::fonts::install(ui.ctx(), true); + crate::theme::apply(ui.ctx(), false); + ready = true; + return; + } + let track = app.engine.session().track(*kick).cloned().unwrap(); + let r = Rect::from_min_size(ui.max_rect().min + vec2(20.0, 20.0), vec2(120.0, 24.0)); + send_slot(app, ui, &track, 0, r); + }, + (app, kick), + ); + (h, kick) + } + + /// Creating a send opens its Send window, so the new send's fader is on screen right away (#1269). + #[test] + fn creating_a_send_opens_its_send_window() { + let _palette = crate::theme::lock_test_palette(); + let (mut h, kick) = kick_send_harness(false); + h.run(); + h.get_by_label("Send A for Kick").click(); + h.run(); + h.get_by_label("Aux").click(); + h.run(); + let (app, _) = h.state(); + let send = app.engine.session().track(kick).and_then(|t| t.mixer.sends.first().cloned().flatten()); + assert!(send.is_some(), "the send was created"); + assert_eq!(app.ui.send_window, Some((kick, 0)), "the new send's window is open"); + } + + /// So does a send made with "new bus...". + #[test] + fn a_send_to_a_new_bus_opens_its_send_window() { + let _palette = crate::theme::lock_test_palette(); + let (mut h, kick) = kick_send_harness(false); + h.run(); + h.get_by_label("Send A for Kick").click(); + h.run(); + h.get_by_label("new bus...").click(); + h.run(); + let (app, _) = h.state(); + assert!(app.engine.session().track(kick).and_then(|t| t.mixer.sends.first().cloned().flatten()).is_some(), "the send was created"); + assert_eq!(app.ui.send_window, Some((kick, 0)), "the new send's window is open"); + } + + /// Moving an existing send to another bus keeps the Send window as it was. + #[test] + fn changing_a_sends_destination_leaves_the_send_window_alone() { + let _palette = crate::theme::lock_test_palette(); + let (mut h, kick) = kick_send_harness(true); + h.run(); + h.get_by_label("Send A for Kick").click(); + h.run(); + h.get_by_label("Aux").click(); + h.run(); + let (app, _) = h.state(); + let target = app.engine.session().track(kick).and_then(|t| t.mixer.sends.first().cloned().flatten()).map(|s| route_name(app, &s.target)); + assert_eq!(target.as_deref(), Some("Aux"), "the send moved"); + assert_eq!(app.ui.send_window, None, "no window opened"); + } } diff --git a/crates/ui-egui/tests/it/send_window.rs b/crates/ui-egui/tests/it/send_window.rs index ac3a6dad..30a2d7bd 100644 --- a/crates/ui-egui/tests/it/send_window.rs +++ b/crates/ui-egui/tests/it/send_window.rs @@ -1,5 +1,6 @@ //! A send's level opens its Send window, as in Pro Tools: a fader with Alt-click for 0 dB, moved to -//! another send by clicking that send's level, closed by clicking the same one again. +//! another send by clicking that send's level, closed by clicking the same one again. Creating a send +//! opens it too. use egui::{Event, Modifiers, PointerButton, Pos2, vec2}; use egui_kittest::Harness; @@ -75,6 +76,33 @@ fn a_sends_level_opens_its_send_window() { assert!(h.query_by_label_contains("Vox · Send").is_none()); } +/// Creating a send opens its Send window, so the new send's fader is on screen right away (#1269). The +/// window moves there from the send it was showing, and undoing the new send closes it. +#[test] +fn a_new_send_shows_its_fader_at_once() { + let (mut h, vox) = harness(); + h.get_by_label("Send A level for Vox").click(); + h.run_steps(3); + assert!(h.query_by_label_contains("Vox · Send A").is_some()); + + h.get_by_label("Send C for Vox").click(); + h.run_steps(3); + // Send A's own "Verb" label is on screen too, so pick the menu's button. + h.get_by_role_and_label(egui::accesskit::Role::Button, "Verb").click(); + h.run_steps(3); + assert!(h.state().engine.session().tracks[0].mixer.sends[2].is_some(), "send C was created"); + assert_eq!(h.state().ui.send_window, Some((vox, 2))); + assert!(h.query_by_label_contains("Vox · Send C").is_some(), "the window shows the new send"); + assert!(h.query_by_label_contains("Vox · Send A").is_none(), "and no longer send A"); + assert!(h.query_by_label("Send level").is_some(), "the new send's fader is on screen"); + + h.state_mut().engine.execute("edit.undo", &json!({})).unwrap(); + h.run_steps(3); + assert!(h.state().engine.session().tracks[0].mixer.sends[2].is_none(), "the send was undone"); + assert_eq!(h.state().ui.send_window, None, "undoing the send closes its window"); + assert!(h.query_by_label_contains("Vox · Send").is_none()); +} + /// Escape first closes a send menu or cancels level typing, then a further Escape closes the send window. #[test] fn escape_closes_the_send_window_after_menus_and_typing() {