Merge pull request #1291 from ljkx/new-send-opens-send-window

A new send opens its Send window, so its fader is on screen right away
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-11-03 10:04:32 -05:00
commit 5ba9f9d7b8
2 files changed
+119 -5

No files matched your search

+90 -4
View File
@@ -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");
}
}
+29 -1
View File
@@ -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() {