From 70df1191495108c675821e73825461abd41fd01f Mon Sep 17 00:00:00 2001 From: moppeeer <339764117+moppeeer@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:41:49 +0200 Subject: [PATCH] TUT3.2: show captured and pending paragraph style settings --- ROADMAP.md | 3 + crates/ui-egui/src/dialogs.rs | 108 ++++++++++++++++++++++++++++++---- docs/ui-parity.md | 5 ++ 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d60a6bf49..c48816d49 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -343,6 +343,9 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m ## Progress log +- **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. Newest first. diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7d02776a2..03e0659c4 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3271,22 +3271,61 @@ 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()); + }); } }); }); } +/// 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); @@ -4809,6 +4848,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 30512bdf9..94fea0ae2 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -61,6 +61,11 @@ context menus, typing at a caret) are things a layout artist does every minute, ## Remaining effort +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 |