TUT4.1: show and clear text style overrides in Styles panels

This commit is contained in:
moppeeer
2026-10-15 19:00:08 +02:00
parent a930276fd7
commit 7313bdfc93
4 changed files with 105 additions and 15 deletions
+3
View File
@@ -343,6 +343,9 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
## Progress log
- **2026-10-15** · Paragraph Styles marks local character overrides and offers Clear Overrides
in the style row menu; named character styles and equal local values are not false overrides.
- **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.
+10 -1
View File
@@ -1279,9 +1279,18 @@ fn selection_attrs(s: &mut Session, _p: &Value) -> Result<Value> {
let (pp, base) = st.doc.styles.resolve_para(pf);
let cf = effective_char_format(s, &Target { story: sid, cell, range: r.clone() })?;
let cp = st.doc.styles.resolve_char(&base, &cf);
let (style_para, style_chars) = st.doc.styles.resolve_para_style(&pf.style);
let mut character_base = base.clone();
let character_style = st.doc.styles.char_style_attrs(&cf.style);
character_base.apply(&character_style);
let char_delta = character_base.delta(&cp);
let style_keys = character_style.keys();
let character_style_overrides = char_delta.keys().iter().filter(|key| style_keys.contains(key)).count();
let selected = selected_text_preview(story.text.get(r.start..r.end).unwrap_or_default().chars());
Ok(json!({"story": sid.0, "paragraphStyle": pf.style, "characterStyle": cf.style, "para": pp, "chars": cp,
"paraOverrides": pf.para.count(), "charOverrides": cf.over.count(), "selectedText": selected.trim()}))
"paraOverrides": style_para.delta(&pp).count() + style_chars.delta(&base).count(),
"charOverrides": char_delta.count(), "characterStyleOverrides": character_style_overrides,
"selectedText": selected.trim()}))
}
/// The first target character, with any pending caret overrides. Keep character-style
+83 -13
View File
@@ -16,7 +16,16 @@ fn list(app: &mut DesignApp, ui: &mut egui::Ui, para: bool) {
};
let attrs = super::text_attrs(app);
let current = attrs.as_ref().and_then(|a| a[if para { "paragraphStyle" } else { "characterStyle" }].as_str().map(str::to_string));
let overrides = attrs.as_ref().map(|a| a[if para { "paraOverrides" } else { "charOverrides" }].as_u64().unwrap_or(0)).unwrap_or(0);
let overrides = attrs
.as_ref()
.map(|a| {
if para {
a["charOverrides"].as_u64().unwrap_or(0) + a["paraOverrides"].as_u64().unwrap_or(0)
} else {
a["characterStyleOverrides"].as_u64().unwrap_or(0)
}
})
.unwrap_or(0);
crate::rtl::label(
ui,
egui::RichText::new(match &current {
@@ -37,12 +46,12 @@ fn list(app: &mut DesignApp, ui: &mut egui::Ui, para: bool) {
if para && n == designcraft_doc::NO_PARA_STYLE {
continue;
}
style_row(app, ui, para, n, current.as_deref(), &groups, &t);
style_row(app, ui, para, n, current.as_deref(), overrides > 0, &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);
style_row(app, ui, para, n, current.as_deref(), overrides > 0, &groups, &t);
}
});
}
@@ -50,7 +59,12 @@ fn list(app: &mut DesignApp, ui: &mut egui::Ui, para: bool) {
ui.horizontal(|ui| {
ui.label(crate::rtl::widget(
ui,
egui::RichText::new(crate::i18n::tr(&app.ui.language, "⌥-click clears overrides")).size(10.5).color(t.text_disabled),
egui::RichText::new(crate::i18n::tr(
&app.ui.language,
if cfg!(target_os = "macos") { "⌥-click clears overrides" } else { "Alt-click clears overrides" },
))
.size(10.5)
.color(t.text_disabled),
));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if icons::button(
@@ -80,7 +94,7 @@ pub fn character(app: &mut DesignApp, ui: &mut egui::Ui) {
/// One style in the list: click applies (⌥ clears overrides), double-click edits,
/// right-click: edit, 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) {
fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, current: Option<&str>, overridden: bool, 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);
@@ -88,14 +102,10 @@ fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, curren
ui.painter().rect_filled(row, 0.0, t.hover);
}
let shown = n.rsplit('/').next().unwrap_or(n);
crate::rtl::paint(
ui.painter(),
row.min + vec2(8.0, 11.0),
egui::Align2::LEFT_CENTER,
if n.contains('/') { shown } else { crate::i18n::style_name(&app.ui.language, shown) },
egui::FontId::proportional(12.5),
t.text,
);
let label = if n.contains('/') { shown } else { crate::i18n::style_name(&app.ui.language, shown) };
let row_label = format!("{label}{}", if current == Some(n) && overridden { "+" } else { "" });
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, current == Some(n), &row_label));
crate::rtl::paint(ui.painter(), row.min + vec2(8.0, 11.0), egui::Align2::LEFT_CENTER, &row_label, egui::FontId::proportional(12.5), t.text);
let editable = n != if para { designcraft_doc::NO_PARA_STYLE } else { designcraft_doc::NO_CHAR_STYLE };
if resp.double_clicked() && editable {
crate::dialogs::open_style_options(app, para, n);
@@ -116,6 +126,14 @@ fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, curren
let _ = app.run(cmd, json!({"name": n}));
ui.close();
}
if para
&& ui
.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply \"{name}\", Clear Overrides").replace("{name}", label)))
.clicked()
{
let _ = app.run("style.paragraph.apply", json!({"name": n, "clearOverrides": true}));
ui.close();
}
if current == Some(n) && ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Break Link to Style"))).clicked() {
let _ = app.run("style.breakLink", json!({"kind": kind}));
ui.close();
@@ -171,6 +189,58 @@ mod tests {
use super::*;
#[test]
fn paragraph_color_override_is_marked_and_cleared_from_the_row_menu() {
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": "Header"})).unwrap();
app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 6})).unwrap();
app.run("style.paragraph.create", json!({"name": "Header", "chars": {"fill": "[Black]"}})).unwrap();
app.run("style.paragraph.apply", json!({"name": "Header"})).unwrap();
app.run("style.character.create", json!({"name": "Emphasis", "chars": {"underline": true}})).unwrap();
app.run("style.character.apply", json!({"name": "Emphasis"})).unwrap();
app.run("type.char", json!({"fill": "[Paper]"})).unwrap();
let mut h = Harness::builder().with_size(vec2(460.0, 300.0)).build_ui_state(|ui, app: &mut DesignApp| paragraph(app, ui), app);
h.run_steps(3);
let at = h.get_all_by_label("Header+").next_back().unwrap().rect().center();
h.event(egui::Event::PointerMoved(at));
for pressed in [true, false] {
h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() });
h.run_steps(2);
}
h.get_by_label("Apply \"Header\", Clear Overrides").click();
h.run_steps(3);
let app = h.state_mut();
let a = app.run("type.selectionAttrs", json!({})).unwrap();
assert_eq!(a["chars"]["fill"], "[Black]");
assert_eq!(a["chars"]["underline"], true);
assert_eq!(a["characterStyle"], "Emphasis");
assert_eq!(a["charOverrides"], 0);
assert_eq!(a["paraOverrides"], 0);
app.run("edit.undo", json!({})).unwrap();
assert_eq!(app.run("type.selectionAttrs", json!({})).unwrap()["chars"]["fill"], "[Paper]");
}
#[test]
fn named_character_styles_and_redundant_local_values_are_not_paragraph_overrides() {
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": "Body text"})).unwrap();
app.run("text.select", json!({"story": f["story"], "anchor": 0, "focus": 4})).unwrap();
app.run("style.paragraph.create", json!({"name": "Body", "para": {"spaceAfter": 6}, "chars": {"size": 10}})).unwrap();
app.run("style.paragraph.apply", json!({"name": "Body"})).unwrap();
app.run("style.character.create", json!({"name": "Italic", "chars": {"fontStyle": "Italic"}})).unwrap();
app.run("style.character.apply", json!({"name": "Italic"})).unwrap();
app.run("type.char", json!({"size": 10, "fontStyle": "Italic"})).unwrap();
app.run("type.para", json!({"spaceAfter": 6})).unwrap();
let a = app.run("type.selectionAttrs", json!({})).unwrap();
assert_eq!((a["paraOverrides"].as_u64(), a["charOverrides"].as_u64()), (Some(0), Some(0)));
let mut h = Harness::builder().with_size(vec2(360.0, 240.0)).build_ui_state(|ui, app: &mut DesignApp| paragraph(app, ui), app);
h.run_steps(3);
assert!(h.query_by_label("Body+").is_none());
assert!(h.query_all_by_label("Body").next().is_some());
}
#[test]
fn character_style_plus_captures_selection_for_reuse() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+9 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (document views row: scrollbars, Alt pans while editing text) · **Target:** Adobe InDesign 2026 (21.6, Medium Dark, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (paragraph style override feedback and clearing) · **Target:** Adobe InDesign 2026 (21.6, Medium Dark, macOS)
Tools, handles, snapping, nudging, modifiers, shortcuts, panels, menus, context menus and feel,
against InDesign 2026. The look was measured from the running app
@@ -61,6 +61,14 @@ context menus, typing at a caret) are things a layout artist does every minute,
## Remaining effort
The Paragraph Styles panel now includes character formatting in its `+` indicator, on both
the current-style summary and the selected row. The row menu offers Apply "name", Clear
Overrides through the existing command, retaining named character styles. Selection attribute
counts compare effective values with their style definitions, so equal local values and named
character styles alone do not report overrides. This covers the color-override/clear workflow
at 4:25–5:20 in [Jessica Garrity's styles tutorial](https://www.youtube.com/watch?v=pG9JkAPLgs8).
Redefine Style and style-dialog Preview remain separate gaps.
| Work | Opus 5.5 h |
|---|---|
| Copy/paste, context menus, threading clicks, caret formatting on every platform, with control-channel regression tests | 30–50 |