diff --git a/crates/ui-egui/src/panels/keyword_list.rs b/crates/ui-egui/src/panels/keyword_list.rs index f37e8ff64..5b92c4b50 100644 --- a/crates/ui-egui/src/panels/keyword_list.rs +++ b/crates/ui-egui/src/panels/keyword_list.rs @@ -25,7 +25,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { register(ui.ctx(), "keywordList:topLevel", title); let dragging = app.ui.dragging_keyword.clone(); let (pointer, released) = ui.input(|i| (i.pointer.latest_pos(), i.pointer.any_released())); - let over_title = pointer.is_some_and(|p| title.contains(p)); + let over_title = shown_under(ui, title, pointer); if dragging.is_some() && over_title { ui.painter().rect_stroke(title.shrink2(vec2(16.0, 6.0)), 4.0, Stroke::new(1.5, t.accent), StrokeKind::Inside); } @@ -89,6 +89,12 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } } +/// The pointer is on the part of `rect` that is shown: inside it and inside the panel's visible +/// (scrolled) area, so a drop never lands on a row or title hidden under the top bar. +fn shown_under(ui: &egui::Ui, rect: Rect, pointer: Option) -> bool { + pointer.is_some_and(|p| rect.contains(p) && ui.clip_rect().contains(p)) +} + /// The dragged keyword's name, following the pointer. fn drag_label(ctx: &egui::Context, keyword: &str, at: egui::Pos2) { let t = Tokens::get(ctx); @@ -236,7 +242,7 @@ fn row(app: &mut LightcraftApp, ui: &mut egui::Ui, r: &Row, selection: &[PhotoId /// keyword inside this one, or gives this keyword to the photos. fn drop_target(app: &mut LightcraftApp, ui: &mut egui::Ui, rect: Rect, path: &str) { let (pointer, released) = ui.input(|i| (i.pointer.latest_pos(), i.pointer.any_released())); - if !pointer.is_some_and(|p| rect.contains(p)) { + if !shown_under(ui, rect, pointer) { return; } let t = Tokens::get(ui.ctx()); diff --git a/crates/ui-egui/src/tests_keyword_list.rs b/crates/ui-egui/src/tests_keyword_list.rs index d63cc5768..9c840811d 100644 --- a/crates/ui-egui/src/tests_keyword_list.rs +++ b/crates/ui-egui/src/tests_keyword_list.rs @@ -270,3 +270,26 @@ fn dropping_photos_on_a_keyword_tags_them() { assert!(ids.iter().all(|id| keywords_of(&h, *id).contains(&"Portugal".to_string())), "the selection got it"); assert!(h.app.ui.dragging_photos.is_none()); } + +fn rect(h: &Headless, id: &str) -> egui::Rect { + h.app.widgets.iter().rev().find(|(w, _)| w == id).map(|(_, r)| *r).unwrap_or_else(|| panic!("no {id}")) +} + +/// A drop lands only on what is shown: with the list scrolled so that its title is hidden under +/// the top bar, a keyword released over the top bar stays where it is. +#[test] +fn drops_land_only_on_what_is_shown() { + let (mut h, ids) = keywords_panel_at([1400.0, 640.0]); + run(&mut h, "photo.setMeta", json!({"ids": [ids[0]], "keywords": ["aaa|zz"]})); + ask(&mut h, "ui.clickWidget", json!({"id": "keywordRowToggle:aaa"})); + let title = rect(&h, "keywordList:topLevel"); + ask(&mut h, "ui.hoverWidget", json!({"id": "keywordRow:aaa"})); + ask(&mut h, "ui.scroll", json!({"dx": 0, "dy": -(title.top() - 20.0)})); + let title = rect(&h, "keywordList:topLevel"); + let hidden = egui::pos2(title.center().x, title.top() + 8.0); + assert!(hidden.y < 44.0 && title.contains(hidden), "the title's top is under the top bar: {title:?}"); + let row = rect(&h, "keywordRow:aaa|zz"); + assert!(row.top() > 44.0, "the row is shown: {row:?}"); + ask(&mut h, "ui.dragWidget", json!({"id": "keywordRow:aaa|zz", "toX": hidden.x, "toY": hidden.y, "steps": 12})); + assert_eq!(keywords_of(&h, ids[0]), ["aaa|zz"], "not moved to the top level"); +}