From 03bf8b1f33b099d0638e861bb47397cfb314d262 Mon Sep 17 00:00:00 2001 From: Vikram Bhaskaran Date: Sun, 11 Oct 2026 10:09:44 +1100 Subject: [PATCH] Fix OS drops on an empty Timeline creating a sequence. Dragging files from the explorer onto the Timeline imported them into the Project bin but never placed them when no sequence was open, because only the clip-area interact path consumed pending_timeline_drop. The empty Timeline now creates a sequence from the first imported item and places any further items after it, matching the existing Project-panel drop. --- crates/ui-egui/src/panels/timeline.rs | 20 +++++++++++++ crates/ui-egui/tests/timeline_drop_ui.rs | 37 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index ca4fab0dc..adfed455d 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -627,6 +627,26 @@ fn empty_state(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) { { let _ = app.session.execute("file.newSequence", json!({"fromItem": item.0})); } + // OS files dropped on an empty Timeline: import already happened; create a sequence from the first item + if let Some((items, p)) = app.pending_timeline_drop.clone() + && rect.contains(p) + && let Some((first, rest)) = items.split_first() + { + app.pending_timeline_drop = None; + if let Err(e) = app.session.execute("file.newSequence", json!({"fromItem": first.0})) { + app.ui.status = e.to_string(); + } else { + let mut t = app.session.project.item(*first).map(|i| i.duration()).unwrap_or(Tick::ZERO); + for item in rest { + let params = json!({"item": item.0, "time": t.0, "insert": false}); + if let Err(e) = app.session.execute("timeline.place", params) { + app.ui.status = e.to_string(); + } else { + t = Tick(t.0.saturating_add(app.session.project.item(*item).map(|i| i.duration().0.max(0)).unwrap_or(0))); + } + } + } + } } /// The through-edit mark: two small triangles pointing at the cut, centred on the track, with a diff --git a/crates/ui-egui/tests/timeline_drop_ui.rs b/crates/ui-egui/tests/timeline_drop_ui.rs index 76bdde9fa..3ad0510cc 100644 --- a/crates/ui-egui/tests/timeline_drop_ui.rs +++ b/crates/ui-egui/tests/timeline_drop_ui.rs @@ -143,6 +143,43 @@ fn tone_dropped_on_a_video_row_goes_to_the_audio_track_without_error() { assert_eq!(clip_counts(&h), (0, 1), "status: {}", h.state().ui.status); } +/// Empty project: no sequence yet. Dropping on the empty Timeline must import *and* make a sequence. +fn empty_harness() -> Harness<'static, FilmcraftApp> { + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 980.0)).build_eframe(move |_cc| FilmcraftApp::new(Session::default())); + for _ in 0..4 { + step(&mut h); + } + h +} + +fn drop_on_empty_timeline(h: &mut Harness<'static, FilmcraftApp>, path: &Path) { + let panel = control_rect(h, "panel.Timeline"); + pointer_to(h, panel.center()); + h.input_mut().dropped_files.push(std::sync::Arc::new(OsFile(path.to_path_buf()))); + step(h); + h.input_mut().dropped_files.clear(); + step(h); +} + +#[test] +fn still_dropped_on_empty_timeline_creates_a_sequence() { + let mut h = empty_harness(); + assert!(h.state().session.active_sequence().is_none()); + drop_on_empty_timeline(&mut h, &still("drop_still_empty_timeline.png")); + assert_eq!(h.state().session.project.items.len(), 2, "still plus the new sequence"); + assert!(h.state().session.active_sequence().is_some(), "status: {}", h.state().ui.status); + assert_eq!(clip_counts(&h), (1, 0), "status: {}", h.state().ui.status); +} + +#[test] +fn with_drop_import_off_empty_timeline_does_not_create_a_sequence() { + let mut h = empty_harness(); + h.state_mut().session.prefs.timeline.drop_import_to_timeline = false; + drop_on_empty_timeline(&mut h, &still("drop_still_empty_pref_off.png")); + assert_eq!(h.state().session.project.items.len(), 1, "imported to the bin only"); + assert!(h.state().session.active_sequence().is_none()); +} + #[test] fn with_drop_import_off_the_file_is_only_imported() { let mut h = harness();