LR-BEHAV-TEXTFIELD: the unnamed faces' name field is a shared text field

It gets the Cut / Copy / Paste / Select All menu, and Esc in it now
clears the selection as Clear does, as the code meant: it checked the
field's focus on the Esc frame, which egui has already taken away, so
Esc only left the field.

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 7baf93ecdb
commit a92c38fdd4
2 changed files with 43 additions and 4 deletions
+39
View File
@@ -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]
+4 -4
View File
@@ -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();