mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
TUT4.1: show and clear text style overrides in Styles panels
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¤t {
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user