From 11b1566c099aff00974010a756bc8204c240a106 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Wed, 14 Oct 2026 19:22:54 +0200 Subject: [PATCH] Type tool: a press over a locked empty frame draws a new text frame instead of converting it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adobe's "Set up a modern magazine layout" (Brian Wood, 2:12–4:23) locks a page frame drawn round the spread (Cmd+L), then draws the text frames on top of it with the Type tool. In DesignCraft the press landed in the locked frame, which the Type tool turned into a text frame, putting the caret in it; no new frame was drawn. - The Type tool takes a hit only on text frames (locked or not, as InDesign lets you edit their text) and unlocked empty frames; over a locked empty frame, the press starts a new text frame. - text.placeCaret refuses to convert a locked empty frame (or one on a locked layer). Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/engine/src/cmd/text.rs | 9 +++++++-- crates/engine/src/tests.rs | 35 +++++++++++++++++++++++++++++++++++ crates/tools/src/text.rs | 3 +++ 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9f8168e5..1302df7c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -132,6 +132,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change. - **2026-10-13** · Object › Fitting has InDesign's shortcuts: Fill Frame Proportionally ⌘⌥⇧C, Fit Content Proportionally ⌘⌥⇧E, Fit Frame to Content ⌘⌥C, Fit Content to Frame ⌘⌥E, Center Content ⌘⇧E. No readiness percentage change. - **2026-10-13** · Gradients between process colours blend as inks, as InDesign shows them: magenta to cyan passes through C 50 M 50 (a light lilac) on screen instead of the darker RGB mix, and a midpoint between CMYK or grey stops no longer turns the PDF shading RGB. No readiness percentage change. - **2026-10-14** · The Swatches panel has InDesign's header: a Fill/Stroke proxy, Formatting Affects Container/Text and the Tint of the colour in front (a tint alone keeps each object's own swatch). Clicking a swatch applies it to the fill or stroke in front (of the text, when formatting affects text), and the bottom row has New Color Group, New Swatch and Delete Swatch. No readiness percentage change. diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index add94aca..990a98db 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -420,8 +420,13 @@ fn line_at(cs: &compose::ComposedStory, pos: usize) -> Option<(usize, usize)> { fn place(s: &mut Session, p: &Value, extend: bool) -> Result { let frame = id_param(p, "frame").ok_or_else(|| bad("text.placeCaret", "missing frame"))?; let pt = point_param(p, "point").unwrap_or(Point::ZERO); - // Empty frames become text frames. - let needs_convert = s.doc()?.doc.item(frame).is_some_and(|i| matches!(i.content, Content::Unassigned)); + // Empty frames become text frames; a locked one stays as it is. + let st = s.doc()?; + let locked = st.doc.item(frame).is_some_and(|i| i.locked || st.doc.layer(i.layer).is_some_and(|l| l.locked)); + let needs_convert = st.doc.item(frame).is_some_and(|i| matches!(i.content, Content::Unassigned)); + if needs_convert && locked { + return Err(bad("text.placeCaret", "the frame is locked")); + } if needs_convert { s.execute("object.content", &json!({"ids": [frame.0], "type": "text"}))?; } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 712dc06b..a025a538 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -877,3 +877,38 @@ fn duplicating_a_page_to_an_index_is_one_undo_step() { assert!(s.execute("layout.pages.duplicateSpread", &json!({"spread": 0, "to": 4})).is_err()); assert_eq!(widths(&s), [300.0, 400.0, 500.0]); } + +#[test] +fn the_type_tool_draws_a_text_frame_over_a_locked_frame() { + // Adobe's "Set up a modern magazine layout" (4:03): with the page frame locked (Cmd+L), the + // Type tool draws text frames on top of it; the locked frame stays an empty frame. + use designcraft_tools::{PointerEvent, PointerKind, SnapView}; + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 612, "height": 792, "facingPages": false})).unwrap(); + let big = s.execute("frame.create", &json!({"rect": [36, 36, 576, 756], "content": "unassigned"})).unwrap()["id"].as_u64().unwrap(); + s.execute("object.stroke", &json!({"ids": [big], "swatch": "[Black]", "weight": 6})).unwrap(); + s.execute("object.lock", &json!({"ids": [big]})).unwrap(); + s.execute("edit.deselectAll", &json!({})).unwrap(); + s.set_tool("type"); + let v = ViewInfo { snap: SnapView::OFF, ..ViewInfo::at_zoom(1.0) }; + let layout = s.layout(); + let at = |x: f64, y: f64| layout.to_canvas(designcraft_doc::SpreadRef::Doc(0), designcraft_geom::Point::new(x, y)); + let (a, b) = (at(100.0, 100.0), at(300.0, 200.0)); + s.pointer(&PointerEvent::new(PointerKind::Down, a.x, a.y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0 + a.x - 100.0, a.y + 50.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, b.x, b.y), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Up, b.x, b.y), v).unwrap(); + let d = &s.doc().unwrap().doc; + let it = d.item(designcraft_doc::ItemId(big)).unwrap(); + assert!(matches!(it.content, designcraft_doc::Content::Unassigned), "the locked frame stays empty"); + let frames: Vec<_> = d.spreads[0].items.iter().filter(|i| i.is_text_frame()).collect(); + assert_eq!(frames.len(), 1, "a new text frame"); + let r = frames[0].bounds(); + assert!((r.x0 - 100.0).abs() < 1e-6 && (r.y1 - 200.0).abs() < 1e-6, "{r:?}"); + // The engine refuses to turn a locked empty frame into a text frame. + assert!(s.execute("text.placeCaret", &json!({"frame": big, "point": [300, 400]})).is_err()); + // Unlocked, a press inside converts it, as before. + s.execute("object.unlockAll", &json!({})).unwrap(); + s.execute("text.placeCaret", &json!({"frame": big, "point": [300, 400]})).unwrap(); + assert!(s.doc().unwrap().doc.item(designcraft_doc::ItemId(big)).unwrap().is_text_frame()); +} diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index e6ef1a5f..e2fe043d 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -50,9 +50,12 @@ impl Tool for TypeTool { self.selecting = None; self.cell_drag = None; self.unit_selected = false; + // Text frames take the caret (locked ones too, as in InDesign); an empty frame + // becomes one, unless it's locked: then the press draws a new text frame over it. if let Some((_, id)) = cx.hit(ev.pos) && let Some(it) = cx.doc.item(id) && !matches!(it.content, designcraft_doc::Content::Graphic(_) | designcraft_doc::Content::Group { .. }) + && (it.is_text_frame() || !it.locked) { let sp = cx.layout.spread_at(ev.pos).map(|(_, p)| p).unwrap_or(ev.pos); let clicks = ev.click_count();