diff --git a/crates/ui-egui/src/canvas/input.rs b/crates/ui-egui/src/canvas/input.rs index fc518d2f..e7f96152 100644 --- a/crates/ui-egui/src/canvas/input.rs +++ b/crates/ui-egui/src/canvas/input.rs @@ -248,11 +248,14 @@ pub fn open_context_menu(app: &mut FigApp, pos: Pos2, deep: bool) { let p = app.to_page(pos); if let Ok(ds) = app.session.ds() { let (doc, page, sel, zoom) = (ds.doc.clone(), ds.page, ds.selection.clone(), ds.view.zoom.max(1e-6)); - if let Some(leaf) = app.session.hit(&doc, page, p, 2.0 / zoom) { - let target = figcraft_doc::geometry::select_target(&doc, leaf, &sel, deep); - if !sel.contains(&target) { - app.session.set_selection(vec![target]); - } + // A frame's name label is the frame, as on a press, even where it sits over a section. + let label = figcraft_engine::tools::label_at(&doc, page, &app.session.tool.labels, p); + let hit = app.session.hit(&doc, page, p, 2.0 / zoom); + let target = label.or_else(|| hit.map(|leaf| figcraft_doc::geometry::select_target(&doc, leaf, &sel, deep))); + if let Some(target) = target + && !sel.contains(&target) + { + app.session.set_selection(vec![target]); } } app.ui.context_menu = Some((pos, app.session.selection())); diff --git a/crates/ui-egui/src/context_menu_tests.rs b/crates/ui-egui/src/context_menu_tests.rs index a447347d..9e2c0d68 100644 --- a/crates/ui-egui/src/context_menu_tests.rs +++ b/crates/ui-egui/src/context_menu_tests.rs @@ -84,3 +84,46 @@ fn copying_a_link_with_nothing_selected_copies_nothing() { assert!(copied.lock().unwrap().is_empty(), "no link to node 0"); assert!(u.app.ui.toast.is_none()); } + +#[test] +fn copying_a_link_on_a_frame_inside_a_selected_section_links_that_frame() { + // The section holds both frames; right-clicking each one must link that frame, not the section + // that was selected before. + let mut s = Session::new(); + let sec = id(&s.execute("node.create", &json!({"type": "section", "x": 0, "y": 0, "width": 800, "height": 400, "name": "Flow"})).unwrap()); + let a = id(&s.execute("frame.create", &json!({"x": 20, "y": 60, "width": 300, "height": 200, "name": "main", "parent": sec})).unwrap()); + let b = id(&s.execute("frame.create", &json!({"x": 400, "y": 60, "width": 300, "height": 200, "name": "dropdown", "parent": sec})).unwrap()); + s.set_selection(vec![sec]); + 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![]); + for (frame, x) in [(a, 170.0), (b, 550.0)] { + let at = u.app.to_screen(x, 160.0); + crate::canvas::open_context_menu(&mut u.app, at, false); + crate::menus::invoke(&mut u.app, "edit.copyLink"); + let last = copied.lock().unwrap().last().cloned(); + assert_eq!(last, Some(format!("figcraft://node/{}", frame.0)), "the frame at x={x}"); + } +} + +#[test] +fn copying_a_link_from_a_frames_name_label_links_that_frame() { + // A frame inside a section has its label above it, inside the section: a right-click there + // must pick the frame whose label it is, not the section behind the label. + let mut s = Session::new(); + let sec = id(&s.execute("node.create", &json!({"type": "section", "x": 0, "y": 0, "width": 800, "height": 400, "name": "Flow"})).unwrap()); + let a = id(&s.execute("frame.create", &json!({"x": 20, "y": 80, "width": 300, "height": 200, "name": "main", "parent": sec})).unwrap()); + s.set_selection(vec![sec]); + 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![]); + let quad = u.app.session.tool.labels.iter().find(|(i, _)| *i == a).map(|(_, q)| *q).expect("the frame has a label"); + let (cx, cy) = (quad.iter().map(|p| p.x).sum::() / 4.0, quad.iter().map(|p| p.y).sum::() / 4.0); + let at = u.app.to_screen(cx, cy); + crate::canvas::open_context_menu(&mut u.app, at, false); + assert_eq!(u.app.session.selection(), vec![a], "the label's frame is selected"); + crate::menus::invoke(&mut u.app, "edit.copyLink"); + assert_eq!(copied.lock().unwrap().last().cloned(), Some(format!("figcraft://node/{}", a.0))); +}