Merge pull request #662 from Ni-zav/fix/649-empty-timeline-project-drop-s20261010T2145Z-e7a2

This commit is contained in:
Claude 2026-10-10 22:28:36 +00:00
commit 44e3fe5f6e
No known key found for this signature in database
2 changed files with 75 additions and 0 deletions

View File

@ -1207,6 +1207,36 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let ctx = ui.ctx().clone();
let Some(cid) = app.session.active_comp_id() else {
ui.painter().text(rect.center(), Align2::CENTER_CENTER, "(no composition)", Tokens::ui(12.0), t.text_faint);
// Dropping footage into an empty Timeline should create its first composition,
// just like dropping onto the Project panel's New Composition button (#649).
if let Some(crate::panels::DragPayload::Item(dragged)) = egui::DragAndDrop::payload::<crate::panels::DragPayload>(&ctx).as_deref()
&& ui.rect_contains_pointer(rect)
{
let items: Vec<ItemId> = crate::panels::project_drop_items(app, *dragged)
.into_iter()
.map(ItemId)
.filter(|id| {
app.session.project.item(*id).is_some_and(|item| match &item.kind {
effectcraft_engine::project::ItemKind::Footage(footage) => {
footage.kind != effectcraft_engine::project::FootageKind::Data
}
effectcraft_engine::project::ItemKind::Solid(_) => true,
_ => false,
})
})
.collect();
if !items.is_empty() {
ui.painter().rect_stroke(rect.shrink(2.0), 0.0, Stroke::new(2.0, t.accent), StrokeKind::Inside);
app.auto.add("timeline.newCompDrop", rect, "Drop footage to create a composition");
if ctx.input(|i| i.pointer.any_released()) {
app.session.state.project_selection = items;
if let Err(e) = crate::menus::invoke(app, &ctx, "file.newCompFromSelection", json!({})) {
app.ui.status = e;
}
egui::DragAndDrop::clear_payload(&ctx);
}
}
}
return;
};
let Some(comp) = app.session.project.comp_arc(cid) else { return };

View File

@ -210,6 +210,51 @@ fn project_items_dropped_on_new_comp_make_a_composition() {
assert_eq!(comps(&h), before + 1, "nothing made yet");
}
/// #649: a Project footage item dropped into an empty Timeline makes a composition
/// with that item's dimensions, frame rate, duration, and a source layer.
#[test]
fn project_footage_dropped_on_empty_timeline_creates_first_comp() {
let mut s = Session::default();
let footage = s.execute(
"file.importPlaceholder",
json!({"name": "Landscape.png", "width": 640, "height": 360, "frameRate": 24, "duration": 5}),
).unwrap()["item"].as_u64().unwrap();
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
assert!(h.state().session.active_comp().is_none());
let from = rect(&h, &format!("project.item.{footage}.name")).center();
let to = rect(&h, "panel.Timeline").center();
drag(&mut h, from, to, Modifiers::NONE);
let comp = h.state().session.active_comp().expect("created a composition");
assert_eq!((comp.width, comp.height), (640, 360));
assert_eq!(comp.frame_rate, effectcraft_engine::time::FrameRate::from_f64(24.0));
assert_eq!(comp.duration.seconds(), 5.0);
assert_eq!(comp.layers.len(), 1);
assert_eq!(comp.layers[0].name, "Landscape.png");
assert!(h.state().dialog.is_none());
}
/// Multiple source items use the existing New Comp from Selection dialog, rather than
/// quietly creating only the dragged item or changing the composition settings.
#[test]
fn multiple_selected_footage_dropped_on_empty_timeline_asks_how_to_create_comps() {
let mut s = Session::default();
let first = s.execute("file.importPlaceholder", json!({"name": "First.png", "width": 160, "height": 90})).unwrap()["item"].as_u64().unwrap();
let second = s.execute("file.importPlaceholder", json!({"name": "Second.png", "width": 300, "height": 200})).unwrap()["item"].as_u64().unwrap();
s.state.project_selection = vec![
effectcraft_engine::project::ItemId(first),
effectcraft_engine::project::ItemId(second),
];
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
let from = rect(&h, &format!("project.item.{first}.name")).center();
let to = rect(&h, "panel.Timeline").center();
drag(&mut h, from, to, Modifiers::NONE);
assert_eq!(h.state().dialog, Some(effectcraft_ui_egui::Dialog::Form));
assert!(h.state().auto.find("form.field.single").is_some());
assert!(h.state().session.active_comp().is_none(), "no composition until the user chooses");
}
/// A file dropped on the window (egui hands it over as a dropped file).
#[derive(Debug)]
struct Dropped(std::path::PathBuf);