From 5d40b3f42f8b6d9ef8f324ea9a530cbff4bb58de Mon Sep 17 00:00:00 2001 From: AganzeFelicite Date: Mon, 19 Oct 2026 10:53:34 +0200 Subject: [PATCH] fix(pages): Delete deletes the page picked in the Pages list (#859) Clicking a page row (or the add-page button) gives the Pages list the keyboard: Delete or Backspace with no layers selected now deletes the current page instead of failing with "nothing is selected". No confirmation; a "Page deleted" toast says so and Undo brings it back. The context menu's Delete page shows the same toast. Selecting layers, clicking a layer row or pressing on the canvas gives Delete back to the layers. --- crates/ui-egui/locales/cs.toml | 1 + crates/ui-egui/locales/de.toml | 1 + crates/ui-egui/locales/en.toml | 1 + crates/ui-egui/locales/es.toml | 1 + crates/ui-egui/locales/fi.toml | 1 + crates/ui-egui/locales/fr.toml | 1 + crates/ui-egui/locales/id.toml | 1 + crates/ui-egui/locales/ja.toml | 1 + crates/ui-egui/locales/ko.toml | 1 + crates/ui-egui/locales/nl.toml | 1 + crates/ui-egui/locales/pt-BR.toml | 1 + crates/ui-egui/locales/ru.toml | 1 + crates/ui-egui/locales/sv.toml | 1 + crates/ui-egui/locales/tr.toml | 1 + crates/ui-egui/locales/zh-Hans.toml | 1 + crates/ui-egui/src/canvas.rs | 1 + crates/ui-egui/src/layers.rs | 13 +++- crates/ui-egui/src/layers_keys_tests.rs | 81 ++++++++++++++++++++++++- crates/ui-egui/src/lib.rs | 5 ++ crates/ui-egui/src/menus.rs | 6 ++ crates/ui-egui/src/selection_spacing.rs | 1 + 21 files changed, 119 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/locales/cs.toml b/crates/ui-egui/locales/cs.toml index 98051314..a7e8be38 100644 --- a/crates/ui-egui/locales/cs.toml +++ b/crates/ui-egui/locales/cs.toml @@ -929,6 +929,7 @@ status = "draft — needs native review" "PDF print settings" = "Nastavení tisku PDF" "Padding" = "Vnitřní odsazení" "Page" = "Stránka" +"Page deleted" = "Stránka smazána" "Pages" = "Stránky" "Pages added to the document" = "Stránky přidány do dokumentu" "Placed on the current page" = "Umístěno na aktuální stránku" diff --git a/crates/ui-egui/locales/de.toml b/crates/ui-egui/locales/de.toml index 07669c1a..e910863b 100644 --- a/crates/ui-egui/locales/de.toml +++ b/crates/ui-egui/locales/de.toml @@ -898,6 +898,7 @@ status = "draft — needs native review" "PDF print settings" = "PDF-Druckeinstellungen" "Padding" = "Innenabstand" "Page" = "Seite" +"Page deleted" = "Seite gelöscht" "Pages" = "Seiten" "Pages added to the document" = "Seiten zum Dokument hinzugefügt" "Placed on the current page" = "Auf der aktuellen Seite platziert" diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml index 17f6b4c5..7220519d 100644 --- a/crates/ui-egui/locales/en.toml +++ b/crates/ui-egui/locales/en.toml @@ -950,6 +950,7 @@ name = "English" "PDF print settings" = "PDF print settings" "Padding" = "Padding" "Page" = "Page" +"Page deleted" = "Page deleted" "Pages" = "Pages" "Pages added to the document" = "Pages added to the document" "Placed on the current page" = "Placed on the current page" diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml index d054e222..63c73131 100644 --- a/crates/ui-egui/locales/es.toml +++ b/crates/ui-egui/locales/es.toml @@ -951,6 +951,7 @@ status = "draft — needs native review" "PDF print settings" = "Ajustes de impresión PDF" "Padding" = "Margen interno" "Page" = "Página" +"Page deleted" = "Página eliminada" "Pages" = "Páginas" "Pages added to the document" = "Páginas añadidas al documento" "Placed on the current page" = "Colocado en la página actual" diff --git a/crates/ui-egui/locales/fi.toml b/crates/ui-egui/locales/fi.toml index 602574d7..6f12dd55 100644 --- a/crates/ui-egui/locales/fi.toml +++ b/crates/ui-egui/locales/fi.toml @@ -952,6 +952,7 @@ status = "draft — needs native review" "PDF print settings" = "PDF-tulostusasetukset" "Padding" = "Sisämarginaali" "Page" = "Sivu" +"Page deleted" = "Sivu poistettu" "Pages" = "Sivut" "Pages added to the document" = "Sivut lisätty asiakirjaan" "Pages: {pages}" = "Sivut: {pages}" diff --git a/crates/ui-egui/locales/fr.toml b/crates/ui-egui/locales/fr.toml index 5edf1a62..4af37b1d 100644 --- a/crates/ui-egui/locales/fr.toml +++ b/crates/ui-egui/locales/fr.toml @@ -947,6 +947,7 @@ status = "draft — needs native review" "PDF print settings" = "Réglages d’impression PDF" "Padding" = "Marge intérieure" "Page" = "Page" +"Page deleted" = "Page supprimée" "Pages" = "Pages" "Pages added to the document" = "Pages ajoutées au document" "Pick the Frame tool, then drag on the canvas or choose a device size" = "Choisissez l’outil Cadre, puis faites glisser sur le canevas ou choisissez une taille d’appareil" diff --git a/crates/ui-egui/locales/id.toml b/crates/ui-egui/locales/id.toml index 65a9939c..9c5aa4a6 100644 --- a/crates/ui-egui/locales/id.toml +++ b/crates/ui-egui/locales/id.toml @@ -933,6 +933,7 @@ status = "draft — needs native review" "PDF print settings" = "Pengaturan cetak PDF" "Padding" = "Padding" "Page" = "Halaman" +"Page deleted" = "Halaman dihapus" "Pages" = "Halaman" "Pages added to the document" = "Halaman ditambahkan ke dokumen" "Placed on the current page" = "Ditempatkan di halaman saat ini" diff --git a/crates/ui-egui/locales/ja.toml b/crates/ui-egui/locales/ja.toml index def8ebc4..b5dcaf27 100644 --- a/crates/ui-egui/locales/ja.toml +++ b/crates/ui-egui/locales/ja.toml @@ -938,6 +938,7 @@ status = "draft — needs native review" "PDF print settings" = "PDF 印刷設定" "Padding" = "パディング" "Page" = "ページ" +"Page deleted" = "ページは削除済み" "Pages" = "ページ" "Pages added to the document" = "ページをドキュメントに追加した" "Placed on the current page" = "現在のページに配置した" diff --git a/crates/ui-egui/locales/ko.toml b/crates/ui-egui/locales/ko.toml index 3e191623..70669778 100644 --- a/crates/ui-egui/locales/ko.toml +++ b/crates/ui-egui/locales/ko.toml @@ -892,6 +892,7 @@ status = "draft — needs native review" "PDF print settings" = "PDF 인쇄 설정" "Padding" = "패딩" "Page" = "페이지" +"Page deleted" = "페이지가 삭제됨" "Pages" = "페이지" "Pages added to the document" = "문서에 페이지 추가됨" "Placed on the current page" = "현재 페이지에 배치됨" diff --git a/crates/ui-egui/locales/nl.toml b/crates/ui-egui/locales/nl.toml index 8d8cac68..d49e5f03 100644 --- a/crates/ui-egui/locales/nl.toml +++ b/crates/ui-egui/locales/nl.toml @@ -952,6 +952,7 @@ status = "draft — needs native review" "PDF print settings" = "PDF-afdrukinstellingen" "Padding" = "Padding" "Page" = "Pagina" +"Page deleted" = "Pagina verwijderd" "Pages" = "Pagina's" "Pages added to the document" = "Pagina's toegevoegd aan het document" "Pages: {pages}" = "Pagina's: {pages}" diff --git a/crates/ui-egui/locales/pt-BR.toml b/crates/ui-egui/locales/pt-BR.toml index d970db14..00ec38a9 100644 --- a/crates/ui-egui/locales/pt-BR.toml +++ b/crates/ui-egui/locales/pt-BR.toml @@ -891,6 +891,7 @@ status = "draft — needs native review" "PDF print settings" = "Configurações de impressão em PDF" "Padding" = "Espaçamento interno" "Page" = "Página" +"Page deleted" = "Página excluída" "Pages" = "Páginas" "Pages added to the document" = "Páginas adicionadas ao documento" "Placed on the current page" = "Colocado na página atual" diff --git a/crates/ui-egui/locales/ru.toml b/crates/ui-egui/locales/ru.toml index 8d371cbe..c72c3edb 100644 --- a/crates/ui-egui/locales/ru.toml +++ b/crates/ui-egui/locales/ru.toml @@ -932,6 +932,7 @@ name = "Русский" "PDF print settings" = "Настройки печати PDF" "Padding" = "Внутренние отступы" "Page" = "Страница" +"Page deleted" = "Страница удалена" "Pages" = "Страницы" "Pages added to the document" = "Страницы добавлены в документ" "Placed on the current page" = "Размещено на текущей странице" diff --git a/crates/ui-egui/locales/sv.toml b/crates/ui-egui/locales/sv.toml index d57bb25a..7c317414 100644 --- a/crates/ui-egui/locales/sv.toml +++ b/crates/ui-egui/locales/sv.toml @@ -942,6 +942,7 @@ name = "Svenska" "PDF print settings" = "PDF-utskriftsinställningar" "Padding" = "Utfyllnad" "Page" = "Sida" +"Page deleted" = "Sidan har raderats" "Pages" = "Sidor" "Pages added to the document" = "Sidor tillagda i dokumentet" "Placed on the current page" = "Placerat på den aktuella sidan" diff --git a/crates/ui-egui/locales/tr.toml b/crates/ui-egui/locales/tr.toml index a9da62ef..c9339e1f 100644 --- a/crates/ui-egui/locales/tr.toml +++ b/crates/ui-egui/locales/tr.toml @@ -933,6 +933,7 @@ name = "Türkçe" "PDF print settings" = "PDF baskı ayarları" "Padding" = "İç boşluk" "Page" = "Sayfa" +"Page deleted" = "Sayfa silindi" "Pages" = "Sayfalar" "Pages added to the document" = "Sayfalar belgeye eklendi" "Placed on the current page" = "Geçerli sayfaya yerleştirildi" diff --git a/crates/ui-egui/locales/zh-Hans.toml b/crates/ui-egui/locales/zh-Hans.toml index a90c1c03..e726ca32 100644 --- a/crates/ui-egui/locales/zh-Hans.toml +++ b/crates/ui-egui/locales/zh-Hans.toml @@ -893,6 +893,7 @@ status = "draft — needs native review" "PDF print settings" = "PDF 打印设置" "Padding" = "内边距" "Page" = "页面" +"Page deleted" = "页面已删除" "Pages" = "页面" "Pages added to the document" = "页面已添加到文档" "Placed on the current page" = "已放置在当前页面" diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 9e0c1d79..94daf657 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -544,6 +544,7 @@ fn input(app: &mut FigApp, ui: &mut Ui, resp: &egui::Response) -> bool { // A press on the canvas gives the keyboard back to it (arrows nudge again). if resp.hovered() && ctx.input(|i| i.pointer.any_pressed()) { app.ui.layers_focus = false; + app.ui.pages_focus = false; app.ui.key_box = None; } // Zoom (pinch, Cmd/Ctrl+scroll) about the pointer; plain scroll pans. diff --git a/crates/ui-egui/src/layers.rs b/crates/ui-egui/src/layers.rs index 32f66f80..514b8f75 100644 --- a/crates/ui-egui/src/layers.rs +++ b/crates/ui-egui/src/layers.rs @@ -199,6 +199,7 @@ fn pages(app: &mut FigApp, ui: &mut Ui) { } icons::paint(ui.painter(), ar.shrink(5.0), Icon::Plus, t.icon); if aresp.clicked() { + app.ui.pages_focus = true; app.run("page.add", json!({})); } if !app.ui.pages_open { @@ -257,6 +258,8 @@ fn pages(app: &mut FigApp, ui: &mut Ui) { } if resp.clicked() && !divider { select = Some(id); + app.ui.pages_focus = true; + app.ui.layers_focus = false; } if resp.double_clicked() { app.ui.rename_page = Some((id, name.clone())); @@ -274,7 +277,7 @@ fn pages(app: &mut FigApp, ui: &mut Ui) { ui.close(); } if ui.button(tr("Delete page")).clicked() { - app.run("page.delete", json!({"id": id})); + delete_page(app, id); ui.close(); } }); @@ -311,6 +314,13 @@ fn pages(app: &mut FigApp, ui: &mut Ui) { pages_edge(app, ui, moved.inner_rect.height().min(moved.content_size.y)); } +/// Delete `page` (no confirmation: Undo brings it back) and say so. +pub(crate) fn delete_page(app: &mut FigApp, page: NodeId) { + if app.run("page.delete", json!({"id": page})).is_some() { + app.toast(tr("Page deleted")); + } +} + /// The pages list's height in a panel with `avail` below the Pages header: the height asked for /// (by default 40% of it), at least three rows, and leaving the layers their header and three rows. pub(crate) fn pages_list_height(asked: Option, avail: f32) -> f32 { @@ -1064,6 +1074,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) { } if let Some((id, m)) = clicked { app.ui.layers_focus = true; + app.ui.pages_focus = false; let order: Vec = rows.iter().map(|r| r.id).collect(); let pos = |x: NodeId| order.iter().position(|o| *o == x); let anchor = app.ui.layer_anchor.filter(|a| sel.contains(a)).and_then(pos); diff --git a/crates/ui-egui/src/layers_keys_tests.rs b/crates/ui-egui/src/layers_keys_tests.rs index b78890a8..d0b5f786 100644 --- a/crates/ui-egui/src/layers_keys_tests.rs +++ b/crates/ui-egui/src/layers_keys_tests.rs @@ -1,8 +1,8 @@ //! Arrow keys in the layers panel once a row has been clicked: up/down through the rows (Shift //! extends), left/right to collapse, expand, and go to the parent or first child; a press on the -//! canvas gives arrows back to nudging. +//! canvas gives arrows back to nudging. Delete after a click in the Pages list deletes the page. -use egui::{Event, Key, Modifiers, pos2}; +use egui::{Event, Key, Modifiers, Pos2, pos2, vec2}; use figcraft_doc::NodeId; use figcraft_engine::Session; use serde_json::json; @@ -107,3 +107,80 @@ fn a_canvas_press_gives_arrows_back_to_nudging() { let doc = u.app.session.doc().unwrap(); assert_eq!(doc.node(c).unwrap().transform.translation().x, 201.0); } + +/// The middle of `page`'s row in the Pages list, found by clicking down the list until it opens; +/// then back to `back`. +fn page_row(u: &mut Ui, page: NodeId, back: NodeId) -> Pos2 { + for y in (90..400).step_by(4) { + let p = pos2(150.0, y as f32); + u.button(p, true); + u.button(p, false); + if u.app.session.page().unwrap() == page { + u.app.session.execute("page.select", &json!({"id": back})).unwrap(); + u.frame(vec![]); + // Rows are 32 high: aim at the middle, past the hit tolerance at its edge. + return p + vec2(0.0, 12.0); + } + } + panic!("no row for page {page:?}"); +} + +fn pages(u: &Ui) -> usize { + u.app.session.doc().unwrap().pages.len() +} + +#[test] +fn delete_after_clicking_a_page_deletes_it_without_asking() { + // #859: Delete on a page picked in the Pages list said "nothing is selected". + let (mut u, ..) = setup(); + let first = u.app.session.page().unwrap(); + let second = id(u.app.session.execute("page.add", &json!({})).unwrap()); + u.app.session.execute("page.select", &json!({"id": first})).unwrap(); + u.frame(vec![]); + let row = page_row(&mut u, second, first); + u.button(row, true); + u.button(row, false); + assert_eq!(u.app.session.page().unwrap(), second); + key(&mut u, Key::Delete, false); + // Gone at once, no confirmation: a toast says so and Undo brings it back. + assert_eq!(pages(&u), 1); + assert!(!u.app.session.doc().unwrap().pages.contains(&second)); + assert_eq!(u.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Page deleted")); + u.app.run("edit.undo", json!({})); + assert_eq!(pages(&u), 2); + // Backspace (the Mac's delete key) does the same. + let row = page_row(&mut u, second, first); + u.button(row, true); + u.button(row, false); + key(&mut u, Key::Backspace, false); + assert_eq!(pages(&u), 1); + // The last page stays. + key(&mut u, Key::Delete, false); + assert_eq!(pages(&u), 1); +} + +#[test] +fn selected_layers_or_a_canvas_press_take_delete_back_from_the_pages_list() { + let (mut u, a, ..) = setup(); + let first = u.app.session.page().unwrap(); + let second = id(u.app.session.execute("page.add", &json!({})).unwrap()); + u.frame(vec![]); + let row = page_row(&mut u, first, second); + u.button(row, true); + u.button(row, false); + assert_eq!(u.app.session.page().unwrap(), first); + assert!(u.app.ui.pages_focus); + // Layers selected after the click (Select all, say): Delete deletes them, not the page. + u.app.run("select.set", json!({"ids": [a]})); + key(&mut u, Key::Delete, false); + assert_eq!(pages(&u), 2); + assert!(u.app.session.doc().unwrap().node(a).is_none()); + // A press on the canvas gives Delete back to it. + let p = pos2(900.0, 700.0); + u.button(p, true); + u.button(p, false); + assert!(!u.app.ui.pages_focus); + u.app.run("select.none", json!({})); + key(&mut u, Key::Delete, false); + assert_eq!(pages(&u), 2, "Delete with nothing selected on the canvas deletes no page"); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 450317b4..9a1cc3a8 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -500,6 +500,10 @@ pub struct UiState { /// The layers panel has the keyboard: arrow keys move through its rows instead of nudging /// (set by a click on a row, cleared by a press on the canvas). pub layers_focus: bool, + /// The pages list has the keyboard: Delete with no layers selected deletes the current page (set + /// by a click on a page row or the add-page button, cleared by a click on a layer row or a press + /// on the canvas). + pub pages_focus: bool, pub layers_seen_sel: Vec, pub dark: bool, pub follow_system: bool, @@ -634,6 +638,7 @@ impl Default for UiState { layer_filter: String::new(), layer_anchor: None, layers_focus: false, + pages_focus: false, layers_seen_sel: Vec::new(), dark: false, follow_system: false, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index e32f50c8..dfdf290a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -117,6 +117,12 @@ pub fn invoke(app: &mut FigApp, id: &str) { // Edit ▸ Delete (and the Delete binding) with an interaction open in the Prototype tab // removes that interaction, not the layer (#767). "edit.delete" if app.ui_context.clone().is_some_and(|ctx| crate::proto_panel::delete_selected_interaction(app, &ctx)) => {} + // After a click in the Pages list, with no layers selected, it deletes the current page (#859). + "edit.delete" if app.ui.pages_focus && app.session.selection().is_empty() => { + if let Ok(page) = app.session.page() { + crate::layers::delete_page(app, page); + } + } "file.open" => app.open_dialog(), "plugin.import" => crate::plugins_ui::import_dialog(app), "file.import" => app.import_dialog(false), diff --git a/crates/ui-egui/src/selection_spacing.rs b/crates/ui-egui/src/selection_spacing.rs index 5110be1c..c05b9ec3 100644 --- a/crates/ui-egui/src/selection_spacing.rs +++ b/crates/ui-egui/src/selection_spacing.rs @@ -217,6 +217,7 @@ pub fn input(app: &mut FigApp, ctx: &egui::Context, pos: Option, hovered: let drag = Drag { ids, axis: axis.clone(), start: p, value: h.value, uid: ds.uid, page: ds.page, moved: false, last_value: axis.value }; app.ui.layers_focus = false; + app.ui.pages_focus = false; app.ui.key_box = None; ctx.memory_mut(|m| { if let Some(f) = m.focused() {