This commit is contained in:
Brandon Thomas
2026-10-14 22:05:12 -04:00
4 changed files with 46 additions and 2 deletions
+1
View File
@@ -135,6 +135,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.
+7 -2
View File
@@ -430,8 +430,13 @@ fn line_at(cs: &compose::ComposedStory, pos: usize) -> Option<(usize, usize)> {
fn place(s: &mut Session, p: &Value, extend: bool) -> Result<Value> {
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"}))?;
}
+35
View File
@@ -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());
}
+3
View File
@@ -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();