collective

Browse / github.com/storytold/soundcraft / patch

Clip moves: use the actual pointer release track and position

Clip moves: use the actual pointer release track and position A clip release can arrive at a new pointer position without an intervening held-pointer frame there. `finish_drag` currently commits the previous preview's track and delta, so a quick release can leave a sliced clip on the old track or at the wrong position. This draft recomputes the destination, drag plan, snapping and movement from the actual release position and modifiers, using the same row lookup as the held drag. Refs #721. This fixes a concrete related event-ordering bug; it does **not** claim the reporter's original Windows 0.7.0 slice-and-move workflow is fully resolved. ### Execute

Status
open
Score
0.00
Adopters
0 other operators
Author
jackcanon (imported from pull request #815, open)
Base
e8940766d77601942b31e62570a06c44db1ec68e
Size
+50 −5 in 2 files
Review
pending
Shared
2026-10-28
Id
e87d107c252983821d4d2612

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/edit_window.rs crates/ui-egui/tests/edit_modes.rs

diff --git a/crates/ui-egui/src/edit_window.rs b/crates/ui-egui/src/edit_window.rs--- a/crates/ui-egui/src/edit_window.rs+++ b/crates/ui-egui/src/edit_window.rs@@ -2301,15 +2301,14 @@ fn drive_drag(app: &mut SoundApp, ctx: &egui::Context) {     let (s, tl) = (app.engine.session_arc(), rect(app.edit_layout.timeline));     let p = ctx.pointer_latest_pos();     if !ctx.input(|i| i.pointer.any_down()) {-        finish_drag(app, &s, tl, p);+        finish_drag(app, &s, tl, p, ctx);         return;     }     let Some(p) = p else { return };     let at = sample_at(&s, tl, p.x).max(0);     let inside = pos2(p.x.max(tl.min.x).min(tl.max.x), p.y.max(tl.min.y).min(tl.max.y));     let rows = &app.edit_layout.rows;-    let gap = |r: &[f32; 4]| (r[1] - inside.y).max(inside.y - r[3]).max(0.0);-    let to_track = rows.iter().min_by(|a, b| gap(&a.1).total_cmp(&gap(&b.1))).map(|(i, _)| TrackId(*i));+    let to_track = drag_track(rows, tl, p);     let mut invalid_fade = false;     match &mut app.gesture {         Some(Gesture::Select { track, tracks, .. }) => {@@ -2416,9 +2415,24 @@ fn drive_drag(app: &mut SoundApp, ctx: &egui::Context) {     } } -fn finish_drag(app: &mut SoundApp, s: &Session, tl: Rect, p: Option<Pos2>) {+fn drag_track(rows: &[(u64, [f32; 4])], tl: Rect, p: Pos2) -> Option<TrackId> {+    let y = p.y.max(tl.min.y).min(tl.max.y);+    let gap = |r: &[f32; 4]| (r[1] - y).max(y - r[3]).max(0.0);+    rows.iter().min_by(|a, b| gap(&a.1).total_cmp(&gap(&b.1))).map(|(id, _)| TrackId(*id))+}++fn finish_drag(app: &mut SoundApp, s: &Session, tl: Rect, p: Option<Pos2>, ctx: &egui::Context) {     match app.gesture.take() {-        Some(Gesture::MoveClips { clips, delta, from_track, to_track, .. }) => drop_clips(app, clips, delta, from_track, to_track),+        Some(Gesture::MoveClips { clips, mut delta, from_track, mut to_track, grab_at }) => {+            // A release can carry a new pointer position without an intervening held+            // frame. Commit that position rather than the previous frame's preview.+            if let Some(p) = p {+                to_track = drag_track(&app.edit_layout.rows, tl, p).unwrap_or(to_track);+                let plan = drag_plan(ctx, s, app.engine.revision, &clips, (from_track, to_track), grab_at);+                delta = move_delta(&plan, s, from_track, sample_at(s, tl, p.x).max(0) - grab_at, ctx.input(|i| i.modifiers.command));+            }+            drop_clips(app, clips, delta, from_track, to_track);+        }         Some(Gesture::Fade { clip, fade_in, to, .. }) => {             let to = p.map_or(to, |p| sample_at(s, tl, p.x).max(0));             if let Some(fade) = s.find_clip(clip).and_then(|(_, c)| fade_drag_preview(c, fade_in, to)) {diff --git a/crates/ui-egui/tests/edit_modes.rs b/crates/ui-egui/tests/edit_modes.rs--- a/crates/ui-egui/tests/edit_modes.rs+++ b/crates/ui-egui/tests/edit_modes.rs@@ -8,6 +8,37 @@ use soundcraft_ui_egui::{Services, SoundApp, edit_window};  const SEC: i64 = 48_000; +#[test]+fn sliced_stereo_clip_uses_the_track_and_position_where_the_pointer_is_released() {+    for (mode, tool) in [("slip", "grabber"), ("slip", "smart"), ("grid", "grabber"), ("grid", "smart")] {+        let (mut app, ctx, tracks, _) = tracks(mode, tool, &[&[(0, 4 * SEC)], &[]]);+        for t in &tracks {+            app.engine.session_mut().track_mut(*t).unwrap().format = soundcraft_model::ChannelFormat::Stereo;+        }+        app.engine.session_mut().sources[0].channels = 2;+        app.engine.execute("edit.select", &json!({"tracks": [tracks[0].0], "start": 2 * SEC, "end": 2 * SEC})).unwrap();+        app.engine.execute("edit.separate", &json!({})).unwrap();+        frame(&ctx, &mut app, vec![]);+        let clip = app.engine.session().track(tracks[0]).unwrap().clips()[1].clone();+        let before = app.engine.session().clone();+        let from = point(&app, tracks[0], 3 * SEC);+        let held = from + vec2(20.0, 0.0);+        let release = point(&app, tracks[1], 3 * SEC + SEC / 2);+        frame(&ctx, &mut app, vec![Event::PointerMoved(from), button(from, true)]);+        frame(&ctx, &mut app, vec![Event::PointerMoved(held)]);+        frame(&ctx, &mut app, vec![]);+        // A release may arrive at a new position with no preceding held-pointer frame there.+        frame(&ctx, &mut app, vec![Event::PointerMoved(release), button(release, false)]);+        let (track, moved) = app.engine.session().find_clip(clip.id).unwrap();+        assert_eq!(track, tracks[1], "{tool}: clip stayed on the previous hover track");+        assert_eq!(moved.start, clip.start + SEC / 2, "{tool}: final pointer position was ignored");+        assert_eq!(moved.length, clip.length);+        assert_eq!(moved.content, clip.content);+        assert!(app.engine.undo());+        assert_eq!(app.engine.session(), &before);+    }+}+ fn frame(ctx: &egui::Context, app: &mut SoundApp, events: Vec<Event>) {     let raw = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))), events, ..Default::default() };     ctx.run_ui(raw, |ui| {