From 5b98ed3b7d9b4601716bd817fe6600757965642e Mon Sep 17 00:00:00 2001 From: Francisco Herrera <14956584+alosslessdev@users.noreply.github.com> Date: Sat, 7 Nov 2026 18:25:32 -0500 Subject: [PATCH 1/2] Layers: Delete clears the selection after the last row is deleted (#5870) --- crates/ui-egui/src/panels/layers.rs | 6 ++++-- crates/ui-egui/src/panels/tests_appearance.rs | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 65f7c6bc3..42bd09a4a 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -361,9 +361,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // Keyboard: Delete removes the highlighted rows while the panel has the keyboard (a row was // clicked last, not the canvas). // The panel keeps it while it is drawn ([`super::has_row_keys`]); the app's shortcuts leave - // Delete and Backspace to it meanwhile ([`super::rows_take_delete`]). + // Delete and Backspace to it meanwhile ([`super::rows_take_delete`]). With no highlighted rows + // left (the last one was deleted) the panel gives the keys back, so Delete clears the selected + // art again instead of doing nothing. let pressed_outside = ui.input(|i| i.pointer.any_pressed() && i.pointer.interact_pos().is_some_and(|p| !list_rect.contains(p))); - let focus = super::has_row_keys(ui.ctx(), ROW_KEYS) && !pressed_outside; + let focus = super::has_row_keys(ui.ctx(), ROW_KEYS) && !pressed_outside && !view.rows.is_empty(); super::set_row_keys(ui.ctx(), ROW_KEYS, focus); if focus && !view.rows.is_empty() diff --git a/crates/ui-egui/src/panels/tests_appearance.rs b/crates/ui-egui/src/panels/tests_appearance.rs index ba4eebdda..b7ed12486 100644 --- a/crates/ui-egui/src/panels/tests_appearance.rs +++ b/crates/ui-egui/src/panels/tests_appearance.rs @@ -1481,6 +1481,25 @@ fn after_a_row_click_delete_reaches_the_layers_panel_not_edit_clear() { assert!(d.node(rect).is_some(), "the rectangle on Layer 1 stays"); } +/// After Delete removed the last highlighted Layers row, the panel has nothing left to delete: the +/// next Delete must go back to the app and clear the selected object, not be swallowed by the +/// panel's row keys. +#[test] +fn delete_after_the_last_highlighted_layer_row_is_deleted_clears_the_selection() { + let ctx = drop_ctx(); + let mut app = app_with_rect(); + run(&mut app, "layer.new", json!({})); + let rect = first_selected(&app).unwrap().id; + let texts = frame_events(&ctx, &mut app, vec![], boxed_layers); + let row = text_rect(&texts, "Layer 2").center(); + app_click(&ctx, &mut app, row, boxed_layers); + app_key(&ctx, &mut app, egui::Key::Delete, boxed_layers); + assert_eq!(app.session.active().unwrap().doc.layers.len(), 1, "Layer 2 is deleted"); + assert!(app.session.active().unwrap().selection.contains(rect), "the rectangle stays selected"); + app_key(&ctx, &mut app, egui::Key::Delete, boxed_layers); + assert!(app.session.active().unwrap().doc.node(rect).is_none(), "Delete clears the selected object once the panel has no rows"); +} + /// A tool that claims Delete and Backspace (the Artboard tool's active artboard) leaves them to a /// clicked Layers row. #[test] From a3ed60650e3dffdcc4a81ec82e35c8c43f54af6f Mon Sep 17 00:00:00 2001 From: Francisco Herrera <14956584+alosslessdev@users.noreply.github.com> Date: Sat, 7 Nov 2026 19:10:22 -0500 Subject: [PATCH 2/2] Layers: row keys follow the click and release after the last row (#5870) --- crates/ui-egui/src/panels/layers.rs | 15 ++++++- crates/ui-egui/src/panels/layers/rows.rs | 3 ++ crates/ui-egui/src/panels/tests_appearance.rs | 41 +++++++++++++++++-- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 42bd09a4a..313242bb4 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -176,6 +176,9 @@ struct Out { column: Option>, /// Rows to open or close, and whether to open them. toggle: Vec<(NodeId, bool)>, + /// The highlighted rows after this frame's row click, when one happened: the click's action + /// runs after `delete_key`, so `view.rows` is still the state before it. + rows_after_click: Option>, } /// The panel state that says the Layers panel's rows have the keyboard ([`super::has_row_keys`]). @@ -365,10 +368,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { // left (the last one was deleted) the panel gives the keys back, so Delete clears the selected // art again instead of doing nothing. let pressed_outside = ui.input(|i| i.pointer.any_pressed() && i.pointer.interact_pos().is_some_and(|p| !list_rect.contains(p))); - let focus = super::has_row_keys(ui.ctx(), ROW_KEYS) && !pressed_outside && !view.rows.is_empty(); + // A click's highlight action runs after this block, so read the rows it will leave, not the + // snapshot `view.rows` took before it. + let rows = out.rows_after_click.as_deref().unwrap_or(&view.rows); + let focus = super::has_row_keys(ui.ctx(), ROW_KEYS) && !pressed_outside && !rows.is_empty(); super::set_row_keys(ui.ctx(), ROW_KEYS, focus); if focus - && !view.rows.is_empty() + && !rows.is_empty() && ui.ctx().memory(|m| m.focused().is_none()) && ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Delete) || i.consume_key(egui::Modifiers::NONE, egui::Key::Backspace)) { @@ -378,6 +384,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { for (c, p) in out.actions { app.run(&c, p).ok(); } + // A deletion that took the last highlighted row leaves the panel nothing to delete. Release the + // keys now, not at the next draw, so the very next frame's Delete reaches Edit › Clear. + if app.session.active().is_some_and(|st| st.highlighted_rows().is_empty()) { + super::set_row_keys(ui.ctx(), ROW_KEYS, false); + } see_selection(app, ui, selected); } diff --git a/crates/ui-egui/src/panels/layers/rows.rs b/crates/ui-egui/src/panels/layers/rows.rs index 000a3c07e..376c021c4 100644 --- a/crates/ui-egui/src/panels/layers/rows.rs +++ b/crates/ui-egui/src/panels/layers/rows.rs @@ -91,6 +91,9 @@ pub(super) fn resolve_click(ui: &Ui, view: &View, list: &[Row], out: &mut Out) { if click.moves_anchor() { ui.data_mut(|d| d.insert_temp(key("anchor"), id)); } + let mut rows = view.rows.clone(); + widgets::apply_row_click(&mut rows, &click); + out.rows_after_click = Some(rows); let ids = |rows: &[NodeId]| rows.iter().map(|r| r.0).collect::>(); out.actions.push(match click { widgets::RowClick::Only(id) => ("layer.setCurrent".into(), json!({"id": id.0})), diff --git a/crates/ui-egui/src/panels/tests_appearance.rs b/crates/ui-egui/src/panels/tests_appearance.rs index b7ed12486..0431d6dec 100644 --- a/crates/ui-egui/src/panels/tests_appearance.rs +++ b/crates/ui-egui/src/panels/tests_appearance.rs @@ -1348,10 +1348,15 @@ fn app_click(ctx: &egui::Context, app: &mut VectorcraftApp, pos: egui::Pos2, f: } } -/// `key` pressed and released in an app-like frame of `f`, then one more frame. -fn app_key(ctx: &egui::Context, app: &mut VectorcraftApp, key: egui::Key, f: fn(&mut VectorcraftApp, &mut egui::Ui)) { +/// `key` pressed and released in one app-like frame of `f`, with no idle frame after it. +fn app_key_now(ctx: &egui::Context, app: &mut VectorcraftApp, key: egui::Key, f: fn(&mut VectorcraftApp, &mut egui::Ui)) { let ev = |pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers: egui::Modifiers::NONE }; app_frame(ctx, app, vec![ev(true), ev(false)], f); +} + +/// `key` pressed and released in an app-like frame of `f`, then one more frame. +fn app_key(ctx: &egui::Context, app: &mut VectorcraftApp, key: egui::Key, f: fn(&mut VectorcraftApp, &mut egui::Ui)) { + app_key_now(ctx, app, key, f); app_frame(ctx, app, vec![], f); } @@ -1493,13 +1498,41 @@ fn delete_after_the_last_highlighted_layer_row_is_deleted_clears_the_selection() let texts = frame_events(&ctx, &mut app, vec![], boxed_layers); let row = text_rect(&texts, "Layer 2").center(); app_click(&ctx, &mut app, row, boxed_layers); - app_key(&ctx, &mut app, egui::Key::Delete, boxed_layers); + app_key_now(&ctx, &mut app, egui::Key::Delete, boxed_layers); assert_eq!(app.session.active().unwrap().doc.layers.len(), 1, "Layer 2 is deleted"); assert!(app.session.active().unwrap().selection.contains(rect), "the rectangle stays selected"); - app_key(&ctx, &mut app, egui::Key::Delete, boxed_layers); + app_key_now(&ctx, &mut app, egui::Key::Delete, boxed_layers); assert!(app.session.active().unwrap().doc.node(rect).is_none(), "Delete clears the selected object once the panel has no rows"); } +/// Clicking a row right after the last highlighted row was deleted restores the row keys: Delete +/// then deletes that row, not the selected art. +#[test] +fn clicking_a_row_after_the_last_one_was_deleted_keeps_delete_with_the_layers_panel() { + let ctx = drop_ctx(); + let mut app = app_with_rect(); + run(&mut app, "layer.new", json!({})); + run(&mut app, "layer.new", json!({})); + let rect = first_selected(&app).unwrap().id; + let texts = frame_events(&ctx, &mut app, vec![], boxed_layers); + let layer2 = text_rect(&texts, "Layer 2").center(); + app_click(&ctx, &mut app, layer2, boxed_layers); + app_key_now(&ctx, &mut app, egui::Key::Delete, boxed_layers); + assert_eq!(app.session.active().unwrap().doc.layers.len(), 2, "Layer 2 is deleted"); + // Click Layer 3 and press Delete in the very next frame: the click's highlight action runs at + // the end of its frame, so the panel must read the rows the click will leave. + let texts = frame_events(&ctx, &mut app, vec![], boxed_layers); + let layer3 = text_rect(&texts, "Layer 3").center(); + let button = + |pressed| egui::Event::PointerButton { pos: layer3, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + for e in [egui::Event::PointerMoved(layer3), button(true), button(false)] { + app_frame(&ctx, &mut app, vec![e], boxed_layers); + } + app_key_now(&ctx, &mut app, egui::Key::Delete, boxed_layers); + assert_eq!(app.session.active().unwrap().doc.layers.len(), 1, "Delete removes the clicked row"); + assert!(app.session.active().unwrap().doc.node(rect).is_some(), "the rectangle on Layer 1 stays"); +} + /// A tool that claims Delete and Backspace (the Artboard tool's active artboard) leaves them to a /// clicked Layers row. #[test]