LR-BEHAV-TEXTFIELD: Rename Keyword's name field is a shared text field

It gets the Cut / Copy / Paste / Select All menu and opens with the
name selected, so typing replaces it. Return now renames: the field
took the focus back on every frame, so egui never reported the focus
Return gives up and only the OK button renamed. The field still keeps
the focus, except while its own menu has it; Esc still cancels the
dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford
2026-10-09 13:23:10 -04:00
co-authored by Claude Opus 5.5
parent aa57db05a1
commit d1ec843863
2 changed files with 51 additions and 4 deletions
+40
View File
@@ -1560,6 +1560,46 @@ mod tests {
h.settle(SETTLE);
}
/// Rename Keyword's name field is a shared text field. It opens with the name selected, so
/// typing replaces it, and Return renames; right-click ▸ Paste works too; Esc cancels the
/// dialog and renames nothing.
#[test]
fn the_rename_keyword_field_selects_pastes_and_cancels() {
let mut h = demo([1200.0, 800.0]);
let t = Duration::from_secs(10);
let first = h.app.session.visible_cloned()[0].0;
h.request("engine.execute", json!({"command": "photo.setMeta", "params": {"ids": [first], "addKeywords": ["travel"]}}), t);
let keywords = |h: &Headless| h.app.session.catalog.photo(lightcraft_catalog::PhotoId(first)).unwrap().meta.keywords.clone();
let open = |h: &mut Headless, name: &str| {
h.app.ui.dialog = Some(crate::state::Dialog::RenameKeyword { from: name.into(), to: name.into() });
h.settle(SETTLE);
};
let ask = |h: &mut Headless, method: &str, params: serde_json::Value| {
let r = h.request(method, params.clone(), t);
assert_eq!(r["ok"], true, "{method} {params}: {r}");
h.settle(SETTLE);
};
// typing replaces the selected name
open(&mut h, "travel");
ask(&mut h, "ui.text", json!({"text": "trips"}));
ask(&mut h, "ui.key", json!({"key": "Enter"}));
assert!(keywords(&h).contains(&"trips".to_string()) && !keywords(&h).contains(&"travel".to_string()), "{:?}", keywords(&h));
assert_eq!(h.app.ui.dialog, None);
// the menu's Paste
open(&mut h, "trips");
h.view.clipboard = "weddings".into();
ask(&mut h, "ui.clickWidget", json!({"id": "field:keywordName", "button": "right"}));
ask(&mut h, "ui.clickWidget", json!({"id": "field:keywordName:paste"}));
ask(&mut h, "ui.key", json!({"key": "Enter"}));
assert!(keywords(&h).contains(&"weddings".to_string()), "{:?}", keywords(&h));
// Esc cancels
open(&mut h, "weddings");
ask(&mut h, "ui.text", json!({"text": "parties"}));
ask(&mut h, "ui.key", json!({"key": "Escape"}));
assert_eq!(h.app.ui.dialog, None, "Esc closed the dialog");
assert!(keywords(&h).contains(&"weddings".to_string()), "renamed nothing: {:?}", keywords(&h));
}
/// The sidebar's file-system checks run on worker threads and add rows when they land, which
/// moves every row below them: `busy()` counts them, so `settle` waits for them before a test
/// reads widget positions (a click aimed at a stale rect hits the neighbouring row).
+11 -4
View File
@@ -410,10 +410,17 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
}
Dialog::RenameKeyword { from, to } => {
let n = app.session.catalog.photos().filter(|p| p.meta.keywords.iter().any(|k| lightcraft_catalog::keywords::is_under(k, from))).count();
let r = ui.add(egui::TextEdit::singleline(to).hint_text(crate::i18n::tr("New name")).desired_width(f32::INFINITY));
crate::widgets::register(ui.ctx(), "field:keywordName", r.rect);
r.request_focus();
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
// the name, selected so typing replaces it; Esc cancels the dialog (below)
let r = crate::text_field::TextField::singleline("field:keywordName", to)
.hint(crate::i18n::tr("New name"))
.width(f32::INFINITY)
.select_on_focus(true)
.show(ui);
// the field keeps the focus, but leaves it to its own menu
if !r.editing {
r.response.request_focus();
}
if r.ending == Some(crate::text_field::Ending::Return) {
confirm = true;
}
ui.label(