diff --git a/ROADMAP.md b/ROADMAP.md index 0b08d8c95..49ed8c987 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -353,6 +353,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m in the style row menu; named character styles and equal local values are not false overrides. - **2026-10-15** · Existing paragraph and character styles preview their attribute edits in Style Options; Cancel restores the document and OK records one edit. +- **2026-10-15** · Paragraph Style Options summarizes captured and pending color, size and + paragraph settings relative to the Based On style. - **2026-10-13** · Switching between document and parent pages refreshes the canvas immediately, preventing black parent pages and stale document objects when the zoom stays the same. Ordinary edits retain partial repainting. No readiness percentage change. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index f55c33033..7d9e135e2 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -4112,17 +4112,11 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo ui.end_row(); }); ui.add_space(8.0); - crate::rtl::label( - ui, - egui::RichText::new(format!( - "{} {} {:.1} pt · {}", - cp.font_family, - cp.font_style, - cp.size, - crate::i18n::tr(&app.ui.language, pp.align.label()) - )) - .color(crate::theme::Tokens::get(ui.ctx()).text_dim), - ); + crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Style Settings:")).font(semibold(12.0))); + let summary = paragraph_style_summary(&app.ui.language, d, &style, &doc.styles); + egui::ScrollArea::vertical().id_salt("paragraph_style_settings").max_height(220.0).show(ui, |ui| { + ui.add(egui::Label::new(egui::RichText::new(summary).color(crate::theme::Tokens::get(ui.ctx()).text_dim)).wrap()); + }); } }); }); @@ -4133,6 +4127,51 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo } } +/// Show what the style changes from its parent, including edits waiting for OK. Fully captured +/// styles contain every default, so comparing resolved values keeps the summary readable. +fn paragraph_style_summary(lang: &str, d: &Dialog, style: &designcraft_doc::ParagraphStyle, styles: &designcraft_doc::Styles) -> String { + let based = d.fields.get("basedOn").and_then(Value::as_str).or(style.based_on.as_deref()).unwrap_or(designcraft_doc::NO_PARA_STYLE); + let (base_para, base_chars) = styles.resolve_para_style(based); + let (mut para, mut chars) = (base_para.clone(), base_chars.clone()); + para.apply(&style.para); + chars.apply(&style.chars); + for (key, value) in &d.fields { + if let Some(key) = key.strip_prefix("c.") { + let mut delta = designcraft_doc::CharAttrs::default(); + if delta.set_json(key, value).is_ok() { + chars.apply(&delta); + } + } else if let Some(key) = key.strip_prefix("p.") { + let (key, value) = key.split_once('.').map_or((key, value.clone()), |(attr, field)| (attr, json!({field:value}))); + let mut delta = designcraft_doc::ParaAttrs::default(); + if delta.set_json_over(key, &value, ¶).is_ok() { + para.apply(&delta); + } + } + } + let mut char_delta = base_chars.delta(&chars); + if base_chars.font_family != chars.font_family { + char_delta.font_style = Some(chars.font_style.clone()); + } + let mut parts = vec![crate::i18n::style_name(lang, based).to_string()]; + for value in [serde_json::to_value(base_para.delta(¶)), serde_json::to_value(char_delta)] { + if let Ok(Value::Object(attrs)) = value { + for (key, value) in attrs { + let suffix = if matches!( + key.as_str(), + "size" | "spaceBefore" | "spaceAfter" | "leftIndent" | "rightIndent" | "firstLineIndent" | "lastLineIndent" + ) { + " pt" + } else { + "" + }; + parts.push(format!("{}: {}{suffix}", setting_label(lang, &key), setting_text(&value))); + } + } + } + parts.join(" + ") +} + fn delete_paragraph_style(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { let name = d.s("name"); ui.set_min_width(380.0); @@ -6706,6 +6745,53 @@ fn find_font(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { mod tests { use super::*; + #[test] + fn paragraph_style_settings_show_captured_color_and_pending_spacing_without_changing_the_document() { + 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,300,200],"content":"text","text":"Heading"})).unwrap(); + app.run("text.select", json!({"story":f["story"],"anchor":0,"focus":7})).unwrap(); + app.run("type.char", json!({"fontFamily":"Source Sans 3","fontStyle":"Bold","size":16,"fill":"[Paper]"})).unwrap(); + app.run("style.paragraph.create", json!({"name":"Header","fromSelection":true})).unwrap(); + let doc = app.session.doc().unwrap().doc.clone(); + let style = doc.styles.para("Header").unwrap(); + let mut d = Dialog::new("paragraphStyleOptions", json!({"name":"Header"})); + let summary = paragraph_style_summary("", &d, style, &doc.styles); + for value in ["Source Sans 3", "Bold", "16 pt", "[Paper]"] { + assert!(summary.contains(value), "{summary}"); + } + assert!(summary.len() < 250, "default attributes do not swamp the captured settings: {summary}"); + d.fields.insert("c.fill".into(), json!("[Registration]")); + d.fields.insert("p.spaceAfter".into(), json!(4.5)); + let summary = paragraph_style_summary("", &d, style, &doc.styles); + assert!(summary.contains("[Registration]") && !summary.contains("[Paper]"), "{summary}"); + assert!(summary.contains("4.5 pt"), "{summary}"); + assert_eq!(app.session.doc().unwrap().doc, doc); + } + + #[test] + fn paragraph_style_settings_compare_with_the_chosen_parent() { + let mut styles = designcraft_doc::Styles::default(); + styles.paragraph.push(designcraft_doc::ParagraphStyle { + name: "Parent".into(), + chars: designcraft_doc::CharAttrs { size: Some(20.0), ..Default::default() }, + based_on: None, + next_style: None, + para: Default::default(), + shortcut: String::new(), + }); + let style = designcraft_doc::ParagraphStyle { + name: "Child".into(), + based_on: Some("Parent".into()), + next_style: None, + para: Default::default(), + chars: Default::default(), + shortcut: String::new(), + }; + let d = Dialog::new("paragraphStyleOptions", json!({"name":"Child"})); + assert_eq!(paragraph_style_summary("", &d, &style, &styles), "Parent"); + } + #[test] fn step_and_repeat_dialog_previews_and_makes_the_copies_as_one_undo_step() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 42df992d0..55d38e9f3 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -104,6 +104,11 @@ cancelled. OK applies the final edit once. The color-preview step at about 4:00 offscreen. New-style creation, renaming and export tagging are committed on OK; they are not included in the visual preview. +Paragraph Style Options' General pane shows a Style Settings summary of differences from its +parent, including captured color, size and spacing. Pending edits appear before OK without +changing the document. Fully captured default attributes do not overwhelm the summary. This +covers checking the sampled and redefined settings in the styles tutorial (2:45 and 5:15). + | Work | Opus 5.5 h | |---|---| | Copy/paste, context menus, threading clicks, caret formatting on every platform, with control-channel regression tests | 30–50 |