Merge pull request #297 from grovebit/track-list-reorder

Reorder tracks by dragging them in the Tracks list
This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-10-13 04:43:05 -04:00
commit 116ccfac5b
2 files changed
+118 -1

No files matched your search

+26 -1
View File
@@ -6,6 +6,28 @@ use egui::{Align2, Color32, Rect, Sense, Stroke, Ui, pos2, vec2};
use serde_json::json;
use soundcraft_model::{ClipId, SourceId};
/// Drag a Tracks list row to reorder the tracks: a line marks the gap it will land in.
fn drag_track(app: &mut SoundApp, ui: &Ui, row: Rect, id: u64, dropped: bool) {
let s = app.engine.session();
let (Some(from), Some(p)) = (s.track_index(soundcraft_model::TrackId(id)), ui.ctx().pointer_latest_pos()) else { return };
let spacing = ui.spacing().item_spacing.y;
let pitch = row.height() + spacing;
let gap = (from as f32 + ((p.y - row.center().y) / pitch).floor() + 1.0).clamp(0.0, s.tracks.len() as f32);
if dropped {
let to = gap as usize;
let to = if to > from { to - 1 } else { to };
if to != from {
let _ = app.run("track.move", json!({"track": id, "to": to}));
}
} else {
let y = row.min.y + (gap - from as f32) * pitch - spacing / 2.0;
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
ui.ctx()
.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("track_list_reorder")))
.line_segment([pos2(row.min.x, y), pos2(row.max.x, y)], Stroke::new(2.0, Tokens::current().accent));
}
}
fn panel_header(ui: &mut Ui, title: &str) {
let t = Tokens::current();
let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::hover());
@@ -37,7 +59,7 @@ pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) {
egui::ScrollArea::vertical().id_salt("tracks_scroll").max_height(total * 0.62).auto_shrink([false, false]).scroll_bar_visibility(hidden);
let out = lists.show(ui, |ui| {
for (id, name, shown, color, sel) in tracks {
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 17.0), Sense::click());
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 17.0), Sense::click_and_drag());
if sel {
ui.painter().rect_filled(r, 0.0, t.row_selected);
}
@@ -52,6 +74,9 @@ pub fn tracks_and_groups(app: &mut SoundApp, ui: &mut Ui) {
if resp.clicked() {
crate::edit_window::click_track(app, soundcraft_model::TrackId(id), ui.input(|i| i.modifiers), |_, _| true);
}
if resp.dragged() || resp.drag_stopped() {
drag_track(app, ui, r, id, resp.drag_stopped());
}
}
});
gutter_scroll(ui, gutter, &out);
+92
View File
@@ -0,0 +1,92 @@
//! Dragging rows in the Tracks list reorders the tracks (#170); a click still selects.
use egui::{Event, PointerButton, Pos2, RawInput, Rect, Shape, epaint::ClippedShape, pos2, vec2};
use serde_json::json;
use soundcraft_model::TrackId;
use soundcraft_ui_egui::{Services, SoundApp};
struct Rig {
ctx: egui::Context,
app: SoundApp,
shapes: Vec<ClippedShape>,
}
impl Rig {
fn new() -> Rig {
let mut engine = soundcraft_engine::Engine::default();
engine.execute("track.new", &json!({"count": 6})).unwrap();
let mut r = Rig { ctx: egui::Context::default(), app: SoundApp::new(engine, None, Services::default()), shapes: Vec::new() };
for _ in 0..4 {
r.frame(vec![]);
}
r
}
fn frame(&mut self, events: Vec<Event>) {
let raw = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))), events, ..Default::default() };
let mut out = self.ctx.run_ui(raw, |ui| {
self.app.logic(ui.ctx());
self.app.ui(ui);
});
out.textures_delta.clear();
self.shapes = out.shapes;
}
fn order(&self) -> Vec<TrackId> {
self.app.engine.session().tracks.iter().map(|t| t.id).collect()
}
/// Centre of the Tracks list row naming the track at session index `i`.
fn row(&self, i: usize) -> Pos2 {
let name = &self.app.engine.session().tracks[i].name;
self.shapes
.iter()
.find_map(|c| match &c.shape {
Shape::Text(t) if t.galley.text() == name && t.pos.x < 150.0 => {
Some(pos2(t.pos.x + 20.0, t.galley.rect.translate(t.pos.to_vec2()).center().y))
}
_ => None,
})
.expect("row not in the Tracks list")
}
fn press(&mut self, at: Pos2, pressed: bool) {
self.frame(vec![
Event::PointerMoved(at),
Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() },
]);
}
fn drag(&mut self, from: Pos2, to: Pos2) {
self.press(from, true);
for k in 1..=6 {
self.frame(vec![Event::PointerMoved(from.lerp(to, k as f32 / 6.0))]);
}
self.press(to, false);
self.frame(vec![]);
}
}
#[test]
fn dragging_a_track_list_row_moves_the_track_to_the_gap_it_is_dropped_in() {
let mut r = Rig::new();
let ids = r.order();
let (from, lower_half_of_fifth) = (r.row(0), r.row(4) + vec2(0.0, 5.0));
r.drag(from, lower_half_of_fifth);
assert_eq!(r.order(), vec![ids[1], ids[2], ids[3], ids[4], ids[0], ids[5]]);
let (from, upper_half_of_second) = (r.row(5), r.row(1) - vec2(0.0, 5.0));
r.drag(from, upper_half_of_second);
assert_eq!(r.order(), vec![ids[1], ids[5], ids[2], ids[3], ids[4], ids[0]]);
let (from, own_lower_half) = (r.row(2), r.row(2) + vec2(0.0, 8.0));
r.drag(from, own_lower_half);
assert_eq!(r.order(), vec![ids[1], ids[5], ids[2], ids[3], ids[4], ids[0]]);
}
#[test]
fn clicking_a_track_list_row_still_selects_the_track() {
let mut r = Rig::new();
let at = r.row(2);
r.press(at, true);
r.press(at, false);
assert_eq!(r.app.engine.session().edit.selected_tracks, vec![r.order()[2]]);
}