From 5d4ebe5132d49132d232ad22c226e198a8879f1a Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 14:55:15 -0400 Subject: [PATCH] LRC-LIB-KEYWORDS: drops land only on what is shown The Keyword List took a drop wherever the pointer was inside a row's or the title's rectangle, even where the panel had scrolled it out of sight: a keyword released over the app's top bar, where the list's title was hidden, went to the top level. Drops now need the pointer on the visible part of the panel too; photos dropped from the grid alike. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/keyword_list.rs | 10 ++++++++-- crates/ui-egui/src/tests_keyword_list.rs | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) 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"); +}