diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index fc7551780..c5023c5fd 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -86,6 +86,11 @@ fn undo_list_row_id(i: usize) -> egui::Id { undo_list_button_id().with(("row", i)) } +/// The footer Cancel row's id (#1903). +pub(crate) fn undo_list_cancel_id() -> egui::Id { + undo_list_button_id().with("cancel") +} + /// The Undo list's rows, newest first: each step named as the Undo button's tooltip names it. pub(crate) fn undo_list_rows(app: &WordApp) -> Vec { app.session.undo_history().into_iter().map(|l| tl!(l).to_string()).collect() @@ -147,8 +152,21 @@ fn undo_list(ui: &mut Ui, app: &mut WordApp) { } }); ui.separator(); - let footer = undo_list_footer(hovered.map_or(0, |h| h + 1)); - ui.vertical_centered(|ui| ui.label(egui::RichText::new(footer).font(regular(12.0)).color(t.text))); + let n = hovered.map_or(0, |h| h + 1); + if n == 0 { + // Cancel is a real row: it closes the popup without undoing anything (#1903). + let (r, _) = ui.allocate_exact_size(vec2(ui.available_width(), ROW), Sense::hover()); + let resp = ui.interact(r, undo_list_cancel_id(), Sense::click()); + if resp.hovered() { + ui.painter().rect_filled(r, 0.0, t.selection); + } + ui.painter().text(pos2(r.min.x + 6.0, r.center().y), Align2::LEFT_CENTER, undo_list_footer(0), regular(12.0), t.text); + if resp.clicked() { + ui.close(); + } + } else { + ui.vertical_centered(|ui| ui.label(egui::RichText::new(undo_list_footer(n)).font(regular(12.0)).color(t.text))); + } if let Some(i) = clicked { let _ = app.run("edit.undoTo", json!({"count": i + 1})); ui.close(); @@ -680,6 +698,29 @@ mod tests { } } + /// #1903: the footer's Cancel row closes the Undo list without undoing anything. + #[test] + fn undo_list_cancel_closes_without_undoing() { + let mut a = WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::new()), crate::Services::default()); + a.run("text.insert", json!({"text": "Hello"})).unwrap(); + let before: Vec = a.session.undo_history().into_iter().map(str::to_string).collect(); + assert!(!before.is_empty()); + + let (ctx, mut n) = (ctx(), 0); + frame(&ctx, &mut n, vec![], &mut a); + let arrow = ctx.read_response(undo_list_button_id()).unwrap(); + let popup = egui::Popup::default_response_id(&arrow); + click(&ctx, &mut n, arrow.rect.center(), &mut a); + assert!(egui::Popup::is_id_open(&ctx, popup), "the arrow opens the Undo list"); + frame(&ctx, &mut n, vec![], &mut a); + let cancel = ctx.read_response(undo_list_cancel_id()).expect("Cancel is drawn"); + click(&ctx, &mut n, cancel.rect.center(), &mut a); + let after: Vec = a.session.undo_history().into_iter().map(str::to_string).collect(); + assert_eq!(after, before, "nothing undone"); + frame(&ctx, &mut n, vec![], &mut a); + assert!(!egui::Popup::is_id_open(&ctx, popup), "Cancel closes the list"); + } + #[test] fn mac_title_bar_centres_and_clears_the_traffic_lights() { // The lights are centred in the bar, and content keeps the same gap after them.