mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 20:26:57 -06:00
Merge remote-tracking branch 'origin/main' into properties-more-options
# Conflicts: # ROADMAP.md
This commit is contained in:
+17
-1
@@ -33,7 +33,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Index: page references (topic from the selection or up to 4 levels, sort keys; current page, to end of story, next n paragraphs, suppressed, See / See also), Generate / Update Index (section headings, nested or run-in, page ranges merged, Index Title / Section Head / Level 1–4 styles); IDML export/import as InDesign topics, page references and topic cross-references (verified in InDesign 2026); `index.*` commands.
|
||||
- Text clipboard keeps formatting (character/paragraph formats, footnotes, cross-references, index markers, tables) and falls back to plain text when the system clipboard changed; Paste without Formatting (⇧⌘V); Change Case (UPPERCASE, lowercase, Title Case, Sentence case — formatting kept); Type › Insert Special Character / White Space / Break Character submenus.
|
||||
- Anchored objects: items flow in the text inline (on the baseline, with Y offset; lines grow to fit) or above the line (left/center/right, space before/after); paste copied items into text to anchor them; Anchored Object Options and Release (back onto the page where shown); rendered, exported to PDF and to/from IDML (InDesign anchored object settings; verified in InDesign 2026); copy/paste and threading carry them; `anchored.*` commands.
|
||||
- Ruler guides: drag out of the rulers (page guides, or spread guides on the pasteboard), drag to move, drop on a ruler to delete; Layout › Create Guides (rows/columns with gutters, fit to margins or page); Delete All Guides on Spread; `guide.*` commands.
|
||||
- Ruler guides: drag out of the rulers (page guides, or spread guides on the pasteboard), drag to move, drop on a ruler to delete; a dragged guide snaps along its axis to other guides, margins, columns, bleed, page and object edges and centers and the document grid (Shift: the ruler's ticks; Cmd suspends snapping); selected like objects (click, Shift-click, a marquee that touches no object, Edit › Select All Guides ⌥⌘G), moved together, nudged, deleted, and placed exactly with the X or Y field; Layout › Create Guides (rows/columns with gutters, fit to margins or page); Delete All Guides on Spread; `guide.*` commands.
|
||||
- Links panel: every placed graphic with status (OK / Modified / Missing / Embedded, checked against the file on disk), page and effective PPI (low resolution flagged); Relink…, Go To, Update, Embed; `links.*` commands.
|
||||
- Missing fonts: highlighted pink on screen (never in output), listed by Preflight; Type › Find/Replace Font (fonts in the document, missing first; replace in text, cells, footnotes and styles); `font.list` / `font.replace`.
|
||||
- Missing glyphs, as in InDesign: a character the applied font lacks is drawn as that font's missing-glyph box (its .notdef; a drawn box when the font's is empty), on screen and in PDF (as a path, so PDF/A and PDF/UA exports take it), and Preflight lists the characters by font. Preferences › Composition › Draw Missing Glyphs from Fallback Fonts (`document.preferences {glyphFallback}`, stored with the document) draws them from other fonts instead; it is off in new documents and IDML imports, on in documents saved before it existed. A missing font's substitute always uses fallback fonts.
|
||||
@@ -348,9 +348,17 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-22** · A ruler guide dragged with the Selection tools, out of a ruler, on its own or with other selected guides, snaps along its axis as objects do (#1221): to other ruler guides, margins, columns, bleed, page and object edges and centers (with the smart-guide mark) and the document grid, within the snap zone and following Snap to Guides and Smart Guides. Shift snaps to the ruler's ticks and Cmd suspends snapping. The preview line, the position readout and the drop use the same snapped position. No readiness percentage change.
|
||||
- **2026-10-22** · macOS: the app menu has a Language submenu after Settings… with each interface language in its own name and the current one checked, as in the suite's other apps; Preferences › General › Interface Language stays. No readiness percentage change.
|
||||
- **2026-10-22** · macOS menu bar: the menus keep working after the interface language or Show CJK Features changes (a rebuilt menu bar lost every click, Quit included), and its key equivalents are the shortcuts in effect, so a shortcut moved in the Keyboard Shortcuts editor leaves its old menu item. No readiness percentage change.
|
||||
- **2026-10-22** · Paragraph rules: Rule Above's offset is measured from the first line's baseline to the rule's bottom edge, as in InDesign. Rules sat one ascent too high and crossed the previous paragraph's text (reported from an imported INDD document). No readiness percentage change.
|
||||
- **2026-10-21** · Ruler guides are selected like objects (#1282): a click with the Selection tools selects a guide (also where it crosses an object), Shift-click adds or removes one, a new guide from the rulers is selected, a marquee that touches no object selects the guides it crosses, and Edit › Select All Guides (⌥⌘G, `guide.selectAll`) selects every unlocked guide on the spread. Selected guides draw thicker in their layer's color, drag together (all of them shown moving), nudge with the arrow keys and go with Delete, each as one undo step (`guide.select`, `guide.deselect`, `guide.moveBy`, `guide.delete {}`). With one guide selected the Control bar's X (vertical guide) or Y (horizontal guide) field and Properties › Transform show where it is, as the rulers measure, and typing a value moves it there. No readiness percentage change.
|
||||
- **2026-10-16** · Selection: drag a column guide to move its gutter, as in InDesign (#1096). Both guides of the gutter move together and keep its width, stop at the neighbouring guides and the margins, and snap to the document grid, guides and objects, or with Shift to the ruler's tick marks; Escape cancels and the drag is one undo step. Locked column guides (the default) are not picked. A document page takes the result as its own columns; on a parent page it passes to the pages that follow it. `layout.moveColumnGuide` does the same over the control channel and MCP. No readiness percentage change.
|
||||
- **2026-10-16** · Layout: autoflow, inserted pages and new parents follow dragged column guides (#1096). Autoflow sets one frame per dragged column and counts the pages it adds from them; Insert Pages and New Parent keep the custom positions with the column count and gutter; a page that moves to the other side of the spine (inserted, deleted or moved pages, Duplicate Spread, a new parent's other page) has them mirrored across its margin box, as InDesign does; `document.inspect` reports each page's gutter, column rects and whether its columns are custom. No readiness percentage change.
|
||||
- **2026-10-16** · Layout: Margins and Columns keeps dragged column guides unless the column count or gutter changes (#1096). The dialog sends every field on OK, so a margin-only change re-divided the columns evenly. Margin changes keep the positions; the columns follow them only while they fit the margin box. No readiness percentage change.
|
||||
- **2026-10-16** · Lock Guides and Lock Column Guides are commands in View ▸ Grids & Guides (`view.lockGuides`, ⌥⌘;, and `view.lockColumnGuides`), so the control channel and MCP reach them. Lock Column Guides is an undo step, is saved with the document, on in new ones, and read from and written to IDML's `ColumnGuideLocked`, which export always wrote as locked (#1096). No readiness percentage change.
|
||||
- **2026-10-16** · IDML: a page keeps column guides that were dragged off centre (InDesign's Custom columns). They were divided evenly on import, so the gutter sprang back to the middle; the imported positions now place the columns, with the canvas guides, snapping and text placement following them, and they are written back on export. Layout › Margins and Columns re-divides them evenly when the count or gutter changes, as InDesign's does. No readiness percentage change.
|
||||
- **2026-10-16** · IDML: custom column positions are checked before they are used (#1096). A set must give two edges per column, ascending from the left margin to the right one; any other (a wrong count, unsorted or non-numeric values, a list longer than 216 columns allow, or positions left behind by a margin or page-size change) is the even split, in the canvas, on import and on export, instead of columns drawn outside the margins. No readiness percentage change.
|
||||
- **2026-10-21** · Properties › Character and Paragraph: ••• (More Options) expands the section in place, as InDesign's: Character adds vertical and horizontal scale, baseline shift, skew, language and OpenType, and All Caps, Small Caps, Superscript, Subscript, Underline and Strikethrough; Paragraph adds the left, right, first line and last line indents, space before and after, the drop cap's lines and characters, Align to Baseline Grid and Hyphenate. It used to open the Character or Paragraph panel; the language field moved under ••• as in InDesign. Found in the Learn Skills Daily course, chapter 16. 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.
|
||||
@@ -359,6 +367,14 @@ Newest first.
|
||||
- **2026-10-17** · Object strokes reject malformed types and unknown type fields before changing the document; command help lists every stroke kind and its required fields. No readiness percentage change.
|
||||
- **2026-10-17** · Docs: documented a persistent source CLI install with Cargo, MCP-client PATH setup and bare-name server registration in `docs/mcp.md`. No readiness percentage change.
|
||||
|
||||
- **2026-10-16** · Selection: drag a column guide to move its gutter, as in InDesign (#1096). Both guides of the gutter move together and keep its width, stop at the neighbouring guides and the margins, and snap to the document grid, guides and objects, or with Shift to the ruler's tick marks; Escape cancels and the drag is one undo step. Locked column guides (the default) are not picked. A document page takes the result as its own columns; on a parent page it passes to the pages that follow it. `layout.moveColumnGuide` does the same over the control channel and MCP. No readiness percentage change.
|
||||
- **2026-10-16** · Layout: Margins and Columns keeps dragged column guides unless the column count or gutter changes (#1096). The dialog sends every field on OK, so a margin-only change re-divided the columns evenly. Margin changes keep the positions; the columns follow them only while they fit the margin box. No readiness percentage change.
|
||||
- **2026-10-16** · Layout: autoflow, inserted pages and new parents follow dragged column guides (#1096). Autoflow sets one frame per dragged column and counts the pages it adds from them; Insert Pages and New Parent keep the custom positions with the column count and gutter; `document.inspect` reports each page's gutter, column rects and whether its columns are custom. No readiness percentage change.
|
||||
- **2026-10-16** · Layout: autoflow, inserted pages and new parents follow dragged column guides (#1096). Autoflow sets one frame per dragged column and counts the pages it adds from them; Insert Pages and New Parent keep the custom positions with the column count and gutter; a page that moves to the other side of the spine (inserted, deleted or moved pages, Duplicate Spread, a new parent's other page) has them mirrored across its margin box, as InDesign does; `document.inspect` reports each page's gutter, column rects and whether its columns are custom. No readiness percentage change.
|
||||
- **2026-10-16** · IDML: custom column positions are checked before they are used (#1096). A set must give two edges per column, ascending from the left margin to the right one; any other (a wrong count, unsorted or non-numeric values, a list longer than 216 columns allow, or positions left behind by a margin or page-size change) is the even split, in the canvas, on import and on export, instead of columns drawn outside the margins. No readiness percentage change.
|
||||
- **2026-10-16** · IDML: a page keeps column guides that were dragged off centre (InDesign's Custom columns). They were divided evenly on import, so the gutter sprang back to the middle; the imported positions now place the columns, with the canvas guides, snapping and text placement following them, and they are written back on export. Layout › Margins and Columns re-divides them evenly when the count or gutter changes, as InDesign's does. No readiness percentage change.
|
||||
- **2026-10-16** · Lock Guides and Lock Column Guides are commands in View ▸ Grids & Guides (`view.lockGuides`, ⌥⌘;, and `view.lockColumnGuides`), so the control channel and MCP reach them. Lock Column Guides is saved with the document, on in new ones, and read from and written to IDML's `ColumnGuideLocked`, which export always wrote as locked (#1096). No readiness percentage change.
|
||||
- **2026-10-16** · Lock Guides and Lock Column Guides are commands in View ▸ Grids & Guides (`view.lockGuides`, ⌥⌘;, and `view.lockColumnGuides`), so the control channel and MCP reach them. Lock Column Guides is an undo step, is saved with the document, on in new ones, and read from and written to IDML's `ColumnGuideLocked`, which export always wrote as locked (#1096). No readiness percentage change.
|
||||
- **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change.
|
||||
- **2026-10-16** · Greek text hyphenates. The patterns were never compiled in, so Greek set unhyphenated however narrow the column; `"Greek"`, `"Greek: Greece"`, `el` and `el-GR` all resolve now and ανθρωπος breaks αν-θρω-πος. They are the monotonic patterns (hyph-el-monoton, Dimitrios Filippou, under MIT), which is what polytonic text hyphenates by too. Five of InDesign's ~40 hyphenation languages. No readiness percentage change.
|
||||
- **2026-10-16** · A swatch applied to a placed greyscale or bitmap image colours it, as in InDesign: IDML's `<Image FillColor>` is read and written, greyscale pixels become a tint of the swatch on opaque white and a bitmap's white lets the frame show through, on screen and in PDF (spot swatches print on their plate). Colour images keep their colours. Applying the swatch from the Swatches panel comes next. No readiness percentage change.
|
||||
|
||||
@@ -430,6 +430,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
})
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"layout.moveColumnGuide",
|
||||
"Move Column Guide",
|
||||
[],
|
||||
None,
|
||||
"{spread?, parent? (a parent's prefix, label or index: its spread instead of `spread`), page (index in the spread), gutter (0 = between the first two columns), position (the gutter's new left edge, spread x)} → {positions (each column's left and right edge from the margin box's left edge), left (where the gutter's left edge went, spread x)} — both guides of the gutter move, keeping its width, and stop at the neighbouring guides and the margins; a document page takes the result as its own columns, a parent page passes it to the pages that follow it; refused while column guides are locked",
|
||||
column_guides_unlocked,
|
||||
move_column_guide
|
||||
),
|
||||
cmd!(
|
||||
"layout.documentSetup",
|
||||
"Document Setup…",
|
||||
@@ -870,6 +879,52 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
]
|
||||
}
|
||||
|
||||
/// Column guides can be dragged: a document is open and View › Grids & Guides › Lock Column
|
||||
/// Guides is off.
|
||||
fn column_guides_unlocked(s: &Session) -> std::result::Result<(), String> {
|
||||
has_doc(s)?;
|
||||
match s.active() {
|
||||
Some(st) if st.doc.settings.column_guides_locked => Err("column guides are locked (View › Grids & Guides › Lock Column Guides)".into()),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A column guide dragged with the Selection tool: gutter `gutter` of a page moves to `position`.
|
||||
fn move_column_guide(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const CMD: &str = "layout.moveColumnGuide";
|
||||
let index = |key: &str| -> Result<usize> {
|
||||
let v = p.get(key).ok_or_else(|| bad(CMD, format!("missing `{key}`")))?;
|
||||
v.as_u64().and_then(|n| usize::try_from(n).ok()).ok_or_else(|| bad(CMD, format!("`{key}` must be a non-negative integer")))
|
||||
};
|
||||
let (page, gutter) = (index("page")?, index("gutter")?);
|
||||
let position =
|
||||
p.get("position").and_then(Value::as_f64).filter(|v| v.is_finite()).ok_or_else(|| bad(CMD, "`position` must be a finite number"))?;
|
||||
let r = match p.get("parent") {
|
||||
None | Some(Value::Null) => super::spread_param(p, CMD)?,
|
||||
Some(v) => designcraft_doc::SpreadRef::Parent(parent_spread_index(&s.doc()?.doc, v).ok_or_else(|| bad(CMD, "no such parent"))?),
|
||||
};
|
||||
s.edit(|d, _| {
|
||||
if d.settings.column_guides_locked {
|
||||
return Err(bad(CMD, "column guides are locked"));
|
||||
}
|
||||
let pg = d.spread_mut(r).ok_or_else(|| bad(CMD, "no such spread"))?.pages.get_mut(page).ok_or_else(|| bad(CMD, "no such page"))?;
|
||||
let m = pg.margin_rect();
|
||||
let count = pg.columns.count;
|
||||
if gutter.checked_add(1).is_none_or(|g| g >= count as usize) {
|
||||
return Err(bad(CMD, format!("the page has {} gutters", count.saturating_sub(1))));
|
||||
}
|
||||
let positions =
|
||||
pg.columns.with_gutter_moved(m.width(), gutter, position - m.x0).ok_or_else(|| bad(CMD, "the columns cannot take that position"))?;
|
||||
let left = positions.get(gutter * 2 + 1).map_or(position, |v| m.x0 + v);
|
||||
pg.columns.positions = Some(positions.clone());
|
||||
match r {
|
||||
designcraft_doc::SpreadRef::Doc(_) => pg.local_margins = true,
|
||||
designcraft_doc::SpreadRef::Parent(_) => d.sync_parent_margins(),
|
||||
}
|
||||
Ok(json!({"positions": positions, "left": left}))
|
||||
})
|
||||
}
|
||||
|
||||
/// A parent spread named by prefix, label (`A-Parent`) or index.
|
||||
fn parent_spread_index(d: &designcraft_doc::Document, v: &Value) -> Option<usize> {
|
||||
match v {
|
||||
@@ -2227,6 +2282,114 @@ mod parent_tests {
|
||||
assert_eq!((page["columns"].clone(), page["customColumns"].clone()), (json!(2), json!(true)), "{page}");
|
||||
assert_eq!(page["columnRects"][0], json!([36.0, 36.0, 136.0, 756.0]), "{page}");
|
||||
}
|
||||
|
||||
fn columns_of(s: &crate::Session, abs: usize) -> (Option<Vec<f64>>, bool) {
|
||||
let pg = s.doc().unwrap().doc.page(abs).unwrap().clone();
|
||||
(pg.columns.positions, pg.local_margins)
|
||||
}
|
||||
|
||||
/// Three columns across a 540 pt margin box (36 pt margins), column guides unlocked.
|
||||
fn three_columns(pages: u64) -> crate::Session {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({"pages": pages})).unwrap();
|
||||
s.execute("layout.marginsAndColumns", &json!({"parent": "A", "columns": 3, "gutter": 12})).unwrap();
|
||||
s.execute("view.lockColumnGuides", &json!({"on": false})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// Dragging a column guide moves its gutter's two guides together, stops at the neighbouring
|
||||
/// gutter and the margin, makes a following page's columns its own, and undoes in one step.
|
||||
#[test]
|
||||
fn move_column_guide_moves_the_gutter_pair_within_its_neighbours() {
|
||||
let mut s = three_columns(1);
|
||||
assert_eq!(columns_of(&s, 0), (None, false), "the page follows A");
|
||||
let mv =
|
||||
|s: &mut crate::Session, gutter: u64, x: f64| s.execute("layout.moveColumnGuide", &json!({"page": 0, "gutter": gutter, "position": x}));
|
||||
let r = mv(&mut s, 0, 236.0).unwrap();
|
||||
assert_eq!(r, json!({"positions": [0.0, 200.0, 212.0, 356.0, 368.0, 540.0], "left": 236.0}));
|
||||
assert_eq!(columns_of(&s, 0), (Some(vec![0.0, 200.0, 212.0, 356.0, 368.0, 540.0]), true));
|
||||
let inspect = s.execute("document.inspect", &json!({})).unwrap();
|
||||
assert_eq!(inspect["spreads"][0]["pages"][0]["columnRects"][0], json!([36.0, 36.0, 236.0, 756.0]));
|
||||
// Past the next gutter: it stops there, and the middle column has no width.
|
||||
assert_eq!(mv(&mut s, 0, 1000.0).unwrap()["left"], json!(36.0 + 344.0));
|
||||
// Past the right margin: the last gutter stops 12 pt (its width) inside it.
|
||||
assert_eq!(mv(&mut s, 1, 1e12).unwrap()["positions"], json!([0.0, 344.0, 356.0, 528.0, 540.0, 540.0]));
|
||||
// Past the left margin.
|
||||
assert_eq!(mv(&mut s, 0, -1e12).unwrap()["positions"], json!([0.0, 0.0, 12.0, 528.0, 540.0, 540.0]));
|
||||
for _ in 0..3 {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
assert_eq!(columns_of(&s, 0), (Some(vec![0.0, 200.0, 212.0, 356.0, 368.0, 540.0]), true));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(columns_of(&s, 0), (None, false), "one undo step per drag");
|
||||
}
|
||||
|
||||
/// On a parent page the drag changes the parent, and the document pages that follow it get
|
||||
/// the new columns; a page with columns of its own keeps them.
|
||||
#[test]
|
||||
fn move_column_guide_on_a_parent_reaches_the_pages_that_follow_it() {
|
||||
let mut s = three_columns(3);
|
||||
// Page 1, the left page of spread 1, gets columns of its own.
|
||||
let x = s.doc().unwrap().doc.spreads[1].pages[0].margin_rect().x0 + 300.0;
|
||||
s.execute("layout.moveColumnGuide", &json!({"spread": 1, "page": 0, "gutter": 1, "position": x})).unwrap();
|
||||
let own = columns_of(&s, 1);
|
||||
assert!(own.1 && own.0.is_some());
|
||||
let d = &s.doc().unwrap().doc;
|
||||
// A's right page: the one page 0 (a right page) and page 2 follow.
|
||||
let x0 = d.parents[0].pages[1].margin_rect().x0;
|
||||
s.execute("layout.moveColumnGuide", &json!({"parent": "A", "page": 1, "gutter": 0, "position": x0 + 100.0})).unwrap();
|
||||
let moved = vec![0.0, 100.0, 112.0, 356.0, 368.0, 540.0];
|
||||
assert_eq!(s.doc().unwrap().doc.parents[0].pages[1].columns.positions, Some(moved.clone()));
|
||||
assert_eq!(columns_of(&s, 0), (Some(moved.clone()), false));
|
||||
assert_eq!(columns_of(&s, 2), (Some(moved), false));
|
||||
assert_eq!(columns_of(&s, 1), own, "page 1's columns are its own");
|
||||
// The same parent page by spread reference.
|
||||
let spread = json!({"kind": "parent", "index": 0});
|
||||
s.execute("layout.moveColumnGuide", &json!({"spread": spread, "page": 1, "gutter": 0, "position": x0 + 50.0})).unwrap();
|
||||
assert_eq!(columns_of(&s, 0).0.unwrap()[1], 50.0);
|
||||
}
|
||||
|
||||
/// A drag's preview in one document is put back there when another document became active.
|
||||
#[test]
|
||||
fn a_column_guide_preview_is_cancelled_in_its_own_document() {
|
||||
let mut s = three_columns(1);
|
||||
let uid = s.doc().unwrap().uid;
|
||||
s.begin_interaction("Move Column Guide").unwrap();
|
||||
s.preview("layout.moveColumnGuide", json!({"page": 0, "gutter": 0, "position": 236})).unwrap();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.cancel_interaction_of(uid);
|
||||
s.set_active(0);
|
||||
assert!(s.doc().unwrap().interaction.is_none());
|
||||
assert_eq!(columns_of(&s, 0), (None, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_column_guide_refuses_while_locked_and_hostile_params() {
|
||||
let mut s = three_columns(1);
|
||||
s.execute("view.lockColumnGuides", &json!({"on": true})).unwrap();
|
||||
assert!(s.execute("layout.moveColumnGuide", &json!({"page": 0, "gutter": 0, "position": 200})).is_err());
|
||||
assert_eq!(columns_of(&s, 0), (None, false));
|
||||
s.execute("view.lockColumnGuides", &json!({"on": false})).unwrap();
|
||||
for p in [
|
||||
json!({"page": 0, "gutter": 0}),
|
||||
json!({"page": 0, "gutter": 0, "position": f64::NAN}),
|
||||
json!({"page": 0, "gutter": 0, "position": "200"}),
|
||||
json!({"page": 0, "gutter": 2, "position": 200}),
|
||||
json!({"page": 0, "gutter": u64::MAX, "position": 200}),
|
||||
json!({"page": 0, "gutter": -1, "position": 200}),
|
||||
json!({"page": 0, "gutter": 0.5, "position": 200}),
|
||||
json!({"page": 9, "gutter": 0, "position": 200}),
|
||||
json!({"page": u64::MAX, "gutter": 0, "position": 200}),
|
||||
json!({"gutter": 0, "position": 200}),
|
||||
json!({"spread": 99, "page": 0, "gutter": 0, "position": 200}),
|
||||
json!({"parent": "Z", "page": 0, "gutter": 0, "position": 200}),
|
||||
json!({"page": 0, "gutter": 0, "position": f64::MAX}),
|
||||
] {
|
||||
let r = s.execute("layout.moveColumnGuide", &p);
|
||||
// A huge but finite position is clamped to the margin; everything else is refused.
|
||||
assert!(r.is_err() || p["position"] == json!(f64::MAX), "{p}: {r:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -331,4 +331,18 @@ impl Session {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the interaction of the document with `uid`, active or not: a gesture that began in
|
||||
/// one document and lost it to another tab ends there, leaving the document as it began.
|
||||
pub fn cancel_interaction_of(&mut self, uid: u64) {
|
||||
if self.active().is_some_and(|st| st.uid == uid) {
|
||||
self.cancel_interaction();
|
||||
} else if let Some(st) = self.docs.iter_mut().find(|st| st.uid == uid)
|
||||
&& let Some(it) = st.interaction.take()
|
||||
{
|
||||
st.doc = it.doc;
|
||||
st.selection = it.selection;
|
||||
st.revision = it.revision;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,6 +623,19 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
} else {
|
||||
app.session.cursor(xf.to_canvas(p), ui.input(|i| mods(i, false)), app.view_info())
|
||||
};
|
||||
// A column guide that a press would drag, or one being dragged: a resize cursor across it.
|
||||
let dragging = ui.data(|d| d.get_temp::<ColumnDrag>(egui::Id::new((COLUMN_DRAG, app.pane)))).map(|g| g.spread);
|
||||
let column = dragging.or_else(|| {
|
||||
let menu_open = context_menu_open(ui.ctx());
|
||||
(!space && !zoom && !middle && !alt_hand && !tool_drag && !menu_open && xf.rot == 0)
|
||||
.then(|| column_guide_at(app, &xf, p).map(|h| h.0))
|
||||
.flatten()
|
||||
});
|
||||
let c = match column {
|
||||
Some(r) if spread_x_across(app, r) => Cursor::ResizeH,
|
||||
Some(_) => Cursor::ResizeV,
|
||||
None => c,
|
||||
};
|
||||
let icon = crate::cursors::show(ui.ctx(), &mut app.cursors, c, p);
|
||||
ui.ctx().set_cursor_icon(icon);
|
||||
draw_cursor_badge(&painter, p, c, &t);
|
||||
@@ -2393,6 +2406,11 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
|
||||
// Space-drag = hand (unless a tool drag is under way: then Space is a modifier, e.g. Live
|
||||
// Distribute while resizing). Middle-drag pans too, with any tool; tools only
|
||||
// see the primary button.
|
||||
// A column guide drag keeps the pointer until the button is up, whatever Space or a menu does
|
||||
// meanwhile.
|
||||
if ui.data(|d| d.get_temp::<ColumnDrag>(egui::Id::new((COLUMN_DRAG, app.pane)))).is_some() && column_guide_drag(app, ui, resp, rect, &xf) {
|
||||
return;
|
||||
}
|
||||
let tool_drag: bool = ui.data(|d| d.get_temp(egui::Id::new(("canvas_pointer_down", app.pane)))).unwrap_or(false);
|
||||
let hand = space && !ui.input(|i| i.modifiers.command);
|
||||
if !tool_drag && ((hand && resp.dragged()) || resp.dragged_by(egui::PointerButton::Middle)) {
|
||||
@@ -2428,6 +2446,9 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
|
||||
if !space && !menu_open && xf.rot == 0 && guide_drag(app, ui, resp, rect, &xf) {
|
||||
return;
|
||||
}
|
||||
if !space && !menu_open && xf.rot == 0 && column_guide_drag(app, ui, resp, rect, &xf) {
|
||||
return;
|
||||
}
|
||||
let m = ui.input(|i| mods(i, space));
|
||||
let vi = app.view_info();
|
||||
let pos = |p: Pos2| xf.to_canvas(p);
|
||||
@@ -3263,6 +3284,222 @@ fn moving_guides(app: &DesignApp, xf: &Xf, rect: Rect, g: &GuideDrag, press: Gui
|
||||
Some(MovingGuides { guides, dx, dy })
|
||||
}
|
||||
|
||||
/// An object is under screen point `p` where the Selection tool would pick it (a document
|
||||
/// spread's, or a parent spread's while editing parents): it takes the press before the guides
|
||||
/// behind it.
|
||||
fn over_item(app: &DesignApp, xf: &Xf, p: Pos2) -> bool {
|
||||
let Some(st) = app.session.active() else { return false };
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
let cx = designcraft_tools::ToolContext {
|
||||
doc: &st.doc,
|
||||
selection: &st.selection,
|
||||
cache: &app.session.cache,
|
||||
layout: &layout,
|
||||
zoom: xf.zoom.max(1e-6),
|
||||
layer: st.active_layer,
|
||||
snap: app.ui.snap_view(),
|
||||
unit: app.ui.units,
|
||||
stroke_in_front: false,
|
||||
};
|
||||
cx.hit(xf.to_canvas(p)).is_some()
|
||||
}
|
||||
|
||||
const COLUMN_DRAG: &str = "canvas_column_drag";
|
||||
|
||||
/// A column guide being dragged with a Selection tool: its gutter follows the pointer.
|
||||
#[derive(Clone, Debug)]
|
||||
struct ColumnDrag {
|
||||
/// The document the drag's interaction belongs to.
|
||||
doc: u64,
|
||||
spread: SpreadRef,
|
||||
page: usize,
|
||||
gutter: usize,
|
||||
/// The pointer's spread x minus the gutter's left edge at the press: the gutter keeps that
|
||||
/// distance from the pointer.
|
||||
grab: f64,
|
||||
/// The left edge last previewed.
|
||||
last: Option<f64>,
|
||||
}
|
||||
|
||||
/// The interior column guide under screen point `p` (within 3 px) that a press there would drag,
|
||||
/// as (spread, page, gutter, the gutter's left edge in spread x): a Selection tool, guides shown
|
||||
/// (not in Preview or Presentation), column guides unlocked, and no object or ruler guide that
|
||||
/// takes the press there.
|
||||
fn column_guide_at(app: &DesignApp, xf: &Xf, p: Pos2) -> Option<(SpreadRef, usize, usize, f64)> {
|
||||
let preview = matches!(app.ui.screen_mode, ScreenMode::Preview | ScreenMode::Presentation);
|
||||
if !app.ui.guides || preview || !matches!(app.session.tool_id(), "selection" | "directSelection") {
|
||||
return None;
|
||||
}
|
||||
let st = app.session.active()?;
|
||||
if st.doc.settings.column_guides_locked {
|
||||
return None;
|
||||
}
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
let (r, sp) = layout.spread_at(xf.to_canvas(p))?;
|
||||
let tol = 3.0 / xf.zoom.max(1e-9);
|
||||
let hit = st.doc.spread(r)?.pages.iter().enumerate().find_map(|(pi, pg)| {
|
||||
let m = pg.margin_rect();
|
||||
if sp.y < m.y0 - tol || sp.y > m.y1 + tol {
|
||||
return None;
|
||||
}
|
||||
pg.column_rects().windows(2).enumerate().find_map(|(gi, w)| match w {
|
||||
// Columns that cannot move (gutters wider than the margin box) offer no guide.
|
||||
[a, b]
|
||||
if ((sp.x - a.x1).abs() <= tol || (sp.x - b.x0).abs() <= tol)
|
||||
&& pg.columns.with_gutter_moved(m.width(), gi, a.x1 - m.x0).is_some() =>
|
||||
{
|
||||
Some((r, pi, gi, a.x1))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})?;
|
||||
// A ruler guide there takes the press first (see `guide_drag`).
|
||||
let ruler_guide = !app.ui.guides_locked && guide_at(app, xf, p).is_some();
|
||||
(!ruler_guide && !over_item(app, xf, p)).then_some(hit)
|
||||
}
|
||||
|
||||
/// Whether spread `r`'s x axis runs across the screen (the spread view is not turned a quarter).
|
||||
fn spread_x_across(app: &DesignApp, r: SpreadRef) -> bool {
|
||||
app.session.active().is_none_or(|st| {
|
||||
let [a, b, ..] = CanvasLayout::new(&st.doc, st.editing_parents).xf(r).as_coeffs();
|
||||
a.abs() >= b.abs()
|
||||
})
|
||||
}
|
||||
|
||||
/// Canvas x `x` moved to the nearest tick mark of the horizontal ruler (its finest increment).
|
||||
fn ruler_tick_x(app: &DesignApp, doc: &Document, layout: &CanvasLayout, zoom: f64, x: f64) -> Option<f64> {
|
||||
let pieces = ruler_pieces(app, doc, layout, doc.zero_point());
|
||||
let &(_, _, o) = pieces.iter().find(|(from, to, _)| *from <= x && x < *to).or(pieces.first())?;
|
||||
let (major, sub) = doc.settings.horizontal_units.ruler_ticks(zoom);
|
||||
let step = major / f64::from(sub.max(1));
|
||||
(step.is_finite() && step > 0.0).then(|| o.x + ((x - o.x) / step).round() * step).filter(|v| v.is_finite())
|
||||
}
|
||||
|
||||
/// Where a dragged gutter's left edge goes for spread x `want`: Shift snaps it to the ruler's tick
|
||||
/// marks; otherwise its edges snap to the document grid, guides and objects as those snaps are
|
||||
/// set. Measured against the document as it was when the drag began.
|
||||
fn snap_gutter(app: &DesignApp, g: &ColumnDrag, want: f64, shift: bool, zoom: f64) -> f64 {
|
||||
let Some(st) = app.session.active() else { return want };
|
||||
let doc = st.interaction.as_ref().map_or_else(|| st.doc.clone(), |i| i.doc.clone());
|
||||
let layout = CanvasLayout::new(&doc, st.editing_parents);
|
||||
let Some(slot) = layout.slots.iter().find(|s| s.spread == g.spread) else { return want };
|
||||
let Some(pg) = doc.spread(g.spread).and_then(|sp| sp.pages.get(g.page)) else { return want };
|
||||
let m = pg.margin_rect();
|
||||
if shift {
|
||||
if !spread_x_across(app, g.spread) {
|
||||
return want;
|
||||
}
|
||||
let c = slot.to_canvas(Point::new(want, m.y0));
|
||||
return ruler_tick_x(app, &doc, &layout, zoom, c.x).map_or(want, |x| slot.to_spread(Point::new(x, c.y)).x);
|
||||
}
|
||||
let cols = pg.column_rects();
|
||||
let width = match (cols.get(g.gutter), cols.get(g.gutter + 1)) {
|
||||
(Some(a), Some(b)) => (b.x0 - a.x1).max(0.0),
|
||||
_ => 0.0,
|
||||
};
|
||||
let cx = designcraft_tools::ToolContext {
|
||||
doc: &doc,
|
||||
selection: &st.selection,
|
||||
cache: &app.session.cache,
|
||||
layout: &layout,
|
||||
zoom: zoom.max(1e-6),
|
||||
layer: st.active_layer,
|
||||
// A line has no size or spacing to match.
|
||||
snap: designcraft_tools::SnapView { smart_dimensions: false, smart_spacing: false, ..app.ui.snap_view() },
|
||||
unit: app.ui.units,
|
||||
stroke_in_front: false,
|
||||
};
|
||||
let rect = DRect::new(want, m.y0, want + width, m.y1);
|
||||
let s = designcraft_tools::snap::snap(
|
||||
&cx,
|
||||
designcraft_tools::SnapRequest {
|
||||
spread: g.spread,
|
||||
gesture: designcraft_tools::Gesture::Move,
|
||||
rect,
|
||||
x_edges: [true, false, true],
|
||||
y_edges: [false; 3],
|
||||
exclude: &[],
|
||||
exclude_guides: &[],
|
||||
copying: false,
|
||||
lengths: [None, None],
|
||||
angle: None,
|
||||
radius: 0.0,
|
||||
pointer: Point::new(want, m.center().y),
|
||||
},
|
||||
);
|
||||
let x = want + s.delta.x;
|
||||
if x.is_finite() { x } else { want }
|
||||
}
|
||||
|
||||
/// Column guides: with a Selection tool, drag an interior column guide (not over an object) to
|
||||
/// move its gutter, both guides together; Escape puts it back. One undo step per drag. Returns
|
||||
/// true while it owns the pointer.
|
||||
fn column_guide_drag(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, rect: Rect, xf: &Xf) -> bool {
|
||||
let id = egui::Id::new((COLUMN_DRAG, app.pane));
|
||||
let (pressed, released, origin, latest, shift, escape) = ui.input(|i| {
|
||||
(
|
||||
i.pointer.primary_pressed(),
|
||||
// A release this frame, or one missed while something else had the pointer.
|
||||
i.pointer.primary_released() || !i.pointer.primary_down(),
|
||||
i.pointer.press_origin(),
|
||||
i.pointer.latest_pos(),
|
||||
i.modifiers.shift,
|
||||
i.key_pressed(egui::Key::Escape),
|
||||
)
|
||||
});
|
||||
let mut drag: Option<ColumnDrag> = ui.data(|d| d.get_temp(id));
|
||||
if drag.is_none()
|
||||
&& pressed
|
||||
&& resp.hovered()
|
||||
&& let Some(o) = origin.filter(|o| rect.contains(*o))
|
||||
&& let Some((spread, page, gutter, left)) = column_guide_at(app, xf, o)
|
||||
// Another gesture or a dialog's preview owns the document until it ends.
|
||||
&& let Some((doc, slot_x)) = app
|
||||
.session
|
||||
.active()
|
||||
.filter(|st| st.interaction.is_none())
|
||||
.map(|st| (st.uid, CanvasLayout::new(&st.doc, st.editing_parents).to_spread(spread, xf.to_canvas(o)).x))
|
||||
&& app.session.begin_interaction("Move Column Guide").is_ok()
|
||||
{
|
||||
drag = Some(ColumnDrag { doc, spread, page, gutter, grab: slot_x - left, last: None });
|
||||
}
|
||||
let Some(mut g) = drag else { return false };
|
||||
// Another document became active: the drag ends in its own, as it began.
|
||||
if app.session.active().is_none_or(|st| st.uid != g.doc) {
|
||||
app.session.cancel_interaction_of(g.doc);
|
||||
ui.data_mut(|d| d.remove::<ColumnDrag>(id));
|
||||
return false;
|
||||
}
|
||||
if escape {
|
||||
app.session.cancel_interaction();
|
||||
ui.data_mut(|d| d.remove::<ColumnDrag>(id));
|
||||
return true;
|
||||
}
|
||||
if let Some(p) = latest
|
||||
&& let Some(x) = app.session.active().map(|st| CanvasLayout::new(&st.doc, st.editing_parents).to_spread(g.spread, xf.to_canvas(p)).x)
|
||||
&& x.is_finite()
|
||||
{
|
||||
let want = snap_gutter(app, &g, x - g.grab, shift, xf.zoom);
|
||||
if g.last != Some(want) && (g.last.is_some() || (p - origin.unwrap_or(p)).length() > 0.0) {
|
||||
let params = json!({"spread": g.spread, "page": g.page, "gutter": g.gutter, "position": want});
|
||||
if let Err(e) = app.session.preview("layout.moveColumnGuide", params) {
|
||||
app.status(e.to_string());
|
||||
}
|
||||
g.last = Some(want);
|
||||
}
|
||||
}
|
||||
if released {
|
||||
ui.data_mut(|d| d.remove::<ColumnDrag>(id));
|
||||
if let Err(e) = app.session.commit_interaction() {
|
||||
app.status(e.to_string());
|
||||
}
|
||||
app.after_engine();
|
||||
return true;
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(id, g));
|
||||
true
|
||||
}
|
||||
|
||||
/// Presses in a row on the canvas: the last press's count, and where and when it was released
|
||||
/// (`at` is −∞ while the button is down).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -3498,6 +3735,138 @@ mod tests {
|
||||
assert!(!g[1].liquid);
|
||||
}
|
||||
|
||||
/// Dragging a column guide with the Selection tool moves its gutter (one undo step); with
|
||||
/// column guides locked the same drag leaves the columns alone.
|
||||
#[test]
|
||||
fn the_selection_tool_drags_a_column_guide() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
// Two columns across the 540 pt margin box: the gutter's left edge is at x = 300.
|
||||
app.run("layout.marginsAndColumns", json!({"parent": "A", "columns": 2, "gutter": 12})).unwrap();
|
||||
let mut h = harness(app);
|
||||
let page = |h: &Harness<'_, DesignApp>| h.state().session.doc().unwrap().doc.spreads[0].pages[0].clone();
|
||||
let screen = |h: &Harness<'_, DesignApp>, x: f64| {
|
||||
let app = h.state();
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let layout = CanvasLayout::new(&app.session.active().unwrap().doc, false);
|
||||
let slot = layout.slots.iter().find(|s| s.spread == SpreadRef::Doc(0)).unwrap();
|
||||
(xf.to_screen(slot.xf * Point::new(x, 400.0)), xf.zoom as f32)
|
||||
};
|
||||
let (at, zoom) = screen(&h, 300.0);
|
||||
drag(&mut h, at, at + vec2(72.0 * zoom, 0.0));
|
||||
assert_eq!(page(&h).columns.positions, None, "column guides are locked in a new document");
|
||||
h.hover_at(at);
|
||||
h.run_steps(2);
|
||||
assert_ne!(h.output().platform_output.cursor_icon, egui::CursorIcon::ResizeHorizontal);
|
||||
|
||||
h.state_mut().run("view.lockColumnGuides", json!({"on": false})).unwrap();
|
||||
h.run_steps(2);
|
||||
let (at, zoom) = screen(&h, 300.0);
|
||||
drag(&mut h, at, at + vec2(72.0 * zoom, 0.0));
|
||||
let pg = page(&h);
|
||||
let cols = pg.column_rects();
|
||||
assert!((cols[0].x1 - 372.0).abs() < 1.0 && (cols[1].x0 - cols[0].x1 - 12.0).abs() < 1e-9, "{cols:?}");
|
||||
assert!(pg.local_margins, "the dragged page's columns are its own");
|
||||
// Over the moved guide: a resize cursor.
|
||||
h.hover_at(screen(&h, 372.0).0);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::ResizeHorizontal);
|
||||
h.state_mut().run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(page(&h).columns.positions, None, "one undo step");
|
||||
|
||||
// Escape during the drag puts the gutter back.
|
||||
h.hover_at(at);
|
||||
h.step();
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
h.hover_at(at + vec2(40.0, 0.0));
|
||||
h.step();
|
||||
assert!(page(&h).columns.positions.is_some(), "the drag previews");
|
||||
h.event(egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() });
|
||||
h.step();
|
||||
h.event(egui::Event::PointerButton {
|
||||
pos: at + vec2(40.0, 0.0),
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: Default::default(),
|
||||
});
|
||||
h.run_steps(3);
|
||||
assert_eq!(page(&h).columns.positions, None);
|
||||
assert!(!page(&h).local_margins);
|
||||
|
||||
// Space pressed during the drag and the button released under it: the drag ends with the
|
||||
// release and the gutter stops following the pointer.
|
||||
let key = |pressed| egui::Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat: false, modifiers: Default::default() };
|
||||
h.hover_at(at);
|
||||
h.step();
|
||||
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
h.hover_at(at + vec2(40.0, 0.0));
|
||||
h.step();
|
||||
h.event(key(true));
|
||||
h.step();
|
||||
h.event(egui::Event::PointerButton {
|
||||
pos: at + vec2(40.0, 0.0),
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: Default::default(),
|
||||
});
|
||||
h.step();
|
||||
h.event(key(false));
|
||||
h.run_steps(2);
|
||||
let moved = page(&h).columns.positions;
|
||||
assert!(moved.is_some());
|
||||
h.hover_at(at + vec2(120.0, 0.0));
|
||||
h.run_steps(3);
|
||||
assert_eq!(page(&h).columns.positions, moved, "the gutter stays where the drag ended");
|
||||
assert!(h.state().session.active().unwrap().interaction.is_none());
|
||||
h.state_mut().run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(page(&h).columns.positions, None, "the drag was one undo step");
|
||||
}
|
||||
|
||||
/// A column guide is not picked where the screen mode hides guides, where an object on the
|
||||
/// parent being edited lies over it, or where its columns cannot move.
|
||||
#[test]
|
||||
fn column_guides_are_picked_only_where_a_drag_can_move_them() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({"pages": 1})).unwrap();
|
||||
app.run("layout.marginsAndColumns", json!({"parent": "A", "columns": 2, "gutter": 12})).unwrap();
|
||||
app.run("view.lockColumnGuides", json!({"on": false})).unwrap();
|
||||
let mut h = harness(app);
|
||||
let hit = |h: &Harness<'_, DesignApp>, r: SpreadRef, x: f64| {
|
||||
let app = h.state();
|
||||
let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap());
|
||||
let st = app.session.active().unwrap();
|
||||
let layout = CanvasLayout::new(&st.doc, st.editing_parents);
|
||||
let slot = layout.slots.iter().find(|s| s.spread == r).unwrap();
|
||||
column_guide_at(app, &xf, xf.to_screen(slot.xf * Point::new(x, 400.0)))
|
||||
};
|
||||
assert!(hit(&h, SpreadRef::Doc(0), 300.0).is_some());
|
||||
h.state_mut().ui.screen_mode = ScreenMode::Preview;
|
||||
assert!(hit(&h, SpreadRef::Doc(0), 300.0).is_none(), "Preview hides the guides");
|
||||
h.state_mut().ui.screen_mode = ScreenMode::Normal;
|
||||
|
||||
// Editing parent A: its right page (the one page 0 follows) and a frame over its gutter.
|
||||
h.state_mut().run("layout.parents.edit", json!({"on": true, "parent": "A-Parent"})).unwrap();
|
||||
h.run_steps(2);
|
||||
let parent = SpreadRef::Parent(0);
|
||||
let x = h.state().session.active().unwrap().doc.parents[0].pages[1].column_rects()[0].x1;
|
||||
assert!(hit(&h, parent, x).is_some());
|
||||
h.state_mut()
|
||||
.run("frame.create", json!({"spread": {"kind": "parent", "index": 0}, "rect": [x - 20.0, 300.0, x + 20.0, 500.0], "content": "text"}))
|
||||
.unwrap();
|
||||
h.run_steps(2);
|
||||
assert!(hit(&h, parent, x).is_none(), "the parent's frame takes the press");
|
||||
h.state_mut().run("layout.parents.edit", json!({"on": false})).unwrap();
|
||||
h.run_steps(2);
|
||||
|
||||
// Gutters wider than the margin box: the even split cannot be moved.
|
||||
h.state_mut().run("layout.marginsAndColumns", json!({"pages": [0], "columns": 2, "gutter": 1000})).unwrap();
|
||||
h.run_steps(2);
|
||||
let x = h.state().session.active().unwrap().doc.spreads[0].pages[0].column_rects()[0].x1;
|
||||
assert!(hit(&h, SpreadRef::Doc(0), x).is_none());
|
||||
}
|
||||
|
||||
fn right_click(h: &mut Harness<'_, DesignApp>, p: Pos2) {
|
||||
h.hover_at(p);
|
||||
h.step();
|
||||
|
||||
@@ -1617,6 +1617,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "تحريك نقاط الارتساء"),
|
||||
("Move Column", "نقل عمود"),
|
||||
("Move Guide", "تحريك دليل"),
|
||||
("Move Column Guide", "تحريك دليل العمود"),
|
||||
("Move Layer", "نقل طبقة"),
|
||||
("Move Pages…", "نقل صفحات…"),
|
||||
("Move to Layer", "نقل إلى طبقة"),
|
||||
|
||||
@@ -786,6 +786,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "Přesunout kotvy"),
|
||||
("Move Column", "Přesunout sloupec"),
|
||||
("Move Guide", "Přesunout vodítko"),
|
||||
("Move Column Guide", "Přesunout vodítko sloupce"),
|
||||
("Move Layer", "Přesunout vrstvu"),
|
||||
("Move Pages…", "Přesunout stránky…"),
|
||||
("Move to Layer", "Přesunout do vrstvy"),
|
||||
|
||||
@@ -832,6 +832,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "Anker verschieben"),
|
||||
("Move Column", "Spalte verschieben"),
|
||||
("Move Guide", "Hilfslinie verschieben"),
|
||||
("Move Column Guide", "Spaltenhilfslinie verschieben"),
|
||||
("Move Layer", "Ebene verschieben"),
|
||||
("Move Pages…", "Seiten verschieben …"),
|
||||
("Move to Layer", "Auf Ebene verschieben"),
|
||||
|
||||
@@ -916,6 +916,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "Sposta ancoraggi"),
|
||||
("Move Column", "Sposta colonna"),
|
||||
("Move Guide", "Sposta guida"),
|
||||
("Move Column Guide", "Sposta guida colonna"),
|
||||
("Move Layer", "Sposta livello"),
|
||||
("Move Pages…", "Sposta pagine…"),
|
||||
("Move to Layer", "Sposta su livello"),
|
||||
|
||||
@@ -1020,6 +1020,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "アンカーポイントを移動"),
|
||||
("Move Column", "列を移動"),
|
||||
("Move Guide", "ガイドを移動"),
|
||||
("Move Column Guide", "列ガイドを移動"),
|
||||
("Move Layer", "レイヤーを移動"),
|
||||
("Move Pages…", "ページを移動…"),
|
||||
("Move to Layer", "レイヤーへ移動"),
|
||||
|
||||
@@ -848,6 +848,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "საყრდენი წერტილების გადაადგილება"),
|
||||
("Move Column", "სვეტის გადატანა"),
|
||||
("Move Guide", "დამხმარე ხაზის გადაადგილება"),
|
||||
("Move Column Guide", "სვეტის დამხმარე ხაზის გადაადგილება"),
|
||||
("Move Layer", "შრის გადაადგილება"),
|
||||
("Move Pages…", "გვერდების გადატანა…"),
|
||||
("Move to Layer", "შრეზე გადატანა"),
|
||||
|
||||
@@ -799,6 +799,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "Przesuń kotwice"),
|
||||
("Move Column", "Przesuń kolumnę"),
|
||||
("Move Guide", "Przesuń linię pomocniczą"),
|
||||
("Move Column Guide", "Przesuń linię pomocniczą łamu"),
|
||||
("Move Layer", "Przesuń warstwę"),
|
||||
("Move Pages…", "Przesuń strony…"),
|
||||
("Move to Layer", "Przenieś na warstwę"),
|
||||
|
||||
@@ -1744,6 +1744,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "Mover âncoras"),
|
||||
("Move Column", "Mover coluna"),
|
||||
("Move Guide", "Mover guia"),
|
||||
("Move Column Guide", "Mover guia de coluna"),
|
||||
("Move Layer", "Mover camada"),
|
||||
("Move to Layer", "Mover para a camada"),
|
||||
("New Article", "Novo artigo"),
|
||||
|
||||
@@ -773,6 +773,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "Переместить опорные точки"),
|
||||
("Move Column", "Переместить столбец"),
|
||||
("Move Guide", "Переместить направляющую"),
|
||||
("Move Column Guide", "Переместить направляющую колонки"),
|
||||
("Move Layer", "Переместить слой"),
|
||||
("Move Pages…", "Переместить страницы…"),
|
||||
("Move to Layer", "Переместить на слой"),
|
||||
|
||||
@@ -919,6 +919,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Move Anchors", "Перемістити опорні точки"),
|
||||
("Move Column", "Перемістити стовпець"),
|
||||
("Move Guide", "Перемістити напрямну"),
|
||||
("Move Column Guide", "Перемістити напрямну колонки"),
|
||||
("Move Layer", "Перемістити шар"),
|
||||
("Move Pages…", "Перемістити сторінки…"),
|
||||
("Move to Layer", "Перемістити до шару"),
|
||||
|
||||
@@ -284,6 +284,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- Text clipboard keeps formatting (character/paragraph formats, footnotes, cross-references, index markers, tables) and falls back to plain text when the system clipboard changed; Paste without Formatting (⇧⌘V); Change Case (UPPERCASE, lowercase, Title Case, Sentence case — formatting kept); Type › Insert Special Character / White Space / Break Character submenus.
|
||||
- Anchored objects: items flow in the text inline (on the baseline, with Y offset; lines grow to fit) or above the line (left/center/right, space before/after); paste copied items into text to anchor them; Anchored Object Options and Release (back onto the page where shown); rendered, exported to PDF and to/from IDML (InDesign anchored object settings; verified in InDesign 2026); copy/paste and threading carry them; `anchored.*` commands.
|
||||
- Ruler guides: drag out of the rulers (page guides, or spread guides on the pasteboard), drag to move, drop on a ruler to delete; a dragged guide snaps along its axis to other guides, margins, columns, bleed, page and object edges and centers and the document grid (Shift: the ruler's ticks; Cmd suspends snapping); selected like objects with the Selection tools (click, Shift-click, a marquee that touches no object, Edit › Select All Guides ⌥⌘G), drawn highlighted, moved together by dragging or the arrow keys, deleted with Delete, and placed exactly with the X or Y field of the Control and Properties panels; Layout › Create Guides (rows/columns with gutters, fit to margins or page); Delete All Guides on Spread; `guide.*` commands.
|
||||
- Column guides: with the Selection tool, drag an interior column guide to move its gutter (both guides together, keeping its width, stopped by the neighbouring guides and the margins), snapping to the document grid, guides and objects, or with Shift to the ruler's tick marks; Escape cancels; one undo step. Refused while Lock Column Guides is on (the default). A document page takes the result as its own columns; on a parent page it reaches the pages that follow it. Objects stay where they are (there is no persistent Layout Adjustment setting yet). `layout.moveColumnGuide`.
|
||||
- Links panel: every placed graphic with status (OK / Modified / Missing / Embedded, checked against the file on disk), page and effective PPI (low resolution flagged); Relink…, Go To, Update, Embed; `links.*` commands.
|
||||
- Linked text files: `file.place {link: true}` or Preferences › File Handling › Create Links When Placing Text Files keeps a placed .docx/.rtf/.txt story linked to its file (path, size and hash, and the style map and conflict option used). The Links panel shows it OK / Modified / Missing; Update imports the file again into the same frames and thread with the saved style map, replacing edits made in the layout (the Links panel and menu ask first; the `links.update` command updates and returns a warning; undoable); Relink, Go To and Unlink. Saved in the native format; IDML exports the text unlinked.
|
||||
- Missing fonts: highlighted pink on screen (never in output), listed by Preflight; Type › Find/Replace Font (fonts in the document, missing first; replace in text, cells, footnotes and styles); `font.list` / `font.replace`. Type › Reload Fonts (`font.reload`) scans the font folders again and reads again the installed font files edited or removed since they were read, then recomposes the open documents: a font installed or edited while DesignCraft runs needs no restart (#328, #547). InDesign notices such changes by itself.
|
||||
|
||||
Reference in New Issue
Block a user