From e837dae7b3bbacf96d4ce72810012c45b09fb95d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 15 Nov 2026 08:30:11 -0800 Subject: [PATCH] Export for Screens: artboard name fields close on a first-frame key and commit when another opens Escape or Enter pressed in the first frame that draws an artboard's name field was dropped: the field asked for focus and skipped its key checks, while the dialog kept itself open for the field. The field now ends the edit on either key in that frame too. A double-click on an earlier artboard's name, with both clicks in one frame, replaced the edit of a later artboard before that artboard's field was drawn, so the name typed there was lost. The typed name now goes to its artboard before the new field opens. Follows #7695. --- .../ui-egui/src/dialogs/export_for_screens.rs | 13 +++- crates/ui-egui/src/dialogs/tests_screens.rs | 75 +++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/dialogs/export_for_screens.rs b/crates/ui-egui/src/dialogs/export_for_screens.rs index 5b28affa2f..96b5f63c27 100644 --- a/crates/ui-egui/src/dialogs/export_for_screens.rs +++ b/crates/ui-egui/src/dialogs/export_for_screens.rs @@ -638,15 +638,16 @@ fn edit_artboard_name( widgets::reject_dialog_commit(ui.ctx()); } let response = ui.put(rect, egui::TextEdit::singleline(text).id(field).font(egui::FontId::proportional(11.0)).desired_width(rect.width())); - if *focus { - widgets::focus_field(ui, field, text); - *focus = false; - } else if cancel || enter || response.lost_focus() { + // Escape and Enter also end the edit in the first frame that draws the field. + if cancel || enter || (!*focus && response.lost_focus()) { if !cancel { *rename = Some((*b, std::mem::take(text))); } response.surrender_focus(); *editing = None; + } else if *focus { + widgets::focus_field(ui, field, text); + *focus = false; } (false, true) } else { @@ -654,6 +655,10 @@ fn edit_artboard_name( if let Some(board) = board && response.double_clicked() { + // A name still typed for an artboard whose field this frame has not drawn yet is kept. + if let Some((b, text, _)) = editing.take() { + *rename = Some((b, text)); + } *editing = Some((board, name.into(), true)); ui.ctx().request_repaint(); // If both releases arrived this frame, the first click did not toggle the row. diff --git a/crates/ui-egui/src/dialogs/tests_screens.rs b/crates/ui-egui/src/dialogs/tests_screens.rs index e26c82de60..b99405ecfe 100644 --- a/crates/ui-egui/src/dialogs/tests_screens.rs +++ b/crates/ui-egui/src/dialogs/tests_screens.rs @@ -1547,3 +1547,78 @@ fn an_artboard_name_stays_with_its_document() { assert_eq!(names(&app, 1), ["Artboard 1", "Artboard 2"]); assert!(log.borrow().is_empty(), "renaming must not export"); } + +/// Escape or Enter in the first frame that draws an artboard's name field closes the field, and +/// the dialog stays open. +#[test] +fn a_key_in_the_first_frame_of_an_artboard_name_closes_it() { + for view in ["grid", "list"] { + for key in [egui::Key::Escape, egui::Key::Enter] { + let (mut app, log) = app(2, true); + open(&mut app); + set(&mut app, "view", json!(view)); + let mut h = ScreensFrame::new(); + let out = h.settle(&mut app); + let at = screen_text_at(&out, "1 Artboard 1"); + h.frame(&mut app, vec![egui::Event::PointerMoved(at)]); + for pressed in [true, false, true, false] { + h.frame( + &mut app, + vec![egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }], + ); + } + // The double-click opened the field in the last frame; this frame draws it first. + h.time += 0.1; + let input = egui::RawInput { + events: vec![egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }], + time: Some(h.time), + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1100.0, 900.0))), + ..Default::default() + }; + let mut out = h.ctx.run_ui(input, |ui| { + crate::shortcuts::handle(&mut app, ui.ctx()); + super::show(&mut app, ui.ctx()); + }); + out.textures_delta.clear(); + h.settle(&mut app); + assert!(!h.ctx.egui_wants_keyboard_input(), "{view}, {key:?}: the field closes"); + assert_eq!(kind(&app), Some(export_for_screens::KIND), "{view}, {key:?}"); + assert_eq!(app.session.active().unwrap().doc.artboards[0].name, "Artboard 1", "{view}, {key:?}"); + assert!(log.borrow().is_empty(), "{view}, {key:?}: the key must not export"); + } + } +} + +/// A double-click on an earlier artboard's name, both clicks in one frame, renames the artboard +/// whose name was being typed before it opens the new field. +#[test] +fn double_clicking_another_artboard_name_keeps_the_typed_name() { + for view in ["grid", "list"] { + let (mut app, log) = app(2, true); + open(&mut app); + set(&mut app, "view", json!(view)); + let mut h = ScreensFrame::new(); + let before = field(&app, "boards"); + let double_click = |h: &mut ScreensFrame, app: &mut VectorcraftApp, at: egui::Pos2| { + h.frame(app, vec![egui::Event::PointerMoved(at)]); + let events = [true, false, true, false] + .into_iter() + .map(|pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }) + .collect(); + h.frame(app, events); + h.settle(app); + }; + let out = h.settle(&mut app); + let first = screen_text_at(&out, "1 Artboard 1"); + double_click(&mut h, &mut app, screen_text_at(&out, "2 Artboard 2")); + assert!(h.ctx.egui_wants_keyboard_input(), "{view}: the double-click opens the name"); + h.frame(&mut app, vec![egui::Event::Text("Typed".into())]); + h.time += 1.0; + double_click(&mut h, &mut app, first); + let names: Vec = app.session.active().unwrap().doc.artboards.iter().map(|a| a.name.clone()).collect(); + assert_eq!(names, ["Artboard 1", "Typed"], "{view}"); + assert!(h.ctx.egui_wants_keyboard_input(), "{view}: the second double-click opens its name"); + assert_eq!(field(&app, "boards"), before, "{view}"); + assert!(log.borrow().is_empty(), "{view}: renaming must not export"); + } +}