diff --git a/crates/ui-egui/src/panels/project.rs b/crates/ui-egui/src/panels/project.rs index c14b8131..d46b6508 100644 --- a/crates/ui-egui/src/panels/project.rs +++ b/crates/ui-egui/src/panels/project.rs @@ -2,8 +2,9 @@ //! search, item list with columns (Name, Label, then the visible optional columns: Type, Size, //! Media Duration, Frame Rate, File Path, Comment — shown or hidden from the header's context //! menu), folders (drag items into and out of them), renaming (Enter, or double-click the name), -//! the label colour picker, and the bottom bar (interpret, new folder, new comp, bit depth, -//! delete). Edits are `project.*` engine commands, so they are undoable. +//! the label colour picker, and the bottom bar (interpret, new folder, new comp: click, or drop +//! items on it for a comp from them, bit depth, delete). Edits are engine commands, so they are +//! undoable. use std::sync::{Arc, Mutex}; @@ -699,6 +700,21 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { _ => app.ui.status = "Interpret Footage: select a footage item".into(), } } + // Items dropped on Create a new Composition: New Comp from Selection with them (After + // Effects; several items ask how in its dialog). + if id == "newComp" + && let Some(DragPayload::Item(iid)) = egui::DragAndDrop::payload::(&ctx).as_deref() + && ui.rect_contains_pointer(r) + { + p.rect_stroke(r, 4.0, Stroke::new(2.0, t.accent), egui::StrokeKind::Inside); + if ctx.input(|i| i.pointer.any_released()) { + if !app.session.state.project_selection.contains(&ItemId(*iid)) { + app.session.state.project_selection = vec![ItemId(*iid)]; + } + actions.push(("file.newCompFromSelection".into(), json!({}))); + egui::DragAndDrop::clear_payload(&ctx); + } + } x += 26.0; } let depth = app.session.project.settings.bit_depth.label(); diff --git a/crates/ui-egui/tests/ui_drag_drop.rs b/crates/ui-egui/tests/ui_drag_drop.rs index 0fcdcd19..b05a235a 100644 --- a/crates/ui-egui/tests/ui_drag_drop.rs +++ b/crates/ui-egui/tests/ui_drag_drop.rs @@ -136,3 +136,32 @@ fn effects_dropped_on_the_viewer_go_on_the_layer_under_the_pointer() { let effects = |id: u64| comp.layer(effectcraft_engine::project::LayerId(id)).unwrap().effects().map_or(0, |fx| fx.groups().count()); assert_eq!((effects(top), effects(middle)), (1, 0), "on Top, which is under the pointer"); } + +/// #227: Project items dropped on Create a new Composition (the Project panel's footer) make a +/// composition from them, as File ▸ New Comp from Selection does; several items ask how in its +/// dialog. +#[test] +fn project_items_dropped_on_new_comp_make_a_composition() { + let (mut h, clip) = harness(); + let comps = |h: &Harness<'_, EffectcraftApp>| h.state().session.project.comps().count(); + let before = comps(&h); + let item = rect(&h, &format!("project.item.{clip}.name")).center(); + let button = rect(&h, "project.newComp").center(); + drag(&mut h, item, button, Modifiers::NONE); + assert_eq!(comps(&h), before + 1); + let comp = h.state().session.active_comp().unwrap(); + assert_eq!((comp.width, comp.height, comp.duration.seconds()), (160, 90, 1.0), "the item's settings"); + assert_eq!(stack(&h), ["Clip"], "holding the item"); + assert_ne!(h.state().session.active_comp_id().map(|c| c.0), Some(clip), "a new comp, open"); + assert!(h.state().dialog.is_none()); + + // Two selected items, one dragged: New Composition from Selection asks how. + let main = h.state().session.project.items.values().find(|i| i.name == "Main").unwrap().id; + h.state_mut().session.state.project_selection = vec![effectcraft_engine::project::ItemId(clip), main]; + h.run_steps(2); + let item = rect(&h, &format!("project.item.{clip}.name")).center(); + drag(&mut h, item, button, Modifiers::NONE); + assert_eq!(h.state().dialog, Some(effectcraft_ui_egui::Dialog::Form)); + assert!(h.state().auto.find("form.field.single").is_some(), "the New Composition from Selection dialog"); + assert_eq!(comps(&h), before + 1, "nothing made yet"); +}