Add tracks from the Tracks list and the empty space below the tracks

From #307: adding a track took the Track menu or Shift+Cmd+N
(Shift+Ctrl+N on Windows and Linux). Nothing on screen offered it, not
even in an empty session.

- The TRACKS header in the Tracks list has a + button that opens New
  Tracks. The list is in both the Edit and the Mix window.
- A double-click in the empty space below the last track in the Edit
  window (header column or timeline) opens New Tracks, as in Pro Tools.
  A single click there still clears the selection.
- A double-click right of the last strip in the Mix window does the
  same. With no tracks, that is the whole window.
This commit is contained in:
grovebit 2026-10-12 17:52:12 +01:00
parent 6cf896e76c
commit fe3d759b53
4 changed files with 85 additions and 3 deletions

View File

@ -230,10 +230,15 @@ fn main_area_content(app: &mut SoundApp, ui: &mut Ui) {
track_row(app, &mut child, TrackId(id), row, tl);
}
}
// Empty area click clears track selection.
// Below the tracks a click clears the selection and a double-click opens New Tracks.
let empty = Rect::from_min_max(pos2(full.min.x, y.max(tl.min.y)), tl.max);
if empty.height() > 4.0 && ui.interact(empty, ui.id().with("empty"), Sense::click()).clicked() {
let _ = app.run("edit.select_none", json!({}));
if empty.height() > 4.0 {
let resp = ui.interact(empty, ui.id().with("empty"), Sense::click());
if resp.double_clicked() {
let _ = app.dialogs.open_for_command(&app.engine, "track.new");
} else if resp.clicked() {
let _ = app.run("edit.select_none", json!({}));
}
}
overlay(app, ui, tl, Rect::from_min_max(pos2(tl.min.x, rulers_rect.min.y), tl.max));
let _ = header;

View File

@ -29,6 +29,10 @@ pub(crate) fn content(app: &mut SoundApp, ui: &mut Ui) {
for id in ids {
strip(app, ui, id, snap.as_ref());
}
let rest = Rect::from_min_max(pos2(ui.cursor().min.x, ui.clip_rect().min.y), ui.clip_rect().max);
if rest.width() > 4.0 && ui.interact(rest, ui.id().with("past_strips"), Sense::click()).double_clicked() {
let _ = app.dialogs.open_for_command(&app.engine, "track.new");
}
});
});
}

View File

@ -18,7 +18,13 @@ fn panel_header(ui: &mut Ui, title: &str) {
pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) {
let t = Tokens::current();
let total = ui.available_height();
let header = Rect::from_min_size(ui.cursor().min, vec2(ui.available_width(), 22.0));
panel_header(ui, "TRACKS");
let plus = Rect::from_center_size(pos2(header.max.x - 13.0, header.center().y), vec2(18.0, 18.0));
let tip = format!("New Tracks… ({})", crate::shortcuts::shortcut_label("Cmd+Shift+N", ui.ctx().os().is_mac()));
if crate::widgets::icon_button(&mut ui.new_child(egui::UiBuilder::new().max_rect(plus)), plus.size(), "plus", false, &tip).clicked() {
let _ = app.dialogs.open_for_command(&app.engine, "track.new");
}
let tracks: Vec<(u64, String, bool, [u8; 3], bool)> = {
let s = app.engine.session();
s.tracks.iter().map(|x| (x.id.0, x.name.clone(), !x.hidden, x.color, s.edit.selected_tracks.contains(&x.id))).collect()

View File

@ -0,0 +1,67 @@
//! New Tracks opens from the Tracks list's + button, and from a double-click below the last
//! track (Edit) or right of the last strip (Mix).
use egui::{Pos2, pos2, vec2};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use soundcraft_ui_egui::{MainWindow, Services, SoundApp};
fn harness(tracks: usize) -> Harness<'static, SoundApp> {
let mut engine = soundcraft_engine::Engine::default();
if tracks > 0 {
engine.execute("track.new", &serde_json::json!({"count": tracks})).unwrap();
}
let app = SoundApp::new(engine, None, Services::default());
let mut h = Harness::builder().with_size(vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(
|ui, app: &mut SoundApp| {
app.logic(&ui.ctx().clone());
app.ui(ui);
},
app,
);
h.run_steps(4);
h
}
fn click(h: &mut Harness<SoundApp>, at: Pos2) {
h.hover_at(at);
h.drag_at(at);
h.drop_at(at);
h.step();
}
#[test]
fn the_tracks_list_plus_button_opens_new_tracks() {
let mut h = harness(0);
h.get_by_label_contains("New Tracks").click();
h.run_steps(2);
assert_eq!(h.state().dialogs.open_name(), Some("new_tracks"));
}
#[test]
fn double_click_below_the_last_track_opens_new_tracks() {
let mut h = harness(2);
let rows = h.state().edit_layout.rows.clone();
let below = rows[1].1[3] + 60.0;
for x in [h.state().edit_layout.timeline[0] + 100.0, rows[0].1[0] + 60.0] {
click(&mut h, pos2(x, below));
assert_eq!(h.state().dialogs.open_name(), None, "one click only clears the selection");
assert!(h.state().engine.session().edit.selected_tracks.is_empty());
click(&mut h, pos2(x, below));
assert_eq!(h.state().dialogs.open_name(), Some("new_tracks"));
h.state_mut().dialogs.open = None;
h.run_steps(30);
}
}
#[test]
fn double_click_right_of_the_last_strip_opens_new_tracks() {
let mut h = harness(1);
h.state_mut().ui.window = MainWindow::Mix;
h.run_steps(4);
let strip = h.get_by_label("Record enable").rect();
let at = pos2(strip.max.x + 150.0, strip.center().y);
click(&mut h, at);
click(&mut h, at);
assert_eq!(h.state().dialogs.open_name(), Some("new_tracks"));
}