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
@@ -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)) {@@ -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| {