fix(engine): drag with the Text tool to make a fixed-width text box (#360)

The Text tool made its text on the press and ignored the drag, so a
drag gave auto-width text like a click. The new text now stays in a
TextBox gesture while pressed: past the drag slop it becomes a box as
wide as the drag, its top-left at the drag's, with a fixed width and a
height that follows its text. A click, or a drag within the slop, still
makes auto-width text.
This commit is contained in:
vxMacDev
2026-10-14 17:29:09 -05:00
parent 833f0d2c27
commit b87d2e476f
2 changed files with 70 additions and 1 deletions
+35
View File
@@ -192,3 +192,38 @@ fn paragraph_indent_only_moves_left_aligned_text() {
let (x, w2) = first_x(&s);
assert!((w2 - (w - 30.0)).abs() < 1e-6 && x.abs() < 1e-6, "centered auto-width text: no indent, the box shrinks by it");
}
/// A new text made with the Text tool by a press at `from`, a drag to `to` and a release, holding
/// `chars`: its box and resize mode.
fn text_tool(from: (f64, f64), to: (f64, f64), chars: &str) -> (figcraft_geom::Rect, figcraft_doc::TextAutoResize) {
use crate::tools::{PointerEvent, PointerKind, ToolId};
let mut s = Session::new();
s.set_tool(ToolId::Text);
s.pointer(&PointerEvent::at(PointerKind::Down, from.0, from.1, 1.0)).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Move, to.0, to.1, 1.0)).unwrap();
s.pointer(&PointerEvent::at(PointerKind::Up, to.0, to.1, 1.0)).unwrap();
crate::textedit::insert(&mut s, chars).unwrap();
let t = s.ds().unwrap().text_edit.as_ref().unwrap().id;
let doc = s.doc().unwrap();
let NodeKind::Text(td) = &doc.node(t).unwrap().kind else { panic!("not a text") };
(doc.world_bounds(t).unwrap(), td.auto_resize)
}
#[test]
fn a_drag_with_the_text_tool_makes_a_fixed_width_box() {
// #360: as wide as the drag, its top-left where the drag started, and its text wraps in it; a
// click, or a drag within a few pixels, still makes auto-width text.
use figcraft_doc::TextAutoResize;
figcraft_text::scan_system_fonts();
let long = "A sentence much longer than eighty pixels";
let (line, auto) = text_tool((100.0, 100.0), (100.0, 100.0), long);
assert_eq!(auto, TextAutoResize::WidthAndHeight);
assert_eq!(text_tool((100.0, 100.0), (101.0, 101.0), long).1, TextAutoResize::WidthAndHeight, "a slip of the hand is a click");
let (b, auto) = text_tool((100.0, 100.0), (180.0, 160.0), long);
assert_eq!(auto, TextAutoResize::Height);
assert_eq!((b.x0, b.y0, b.width()), (100.0, 100.0, 80.0));
assert!(b.height() > line.height(), "the text wraps: {} vs one line {}", b.height(), line.height());
// Dragged up and to the left, the box is the same.
let (b2, _) = text_tool((180.0, 160.0), (100.0, 100.0), long);
assert_eq!((b2.x0, b2.y0, b2.width()), (100.0, 100.0, 80.0));
}
+35 -1
View File
@@ -370,6 +370,11 @@ pub(crate) enum Gesture {
TextSelect {
id: NodeId,
},
/// A new text from the Text tool, still pressed: a drag makes it a box as wide as the drag.
TextBox {
id: NodeId,
start: Point,
},
PenDrag {
anchor: Point,
},
@@ -1347,6 +1352,11 @@ impl Session {
self.tool.gesture = g;
return self.draw_drag(ev, start, id, parent, &targets);
}
Gesture::TextBox { id, start } => {
let (id, start) = (*id, *start);
self.tool.gesture = g;
return self.text_box_drag(ev, id, start);
}
Gesture::Arc { id, handle, arc } => {
let (id, handle, arc) = (*id, *handle, *arc);
self.tool.gesture = g;
@@ -1760,7 +1770,7 @@ impl Session {
self.tool.overlay.pen.clear();
self.pencil_finish(&points, &pressure)?;
}
Gesture::TextSelect { .. } | Gesture::Marquee { .. } | Gesture::None => {}
Gesture::TextSelect { .. } | Gesture::TextBox { .. } | Gesture::Marquee { .. } | Gesture::None => {}
Gesture::PenDrag { .. } | Gesture::ShapeBuild { .. } | Gesture::Origin => {}
Gesture::Vector(_) => self.commit_interaction()?,
}
@@ -1888,12 +1898,36 @@ impl Session {
d.insert(n, parent, None)?;
ds.selection = vec![id];
textedit::begin(self, id, Some(0), true)?;
self.tool.gesture = Gesture::TextBox { id, start: ev.p() };
if !self.prefs.keep_tool {
self.tool.tool = ToolId::Move;
}
Ok(())
}
/// Drag out a new text (Figma's text box): past the drag slop it is as wide as the drag, its
/// top-left at the drag's, and its height follows its text.
fn text_box_drag(&mut self, ev: &PointerEvent, id: NodeId, start: Point) -> Result<()> {
if within_slop(ev, start) {
return Ok(());
}
let r = Rect::from_points(start, ev.p());
let d = self.ds_mut()?.doc_mut();
let pw = d.node(id).and_then(|n| n.parent).map(|p| d.world_transform(p)).unwrap_or_default();
let n = d.get_mut(id)?;
n.transform = pw.inverse() * Affine::translate((r.x0.round(), r.y0.round()));
n.width = r.width().round().max(1.0);
n.child_layout.sizing_h = Sizing::Fixed;
if let NodeKind::Text(t) = &mut n.kind {
t.auto_resize = TextAutoResize::Height;
}
if let Some((_, h)) = figcraft_text::auto_size(n) {
n.height = h;
}
self.ds_mut()?.revision += 1;
Ok(())
}
fn eyedrop(&mut self, ev: &PointerEvent) -> Result<()> {
let ds = self.ds()?;
let doc = ds.doc.clone();