mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
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:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
5fd7fd4312
commit
d39e02b3fe
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user