diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index 413e8c401..40f528d55 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -328,8 +328,9 @@ fn same_text(s: &mut Session, cmd: &str, eq: fn(&CharStyle, &CharStyle) -> bool) /// Call `f` with each type object among the selected objects and inside them, in selection order, /// keeping only those [`walk_selectable`] visits: none that is hidden, locked, on a hidden, locked -/// or template layer, or outside the isolated group. A type object inside two selected objects is -/// given twice. The walk stops when `f` returns false. +/// or template layer, or outside the isolated group. A selected layer or sublayer gives the type +/// on it that passes the same test. A type object inside two selected objects is given twice. The +/// walk stops when `f` returns false. fn each_selected_text(s: &Session, mut f: impl FnMut(NodeId) -> bool) { fn inside(n: &Node, f: &mut impl FnMut(NodeId) -> bool) -> bool { if !n.visible || n.locked { @@ -340,12 +341,18 @@ fn each_selected_text(s: &Session, mut f: impl FnMut(NodeId) -> bool) { } n.children().into_iter().flatten().all(|c| inside(c, f)) } + fn from(n: &Node, selectable: &impl Fn(NodeId) -> bool, f: &mut impl FnMut(NodeId) -> bool) -> bool { + if n.is_layer() { + return n.children().into_iter().flatten().all(|c| from(c, selectable, f)); + } + !selectable(n.id) || inside(n, f) + } let Some(st) = s.active() else { return }; let locks = st.doc.locked_artboards(); + let selectable = |id| is_selectable(&st.doc, st.isolation, &locks, id); for id in &st.selection.objects { - if is_selectable(&st.doc, st.isolation, &locks, *id) - && let Some(n) = st.doc.node(*id) - && !inside(n, &mut f) + if let Some(n) = st.doc.node(*id) + && !from(n, &selectable, &mut f) { return; } diff --git a/crates/engine/src/tests/menucmds.rs b/crates/engine/src/tests/menucmds.rs index 18ffe69cb..bdcbc28e2 100644 --- a/crates/engine/src/tests/menucmds.rs +++ b/crates/engine/src/tests/menucmds.rs @@ -1995,6 +1995,36 @@ fn select_same_font_skips_type_that_cannot_be_selected() { assert_eq!(selected(&s), vec![g]); } +/// A selected layer gives the type on it that can be selected as the reference of the font Same +/// commands: a hidden or locked layer gives none. +#[test] +fn select_same_font_takes_its_reference_from_the_type_on_a_selected_layer() { + let mut s = session(); + let typed = + |s: &mut Session, y: f64, font: &str| id_of(&s.execute("text.create", &json!({"x": 0, "y": y, "text": "type", "font": font})).unwrap()); + let arial = typed(&mut s, 20.0, "Arial"); + let courier = typed(&mut s, 60.0, "Courier"); + let l2 = id_of(&s.execute("layer.new", &json!({"top": true})).unwrap()); + let on_layer = typed(&mut s, 100.0, "Courier"); + sel(&mut s, &[l2]); + assert_eq!(s.execute("select.same.fontFamily", &json!({})).unwrap()["count"], 2); + let mut got = selected(&s); + got.sort(); + let mut want = vec![courier, on_layer]; + want.sort(); + assert_eq!(got, want); + assert!(!selected(&s).contains(&arial)); + for (off, on) in [ + (json!({"id": l2.0, "visible": false}), json!({"id": l2.0, "visible": true})), + (json!({"id": l2.0, "locked": true}), json!({"id": l2.0, "locked": false})), + ] { + s.execute("layer.setProps", &off).unwrap(); + sel(&mut s, &[l2]); + assert!(s.execute("select.same.fontFamily", &json!({})).is_err(), "{off}"); + s.execute("layer.setProps", &on).unwrap(); + } +} + /// Select › Same › Text Fill & Stroke Color selects the type with characters whose fill and stroke /// both match the first character of the selected type: the same fill with another stroke, or the /// same stroke with another fill, is left out, and in type with several runs one run has to match