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 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 14:55:15 -04:00
co-authored by Claude Opus 5.5
parent 199e33043f
commit 5d4ebe5132
2 changed files with 31 additions and 2 deletions
+8 -2
View File
@@ -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<egui::Pos2>) -> 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());
+23
View File
@@ -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");
}