From dea0352299f8f721553ad41c30456900b8bcda03 Mon Sep 17 00:00:00 2001 From: moppeeer <339764117+moppeeer@users.noreply.github.com> Date: Thu, 15 Oct 2026 20:30:39 +0200 Subject: [PATCH] TUT4.3: normalize the source run when redefining at a caret --- crates/engine/src/cmd/style.rs | 44 +++++++++++++++++++++++++++++++--- docs/ui-parity.md | 3 +++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 41fefef16..d508a8bee 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -75,6 +75,28 @@ mod redefine_tests { assert_eq!(s.doc().unwrap().doc, before); assert_eq!(s.doc().unwrap().history.undo.len(), undo); } + + #[test] + fn redefine_character_at_a_caret_cleans_the_source_run_but_keeps_other_overrides() { + let (mut s, story, _) = sample(); + s.execute("style.character.create", &json!({"name":"Emphasis","chars":{"underline":true}})).unwrap(); + for (start, end, size) in [(0, 5, 24), (6, 12, 32)] { + s.execute("text.select", &json!({"story":story,"anchor":start,"focus":end})).unwrap(); + s.execute("style.character.apply", &json!({"name":"Emphasis"})).unwrap(); + s.execute("type.char", &json!({"size":size})).unwrap(); + } + let before = s.doc().unwrap().doc.clone(); + s.execute("text.select", &json!({"story":story,"anchor":2,"focus":2})).unwrap(); + s.execute("style.character.redefine", &json!({"name":"Emphasis"})).unwrap(); + s.execute("style.character.edit", &json!({"name":"Emphasis","chars":{"size":30}})).unwrap(); + for (start, end, size) in [(0, 5, 30.0), (6, 12, 32.0)] { + s.execute("text.select", &json!({"story":story,"anchor":start,"focus":end})).unwrap(); + assert_eq!(s.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["size"], size); + } + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().doc, before); + } } use designcraft_color::{Swatch, SwatchValue}; @@ -530,8 +552,24 @@ fn redefine(s: &mut Session, p: &Value, paragraph: bool) -> Result { let styles = d.styles.clone(); for target in &targets { let Some(story) = d.text_story_mut(target.story, target.cell) else { continue }; - let end = if target.range.is_empty() { target.range.start } else { target.range.end.saturating_sub(1) }; - let (first, last) = (story.para_at(target.range.start), story.para_at(end)); + // At a character-style caret, the sampled format belongs to the containing run + // (the preceding run at a boundary). Normalize that source run too, otherwise its + // old local values would mask the next edit of the redefined style. + let mut selected_range = target.range.clone(); + if !paragraph && selected_range.is_empty() { + let caret = selected_range.start; + let mut start = 0usize; + for run in &story.chars { + let end = start.saturating_add(run.len).min(story.len()); + if caret < end || (caret == end && caret > start) { + selected_range = start..end; + break; + } + start = end; + } + } + let end = if selected_range.is_empty() { selected_range.start } else { selected_range.end.saturating_sub(1) }; + let (first, last) = (story.para_at(selected_range.start), story.para_at(end)); for (index, range) in story.para_ranges().into_iter().enumerate().take(last.saturating_add(1)).skip(first) { let Some(pf) = story.paras.get_mut(index) else { continue }; if paragraph && pf.style != name { @@ -545,7 +583,7 @@ fn redefine(s: &mut Session, p: &Value, paragraph: bool) -> Result { } let (_, base) = styles.resolve_para(pf); let end = range.end.saturating_add(1).min(story.len()); - let range = if paragraph { range.start..end } else { range.start.max(target.range.start)..end.min(target.range.end) }; + let range = if paragraph { range.start..end } else { range.start.max(selected_range.start)..end.min(selected_range.end) }; story.format_chars(range, |format| { if !paragraph && format.style != name { return; diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 76e77c8ca..68375ce07 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -61,6 +61,9 @@ context menus, typing at a caret) are things a layout artist does every minute, ## Remaining effort +Redefining a character style at a caret also normalizes the sampled source run, so subsequent +style edits reach that text while different local overrides in adjacent runs remain intact. + The selected style's context menu now offers Redefine Style for paragraph and character styles. It incorporates the source text's local changes, updates every use, and removes matching source overrides in one undo step. Named character styles remain separate when redefining a paragraph