Type: selected overflowing type shows its red out port with any tool (#2323)

The overflow marker was drawn by the Type tools only, so with the Selection tool a text box smaller than its text showed no sign that text was hidden. Session::overlays now adds the marker of every selected area type and type on a path that overflows, unless a Type tool is active (it draws its own). The verdict is cached per object and font generation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-17 13:06:51 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 5fd7fd4312
commit d39e02b3fe
5 changed files with 96 additions and 16 deletions
+3
View File
@@ -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<vectorcraft_tools::rulerguide::NewGuide>,
/// 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<cmd::fontfiles::FontSearch>,
@@ -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,
+20
View File
@@ -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");
}
+6
View File
@@ -399,6 +399,12 @@ impl Session {
pub fn overlays(&mut self, view: ViewInfo) -> Vec<Overlay> {
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);
+49 -16
View File
@@ -305,26 +305,59 @@ impl TypeTool {
}
fn overflow_marker(t: &TextObject, lay: &TextLayout, cx: &ToolContext, o: &mut Vec<Overlay>) {
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<Overlay>) {
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<NodeId, ((u64, TextObject), bool)>,
}
/// 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<Overlay> {
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;
+18
View File
@@ -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");
}