S: style groups (style.group), renames reach table cells and footnotes; Package report preflight fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fed23aa7ee
commit
08727dfd7e
|
|
@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Style groups: paragraph and character styles shown in group folders, Move to Group (new, existing, none); `style.group` renames every use (styles, stories, table cells, footnotes, object styles).
|
||||
- Scrubby zoom: drag the Zoom tool left or right to zoom continuously around the press point.
|
||||
- Guides belong to the active layer: hidden with it (or with its Show Guides off) and locked with it.
|
||||
- Pencil tool (N): freehand strokes simplified to smooth paths (Alt closes); Smooth and Erase are still to come.
|
||||
|
|
|
|||
|
|
@ -713,6 +713,19 @@ impl crate::Document {
|
|||
}
|
||||
|
||||
impl Story {
|
||||
/// Visit this story and every text inside it (table cells, footnotes), mutably.
|
||||
pub fn for_each_text_mut(&mut self, f: &mut impl FnMut(&mut Story)) {
|
||||
f(self);
|
||||
for t in self.tables.values_mut() {
|
||||
for c in &mut std::sync::Arc::make_mut(t).cells {
|
||||
c.text.for_each_text_mut(f);
|
||||
}
|
||||
}
|
||||
for n in &mut self.notes {
|
||||
std::sync::Arc::make_mut(n).text.for_each_text_mut(f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The table whose anchor paragraph is `pi`.
|
||||
pub fn para_table(&self, pi: usize) -> Option<&Arc<Table>> {
|
||||
self.tables.get(&self.paras.get(pi)?.table?)
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
for l in links.as_array().into_iter().flatten() {
|
||||
r += &format!(" {} ({})\n", l["name"].as_str().unwrap_or(""), l["status"].as_str().unwrap_or(""));
|
||||
}
|
||||
if let Some(issues) = pre.as_array() {
|
||||
if let Some(issues) = pre.get("issues").and_then(Value::as_array) {
|
||||
r += &format!("\nPreflight: {} issue(s)\n", issues.len());
|
||||
for i in issues {
|
||||
r += &format!(" {}\n", i.get("message").and_then(Value::as_str).unwrap_or(""));
|
||||
|
|
@ -159,6 +159,7 @@ mod tests {
|
|||
assert!(out.join("Links").join("logo.svg").exists());
|
||||
let report = std::fs::read_to_string(out.join("Instructions.txt")).unwrap();
|
||||
assert!(report.contains("Print on matte.") && report.contains("logo.svg"), "{report}");
|
||||
assert!(report.contains("Preflight:"), "{report}");
|
||||
// The packaged document links to its Links folder.
|
||||
let doc_file = files.iter().find(|f| f.ends_with(".designcraft")).unwrap();
|
||||
let packed = super::super::from_bytes(&std::fs::read(doc_file).unwrap()).unwrap();
|
||||
|
|
|
|||
|
|
@ -60,6 +60,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
super::has_selection,
|
||||
apply_gradient
|
||||
),
|
||||
cmd!(
|
||||
"style.group",
|
||||
"Move to Group",
|
||||
[],
|
||||
None,
|
||||
"{kind: paragraph|character, names: [style names], group: \"Heads\" | \"Heads/Display\" | \"\" (out of any group)} — styles are named Group/Name; every use is renamed → {renamed: {old: new}}",
|
||||
has_doc,
|
||||
group_styles
|
||||
),
|
||||
cmd!(
|
||||
"swatch.load",
|
||||
"Load Swatches…",
|
||||
|
|
@ -238,18 +247,90 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
_ => {}
|
||||
}
|
||||
if let Some(n) = rename.clone() {
|
||||
st.name = n.clone();
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
if let Some(story) = d.story_mut(sid) {
|
||||
for f in &mut story.paras {
|
||||
if f.style == name {
|
||||
f.style = n.clone();
|
||||
}
|
||||
}
|
||||
// Every use: other styles, stories, table cells, footnotes, object styles.
|
||||
rename_style(d, true, &name, &n);
|
||||
}
|
||||
ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// Rename a paragraph or character style everywhere it's used.
|
||||
fn rename_style(d: &mut designcraft_doc::Document, para: bool, from: &str, to: &str) {
|
||||
let st = d.styles_mut();
|
||||
if para {
|
||||
for s in &mut st.paragraph {
|
||||
if s.name == from {
|
||||
s.name = to.to_string();
|
||||
}
|
||||
for r in [&mut s.based_on, &mut s.next_style] {
|
||||
if r.as_deref() == Some(from) {
|
||||
*r = Some(to.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
ok()
|
||||
for o in &mut st.object {
|
||||
if o.paragraph_style.as_deref() == Some(from) {
|
||||
o.paragraph_style = Some(to.to_string());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for s in &mut st.character {
|
||||
if s.name == from {
|
||||
s.name = to.to_string();
|
||||
}
|
||||
if s.based_on.as_deref() == Some(from) {
|
||||
s.based_on = Some(to.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
for sid in d.stories.keys().copied().collect::<Vec<_>>() {
|
||||
let Some(story) = d.story_mut(sid) else { continue };
|
||||
story.for_each_text_mut(&mut |st| {
|
||||
if para {
|
||||
for f in st.paras.iter_mut().filter(|f| f.style == from) {
|
||||
f.style = to.to_string();
|
||||
}
|
||||
} else {
|
||||
for r in st.chars.iter_mut().filter(|r| r.format.style == from) {
|
||||
r.format.style = to.to_string();
|
||||
}
|
||||
}
|
||||
st.rev += 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn group_styles(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let para = match str_param(p, "kind").unwrap_or("paragraph") {
|
||||
"paragraph" => true,
|
||||
"character" => false,
|
||||
k => return Err(bad("style.group", format!("unknown kind `{k}`"))),
|
||||
};
|
||||
let names: Vec<String> =
|
||||
p.get("names").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
|
||||
let group = str_param(p, "group").unwrap_or("").trim_matches('/').to_string();
|
||||
s.edit(|d, _| {
|
||||
let mut renamed = serde_json::Map::new();
|
||||
for n in &names {
|
||||
if n.starts_with('[') {
|
||||
return Err(bad("style.group", format!("`{n}` is built in")));
|
||||
}
|
||||
let exists = if para { d.styles.para(n).is_some() } else { d.styles.character.iter().any(|c| c.name == *n) };
|
||||
if !exists {
|
||||
return Err(bad("style.group", format!("no style `{n}`")));
|
||||
}
|
||||
let base = n.rsplit('/').next().unwrap_or(n);
|
||||
let mut to = if group.is_empty() { base.to_string() } else { format!("{group}/{base}") };
|
||||
let taken = |d: &designcraft_doc::Document, x: &str| {
|
||||
if para { d.styles.para(x).is_some() } else { d.styles.character.iter().any(|c| c.name == x) }
|
||||
};
|
||||
if to != *n {
|
||||
to = Styles::unique_name(|x| taken(d, x), &to);
|
||||
rename_style(d, para, n, &to);
|
||||
renamed.insert(n.clone(), json!(to));
|
||||
}
|
||||
}
|
||||
Ok(json!({"renamed": renamed}))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -643,6 +724,27 @@ fn rename_in(it: &mut designcraft_doc::Item, from: &str, to: &str) {
|
|||
mod color_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn style_groups_rename_every_use() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Head"})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Sub", "basedOn": "Head"})).unwrap();
|
||||
let r = s.execute("frame.create", &json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Hi"})).unwrap();
|
||||
let sid = r["story"].as_u64().unwrap();
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": 0, "focus": 2})).unwrap();
|
||||
s.execute("style.paragraph.apply", &json!({"name": "Head"})).unwrap();
|
||||
let r = s.execute("style.group", &json!({"kind": "paragraph", "names": ["Head", "Sub"], "group": "Titles"})).unwrap();
|
||||
assert_eq!(r["renamed"]["Head"], "Titles/Head");
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
assert_eq!(d.story(designcraft_doc::StoryId(sid)).unwrap().paras[0].style, "Titles/Head");
|
||||
assert_eq!(d.styles.para("Titles/Sub").unwrap().based_on.as_deref(), Some("Titles/Head"));
|
||||
// Out of the group again.
|
||||
s.execute("style.group", &json!({"kind": "paragraph", "names": ["Titles/Head"], "group": ""})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.styles.para("Head").is_some());
|
||||
assert!(s.execute("style.group", &json!({"names": ["[Basic Paragraph]"], "group": "X"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swatches_save_and_load_ase() {
|
||||
let mut s = Session::new();
|
||||
|
|
@ -686,3 +788,21 @@ mod color_tests {
|
|||
assert_eq!(s.doc().unwrap().doc.item(designcraft_doc::ItemId(id)).unwrap().fill.swatch, "Sand");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod group_sample_tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// Grouping the sample's table styles keeps its table cells styled (no missing styles).
|
||||
#[test]
|
||||
fn grouping_reaches_table_cells() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.newSample", &json!({})).unwrap();
|
||||
let before = s.execute("preflight.run", &json!({})).unwrap()["errors"].as_u64().unwrap();
|
||||
s.execute("style.group", &json!({"kind": "paragraph", "names": ["Table Head", "Table Body"], "group": "Tables"})).unwrap();
|
||||
let after = s.execute("preflight.run", &json!({})).unwrap();
|
||||
assert_eq!(after["errors"].as_u64().unwrap(), before, "{after}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,33 +25,23 @@ fn list(app: &mut DesignApp, ui: &mut egui::Ui, para: bool) {
|
|||
.size(11.0)
|
||||
.color(t.text_dim),
|
||||
);
|
||||
for n in &names {
|
||||
// Groups (`Group/Name`) as folders, ungrouped styles first.
|
||||
let groups: Vec<String> = {
|
||||
let mut g: Vec<String> = names.iter().filter_map(|n| n.rsplit_once('/').map(|(g, _)| g.to_string())).collect();
|
||||
g.sort();
|
||||
g.dedup();
|
||||
g
|
||||
};
|
||||
for n in names.iter().filter(|n| !n.contains('/')) {
|
||||
if para && n == designcraft_doc::NO_PARA_STYLE {
|
||||
continue;
|
||||
}
|
||||
let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click());
|
||||
if current.as_deref() == Some(n.as_str()) {
|
||||
ui.painter().rect_filled(row, 0.0, t.row_selected);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(row, 0.0, t.hover);
|
||||
}
|
||||
ui.painter().text(row.min + vec2(8.0, 11.0), egui::Align2::LEFT_CENTER, n, egui::FontId::proportional(12.5), t.text);
|
||||
if resp.double_clicked() && para {
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("paragraphStyleOptions", json!({"name": n})));
|
||||
} else if resp.clicked() {
|
||||
let cmd = if para { "style.paragraph.apply" } else { "style.character.apply" };
|
||||
let clear = ui.input(|i| i.modifiers.alt);
|
||||
let _ = app.run(cmd, json!({"name": n, "clearOverrides": clear}));
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button(format!("Apply \"{n}\"")).clicked() {
|
||||
let cmd = if para { "style.paragraph.apply" } else { "style.character.apply" };
|
||||
let _ = app.run(cmd, json!({"name": n}));
|
||||
ui.close();
|
||||
}
|
||||
if current.as_deref() == Some(n.as_str()) && ui.button("Break Link to Style").clicked() {
|
||||
let _ = app.run("style.breakLink", json!({"kind": if para { "paragraph" } else { "character" }}));
|
||||
ui.close();
|
||||
style_row(app, ui, para, n, current.as_deref(), &groups, &t);
|
||||
}
|
||||
for g in &groups {
|
||||
egui::CollapsingHeader::new(g.as_str()).id_salt(("style_group", para, g)).default_open(true).show(ui, |ui| {
|
||||
for n in names.iter().filter(|n| n.rsplit_once('/').is_some_and(|(x, _)| x == g)) {
|
||||
style_row(app, ui, para, n, current.as_deref(), &groups, &t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -75,3 +65,59 @@ pub fn paragraph(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
pub fn character(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
list(app, ui, false);
|
||||
}
|
||||
|
||||
/// One style in the list: click applies (⌥ clears overrides), double-click edits (paragraph),
|
||||
/// right-click: apply, break link, move to group.
|
||||
fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, current: Option<&str>, groups: &[String], t: &Tokens) {
|
||||
let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click());
|
||||
if current == Some(n) {
|
||||
ui.painter().rect_filled(row, 0.0, t.row_selected);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(row, 0.0, t.hover);
|
||||
}
|
||||
let shown = n.rsplit('/').next().unwrap_or(n);
|
||||
ui.painter().text(row.min + vec2(8.0, 11.0), egui::Align2::LEFT_CENTER, shown, egui::FontId::proportional(12.5), t.text);
|
||||
if resp.double_clicked() && para {
|
||||
app.ui.dialog = Some(crate::dialogs::Dialog::new("paragraphStyleOptions", json!({"name": n})));
|
||||
} else if resp.clicked() {
|
||||
let cmd = if para { "style.paragraph.apply" } else { "style.character.apply" };
|
||||
let clear = ui.input(|i| i.modifiers.alt);
|
||||
let _ = app.run(cmd, json!({"name": n, "clearOverrides": clear}));
|
||||
}
|
||||
let kind = if para { "paragraph" } else { "character" };
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button(format!("Apply \"{shown}\"")).clicked() {
|
||||
let cmd = if para { "style.paragraph.apply" } else { "style.character.apply" };
|
||||
let _ = app.run(cmd, json!({"name": n}));
|
||||
ui.close();
|
||||
}
|
||||
if current == Some(n) && ui.button("Break Link to Style").clicked() {
|
||||
let _ = app.run("style.breakLink", json!({"kind": kind}));
|
||||
ui.close();
|
||||
}
|
||||
if !n.starts_with('[') {
|
||||
ui.menu_button("Move to Group", |ui| {
|
||||
if n.contains('/') && ui.button("[No Group]").clicked() {
|
||||
let _ = app.run("style.group", json!({"kind": kind, "names": [n], "group": ""}));
|
||||
ui.close();
|
||||
}
|
||||
for g in groups {
|
||||
if n.rsplit_once('/').map(|(x, _)| x) != Some(g.as_str()) && ui.button(g).clicked() {
|
||||
let _ = app.run("style.group", json!({"kind": kind, "names": [n], "group": g}));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
let id = egui::Id::new(("new_style_group", para));
|
||||
let mut name: String = ui.data(|d| d.get_temp(id)).unwrap_or_default();
|
||||
let r = ui.add(egui::TextEdit::singleline(&mut name).hint_text("New group…").desired_width(140.0));
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !name.trim().is_empty() {
|
||||
let _ = app.run("style.group", json!({"kind": kind, "names": [n], "group": name.trim()}));
|
||||
name.clear();
|
||||
ui.close();
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(id, name));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ The summary is generated by `cargo xtask parity` from the table at the bottom of
|
|||
Each row of the InDesign feature catalogue is scored **D** (done), **P** (partial: works but lacks options or UI) or **M** (missing), weighted by priority (**P0** core = 3, **P1** = 2, **P2** = 1). Out-of-scope rows (Adobe cloud services, plug-ins, Typekit, Flash) are excluded.
|
||||
|
||||
<!-- SUMMARY -->
|
||||
**Weighted breadth parity: 77%** over 275 features.
|
||||
**Weighted breadth parity: 78%** over 275 features.
|
||||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 54 | 24 | 19 | 68% |
|
||||
| P1 | 97 | 55 | 24 | 18 | 69% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
|
|
@ -25,7 +25,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Placing & links | 78% |
|
||||
| Type & text frames | 82% |
|
||||
| Typography | 83% |
|
||||
| Styles | 71% |
|
||||
| Styles | 82% |
|
||||
| Tables | 73% |
|
||||
| Long documents | 58% |
|
||||
| Interactivity & digital | 30% |
|
||||
|
|
@ -240,7 +240,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Styles | Character styles | P0 | D |
|
||||
| Styles | Object styles | P1 | D |
|
||||
| Styles | Table & cell styles | P1 | M |
|
||||
| Styles | Style groups | P1 | M |
|
||||
| Styles | Style groups | P1 | D |
|
||||
| Styles | Quick Apply | P1 | D |
|
||||
| Styles | Break link to style | P1 | D |
|
||||
| Styles | Style mapping on import | P1 | P |
|
||||
|
|
|
|||
Loading…
Reference in New Issue