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.
This commit is contained in:
AganzeFelicite
2026-10-19 10:53:34 +02:00
parent a0d1162976
commit 5d40b3f42f
21 changed files with 119 additions and 3 deletions
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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}"
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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" = "現在のページに配置した"
+1
View File
@@ -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" = "현재 페이지에 배치됨"
+1
View File
@@ -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}"
+1
View File
@@ -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"
+1
View File
@@ -932,6 +932,7 @@ name = "Русский"
"PDF print settings" = "Настройки печати PDF"
"Padding" = "Внутренние отступы"
"Page" = "Страница"
"Page deleted" = "Страница удалена"
"Pages" = "Страницы"
"Pages added to the document" = "Страницы добавлены в документ"
"Placed on the current page" = "Размещено на текущей странице"
+1
View File
@@ -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"
+1
View File
@@ -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"
+1
View File
@@ -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" = "已放置在当前页面"
+1
View File
@@ -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.
+12 -1
View File
@@ -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<f32>, 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<NodeId> = 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);
+79 -2
View File
@@ -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");
}
+5
View File
@@ -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<NodeId>,
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,
+6
View File
@@ -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),
+1
View File
@@ -217,6 +217,7 @@ pub fn input(app: &mut FigApp, ctx: &egui::Context, pos: Option<Pos2>, 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() {