diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs index ae7758d8..ec26a9f9 100644 --- a/crates/ui-egui/src/edit_window.rs +++ b/crates/ui-egui/src/edit_window.rs @@ -1480,8 +1480,7 @@ pub(crate) fn track_context_menu(app: &mut SoundApp, ui: &mut Ui, id: TrackId) { } // Only the track clicked: the command has one target, and the selection stays as it is. if ui.button("Scroll Into View").clicked() { - app.edit_scroll_into_view = Some(id); - app.mix_scroll_into_view = Some(id); + let _ = app.run("view.scroll_into_view", json!({"track": id.0})); } ui.separator(); ui.menu_button("Track Height", |ui| { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 99e4b503..5cd048d0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -69,6 +69,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, Option<&str>)] = &[ ("window.io_setup", "I/O Setup", "", None), ("window.shortcuts", "Keyboard Shortcuts", "", None), ("view.mix_section", "Mix Window View", "", None), + ("view.scroll_into_view", "Scroll Into View", "", None), ("ui.new_tracks_dialog", "New Tracks…", "", None), ("ui.bounce_dialog", "Bounce Mix…", "", None), ("ui.set", "Set UI State", "", None), @@ -647,6 +648,20 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option { + // `track` (id or name), else the first selected track: the palette passes no params. + let track = match soundcraft_engine::cmd::track_param(&app.engine, id, p, "track") { + Ok(Some(t)) => t, + Ok(None) => match app.engine.session().edit.selected_tracks.first() { + Some(t) => *t, + None => return Some(Err("view.scroll_into_view: pass `track` or select a track".into())), + }, + Err(e) => return Some(Err(e.to_string())), + }; + app.edit_scroll_into_view = Some(track); + app.mix_scroll_into_view = Some(track); + json!({"track": track.0}) + } "ui.new_tracks_dialog" => { return Some(Err("ui.new_tracks_dialog opens a dialog from the menu only; call track.new with parameters".into())); } diff --git a/crates/ui-egui/tests/scroll_into_view.rs b/crates/ui-egui/tests/scroll_into_view.rs index c0d64efc..fbb06d1a 100644 --- a/crates/ui-egui/tests/scroll_into_view.rs +++ b/crates/ui-egui/tests/scroll_into_view.rs @@ -132,3 +132,21 @@ fn a_track_in_a_closed_folder_scrolls_the_mix_but_not_the_edit_window() { h.run_steps(4); assert_eq!(h.state().edit_layout.scroll_y, 0.0); } + +#[test] +fn the_command_takes_a_track_by_name_from_the_control_channel() { + let mut h = harness(20); + h.state_mut().ui.window = MainWindow::Mix; + h.run_steps(4); + let first = plate(&h, "T01").expect("the first strip's name").x; + h.state_mut().run("view.scroll_into_view", json!({"track": "T08"})).unwrap(); + h.run_steps(4); + + let now = plate(&h, "T08").expect("T08 on screen").x; + assert!((now - first).abs() < 1.0, "T08 is the leftmost strip: at {now}, the first was at {first}"); + h.state_mut().ui.window = MainWindow::Edit; + h.run_steps(4); + assert_eq!(edit_row(&h, "T08")[1], h.state().edit_layout.timeline[1], "the Edit window scrolled too"); + assert!(h.state_mut().run("view.scroll_into_view", json!({"track": "T99"})).is_err()); +} + diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 9efe3789..8417148f 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -87,6 +87,10 @@ When the integration cannot report an appearance, System uses Dark. Manual Light fixed when the OS appearance changes. An offscreen render without `--system-theme` supplies no OS appearance and therefore resolves System to Dark. +`view.scroll_into_view {"track":"Bass"}` (a track id or name; without it, the first selected +track) brings the track to the top of the Edit window and the left of the Mix window, the same as +Scroll Into View in the track name menu. A window not on screen catches up when it is next shown. + ## Panel docking Tracks and Groups, Clips, MIDI Editor, Memory Locations, and Undo History use movable panels. Commands target the active Edit or Mix view; floating panels stay inside the current viewport. Edit and Mix retain separate arrangements; Clips and MIDI Editor belong to Edit. Transport, plug-in, and specialist utility windows retain their existing hosts. The document/session view is protected. Layouts, active tabs, panel visibility, and floating rectangles are saved with the app's existing UI preferences. These operations do not edit document contents.