From b8043cc2adf9e317ce862c047e82690f4aac845a Mon Sep 17 00:00:00 2001 From: Barzabel Date: Wed, 14 Oct 2026 22:23:51 +0300 Subject: [PATCH] M3: renaming a style folder no longer closes the app --- crates/ui-egui/src/lib.rs | 2 + .../ui-egui/src/style_folder_rename_tests.rs | 56 +++++++++++++++++++ crates/ui-egui/src/style_picker.rs | 7 ++- 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 crates/ui-egui/src/style_folder_rename_tests.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 613f18f9..8f564ead 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -144,6 +144,8 @@ mod shortcut_keys_tests; #[cfg(test)] mod shortcuts_tests; #[cfg(test)] +mod style_folder_rename_tests; +#[cfg(test)] mod text_enter_tests; #[cfg(test)] mod titlebar_tests; diff --git a/crates/ui-egui/src/style_folder_rename_tests.rs b/crates/ui-egui/src/style_folder_rename_tests.rs new file mode 100644 index 00000000..624edfda --- /dev/null +++ b/crates/ui-egui/src/style_folder_rename_tests.rs @@ -0,0 +1,56 @@ +//! Renaming a folder of local styles, drawn headless with the screen reader tree on: Rename folder +//! in the folder's menu opens its name for editing. On every frame the focused node must be in the +//! AccessKit tree; the platform adapter (UI Automation on Windows) aborts the app when it isn't +//! (the same fault as #224). + +use egui::{Event, Key, Modifiers, PointerButton, Pos2}; +use figcraft_engine::Session; +use serde_json::json; + +use crate::layers_drag_tests::Ui; +use crate::live_tests::{flat, text_center}; + +/// One frame with `events`, checking the focused node is in the screen reader tree. +fn checked_frame(u: &mut Ui, events: Vec, k: usize) -> Vec { + let out = u.frame_output(events); + let tree = out.platform_output.accesskit_update.expect("an AccessKit update"); + assert!(tree.nodes.iter().any(|(i, _)| *i == tree.focus), "frame {k}: the focused node {:?} isn't in the tree", tree.focus); + flat(&out.shapes) +} + +/// A click with `button` at `p` and the frames that follow, each checked. +fn click(u: &mut Ui, p: Pos2, button: PointerButton) -> Vec { + for _ in 0..5 { + u.frame(vec![Event::PointerMoved(p)]); + } + let mut shapes = Vec::new(); + for (k, pressed) in [Some(true), Some(false)].into_iter().chain(std::iter::repeat_n(None, 20)).enumerate() { + let events = pressed.map(|pressed| Event::PointerButton { pos: p, button, pressed, modifiers: Modifiers::NONE }); + shapes = checked_frame(u, events.into_iter().collect(), k); + } + shapes +} + +#[test] +fn renaming_a_style_folder_keeps_the_focus_in_the_screen_reader_tree() { + let mut s = Session::new(); + for name in ["Brand/Primary", "Brand/Accent"] { + s.execute("style.create", &json!({"type": "paint", "name": name, "value": [{"type": "solid", "color": "#0D99FF"}]})).unwrap(); + } + let mut u = Ui::new(s); + u.ctx.enable_accesskit(); + let shapes = checked_frame(&mut u, vec![], 0); + // The Design panel lists the local styles with nothing selected; the folder's menu renames it. + let menu = click(&mut u, text_center(&shapes, "Brand"), PointerButton::Secondary); + click(&mut u, text_center(&menu, "Rename folder"), PointerButton::Primary); + checked_frame(&mut u, vec![Event::Key { key: Key::A, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::COMMAND }], 0); + checked_frame(&mut u, vec![Event::Text("Theme".into())], 1); + for pressed in [true, false] { + checked_frame(&mut u, vec![Event::Key { key: Key::Enter, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }], 2); + } + for k in 0..5 { + checked_frame(&mut u, vec![], k + 3); + } + let names: Vec = u.app.session.doc().unwrap().styles.iter().map(|s| s.name.clone()).collect(); + assert_eq!(names, ["Theme/Primary", "Theme/Accent"]); +} diff --git a/crates/ui-egui/src/style_picker.rs b/crates/ui-egui/src/style_picker.rs index 3a4e7b11..6f0cb2cd 100644 --- a/crates/ui-egui/src/style_picker.rs +++ b/crates/ui-egui/src/style_picker.rs @@ -1042,7 +1042,10 @@ pub fn local_styles(app: &mut FigApp, ui: &mut Ui, doc: &Document) { let (r, resp) = ui.allocate_exact_size(vec2(w, 24.0), Sense::click()); let text_r = Rect::from_min_max(pos2(r.min.x + widgets::PAD, r.min.y + 2.0), pos2(r.max.x - widgets::PAD, r.max.y - 2.0)); if renaming.as_deref() == Some(*f) { - let (out, open) = widgets::rename_field(ui, folder_rename_id().with(*f), f, text_r, regular(11.0), false); + // Focused as it is first drawn: a focus asked for before its field is drawn + // isn't in the screen reader tree (UI Automation aborts the app, #224). + let start = ui.data_mut(|d| d.remove_temp::(folder_rename_id().with("start"))).unwrap_or(false); + let (out, open) = widgets::rename_field(ui, folder_rename_id().with(*f), f, text_r, regular(11.0), start); if let Some(name) = out { run = Some(("style.renameFolder", json!({"folder": f, "name": name}))); } @@ -1055,7 +1058,7 @@ pub fn local_styles(app: &mut FigApp, ui: &mut Ui, doc: &Document) { crate::right::context_popup(&resp).show(|ui| { if crate::right::menu_item(ui, tr("Rename folder"), "") { ui.data_mut(|d| d.insert_temp(folder_rename_id(), f.to_string())); - ui.memory_mut(|m| m.request_focus(folder_rename_id().with(*f).with("te"))); + ui.data_mut(|d| d.insert_temp(folder_rename_id().with("start"), true)); ui.close(); } });