mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 15:49:42 +00:00
Paragraph style edits: Reset To Base with a new Based On resolves over it, is checked against it and previewed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
151f54ee46
commit
f972cebb96
4 files changed
+72
-9
No files matched your search
+1
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
DesignCraft aims at full Adobe InDesign parity — and to be better: faster, open (documented JSON format + IDML), scriptable by agents (MCP), and available on the web.
|
||||
|
||||
## Status (2026-10-01)
|
||||
## Status (2026-10-21)
|
||||
|
||||
**Working today**
|
||||
- Document model: facing/non-facing spreads, parent pages with page-number markers, sections, layers, frames (text / graphic / unassigned), threaded stories, paragraph/character/object styles with based-on, swatches (process/spot/tints/gradients), corner options, text wrap, drop shadow, opacity/blend.
|
||||
|
||||
@@ -955,6 +955,15 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
st.para = ParaAttrs::default();
|
||||
st.chars = CharAttrs::default();
|
||||
}
|
||||
// A new Based On first, so the fields below resolve over the new parent.
|
||||
if based.is_some() {
|
||||
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
|
||||
match &based {
|
||||
Some(Value::String(b)) => st.based_on = Some(b.clone()),
|
||||
Some(Value::Null) => st.based_on = None,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// A rule object changes only the rule fields it names, over the style's resolved rule.
|
||||
let (current, _) = d.styles.resolve_para_style(&name);
|
||||
let mut para: ParaAttrs = attrs("style.paragraph.edit", "para", p, |a: &mut ParaAttrs, k, v| a.set_json_over(k, v, ¤t))?;
|
||||
@@ -964,11 +973,6 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
|
||||
st.para.merge(¶);
|
||||
st.chars.merge(&chars);
|
||||
match based {
|
||||
Some(Value::String(b)) => st.based_on = Some(b),
|
||||
Some(Value::Null) => st.based_on = None,
|
||||
_ => {}
|
||||
}
|
||||
match next {
|
||||
Some(Value::String(n)) => st.next_style = Some(n),
|
||||
Some(Value::Null) => st.next_style = None,
|
||||
|
||||
@@ -1650,3 +1650,18 @@ fn a_line_fits_when_its_baseline_does() {
|
||||
assert!(!overset(&mut s, 78.5), "the line is set; its descenders hang below the frame");
|
||||
assert!(overset(&mut s, 50.0), "a baseline below the frame still oversets the line");
|
||||
}
|
||||
|
||||
/// Reset To Base with a new Based On: a partial rule edit resolves over the new parent, not the
|
||||
/// old one.
|
||||
#[test]
|
||||
fn reset_to_base_with_a_new_parent_resolves_rule_edits_over_it() {
|
||||
let mut s = session();
|
||||
let rule = |weight: f64, offset: f64| json!({"on": true, "weight": weight, "color": "[Black]", "offset": offset});
|
||||
s.execute("style.paragraph.create", &json!({"name": "Old", "para": {"ruleBelow": rule(3.0, 1.0)}})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "New", "para": {"ruleBelow": rule(1.0, 9.0)}})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Child", "basedOn": "Old"})).unwrap();
|
||||
s.execute("style.paragraph.edit", &json!({"name": "Child", "resetToBase": true, "basedOn": "New", "para": {"ruleBelow": {"weight": 2}}}))
|
||||
.unwrap();
|
||||
let r = s.doc().unwrap().doc.styles.resolve_para_style("Child").0.rule_below;
|
||||
assert_eq!((r.weight, r.offset), (2.0, 9.0), "the offset is the new parent's");
|
||||
}
|
||||
@@ -3646,9 +3646,25 @@ fn grep_error(p: &str) -> Option<String> {
|
||||
/// The Justification spacing check of Paragraph Style Options, against what the section showed:
|
||||
/// the style's own values, or a new style's Based On.
|
||||
fn check_paragraph_style_justification(app: &DesignApp, d: &Dialog) -> Result<(), String> {
|
||||
let shown = if d.b("new") { d.s("basedOn") } else { d.s("name") };
|
||||
let base =
|
||||
app.session.active().map(|st| serde_json::to_value(st.doc.styles.resolve_para_style(&shown).0).unwrap_or_default()).unwrap_or_default();
|
||||
// After Reset To Base the style starts from its Based On (the one chosen here, if any).
|
||||
let base_of = |st: &designcraft_engine::DocState| {
|
||||
let chosen = d.s("basedOn");
|
||||
if chosen.is_empty() { st.doc.styles.para(&d.s("name")).and_then(|s| s.based_on.clone()).unwrap_or_default() } else { chosen }
|
||||
};
|
||||
let base = app
|
||||
.session
|
||||
.active()
|
||||
.map(|st| {
|
||||
let shown = if d.b("new") {
|
||||
d.s("basedOn")
|
||||
} else if d.b("resetToBase") {
|
||||
base_of(st)
|
||||
} else {
|
||||
d.s("name")
|
||||
};
|
||||
serde_json::to_value(st.doc.styles.resolve_para_style(&shown).0).unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
check_justification(d, "p.", &base)
|
||||
}
|
||||
|
||||
@@ -3777,6 +3793,10 @@ fn style_options_preview(app: &mut DesignApp, d: &mut Dialog) {
|
||||
_ => based.clone(),
|
||||
};
|
||||
}
|
||||
// Reset To Base shows on the page too.
|
||||
if paragraph && d.b("resetToBase") {
|
||||
params["resetToBase"] = json!(true);
|
||||
}
|
||||
if shown.as_ref() == Some(¶ms) {
|
||||
return;
|
||||
}
|
||||
@@ -7614,6 +7634,30 @@ mod tests {
|
||||
assert_eq!(s.based_on.as_deref(), Some(designcraft_doc::BASIC_PARAGRAPH));
|
||||
}
|
||||
|
||||
/// With Reset To Base, the justification fields are checked against the base style (the
|
||||
/// style's own minimum no longer applies), and Preview shows the reset on the page.
|
||||
#[test]
|
||||
fn reset_to_base_checks_and_previews_against_the_base() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let f = app.run("frame.create", json!({"rect": [36, 36, 400, 300], "content": "text", "text": "Loose words"})).unwrap();
|
||||
app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 5})).unwrap();
|
||||
app.run("style.paragraph.create", json!({"name": "Loose", "basedOn": designcraft_doc::BASIC_PARAGRAPH, "para": {"wordSpaceMin": 1.2, "wordSpaceDesired": 1.3}, "chars": {"size": 30}}))
|
||||
.unwrap();
|
||||
app.run("style.paragraph.apply", json!({"name": "Loose"})).unwrap();
|
||||
let mut d = Dialog::new("paragraphStyleOptions", json!({"name": "Loose"}));
|
||||
d.fields.insert("p.wordSpaceDesired".into(), json!(1.1));
|
||||
assert!(check_paragraph_style_justification(&app, &d).is_err(), "below the style's own minimum");
|
||||
d.fields.insert("resetToBase".into(), json!(true));
|
||||
assert_eq!(check_paragraph_style_justification(&app, &d), Ok(()), "the base's minimum is 100%");
|
||||
d.fields.insert("preview".into(), json!(true));
|
||||
style_options_preview(&mut app, &mut d);
|
||||
assert_eq!(d.fields[PREVIEWED]["resetToBase"], true);
|
||||
let size = app.run("type.selectionAttrs", json!({})).unwrap()["chars"]["size"].clone();
|
||||
assert_eq!(size, json!(12.0), "the page shows the style reset to its base");
|
||||
app.session.cancel_interaction();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_style_options_edit_one_rule_field() {
|
||||
let (mut app, rule) = ruled_paragraph();
|
||||
|
||||
Reference in new issue
Block a user