diff --git a/crates/ui-egui/src/context_menu_tests.rs b/crates/ui-egui/src/context_menu_tests.rs new file mode 100644 index 00000000..eb594c69 --- /dev/null +++ b/crates/ui-egui/src/context_menu_tests.rs @@ -0,0 +1,55 @@ +//! The right-click menu on a layer: "Copy link to selection" puts `figcraft://node/` on the +//! clipboard and says so; with nothing selected the command copies nothing. + +use std::sync::{Arc, Mutex}; + +use egui::Pos2; +use figcraft_engine::Session; +use serde_json::json; + +use crate::layers_drag_tests::Ui; +use crate::live_tests::{flat, text_center}; +use crate::present_tests::id; + +/// A clipboard that keeps what was copied where the test can read it. +struct Copied(Arc>>); + +impl crate::Services for Copied { + fn clipboard_set_text(&mut self, s: &str) { + if let Ok(mut copied) = self.0.lock() { + copied.push(s.to_string()); + } + } +} + +#[test] +fn the_context_menu_copies_a_link_to_the_selected_frame() { + let mut s = Session::new(); + let f = id(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 300, "height": 200, "name": "หน้าแรก"})).unwrap()); + let copied = Arc::new(Mutex::new(Vec::new())); + let mut u = Ui::new(s); + u.app.services = Box::new(Copied(copied.clone())); + u.frame(vec![]); + u.app.ui.context_menu = Some((Pos2::new(700.0, 300.0), vec![f])); + u.frame(vec![]); + let shapes = flat(&u.frame_output(vec![]).shapes); + let at = text_center(&shapes, "Copy link to selection"); + u.button(at, true); + u.button(at, false); + assert_eq!(*copied.lock().unwrap(), vec![format!("figcraft://node/{}", f.0)], "the link to the frame"); + assert_eq!(u.app.ui.toast.as_ref().map(|t| t.0.as_str()), Some("Link copied")); + assert!(u.app.ui.context_menu.is_none(), "the menu closes"); +} + +#[test] +fn copying_a_link_with_nothing_selected_copies_nothing() { + let mut s = Session::new(); + s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 300, "height": 200})).unwrap(); + s.execute("select.none", &json!({})).unwrap(); + let copied = Arc::new(Mutex::new(Vec::new())); + let mut u = Ui::new(s); + u.app.services = Box::new(Copied(copied.clone())); + crate::menus::invoke(&mut u.app, "edit.copyLink"); + assert!(copied.lock().unwrap().is_empty(), "no link to node 0"); + assert!(u.app.ui.toast.is_none()); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index da40bf0c..165ea5db 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -98,6 +98,8 @@ mod comments_tests; #[cfg(test)] mod component_props_reorder_tests; #[cfg(test)] +mod context_menu_tests; +#[cfg(test)] mod corner_individual_tests; #[cfg(test)] mod cursors_tests; diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index eeb25e3d..ef48719d 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -219,8 +219,10 @@ pub fn invoke(app: &mut FigApp, id: &str) { app.toast(tr("Copied as text")); } "edit.copyLink" => { - let ids = app.session.selection(); - app.services.clipboard_set_text(&format!("figcraft://node/{}", ids.first().map(|i| i.0).unwrap_or(0))); + if let Some(id) = app.session.selection().first() { + app.services.clipboard_set_text(&format!("figcraft://node/{}", id.0)); + app.toast(tr("Link copied")); + } } "edit.findReplace" => app.ui.find = Some((String::new(), String::new())), // The Actions palette invokes commands without parameters. These values are edited in @@ -895,6 +897,7 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) { .ok() .filter(|doc| figcraft_engine::cmd::component::can_combine_variants(doc, &app.session.selection())) .map(|_| i18n::command("component.combineVariants", "Combine as variants")); + let copy_link = i18n::command("edit.copyLink", "Copy link to selection"); let mut entries: Vec<(&str, &str)> = if has_sel { vec![ (tr("Copy"), "edit.copy"), @@ -957,6 +960,9 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) { { entries.insert(at + 1, (label, "component.combineVariants")); } + if let Some(at) = entries.iter().position(|e| e.1 == "edit.copyAsPng") { + entries.insert(at + 1, (©_link, "edit.copyLink")); + } // A right-clicked ruler guide (selected, with no layers) can be removed. if !has_sel && app.session.ds().is_ok_and(|d| d.selected_guide().is_some()) { entries.splice(0..0, [(tr("Remove guide"), "guide.remove"), ("-", "")]);