From 440db189a5b38d977473005ae07e376abd0ee6d3 Mon Sep 17 00:00:00 2001 From: zitann2020 Date: Sat, 31 Oct 2026 02:52:35 +0900 Subject: [PATCH 1/2] ui: a file dropped on a floating Timeline joins that composition (#2661) Co-Authored-By: DeepSeek V4.1 Flash --- crates/ui-egui/src/dock_ui.rs | 5 +++++ crates/ui-egui/src/lib.rs | 4 ++++ crates/ui-egui/src/prefs_live.rs | 7 ++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index b66267d4d..acb7cd389 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -530,6 +530,11 @@ impl EffectcraftApp { let typing = ctx.egui_wants_keyboard_input(); let events = events.into_iter().filter(|event| !typing && !matches!(event, egui::Event::Text(_) | egui::Event::Ime(_))); self.forwarded.events.extend(events); + // A floating window's drop names the panel under its pointer, which the main window's + // pointer can never hit (#2661). + if files.iter().any(|window| matches!(window.panel, PanelKind::Timeline)) { + self.forwarded.dropped_on_timeline = true; + } self.forwarded.files.extend(files.into_iter().flat_map(|window| window.dropped)); if !self.forwarded.events.is_empty() || !self.forwarded.files.is_empty() { ctx.request_repaint(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ad1842e3d..ca8c1fb01 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -442,6 +442,10 @@ impl FileDialogsUp { pub(crate) struct ForwardedInput { pub(crate) events: Vec, pub(crate) files: Vec, + /// Files handed over by a floating window whose panel under the pointer was the Timeline. The + /// paths lose the window they came from, so this keeps the one thing the import still needs + /// (#2661); each drop clears it. + pub(crate) dropped_on_timeline: bool, } /// Record the scale the viewer drew at (called by the viewer panel each pass). diff --git a/crates/ui-egui/src/prefs_live.rs b/crates/ui-egui/src/prefs_live.rs index cc283635f..32465a423 100644 --- a/crates/ui-egui/src/prefs_live.rs +++ b/crates/ui-egui/src/prefs_live.rs @@ -63,6 +63,10 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) { errors.push(crate::i18n::tr_args("Skipped {} dropped file(s) without a local path. Use File ▸ Import ▸ File…", &[&skipped])); } + // Taken once per drop, before any early return, so a project-only drop cannot leave it set + // for the next one (#2661). + let handed_by_a_floating_window = std::mem::take(&mut app.forwarded.dropped_on_timeline); + let (proj, rest): (Vec, Vec) = paths.into_iter().partition(|p| { let l = p.to_ascii_lowercase(); l.ends_with(".ecproj") || l.ends_with(".ecprojx") @@ -88,7 +92,8 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) { // Dropping a file on the Timeline adds it to the composition the panel shows, as it does // on the viewer (#2588); the import decides the layer's position. - let over_timeline = ctx.input(|i| i.pointer.hover_pos()).is_some_and(|p| { + let over_timeline = handed_by_a_floating_window + || ctx.input(|i| i.pointer.hover_pos()).is_some_and(|p| { app.auto .elements .iter() From ab4e8295c7ce1e6a0f48fef0e9d88ac5cee73366 Mon Sep 17 00:00:00 2001 From: zitann2020 Date: Sat, 31 Oct 2026 03:08:41 +0900 Subject: [PATCH 2/2] ui: the handed-over drop ignores a window that only saw the file hover Co-Authored-By: DeepSeek V4.1 Flash --- crates/ui-egui/src/dock_ui.rs | 34 +++++++++++++++++++++++++++++--- crates/ui-egui/src/prefs_live.rs | 16 +++++++-------- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/dock_ui.rs b/crates/ui-egui/src/dock_ui.rs index acb7cd389..ace5e7662 100644 --- a/crates/ui-egui/src/dock_ui.rs +++ b/crates/ui-egui/src/dock_ui.rs @@ -530,9 +530,7 @@ impl EffectcraftApp { let typing = ctx.egui_wants_keyboard_input(); let events = events.into_iter().filter(|event| !typing && !matches!(event, egui::Event::Text(_) | egui::Event::Ime(_))); self.forwarded.events.extend(events); - // A floating window's drop names the panel under its pointer, which the main window's - // pointer can never hit (#2661). - if files.iter().any(|window| matches!(window.panel, PanelKind::Timeline)) { + if handed_over_to_the_timeline(files.iter().map(|window| (window.panel, !window.dropped.is_empty()))) { self.forwarded.dropped_on_timeline = true; } self.forwarded.files.extend(files.into_iter().flat_map(|window| window.dropped)); @@ -867,3 +865,33 @@ impl DockContent for EffectContent<'_> { } } } + +/// Whether a handed-over *drop* named the Timeline. A floating window reports its hovered files +/// too, so a drag that only passes over the Timeline window must not count: its flag would survive +/// and send the next drop somewhere else. The panel comes from the window, which the main window's +/// pointer can never reach (#2661). +fn handed_over_to_the_timeline(drops: impl Iterator) -> bool { + drops.into_iter().any(|(panel, has_files)| has_files && matches!(panel, PanelKind::Timeline)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A drop handed over by a floating window says which panel it landed on; only the Timeline + /// turns it into a layer instead of a project item. A window that only saw the file hover over + /// it carries no files and must not count, or its flag would outlive the drag (#2661). + #[test] + fn only_a_timeline_drop_is_handed_over_to_the_timeline() { + let cases = [ + (vec![(PanelKind::Timeline, true)], true), + (vec![(PanelKind::Project, true)], false), + (vec![(PanelKind::Timeline, false)], false), + (vec![(PanelKind::Project, true), (PanelKind::Timeline, true)], true), + (vec![], false), + ]; + for (windows, want) in cases { + assert_eq!(handed_over_to_the_timeline(windows.into_iter()), want, "{want}"); + } + } +} diff --git a/crates/ui-egui/src/prefs_live.rs b/crates/ui-egui/src/prefs_live.rs index 32465a423..b456dc7ce 100644 --- a/crates/ui-egui/src/prefs_live.rs +++ b/crates/ui-egui/src/prefs_live.rs @@ -63,8 +63,8 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) { errors.push(crate::i18n::tr_args("Skipped {} dropped file(s) without a local path. Use File ▸ Import ▸ File…", &[&skipped])); } - // Taken once per drop, before any early return, so a project-only drop cannot leave it set - // for the next one (#2661). + // Taken once per drop frame, so a handed-over flag cannot survive into a later, unrelated + // drop (#2661). let handed_by_a_floating_window = std::mem::take(&mut app.forwarded.dropped_on_timeline); let (proj, rest): (Vec, Vec) = paths.into_iter().partition(|p| { @@ -94,12 +94,12 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) { // on the viewer (#2588); the import decides the layer's position. let over_timeline = handed_by_a_floating_window || ctx.input(|i| i.pointer.hover_pos()).is_some_and(|p| { - app.auto - .elements - .iter() - .find(|e| e.id == "panel.Timeline") - .is_some_and(|e| Rect::from_min_size(egui::pos2(e.rect[0], e.rect[1]), egui::vec2(e.rect[2], e.rect[3])).contains(p)) - }); + app.auto + .elements + .iter() + .find(|e| e.id == "panel.Timeline") + .is_some_and(|e| Rect::from_min_size(egui::pos2(e.rect[0], e.rect[1]), egui::vec2(e.rect[2], e.rect[3])).contains(p)) + }); let at = if proj.is_empty() && !over_timeline { ctx.input(|i| i.pointer.hover_pos()).filter(|p| viewer.is_some_and(|v| v.contains(*p))).and_then(|p| crate::panels::viewer::screen_to_comp(&ctx, p))