M8: Copy link to selection in the layer context menu

Edit > Copy as > Copy link to selection (edit.copyLink) was only in the main
menu. Add it to the right-click menu on a layer, after Copy as PNG, labelled
with the command's own translated label.

The command now says "Link copied" when it copies, and copies nothing when
no layer is selected (it used to put figcraft://node/0 on the clipboard).
This commit is contained in:
watchakorn-18k
2026-10-24 22:34:35 +07:00
parent 10702311f3
commit 09b28f9b2d
3 changed files with 65 additions and 2 deletions
+55
View File
@@ -0,0 +1,55 @@
//! The right-click menu on a layer: "Copy link to selection" puts `figcraft://node/<id>` 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<Mutex<Vec<String>>>);
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());
}
+2
View File
@@ -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;
+8 -2
View File
@@ -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, (&copy_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"), ("-", "")]);