Rebuild on the amended drag-selection (#49)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-11 20:57:17 -04:00
commit 6347aad211
4 files changed
+58 -20

No files matched your search

+9 -1
View File
@@ -105,7 +105,15 @@ pub fn specs() -> Vec<CommandSpec> {
insert_params
),
cmd!("mix.insert_move", "Move Insert", [], None, "{track?, from, to}", has_selection, insert_move),
cmd!("mix.send", "Assign Send", [], None, "{track?, slot?: 0..9|a..j, bus: name, level_db?: 0 (kept when re-targeting), pre_fader?: false}", has_selection, send),
cmd!(
"mix.send",
"Assign Send",
[],
None,
"{track?, slot?: 0..9|a..j, bus: name, level_db?: 0 (kept when re-targeting), pre_fader?: false}",
has_selection,
send
),
cmd!("mix.send_remove", "Remove Send", [], None, "{track?, slot}", has_selection, |e, p| {
let (t, slot) = track_slot(e, p, "mix.send_remove", SEND_SLOTS)?;
if let Some(tr) = e.session_mut().track_mut(t)
+47 -17
View File
@@ -214,7 +214,7 @@ fn main_area_content(app: &mut SoundApp, ui: &mut Ui) {
let mut y = tl.min.y - app.edit_layout.scroll_y;
app.edit_layout.rows.clear();
let mut content_h = 0.0;
for t in app.engine.session().tracks.iter().filter(|x| !x.hidden && !folder_collapsed(app.engine.session(), x)) {
for t in app.engine.session().tracks.iter().filter(|x| in_edit_window(app.engine.session(), x)) {
let h = track_height(t);
app.edit_layout.rows.push((t.id.0, [full.min.x, y, tl.max.x, y + h]));
y += h + 1.0;
@@ -255,8 +255,33 @@ fn main_area_content(app: &mut SoundApp, ui: &mut Ui) {
}
}
fn folder_collapsed(s: &Session, t: &Track) -> bool {
t.folder.and_then(|f| s.track(f)).is_some_and(|f| !f.folder_open)
fn in_edit_window(s: &Session, t: &Track) -> bool {
!t.hidden && t.folder.and_then(|f| s.track(f)).is_none_or(|f| f.folder_open)
}
/// Pro Tools' track-name click: Shift adds the tracks up to the nearest selected one, Cmd/Ctrl adds or removes one.
pub(crate) fn click_track(app: &mut SoundApp, id: TrackId, mods: egui::Modifiers, shown: impl Fn(&Session, &Track) -> bool) {
let s = app.engine.session();
let order: Vec<TrackId> = s.tracks.iter().filter(|t| shown(s, t)).map(|t| t.id).collect();
let row = |t: &TrackId| order.iter().position(|o| o == t);
let mut tracks = s.edit.selected_tracks.clone();
if mods.command {
if tracks.contains(&id) {
tracks.retain(|t| *t != id);
} else {
tracks.push(id);
}
} else if mods.shift
&& let Some(a) = row(&id)
&& let Some(b) = tracks.iter().filter_map(row).min_by_key(|b| b.abs_diff(a))
{
let span = order.get(a.min(b)..=a.max(b)).unwrap_or_default();
tracks.extend(span.iter().filter(|t| !tracks.contains(t)).copied().collect::<Vec<_>>());
} else {
tracks = vec![id];
}
let ids: Vec<u64> = tracks.iter().map(|t| t.0).collect();
let _ = app.run("edit.select", json!({"tracks": ids}));
}
fn scroll_and_zoom(app: &mut SoundApp, ui: &Ui, area: Rect, tl: Rect) {
@@ -766,15 +791,7 @@ fn track_header(app: &mut SoundApp, ui: &mut Ui, track: &Track, head: Rect, sele
// Click on header background selects the track.
let bg = ui.interact(head, ui.id().with(("hdr", id.0)), Sense::click());
if bg.clicked() {
let add = ui.input(|i| i.modifiers.command || i.modifiers.shift);
let mut tracks = if add { app.engine.session().edit.selected_tracks.clone() } else { Vec::new() };
if add && tracks.contains(&id) {
tracks.retain(|x| *x != id);
} else {
tracks.push(id);
}
let ids: Vec<u64> = tracks.iter().map(|x| x.0).collect();
let _ = app.run("edit.select", json!({"tracks": ids}));
click_track(app, id, ui.input(|i| i.modifiers), in_edit_window);
}
bg.context_menu(|ui| track_context_menu(app, ui, id));
// Name field.
@@ -797,7 +814,7 @@ fn track_header(app: &mut SoundApp, ui: &mut Ui, track: &Track, head: Rect, sele
if nresp.double_clicked() {
app.dialogs.open_rename_track(id, &track.name);
} else if nresp.clicked() {
let _ = app.run("edit.select", json!({"tracks": [id.0]}));
click_track(app, id, ui.input(|i| i.modifiers), in_edit_window);
}
// Drag the name plate to reorder tracks.
if nresp.dragged()
@@ -2121,8 +2138,8 @@ mod tests {
h.step();
}
fn shift_click(h: &mut Harness<SoundApp>, at: Pos2) {
h.event(egui::Event::ModifiersChanged(egui::Modifiers::SHIFT));
fn click_with(h: &mut Harness<SoundApp>, at: Pos2, mods: egui::Modifiers) {
h.event(egui::Event::ModifiersChanged(mods));
click(h, at);
h.event(egui::Event::ModifiersChanged(egui::Modifiers::NONE));
}
@@ -2260,16 +2277,29 @@ mod tests {
let (a, b, c) = (lane_point(&h, 0), lane_point(&h, 2) + vec2(100.0, 0.0), lane_point(&h, 3) + vec2(300.0, 0.0));
drag(&mut h, a, a + vec2(200.0, 0.0));
let sel = h.state().engine.session().edit.selection;
shift_click(&mut h, b);
click_with(&mut h, b, egui::Modifiers::SHIFT);
assert_eq!(h.state().engine.session().edit.selected_tracks, [ids[0], ids[2]]);
assert_eq!(h.state().engine.session().edit.selection, sel);
shift_click(&mut h, c);
click_with(&mut h, c, egui::Modifiers::SHIFT);
let s = h.state().engine.session();
assert_eq!(s.edit.selected_tracks, [ids[0], ids[2], ids[3]]);
assert_eq!(s.edit.selection, Range::new(sel.start, sample_at(s, timeline(&h), c.x)));
assert_eq!(highlighted_rows(&h), [0, 2, 3]);
}
#[test]
fn shift_and_cmd_click_on_track_names_extend_the_selection() {
let mut h = harness(5);
let ids = row_ids(&h);
let (none, shift, cmd) = (egui::Modifiers::NONE, egui::Modifiers::SHIFT, egui::Modifiers::COMMAND);
for (i, mods, want) in [(1, none, vec![1]), (3, shift, vec![1, 2, 3]), (2, cmd, vec![1, 3]), (0, cmd, vec![0, 1, 3]), (4, none, vec![4])] {
let at = row(&h, i).min + vec2(60.0, 10.0);
click_with(&mut h, at, mods);
let want: Vec<TrackId> = want.into_iter().map(|k| ids[k]).collect();
assert_eq!(h.state().engine.session().edit.selected_tracks, want, "row {i} clicked with {mods:?}");
}
}
#[test]
fn a_quick_drag_from_a_clip_edge_trims_it() {
let mut h = smart_demo();
+1 -1
View File
@@ -404,7 +404,7 @@ fn strip(app: &mut SoundApp, ui: &mut Ui, id: TrackId, snap: Option<&MeterSnapsh
ui.painter().with_clip_rect(nr).text(nr.center(), Align2::CENTER_CENTER, &track.name, bold(12.0), t.text_dark);
let nresp = ui.interact(nr, ui.id().with(("strip_name", id.0)), Sense::click());
if nresp.clicked() {
let _ = app.run("edit.select", json!({"tracks": [id.0]}));
crate::edit_window::click_track(app, id, ui.input(|i| i.modifiers), |_, t| !t.hidden && t.kind != TrackKind::Video);
}
if nresp.double_clicked() {
app.dialogs.open_rename_track(id, &track.name);
+1 -1
View File
@@ -38,7 +38,7 @@ pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) {
ui.painter().rect_filled(Rect::from_min_size(pos2(r.min.x + 20.0, r.min.y + 3.0), vec2(4.0, 11.0)), 0.0, rgb(color));
ui.painter().text(pos2(r.min.x + 30.0, r.center().y), Align2::LEFT_CENTER, &name, regular(11.5), if shown { t.text } else { t.text_dim });
if resp.clicked() {
let _ = app.run("edit.select", json!({"tracks": [id]}));
crate::edit_window::click_track(app, soundcraft_model::TrackId(id), ui.input(|i| i.modifiers), |_, _| true);
}
}
});