From df12664b8eabb05e699d43ec8539c50f449cdc26 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Tue, 6 Oct 2026 00:37:05 +0700 Subject: [PATCH] Fix #44: double-click the Project panel's empty area to import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As in After Effects, a double-click below the items in the Project panel opens File ▸ Import ▸ File... (rows and the scroll bar keep their own clicks). Dropping files on the window already imported them; the test now covers both. (On Wayland, winit delivers no file drops at all, so dragging files in works on X11, macOS and Windows.) Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/project.rs | 7 +++++ crates/ui-egui/tests/ui_tabs.rs | 44 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/crates/ui-egui/src/panels/project.rs b/crates/ui-egui/src/panels/project.rs index 86e91ff9..959d9c60 100644 --- a/crates/ui-egui/src/panels/project.rs +++ b/crates/ui-egui/src/panels/project.rs @@ -303,6 +303,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let footer_h = 28.0; let list = Rect::from_min_max(pos2(rect.min.x, hdr.max.y), pos2(rect.max.x, rect.max.y - footer_h)); let lp = p.with_clip_rect(list); + // The list's empty area (under the rows and the scroll bar, which take their own clicks): + // a double-click imports, as in After Effects (File ▸ Import ▸ File...). + let empty = ui.interact(list, egui::Id::new("proj-empty"), Sense::click()); + app.auto.add("project.empty", list, "Double-click to import files"); if overflow > 0.0 && ui.rect_contains_pointer(list) { let (dx, dy, shift) = ui.input(|i| (i.smooth_scroll_delta.x, i.smooth_scroll_delta.y, i.modifiers.shift)); let d = if dx.abs() > 0.0 { @@ -686,6 +690,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { actions.push(("project.delete".into(), json!({}))); } app.auto.add("project.delete", tr, "Delete"); + if empty.double_clicked() { + actions.push(("file.import".into(), json!({}))); + } for (id, params) in actions { if let Err(e) = crate::menus::invoke(app, &ctx, &id, params) { app.ui.status = e; diff --git a/crates/ui-egui/tests/ui_tabs.rs b/crates/ui-egui/tests/ui_tabs.rs index 1bb0cf97..c1b3e73e 100644 --- a/crates/ui-egui/tests/ui_tabs.rs +++ b/crates/ui-egui/tests/ui_tabs.rs @@ -219,3 +219,47 @@ fn project_details_stay_inside_the_panel() { let r = rect(&h, "project.details.hint"); assert!(r.min.x >= panel.min.x + 9.0 && r.max.x <= panel.max.x - 9.0, "{r:?} in {panel:?}"); } + +/// A file dropped on the window (what the windowing layer hands egui). +#[derive(Debug)] +struct Dropped(std::path::PathBuf); + +impl egui::DroppedFile for Dropped { + fn path(&self) -> &std::path::Path { + &self.0 + } + fn bytes(&self) -> Result, String> { + std::fs::read(&self.0).map_err(|e| e.to_string()) + } +} + +/// Issue #44: double-clicking the Project panel's empty area asks for files to import (as After +/// Effects' File ▸ Import ▸ File... does), and files dropped on the window are imported. (Tests +/// have no media decoders: what is checked is that the import runs with those files.) +#[test] +fn project_empty_area_double_click_and_dropped_files_import() { + let (mut h, _, _) = harness(); + let png = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/app-icon/hicolor/16x16/apps/ai.storyteller.effectcraft.png"); + let png = std::fs::canonicalize(png).unwrap().to_string_lossy().to_string(); + let asked = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let (a, p) = (asked.clone(), png.clone()); + h.state_mut().hooks.pick_files = Some(Box::new(move |_| { + a.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + vec![p.clone()] + })); + let imports = |h: &Harness<'_, EffectcraftApp>| h.state().session.journal.iter().filter(|(c, p)| c == "file.import" && p["paths"] == json!([png])).count(); + // Below the last row. + let last = rect(&h, "project.item.2"); + let empty = rect(&h, "project.empty"); + click_n(&mut h, pos2(empty.center().x, (last.max.y + empty.max.y) / 2.0), 2); + assert_eq!(asked.load(std::sync::atomic::Ordering::Relaxed), 1, "the file dialog opened"); + assert_eq!(imports(&h), 1, "the picked file was imported"); + // A double-click on a row opens that item instead. + let row = rect(&h, "project.item.2.name"); + click_n(&mut h, row.center(), 2); + assert_eq!(asked.load(std::sync::atomic::Ordering::Relaxed), 1); + // Dropping a file on the window imports it. + h.input_mut().dropped_files.push(std::sync::Arc::new(Dropped(png.clone().into()))); + h.run_steps(2); + assert_eq!(imports(&h), 2, "the dropped file was imported"); +}