diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index 788811fab..e77c45b0a 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -1241,6 +1241,45 @@ mod tests { assert_eq!(named(&h, ids[1], 0), None); } + /// The name field under the unnamed faces is a shared text field: right-click ▸ Paste puts the + /// clipboard's name in, and Esc leaves the field and clears the selection, as Clear does. + #[test] + fn the_unnamed_faces_name_field_pastes_and_escape_clears() { + use lightcraft_catalog::Op; + let mut h = demo([1400.0, 900.0]); + let t = Duration::from_secs(10); + let id = h.app.session.catalog.photos().map(|p| p.id).next().unwrap(); + let mut meta = h.app.session.catalog.photo(id).unwrap().meta.clone(); + meta.regions = vec![lightcraft_meta::Region { + rect: lightcraft_geom::Rect { x0: 0.3, y0: 0.2, x1: 0.5, y1: 0.55 }, + kind: lightcraft_meta::RegionKind::Face, + name: None, + description: None, + }]; + h.app.session.commit("setup", Op::SetMeta { id, meta: Box::new(meta) }).unwrap(); + h.request("engine.execute", json!({"command": "view.people"}), t); + h.settle(SETTLE); + h.view.clipboard = "Wedding Guest".into(); + for (widget, button) in [ + (format!("unnamed-face:{}:0", id.0), "left"), + ("field:unnamedName".to_string(), "left"), + ("field:unnamedName".to_string(), "right"), + ("field:unnamedName:paste".to_string(), "left"), + ] { + let r = h.request("ui.clickWidget", json!({"id": widget, "button": button}), t); + assert_eq!(r["ok"], true, "{widget}: {r}"); + h.step(); + h.step(); + h.step(); + } + assert_eq!(h.app.ui.unnamed_name, "Wedding Guest"); + assert_eq!(h.app.ui.unnamed_selected.len(), 1, "still naming"); + h.request("ui.key", json!({"key": "Escape"}), t); + h.step(); + h.step(); + assert!(h.app.ui.unnamed_selected.is_empty() && h.app.ui.unnamed_name.is_empty(), "Esc cleared the selection and the name"); + } + /// A screenful of faces larger than the picture cache's usual budget (96) is all kept: with a fixed budget the same few /// tiles were evicted and re-requested every frame and stayed blank. #[test] diff --git a/crates/ui-egui/src/panels/unnamed.rs b/crates/ui-egui/src/panels/unnamed.rs index ef50ccaef..0b730db78 100644 --- a/crates/ui-egui/src/panels/unnamed.rs +++ b/crates/ui-egui/src/panels/unnamed.rs @@ -132,14 +132,14 @@ pub fn naming_bar(app: &mut LightcraftApp, ui: &mut egui::Ui) { let n = selected.len(); ui.label(RichText::new(if n == 1 { "1 face selected".to_string() } else { format!("{n} faces selected") }).color(t.text)); let hint = suggested.clone().unwrap_or_else(|| "Name…".to_string()); - let edit = ui.add(egui::TextEdit::singleline(&mut app.ui.unnamed_name).hint_text(hint).desired_width(240.0)); - register(ui.ctx(), "field:unnamedName", edit.rect); + let field = crate::text_field::TextField::singleline("field:unnamedName", &mut app.ui.unnamed_name).hint(hint).width(240.0).show(ui); + let edit = &field.response; if std::mem::take(&mut app.ui.unnamed_focus) { edit.request_focus(); } let typed = app.ui.unnamed_name.trim().to_string(); let chosen = if typed.is_empty() { suggested.clone().unwrap_or_default() } else { typed.clone() }; - if edit.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !chosen.is_empty() { + if field.ending == Some(crate::text_field::Ending::Return) && !chosen.is_empty() { submit = Some(chosen.clone()); } let label = if n == 1 { "Name face".to_string() } else { format!("Name {n} faces") }; @@ -150,7 +150,7 @@ pub fn naming_bar(app: &mut LightcraftApp, ui: &mut egui::Ui) { } let c = ui.button("Clear"); register(ui.ctx(), "unnamed:clear", c.rect); - clear = c.clicked() || (edit.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Escape))); + clear = c.clicked() || field.cancelled(); }); // the people already named, matching what is typed (a click names the faces) let typed = app.ui.unnamed_name.trim().to_lowercase();