diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index a16fc502e..395a83910 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1198,6 +1198,8 @@ pub struct Session { pub(crate) plane_widget_press: bool, /// A guide being dragged out of a ruler ([`Session::ruler_guide`]). pub(crate) ruler_guide: Option, + /// Which selected type overflows its frame, for the markers every tool but the Type tools shows. + pub(crate) overflow_marks: vectorcraft_tools::text::OverflowMarks, /// The last search for the files of missing fonts (`text.findFontFiles`), until another /// starts; dropping it stops it. pub(crate) font_search: Option, @@ -1268,6 +1270,7 @@ impl Session { liquify_stroke: None, plane_widget_press: false, ruler_guide: None, + overflow_marks: Default::default(), font_search: None, link_search: None, search_rules: None, diff --git a/crates/engine/src/tests_textedit.rs b/crates/engine/src/tests_textedit.rs index 28ac51c9a..26713fa92 100644 --- a/crates/engine/src/tests_textedit.rs +++ b/crates/engine/src/tests_textedit.rs @@ -737,3 +737,23 @@ fn a_colour_previewed_while_typing_keeps_the_typing() { s.tool_text("d", v).unwrap(); assert_eq!(obj(&s, id).plain_text(), "abcd"); } + +/// The red out port of selected overflowing area type is drawn with the Selection tool too, and +/// not twice with the Type tool, which draws it itself (#2323). +#[test] +fn overflow_marker_shows_with_the_selection_tool() { + let mut s = session(); + let v = ViewInfo::default(); + let long = "Words that fill more than the frame holds. ".repeat(20); + let r = s.execute("text.create", &json!({"x": 50, "y": 50, "text": long, "size": 20, "area": {"width": 100, "height": 40}})).unwrap(); + let id = NodeId(r["id"].as_u64().unwrap()); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + s.select_tool("selection", v).unwrap(); + let red = |s: &mut Session| s.overlays(v).iter().filter(|o| matches!(o, vectorcraft_tools::Overlay::Path { color: [230, 30, 30], .. })).count(); + assert_eq!(red(&mut s), 1, "the out port"); + s.select_tool("type", v).unwrap(); + assert_eq!(red(&mut s), 1, "the Type tool's own, not a second one"); + s.execute("select.set", &json!({"ids": []})).unwrap(); + s.select_tool("selection", v).unwrap(); + assert_eq!(red(&mut s), 0, "nothing selected"); +} diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index 9fd526e9e..b8609689f 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -399,6 +399,12 @@ impl Session { pub fn overlays(&mut self, view: ViewInfo) -> Vec { let mut v = self.with_tool_cx(view, |t, cx| t.overlays(cx)); + // The Type tools draw the overflow markers of selected type themselves. + if !matches!(self.tool.id(), "type" | "areaType" | "typeOnPath" | "verticalType" | "verticalAreaType" | "verticalTypeOnPath") { + let mut marks = std::mem::take(&mut self.overflow_marks); + v.extend(self.with_tool_cx(view, |_, cx| vectorcraft_tools::text::overflow_markers(&mut marks, cx))); + self.overflow_marks = marks; + } if let Some(g) = self.ruler_guide.take() { v.extend(self.with_tool_cx(view, |_, cx| g.overlays(cx))); self.ruler_guide = Some(g); diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index a48a4b366..56ada31f4 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -305,26 +305,59 @@ impl TypeTool { } fn overflow_marker(t: &TextObject, lay: &TextLayout, cx: &ToolContext, o: &mut Vec) { - if !lay.overflow { - return; + if lay.overflow { + overflow_port(t, cx, o); } - // The out port: bottom-right of the frame (end of the path for type on a path). - let port = match &t.kind { - TextKind::Area { frame } => frame.bounds().map(|b| t.xf * Point::new(b.x1, b.y1)), - TextKind::OnPath { path, .. } => path.to_bezpath().segments().last().map(|s| t.xf * vectorcraft_geom::ParamCurve::end(&s)), - TextKind::Point => None, - }; - let Some(c) = port else { return }; - let h = cx.tol(4.5); - let r = Rect::new(c.x - h, c.y - h, c.x + h, c.y + h); - let red = [230, 30, 30]; - o.push(Overlay::Path { path: r.to_path(0.1), color: red, width: 1.0, dashed: false }); - let (k, m) = (cx.tol(2.5), r.center()); - o.push(Overlay::Line { a: Point::new(m.x - k, m.y), b: Point::new(m.x + k, m.y), color: red, dashed: false }); - o.push(Overlay::Line { a: Point::new(m.x, m.y - k), b: Point::new(m.x, m.y + k), color: red, dashed: false }); } } +/// The red "+" out port of `t` (area type and type on a path whose text doesn't fit): at the +/// bottom right of the frame, at the end of the path for type on a path. +fn overflow_port(t: &TextObject, cx: &ToolContext, o: &mut Vec) { + let port = match &t.kind { + TextKind::Area { frame } => frame.bounds().map(|b| t.xf * Point::new(b.x1, b.y1)), + TextKind::OnPath { path, .. } => path.to_bezpath().segments().last().map(|s| t.xf * vectorcraft_geom::ParamCurve::end(&s)), + TextKind::Point => None, + }; + let Some(c) = port else { return }; + let h = cx.tol(4.5); + let r = Rect::new(c.x - h, c.y - h, c.x + h, c.y + h); + let red = [230, 30, 30]; + o.push(Overlay::Path { path: r.to_path(0.1), color: red, width: 1.0, dashed: false }); + let (k, m) = (cx.tol(2.5), r.center()); + o.push(Overlay::Line { a: Point::new(m.x - k, m.y), b: Point::new(m.x + k, m.y), color: red, dashed: false }); + o.push(Overlay::Line { a: Point::new(m.x, m.y - k), b: Point::new(m.x, m.y + k), color: red, dashed: false }); +} + +/// Which selected text overflows, for [`overflow_markers`]: each text object's layout verdict kept +/// with the object and the fonts it was made from, as overlays run every frame. +#[derive(Default)] +pub struct OverflowMarks { + seen: std::collections::HashMap, +} + +/// The overflow markers of the selected area type and type on a path, for every tool but the Type +/// tools (they draw their own): an out port with a red "+" tells that text is hidden by the size of +/// its frame, whatever tool is active. +pub fn overflow_markers(marks: &mut OverflowMarks, cx: &ToolContext) -> Vec { + let fonts = FontDb::global().generation(); + let mut o = Vec::new(); + let mut kept = std::collections::HashMap::new(); + for id in &cx.selection.objects { + let Some(t) = TypeTool::text(cx, *id).filter(|t| !matches!(t.kind, TextKind::Point)) else { continue }; + let over = match marks.seen.remove(id) { + Some(((g, was), over)) if g == fonts && &was == t => over, + _ => vectorcraft_text::layout(FontDb::global(), t).overflow, + }; + if over { + overflow_port(t, cx, &mut o); + } + kept.insert(*id, ((fonts, t.clone()), over)); + } + marks.seen = kept; + o +} + /// Byte range in `s` of the characters `r` (IME ranges count characters; carets count bytes). /// `None` when `r` runs past the end of `s` or backwards. /// The `type.step` an Alt+arrow asks for, by one step: ←/→ kerning at a `caret`, else tracking; diff --git a/crates/tools/src/text_tests.rs b/crates/tools/src/text_tests.rs index c3005d063..7910d2506 100644 --- a/crates/tools/src/text_tests.rs +++ b/crates/tools/src/text_tests.rs @@ -400,3 +400,21 @@ fn escape_returns_to_previous_tool_when_set() { let acts2 = key(&mut tool2, &d2, ToolKey::Escape, Mods::default()); assert_eq!(acts2.last(), Some(&Action::SwitchTool("selection".into()))); } + +/// Selected area type whose text doesn't fit its frame shows the red out port with any tool, not +/// only the Type tools (#2323); text that fits shows none. +#[test] +fn overflowing_selected_type_shows_its_marker_without_the_type_tool() { + let (mut d, id) = crate::testutil::doc_with_area_type(); + let (mut sel, p) = (Selection::default(), paint()); + sel.set([id]); + let mut marks = OverflowMarks::default(); + assert!(overflow_markers(&mut marks, &cx(&d, &sel, &p)).is_empty(), "the text fits"); + if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) { + t.runs = vec![TextRun::new("Words that fill more than the frame holds. ".repeat(20), CharStyle { size: 20.0, ..Default::default() })]; + } + let ov = overflow_markers(&mut marks, &cx(&d, &sel, &p)); + assert_eq!(ov.len(), 3, "a box and a plus: {ov:?}"); + sel.clear(); + assert!(overflow_markers(&mut marks, &cx(&d, &sel, &p)).is_empty(), "only selected type shows it"); +}