From bc5a2b8fa76a63d93b6000e92c5d445d670e6f42 Mon Sep 17 00:00:00 2001 From: Natnael-Dev Date: Sat, 17 Oct 2026 21:45:51 +0300 Subject: [PATCH 1/5] fix(proto): delete prototype interaction without deleting component (#767) When an interaction noodle or interaction editor was selected in the Prototype tab, pressing Delete or Backspace (or executing edit.delete) targeted the selected layer/component instead of deleting the prototype connection. - Intercept Backspace and Delete in shortcuts::key_event and run_binding when an interaction is selected in the Prototype tab to remove the interaction using proto.interaction.remove rather than deleting the layer. - Ensure clicking on empty canvas or outside noodles/handles deselects the active interaction noodle/editor. - Close the interaction editor when navigating away from the Prototype tab. - Add regression tests in proto_editor_tests verifying interaction deletion with Delete/Backspace keeps the component intact, and Escape closes the interaction editor before deselecting the component. --- crates/ui-egui/src/proto_canvas.rs | 3 ++ crates/ui-egui/src/proto_editor_tests.rs | 61 ++++++++++++++++++++++++ crates/ui-egui/src/proto_panel.rs | 43 +++++++++++++++-- crates/ui-egui/src/right.rs | 3 ++ crates/ui-egui/src/shortcuts.rs | 20 ++++++++ 5 files changed, 126 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/proto_canvas.rs b/crates/ui-egui/src/proto_canvas.rs index 6310c05c..1187944d 100644 --- a/crates/ui-egui/src/proto_canvas.rs +++ b/crates/ui-egui/src/proto_canvas.rs @@ -272,6 +272,9 @@ pub fn input(app: &mut FigApp, ui: &egui::Ui, resp: &egui::Response) -> bool { crate::proto_panel::open_editor(&ctx, Some((n.source, n.index))); return true; } + if pressed && crate::proto_panel::editor(&ctx).is_some() { + crate::proto_panel::open_editor(&ctx, None); + } false } diff --git a/crates/ui-egui/src/proto_editor_tests.rs b/crates/ui-egui/src/proto_editor_tests.rs index 2372c20e..31988a35 100644 --- a/crates/ui-egui/src/proto_editor_tests.rs +++ b/crates/ui-egui/src/proto_editor_tests.rs @@ -226,3 +226,64 @@ fn the_presenter_inspects_the_layer_under_the_pointer() { assert_eq!(presenter(&u)["inspected"], json!(a)); assert_eq!(u.app.inspect()["presentOptions"]["inspect"], json!(true)); } + +#[test] +fn deleting_a_prototype_interaction_removes_the_interaction_not_the_component() { + let (s, _a, b, btn) = setup(); + let mut u = Ui::new(s); + u.app.ui.right_tab = crate::RightTab::Prototype; + u.run("select.set", json!({"ids": [btn]})); + crate::proto_panel::open_editor(&u.ctx, Some((btn, 0))); + u.frames(1, Vec::new); + + assert!(u.app.session.doc().unwrap().node(btn).is_some()); + assert_eq!(u.app.session.doc().unwrap().node(btn).unwrap().interactions.len(), 1); + assert_eq!(crate::proto_panel::editor(&u.ctx), Some((btn, 0))); + + // Pressing Delete when an interaction is selected should delete the interaction, NOT the component. + u.key(pos2(100.0, 100.0), egui::Key::Delete); + + assert!(u.app.session.doc().unwrap().node(btn).is_some(), "Bug #767 regression: component was deleted when deleting prototype interaction!"); + assert_eq!(u.app.session.doc().unwrap().node(btn).unwrap().interactions.len(), 0, "the interaction should be removed"); + assert_eq!(crate::proto_panel::editor(&u.ctx), None); + + // Re-add interaction and test Backspace as well. + u.run("proto.interaction.add", json!({"id": btn, "dest": b})); + assert_eq!(u.app.session.doc().unwrap().node(btn).unwrap().interactions.len(), 1); + crate::proto_panel::open_editor(&u.ctx, Some((btn, 0))); + u.frames(1, Vec::new); + u.key(pos2(100.0, 100.0), egui::Key::Backspace); + assert!(u.app.session.doc().unwrap().node(btn).is_some()); + assert_eq!(u.app.session.doc().unwrap().node(btn).unwrap().interactions.len(), 0); + assert_eq!(crate::proto_panel::editor(&u.ctx), None); + + // When NO interaction is active, Delete deletes the component as normal. + u.key(pos2(100.0, 100.0), egui::Key::Delete); + assert!(u.app.session.doc().unwrap().node(btn).is_none(), "when no interaction is selected, Delete removes the layer"); +} + +#[test] +fn escape_closes_prototype_editor_without_deselecting_layer() { + let (s, a, _b, btn) = setup(); + let mut u = Ui::new(s); + u.app.ui.right_tab = crate::RightTab::Prototype; + u.run("select.set", json!({"ids": [btn]})); + crate::proto_panel::open_editor(&u.ctx, Some((btn, 0))); + u.frames(1, Vec::new); + + assert_eq!(crate::proto_panel::editor(&u.ctx), Some((btn, 0))); + assert_eq!(u.app.session.selection(), vec![btn]); + + // First Escape closes the interaction editor; layer remains selected. + u.key(pos2(100.0, 100.0), egui::Key::Escape); + assert_eq!(crate::proto_panel::editor(&u.ctx), None); + assert_eq!(u.app.session.selection(), vec![btn], "layer remains selected"); + + // Second Escape steps out to parent frame. + u.key(pos2(100.0, 100.0), egui::Key::Escape); + assert_eq!(u.app.session.selection(), vec![a], "steps out to parent frame"); + + // Third Escape deselects all. + u.key(pos2(100.0, 100.0), egui::Key::Escape); + assert!(u.app.session.selection().is_empty(), "selection cleared on third escape"); +} diff --git a/crates/ui-egui/src/proto_panel.rs b/crates/ui-egui/src/proto_panel.rs index 47312e99..20546008 100644 --- a/crates/ui-egui/src/proto_panel.rs +++ b/crates/ui-egui/src/proto_panel.rs @@ -40,6 +40,44 @@ pub fn editor(ctx: &egui::Context) -> Option<(NodeId, usize)> { ctx.data(|d| d.get_temp(editor_id())) } +/// Whether an interaction is selected (its editor is open). +pub fn has_selected_interaction(app: &FigApp, ctx: &egui::Context) -> bool { + let Some((node, index)) = editor(ctx) else { return false }; + let Ok(ds) = app.session.ds() else { return false }; + ds.doc.node(node).and_then(|n| n.interactions.get(index)).is_some() +} + +/// Delete the selected interaction (when one is active in the Prototype tab). +/// Returns true if an interaction was deleted. +pub fn delete_selected_interaction(app: &mut FigApp, ctx: &egui::Context) -> bool { + if app.ui.right_tab != crate::RightTab::Prototype { + return false; + } + let Some((node, index)) = editor(ctx) else { return false }; + let Ok(ds) = app.session.ds() else { return false }; + if ds.doc.node(node).and_then(|n| n.interactions.get(index)).is_none() { + open_editor(ctx, None); + return false; + } + open_editor(ctx, None); + let ids = targets_for(app, node); + app.run("proto.interaction.remove", json!({"ids": ids, "index": index})); + true +} + +/// Close the interaction editor if open. Returns true if it was open. +pub fn close_editor(ctx: &egui::Context) -> bool { + if capturing(ctx) || crate::variable_ui::picker_open(ctx) { + return false; + } + if editor(ctx).is_some() { + open_editor(ctx, None); + true + } else { + false + } +} + const TRIGGERS: [&str; 10] = [ tr_noop("On click"), tr_noop("On drag"), @@ -607,8 +645,6 @@ fn interactions(app: &mut FigApp, ui: &mut Ui, doc: &Document, page: NodeId, nod #[allow(clippy::too_many_arguments)] fn editor_popover(app: &mut FigApp, ctx: &egui::Context, doc: &Document, page: NodeId, node: NodeId, k: usize, it: &Interaction, at: Pos2) { let t = Tokens::get(ctx); - // Escape in a field (it cancels the edit) doesn't close the editor. - let typing = ctx.egui_wants_keyboard_input(); let width = 280.0; let screen = ctx.content_rect(); let pos = pos2(at.x - width, at.y.min(screen.max.y - 420.0).max(screen.min.y + 48.0)); @@ -652,8 +688,7 @@ fn editor_popover(app: &mut FigApp, ctx: &egui::Context, doc: &Document, page: N }); }); }); - let escape = ctx.input(|i| i.key_pressed(egui::Key::Escape)) && !typing && !capturing(ctx) && !crate::variable_ui::picker_open(ctx); - if close || escape { + if close { open_editor(ctx, None); } } diff --git a/crates/ui-egui/src/right.rs b/crates/ui-egui/src/right.rs index 47f3e5d3..3e2d70c3 100644 --- a/crates/ui-egui/src/right.rs +++ b/crates/ui-egui/src/right.rs @@ -135,6 +135,9 @@ pub fn right_panel(app: &mut FigApp, ui: &mut Ui) { } ui.painter().galley(pos2(r.min.x + 8.0, r.center().y - galley.size().y / 2.0), galley, if active { t.text } else { t.text_secondary }); if resp.clicked() { + if app.ui.right_tab != tab { + crate::proto_panel::open_editor(ui.ctx(), None); + } app.ui.right_tab = tab; } x = r.max.x + 2.0; diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 09b2c57b..2408b8f1 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -210,6 +210,11 @@ pub fn swallowed_paste(app: &mut FigApp, ctx: &Context) -> Swallowed { pub fn run_binding(app: &mut FigApp, id: &str) { match id { "select.none" => { + if let Some(ctx) = &app.ui_context { + if crate::proto_panel::close_editor(ctx) { + return; + } + } if let Err(e) = app.session.escape() { app.toast(e.to_string()); } @@ -218,6 +223,14 @@ pub fn run_binding(app: &mut FigApp, id: &str) { "edit.delete" if app.session.tool.pen.is_some() => { let _ = app.session.pen_finish(); } + "edit.delete" + if app.ui.right_tab == crate::RightTab::Prototype + && app.ui_context.as_ref().is_some_and(|ctx| crate::proto_panel::has_selected_interaction(app, ctx)) => + { + if let Some(ctx) = app.ui_context.clone() { + let _ = crate::proto_panel::delete_selected_interaction(app, &ctx); + } + } // Enter with a shape or frame tool chosen places one in the middle of the view. "select.children" if app.session.tool.tool.draws_shape() => { app.run("tool.place", json!({})); @@ -228,6 +241,9 @@ pub fn run_binding(app: &mut FigApp, id: &str) { } fn key_event(app: &mut FigApp, key: Key, physical: Option, m: Modifiers, repeat: bool, now: f64, ctx: &Context) -> bool { + if key == Key::Escape && crate::proto_panel::close_editor(ctx) { + return true; + } if key == Key::Escape && crate::selection_spacing::cancel(app, ctx) { return true; } @@ -238,6 +254,10 @@ fn key_event(app: &mut FigApp, key: Key, physical: Option, m: Modifiers, re if matches!(key, Key::Backspace | Key::Delete) && m.is_none() && app.ui.picker.is_some() && crate::handles::remove_selected_stop(app, ctx) { return true; } + // An interaction selected in the Prototype tab: Delete removes that interaction, not the component. + if matches!(key, Key::Backspace | Key::Delete) && m.is_none() && crate::proto_panel::delete_selected_interaction(app, ctx) { + return true; + } // Vector edit mode and the shape builder have their own keys (tools, nudging points, delete, // join, Enter to finish); the rest fall through. match app.session.tool_key(key.name(), m.shift, m.alt, m.command) { From 1e7983a5509e85b34b73d29a2b9e72ed623b68a2 Mon Sep 17 00:00:00 2001 From: Natnael-Dev Date: Sat, 17 Oct 2026 21:47:40 +0300 Subject: [PATCH 2/5] docs(evidence): record verification evidence for #767 fix --- evidence/fix-delete-prototype-interaction.md | 64 ++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 evidence/fix-delete-prototype-interaction.md diff --git a/evidence/fix-delete-prototype-interaction.md b/evidence/fix-delete-prototype-interaction.md new file mode 100644 index 00000000..54808f9e --- /dev/null +++ b/evidence/fix-delete-prototype-interaction.md @@ -0,0 +1,64 @@ +# Evidence: Fix Delete Prototype Interaction (#767) + +## Task Summary +- **Target Issue:** upstream [#767](https://github.com/storytold/figcraft/issues/767) - "When deleting a prototype, it also deletes the component." +- **PR:** [#776](https://github.com/storytold/figcraft/pull/776) +- **Branch:** `fix/delete-prototype-interaction` +- **Head Commit:** `bc5a2b8` + +## Root Cause +When an interaction noodle or interaction editor was clicked/selected in the Prototype tab, `proto_canvas.rs` set document selection to `[n.source]` (the layer). When Delete or Backspace was pressed, `shortcuts.rs` dispatched `edit.delete` without checking if an interaction was active, deleting the component instead of the interaction connection. + +## Implemented Fix +- Added `has_selected_interaction`, `delete_selected_interaction`, and `close_editor` in `crates/ui-egui/src/proto_panel.rs`. +- Handled `Key::Delete` and `Key::Backspace` in `shortcuts::key_event` and `run_binding` when in `RightTab::Prototype` with an active interaction, removing the interaction with `proto.interaction.remove` rather than deleting the layer. +- Added canvas deselect on click in `crates/ui-egui/src/proto_canvas.rs` and tab-switch close in `crates/ui-egui/src/right.rs`. +- Handled `Key::Escape` to close the interaction editor first before stepping out / deselecting layers. +- Added comprehensive headless regression tests in `crates/ui-egui/src/proto_editor_tests.rs`. + +## Raw Verification Evidence + +### 1. Test Execution (`cargo test -p figcraft-ui-egui --lib -- proto`) +```text +running 21 tests +test proto_editor_tests::the_builder_takes_variable_operator_value_conditions ... ok +test proto_canvas::tests::curves_leave_the_facing_side ... ok +test perf_tests::prototype_sources_follow_edits_and_undo ... ok +test proto_editor_tests::deleting_a_prototype_interaction_removes_the_interaction_not_the_component ... ok +test proto_editor_tests::the_presenter_inspects_the_layer_under_the_pointer ... ok +test present::tail_tests::prototypes_without_gamepad_triggers_dont_poll ... ok +test proto_responsive_tests::custom_sizes_present_as_a_bare_screen ... ok +test present::tail_tests::widgets_react_to_clicks_in_the_presented_prototype ... ok +test proto_editor_tests::a_click_where_nothing_reacts_flashes_the_hotspots ... ok +test proto_responsive_tests::prototype_panel_shows_colours_custom_sizes_and_responsive ... ok +test proto_editor_tests::escape_closes_prototype_editor_without_deselecting_layer ... ok +test proto_editor_tests::the_interaction_editor_shows_conditionals_and_picks_their_variables ... ok +test proto_responsive_tests::library_collections_show_in_the_mode_picker_under_their_library ... ok +test proto_reorder_tests::actions_inside_a_conditional_block_drag_within_their_block ... ok +test proto_reorder_tests::escape_cancels_an_action_drag_and_keeps_the_editor_open ... ok +test proto_reorder_tests::dragging_an_action_row_reorders_the_actions_in_one_step ... ok +test field_tests::scrubbing_a_prototype_delay_sets_it_once_when_let_go ... ok +test proto_editor_tests::typed_conditions_are_checked_and_errors_show_until_fixed ... ok +test proto_responsive_tests::without_a_device_a_responsive_frame_fills_the_window_at_100_percent ... ok +test proto_responsive_tests::responsive_frames_take_the_devices_screen_live ... ok +test idle_tests::the_prototype_curve_preview_plays_then_rests ... ok + +test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 500 filtered out; finished in 0.15s +``` +Exit code: 0 +Passed: 21 +Failed: 0 +Skipped: 0 + +### 2. Format Verification (`cargo fmt --check`) +```text +Exit code: 0 +No formatting diffs found. +``` + +### 3. Git Status & PR +```text +Branch: fix/delete-prototype-interaction +Pull Request: https://github.com/storytold/figcraft/pull/776 +Target: storytold/figcraft:main +``` From 9b489a052ef963a2d601481bbd010e00fb0296ac Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 11:34:42 +0700 Subject: [PATCH 3/5] #776: drop the evidence log --- evidence/fix-delete-prototype-interaction.md | 64 -------------------- 1 file changed, 64 deletions(-) delete mode 100644 evidence/fix-delete-prototype-interaction.md diff --git a/evidence/fix-delete-prototype-interaction.md b/evidence/fix-delete-prototype-interaction.md deleted file mode 100644 index 54808f9e..00000000 --- a/evidence/fix-delete-prototype-interaction.md +++ /dev/null @@ -1,64 +0,0 @@ -# Evidence: Fix Delete Prototype Interaction (#767) - -## Task Summary -- **Target Issue:** upstream [#767](https://github.com/storytold/figcraft/issues/767) - "When deleting a prototype, it also deletes the component." -- **PR:** [#776](https://github.com/storytold/figcraft/pull/776) -- **Branch:** `fix/delete-prototype-interaction` -- **Head Commit:** `bc5a2b8` - -## Root Cause -When an interaction noodle or interaction editor was clicked/selected in the Prototype tab, `proto_canvas.rs` set document selection to `[n.source]` (the layer). When Delete or Backspace was pressed, `shortcuts.rs` dispatched `edit.delete` without checking if an interaction was active, deleting the component instead of the interaction connection. - -## Implemented Fix -- Added `has_selected_interaction`, `delete_selected_interaction`, and `close_editor` in `crates/ui-egui/src/proto_panel.rs`. -- Handled `Key::Delete` and `Key::Backspace` in `shortcuts::key_event` and `run_binding` when in `RightTab::Prototype` with an active interaction, removing the interaction with `proto.interaction.remove` rather than deleting the layer. -- Added canvas deselect on click in `crates/ui-egui/src/proto_canvas.rs` and tab-switch close in `crates/ui-egui/src/right.rs`. -- Handled `Key::Escape` to close the interaction editor first before stepping out / deselecting layers. -- Added comprehensive headless regression tests in `crates/ui-egui/src/proto_editor_tests.rs`. - -## Raw Verification Evidence - -### 1. Test Execution (`cargo test -p figcraft-ui-egui --lib -- proto`) -```text -running 21 tests -test proto_editor_tests::the_builder_takes_variable_operator_value_conditions ... ok -test proto_canvas::tests::curves_leave_the_facing_side ... ok -test perf_tests::prototype_sources_follow_edits_and_undo ... ok -test proto_editor_tests::deleting_a_prototype_interaction_removes_the_interaction_not_the_component ... ok -test proto_editor_tests::the_presenter_inspects_the_layer_under_the_pointer ... ok -test present::tail_tests::prototypes_without_gamepad_triggers_dont_poll ... ok -test proto_responsive_tests::custom_sizes_present_as_a_bare_screen ... ok -test present::tail_tests::widgets_react_to_clicks_in_the_presented_prototype ... ok -test proto_editor_tests::a_click_where_nothing_reacts_flashes_the_hotspots ... ok -test proto_responsive_tests::prototype_panel_shows_colours_custom_sizes_and_responsive ... ok -test proto_editor_tests::escape_closes_prototype_editor_without_deselecting_layer ... ok -test proto_editor_tests::the_interaction_editor_shows_conditionals_and_picks_their_variables ... ok -test proto_responsive_tests::library_collections_show_in_the_mode_picker_under_their_library ... ok -test proto_reorder_tests::actions_inside_a_conditional_block_drag_within_their_block ... ok -test proto_reorder_tests::escape_cancels_an_action_drag_and_keeps_the_editor_open ... ok -test proto_reorder_tests::dragging_an_action_row_reorders_the_actions_in_one_step ... ok -test field_tests::scrubbing_a_prototype_delay_sets_it_once_when_let_go ... ok -test proto_editor_tests::typed_conditions_are_checked_and_errors_show_until_fixed ... ok -test proto_responsive_tests::without_a_device_a_responsive_frame_fills_the_window_at_100_percent ... ok -test proto_responsive_tests::responsive_frames_take_the_devices_screen_live ... ok -test idle_tests::the_prototype_curve_preview_plays_then_rests ... ok - -test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 500 filtered out; finished in 0.15s -``` -Exit code: 0 -Passed: 21 -Failed: 0 -Skipped: 0 - -### 2. Format Verification (`cargo fmt --check`) -```text -Exit code: 0 -No formatting diffs found. -``` - -### 3. Git Status & PR -```text -Branch: fix/delete-prototype-interaction -Pull Request: https://github.com/storytold/figcraft/pull/776 -Target: storytold/figcraft:main -``` From d712f2ffd14c74f285433c7e99be1a32737d9db5 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 11:34:43 +0700 Subject: [PATCH 4/5] #776: collapse the nested if in select.none (clippy) --- crates/ui-egui/src/shortcuts.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 50fee385..695457af 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -212,10 +212,10 @@ pub fn swallowed_paste(app: &mut FigApp, ctx: &Context) -> Swallowed { pub fn run_binding(app: &mut FigApp, id: &str) { match id { "select.none" => { - if let Some(ctx) = &app.ui_context { - if crate::proto_panel::close_editor(ctx) { - return; - } + if let Some(ctx) = &app.ui_context + && crate::proto_panel::close_editor(ctx) + { + return; } if let Err(e) = app.session.escape() { app.toast(e.to_string()); From a819e0a18847a865f6535712a580228f7ae7bde8 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 12:28:27 +0700 Subject: [PATCH 5/5] =?UTF-8?q?#776:=20Edit=20=E2=96=B8=20Delete=20from=20?= =?UTF-8?q?the=20menu=20bar=20removes=20the=20open=20interaction=20too=20(?= =?UTF-8?q?the=20guard=20moves=20to=20menus::invoke,=20which=20the=20Delet?= =?UTF-8?q?e=20binding=20also=20reaches);=20has=5Fselected=5Finteraction?= =?UTF-8?q?=20goes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ui-egui/src/menus.rs | 3 +++ crates/ui-egui/src/proto_editor_tests.rs | 17 +++++++++++++++++ crates/ui-egui/src/proto_panel.rs | 7 ------- crates/ui-egui/src/shortcuts.rs | 8 -------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index c917bd23..e32f50c8 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -114,6 +114,9 @@ pub fn record_recent(app: &mut FigApp, key: &str) { pub fn invoke(app: &mut FigApp, id: &str) { match id { + // 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)) => {} "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/proto_editor_tests.rs b/crates/ui-egui/src/proto_editor_tests.rs index 31988a35..c63e62c6 100644 --- a/crates/ui-egui/src/proto_editor_tests.rs +++ b/crates/ui-egui/src/proto_editor_tests.rs @@ -262,6 +262,23 @@ fn deleting_a_prototype_interaction_removes_the_interaction_not_the_component() assert!(u.app.session.doc().unwrap().node(btn).is_none(), "when no interaction is selected, Delete removes the layer"); } +/// Edit ▸ Delete from the menu bar does the same as the Delete key (#767). +#[test] +fn edit_delete_from_the_menu_removes_the_open_interaction_not_the_layer() { + let (s, _a, _b, btn) = setup(); + let mut u = Ui::new(s); + u.app.ui.right_tab = crate::RightTab::Prototype; + u.run("select.set", json!({"ids": [btn]})); + crate::proto_panel::open_editor(&u.ctx, Some((btn, 0))); + u.frames(1, Vec::new); + crate::menus::invoke(&mut u.app, "edit.delete"); + let doc = u.app.session.doc().unwrap(); + assert!(doc.node(btn).is_some_and(|n| n.interactions.is_empty()), "the interaction goes, the layer stays"); + assert_eq!(crate::proto_panel::editor(&u.ctx), None); + crate::menus::invoke(&mut u.app, "edit.delete"); + assert!(u.app.session.doc().unwrap().node(btn).is_none(), "with no interaction open, Edit ▸ Delete deletes the layer"); +} + #[test] fn escape_closes_prototype_editor_without_deselecting_layer() { let (s, a, _b, btn) = setup(); diff --git a/crates/ui-egui/src/proto_panel.rs b/crates/ui-egui/src/proto_panel.rs index 20546008..85af66c5 100644 --- a/crates/ui-egui/src/proto_panel.rs +++ b/crates/ui-egui/src/proto_panel.rs @@ -40,13 +40,6 @@ pub fn editor(ctx: &egui::Context) -> Option<(NodeId, usize)> { ctx.data(|d| d.get_temp(editor_id())) } -/// Whether an interaction is selected (its editor is open). -pub fn has_selected_interaction(app: &FigApp, ctx: &egui::Context) -> bool { - let Some((node, index)) = editor(ctx) else { return false }; - let Ok(ds) = app.session.ds() else { return false }; - ds.doc.node(node).and_then(|n| n.interactions.get(index)).is_some() -} - /// Delete the selected interaction (when one is active in the Prototype tab). /// Returns true if an interaction was deleted. pub fn delete_selected_interaction(app: &mut FigApp, ctx: &egui::Context) -> bool { diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 695457af..f3197960 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -225,14 +225,6 @@ pub fn run_binding(app: &mut FigApp, id: &str) { "edit.delete" if app.session.tool.pen.is_some() => { let _ = app.session.pen_finish(); } - "edit.delete" - if app.ui.right_tab == crate::RightTab::Prototype - && app.ui_context.as_ref().is_some_and(|ctx| crate::proto_panel::has_selected_interaction(app, ctx)) => - { - if let Some(ctx) = app.ui_context.clone() { - let _ = crate::proto_panel::delete_selected_interaction(app, &ctx); - } - } // Enter with a shape or frame tool chosen places one in the middle of the view. "select.children" if app.session.tool.tool.draws_shape() => { app.run("tool.place", json!({}));