From 04dab602c7cd7f1ee1645919a238568e12ea7d02 Mon Sep 17 00:00:00 2001 From: ljkx <177671075+ljkx@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:38:53 +0100 Subject: [PATCH 1/2] A new send opens its Send window, so its fader is on screen right away (#1269) Picking a destination for an empty send slot, or making one with new bus..., now opens that send's Send window, as in Pro Tools. Moving an existing send to another bus leaves the window as it was. --- crates/ui-egui/src/mix_window/sends.rs | 86 +++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/mix_window/sends.rs b/crates/ui-egui/src/mix_window/sends.rs index 87cac59d..dc338527 100644 --- a/crates/ui-egui/src/mix_window/sends.rs +++ b/crates/ui-egui/src/mix_window/sends.rs @@ -388,8 +388,15 @@ fn send_route_menu(app: &mut SoundApp, ui: &Ui, field: &egui::Response, id: Trac }, ); let Some((picked, footer)) = shown else { return }; + // As in Pro Tools, a new send opens its Send window, so its fader is on screen right away. + let created = |app: &mut SoundApp, sent: bool| { + if sent && !has_send { + app.ui.send_window = Some((id, slot)); + } + }; if let Some(bus) = picked { - let _ = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": bus})); + let sent = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": bus})).is_ok(); + created(app, sent); } match footer { Some(SendFooter::NewBus) => { @@ -400,7 +407,8 @@ 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}")})); + let sent = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": format!("Bus {n}")})).is_ok(); + created(app, sent); } } Some(SendFooter::RemoveSend) => { @@ -464,4 +472,78 @@ 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)); } + + /// A send slot drawn in a test harness, for the track named "Kick", with an "Aux" bus to pick. + fn kick_send_harness(send_first: 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 send_first { + 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"); + } } From 36d42e01369f427e8c00fe6da771e7d1aa4d4362 Mon Sep 17 00:00:00 2001 From: ljkx <177671075+ljkx@users.noreply.github.com> Date: Tue, 3 Nov 2026 01:55:52 +0100 Subject: [PATCH 2/2] Test a new send's Send window end to end, and tidy send_route_menu An integration test creates send C while send A's window is open: the window moves to the new send with its fader on screen, and undoing the send closes it. send_route_menu now opens the window only after the send command succeeds, without the extra bool, and its comment says reassignment leaves the window alone. --- crates/ui-egui/src/mix_window/sends.rs | 28 +++++++++++++----------- crates/ui-egui/tests/it/send_window.rs | 30 +++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/crates/ui-egui/src/mix_window/sends.rs b/crates/ui-egui/src/mix_window/sends.rs index dc338527..90aca765 100644 --- a/crates/ui-egui/src/mix_window/sends.rs +++ b/crates/ui-egui/src/mix_window/sends.rs @@ -388,15 +388,17 @@ fn send_route_menu(app: &mut SoundApp, ui: &Ui, field: &egui::Response, id: Trac }, ); let Some((picked, footer)) = shown else { return }; - // As in Pro Tools, a new send opens its Send window, so its fader is on screen right away. - let created = |app: &mut SoundApp, sent: bool| { - if sent && !has_send { + // 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 { - let sent = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": bus})).is_ok(); - created(app, sent); + 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) => { @@ -406,9 +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 sent = app.run("mix.send", json!({"track": id.0, "slot": slot, "bus": format!("Bus {n}")})).is_ok(); - created(app, sent); + 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) => { @@ -473,13 +476,14 @@ mod tests { assert!((footer(&h).top() - full.top()).abs() < 1.0, "cleared, the footer is back under the full list: {:?} vs {full:?}", footer(&h)); } - /// A send slot drawn in a test harness, for the track named "Kick", with an "Aux" bus to pick. - fn kick_send_harness(send_first: bool) -> (Harness<'static, (SoundApp, TrackId)>, TrackId) { + /// 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 send_first { + if existing_send { app.run("mix.send", json!({"track": kick.0, "slot": 0, "bus": "Other"})).unwrap(); } let mut ready = false; 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() {