diff --git a/ROADMAP.md b/ROADMAP.md index 0128924c0..62dc3a8dc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-20** · Type tool: a drag that starts in an empty frame draws a new text frame over it, as in InDesign; a click still turns the frame into a text frame. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index ee3b362f1..5295708cb 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -1058,6 +1058,30 @@ fn double_click_on_a_port_breaks_its_link() { } } +/// With the Type tool, a drag that starts in an empty rectangle draws a new text frame over it +/// (the rectangle stays as it is), as InDesign's does; a click still turns the rectangle into a +/// text frame with the caret in it. +#[test] +fn type_tool_drag_over_an_empty_frame_draws_a_new_text_frame() { + let mut d = Document::new(&NewDocument::default()); + let id = add_rect(&mut d, Rect::new(100.0, 100.0, 300.0, 300.0)); + let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, false)); + let cx = ctx(&d, &s, &c, &l); + let off = l.xf(SpreadRef::Doc(0)).translation(); + let at = |x: f64, y: f64| (x + off.x, y + off.y); + let mut t = create("type"); + let (x, y) = at(120.0, 120.0); + assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, x, y)).is_empty()); + let (x1, y1) = at(280.0, 200.0); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, x1, y1)); + assert!(matches!(&a[..], [Action::Begin(_), Action::Preview(cmd, p)] if cmd == "frame.create" && p["content"] == "text"), "{a:?}"); + assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, x1, y1)), vec![Action::Commit]); + // A click: the caret goes into the rectangle, which becomes a text frame. + assert!(t.pointer(&cx, &PointerEvent::new(PointerKind::Down, x, y)).is_empty()); + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, x, y)); + assert!(matches!(&a[..], [Action::Exec(cmd, p)] if cmd == "text.placeCaret" && p["frame"] == id.0), "{a:?}"); +} + #[test] fn clicks_in_a_row_edit_and_select_text() { let mut d = Document::new(&NewDocument::default()); diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index 56658f41e..c8cb778a9 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -31,6 +31,9 @@ pub struct TypeTool { start: Option, selecting: Option, drawing: bool, + /// A press on an empty frame: (frame, press point in spread space). A click turns it into a + /// text frame with the caret in it; a drag draws a new text frame over it, as in InDesign. + convert: Option<(u64, Point)>, /// A press on a table whose whole rows or columns are selected: (frame, press point in /// spread space) — a drag moves them, a click places the caret. cell_drag: Option<(u64, Point)>, @@ -226,6 +229,7 @@ impl Tool for TypeTool { self.cell_drag = None; self.unit = None; self.border = None; + self.convert = None; if ev.kind == PointerKind::Down && let Some(b) = border_at(cx, ev.pos) { @@ -246,6 +250,11 @@ impl Tool for TypeTool { self.cell_drag = Some((id.0, sp)); return vec![]; } + // An empty frame: wait to see whether this is a click or a drag. + if clicks == 1 && !ev.mods.shift && !it.is_text_frame() { + self.convert = Some((id.0, sp)); + return vec![]; + } self.selecting = Some(id.0); // Two presses select a word (or a run of spaces), three a line, four a // paragraph, five the story. @@ -296,6 +305,7 @@ impl Tool for TypeTool { let mut out = vec![]; if !self.drawing { self.drawing = true; + self.convert = None; out.push(Action::Begin("Create Text Frame".into())); } out.push(Action::Preview( @@ -323,6 +333,10 @@ impl Tool for TypeTool { self.selecting = None; return vec![Action::Commit]; } + // A click in an empty frame: it becomes a text frame with the caret in it. + if let Some((fid, at)) = self.convert.take() { + return vec![Action::Exec("text.placeCaret".into(), json!({"frame": fid, "point": [at.x, at.y]}))]; + } // Ends a press in text: drops dragged text, or is ignored by the engine. if let Some(fid) = self.selecting.take() { let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos);