mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Merge branch 'pr/667' into batch1
This commit is contained in:
commit
8255d58c2e
3 files changed
+336
-2
No files matched your search
@@ -105,7 +105,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Character Style…",
|
||||
[],
|
||||
None,
|
||||
"{name, basedOn?: style | \"[None]\", chars?: {…}} — the style sets only `chars` → {name} (made unique)",
|
||||
"{name, basedOn?: style | \"[None]\", chars?: {…}, fromSelection?: bool (default false)} — if true, capture the first selected character's style/local overrides (caret: typing format), then overlay chars (null unsets); only explicit fill names are validated; paragraph formatting stays inherited; no text target: chars only → {name} (made unique)",
|
||||
has_doc,
|
||||
create_char
|
||||
),
|
||||
@@ -622,8 +622,27 @@ fn delete_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn create_char(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let base = str_param(p, "name").unwrap_or("Character Style 1").to_string();
|
||||
let chars: CharAttrs = attrs(p.get("chars"), |a: &mut CharAttrs, k, v| a.set_json(k, v))?;
|
||||
let mut chars: CharAttrs = attrs(p.get("chars"), |a: &mut CharAttrs, k, v| a.set_json(k, v))?;
|
||||
// Validate caller-supplied fills; captured formatting may retain a deleted swatch name.
|
||||
super::text::validate_char_fill(&s.doc()?.doc, &chars, "style.character.create")?;
|
||||
if p.get("fromSelection").and_then(Value::as_bool).unwrap_or(false)
|
||||
&& let Some(target) = super::text::format_targets_pub(s).first()
|
||||
{
|
||||
let st = s.doc()?;
|
||||
let story = st.doc.text_story(target.story, target.cell).ok_or(designcraft_doc::DocError::NoStory(target.story))?;
|
||||
// Match paragraph fromSelection's first-character/caret policy, but keep paragraph
|
||||
// attributes inherited instead of freezing the fully resolved character properties.
|
||||
let format = if target.range.is_empty() { story.char_format_at(target.range.start) } else { story.format_after(target.range.start) };
|
||||
let mut captured = st.doc.styles.char_style_attrs(&format.style);
|
||||
captured.merge(&format.over);
|
||||
// Replay the explicit fields so null clears a captured value rather than looking absent.
|
||||
if let Some(overrides) = p.get("chars").and_then(Value::as_object) {
|
||||
for (key, value) in overrides {
|
||||
captured.set_json(key, value).map_err(|e| bad("style", e))?;
|
||||
}
|
||||
}
|
||||
chars = captured;
|
||||
}
|
||||
// Based on [None] is based on nothing.
|
||||
let based_on = str_param(p, "basedOn").filter(|b| *b != designcraft_doc::NO_CHAR_STYLE).map(str::to_string);
|
||||
s.edit(|d, _| {
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
//! Character Styles panel's existing `fromSelection` command contract.
|
||||
use designcraft_doc::{CharAttrs, StoryId};
|
||||
use designcraft_engine::Session;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn story(text: &str) -> (Session, u64) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let frame = s.execute("frame.create", &json!({"rect": [36, 36, 300, 200], "content": "text", "text": text})).unwrap();
|
||||
(s, frame["story"].as_u64().unwrap())
|
||||
}
|
||||
|
||||
fn select(s: &mut Session, sid: u64, anchor: usize, focus: usize) {
|
||||
s.execute("text.select", &json!({"story": sid, "anchor": anchor, "focus": focus})).unwrap();
|
||||
}
|
||||
|
||||
fn chars(s: &Session, name: &str) -> CharAttrs {
|
||||
s.doc().unwrap().doc.styles.char_style(name).unwrap().chars.clone()
|
||||
}
|
||||
|
||||
fn document(s: &Session) -> Value {
|
||||
serde_json::to_value(s.doc().unwrap().doc.as_ref()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_capture_overlays_style_chain_local_and_explicit_attrs_but_inherits_paragraph() {
|
||||
let (mut s, sid) = story("alpha\nbeta");
|
||||
s.execute("style.paragraph.create", &json!({"name": "Source paragraph", "chars": {"size": 18}})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Destination", "chars": {"size": 30, "underline": true}})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Base", "chars": {"underline": true, "tracking": 10, "strikethrough": true}})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Emphasis", "basedOn": "Base", "chars": {"fontStyle": "Italic"}})).unwrap();
|
||||
select(&mut s, sid, 0, 5);
|
||||
s.execute("style.paragraph.apply", &json!({"name": "Source paragraph"})).unwrap();
|
||||
s.execute("style.character.apply", &json!({"name": "Emphasis"})).unwrap();
|
||||
s.execute("type.char", &json!({"tracking": 20, "underline": false})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Captured", "fromSelection": true, "chars": {"tracking": 7}})).unwrap();
|
||||
assert_eq!(
|
||||
chars(&s, "Captured"),
|
||||
CharAttrs { font_style: Some("Italic".into()), underline: Some(false), tracking: Some(7.0), strikethrough: Some(true), ..Default::default() }
|
||||
);
|
||||
select(&mut s, sid, 6, 10);
|
||||
s.execute("style.paragraph.apply", &json!({"name": "Destination"})).unwrap();
|
||||
s.execute("style.character.apply", &json!({"name": "Captured"})).unwrap();
|
||||
let resolved = s.execute("type.selectionAttrs", &json!({})).unwrap();
|
||||
assert_eq!(resolved["chars"]["fontStyle"], "Italic");
|
||||
assert_eq!(resolved["chars"]["strikethrough"], true);
|
||||
assert_eq!(resolved["chars"]["underline"], false);
|
||||
assert_eq!(resolved["chars"]["tracking"], 7.0);
|
||||
assert_eq!(resolved["chars"]["size"], 30.0);
|
||||
s.execute("style.paragraph.edit", &json!({"name": "Destination", "chars": {"size": 34}})).unwrap();
|
||||
assert_eq!(s.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["size"], 34.0);
|
||||
// The captured attributes are a snapshot, independent of future source-style edits.
|
||||
s.execute("style.character.edit", &json!({"name": "Emphasis", "chars": {"fontStyle": "Bold"}})).unwrap();
|
||||
assert_eq!(s.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["fontStyle"], "Italic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_capture_reproducer_is_one_undo_step_and_survives_native_reopen() {
|
||||
let (mut s, sid) = story("alpha beta");
|
||||
select(&mut s, sid, 0, 5);
|
||||
s.execute("type.char", &json!({"underline": true})).unwrap();
|
||||
let before = document(&s);
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let undo_len = s.doc().unwrap().history.undo.len();
|
||||
let source = serde_json::to_value(s.doc().unwrap().doc.story(StoryId(sid)).unwrap()).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Captured", "fromSelection": true})).unwrap();
|
||||
assert_eq!(chars(&s, "Captured"), CharAttrs { underline: Some(true), ..Default::default() });
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo_len + 1);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
assert_eq!(serde_json::to_value(s.doc().unwrap().doc.story(StoryId(sid)).unwrap()).unwrap(), source);
|
||||
let after = document(&s);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(document(&s), before);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(document(&s), after);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
|
||||
// Both commands use the native ZIP format, without a filesystem fixture.
|
||||
let saved = s.execute("file.serialize", &json!({})).unwrap();
|
||||
let mut reopened = Session::new();
|
||||
reopened.execute("file.openBytes", &json!({"name": "capture.designcraft", "base64": saved["base64"]})).unwrap();
|
||||
assert_eq!(chars(&reopened, "Captured"), chars(&s, "Captured"));
|
||||
select(&mut reopened, sid, 6, 10);
|
||||
assert_eq!(reopened.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["underline"], false);
|
||||
let before_apply = document(&reopened);
|
||||
reopened.execute("style.character.apply", &json!({"name": "Captured"})).unwrap();
|
||||
assert_eq!(reopened.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["underline"], true);
|
||||
assert_eq!(reopened.doc().unwrap().doc.story(StoryId(sid)).unwrap().text, "alpha beta");
|
||||
let after_apply = document(&reopened);
|
||||
reopened.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(document(&reopened), before_apply);
|
||||
reopened.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(document(&reopened), after_apply);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_capture_matches_paragraph_selection_and_caret_boundary_policy() {
|
||||
let (mut s, sid) = story("éX");
|
||||
select(&mut s, sid, 0, 2);
|
||||
s.execute("type.char", &json!({"underline": true})).unwrap();
|
||||
select(&mut s, sid, 2, 3);
|
||||
s.execute("type.char", &json!({"underline": false})).unwrap();
|
||||
// A selected character takes the run on the right; a caret takes the typing run on the left.
|
||||
// Reversed and mixed selections start at the lowest byte offset, just like paragraph capture.
|
||||
for (anchor, focus, underlined) in [(3, 2, false), (2, 2, true), (3, 0, true)] {
|
||||
select(&mut s, sid, anchor, focus);
|
||||
let p = s.execute("style.paragraph.create", &json!({"name": "Paragraph", "fromSelection": true})).unwrap();
|
||||
let c = s.execute("style.character.create", &json!({"name": "Character", "fromSelection": true})).unwrap();
|
||||
let p = s.doc().unwrap().doc.styles.para(p["name"].as_str().unwrap()).unwrap();
|
||||
let c = chars(&s, c["name"].as_str().unwrap());
|
||||
assert_eq!(c.underline, Some(underlined));
|
||||
assert_eq!(c.underline, p.chars.underline);
|
||||
assert_eq!(c.size, None, "unlike paragraph capture, character capture must not freeze paragraph defaults");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_capture_is_opt_in_and_no_text_uses_only_explicit_chars() {
|
||||
let (mut s, sid) = story("alpha");
|
||||
select(&mut s, sid, 0, 5);
|
||||
s.execute("type.char", &json!({"underline": true})).unwrap();
|
||||
for options in [json!({"name": "Plain"}), json!({"name": "Plain", "fromSelection": false})] {
|
||||
let created = s.execute("style.character.create", &options).unwrap();
|
||||
assert!(chars(&s, created["name"].as_str().unwrap()).is_empty());
|
||||
}
|
||||
assert!(s.doc().unwrap().doc.styles.char_style("Plain 2").is_some());
|
||||
s.execute("selection.set", &json!({"ids": []})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "No target", "fromSelection": true, "basedOn": "Plain", "chars": {"underline": false}}))
|
||||
.unwrap();
|
||||
assert_eq!(chars(&s, "No target"), CharAttrs { underline: Some(false), ..Default::default() });
|
||||
assert_eq!(s.doc().unwrap().doc.styles.char_style("No target").unwrap().based_on.as_deref(), Some("Plain"));
|
||||
let (mut empty, empty_sid) = story("");
|
||||
select(&mut empty, empty_sid, 0, 0);
|
||||
empty.execute("style.character.create", &json!({"name": "Empty", "fromSelection": true})).unwrap();
|
||||
assert!(chars(&empty, "Empty").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_capture_retains_deleted_fill_and_reuses_it_after_native_reopen() {
|
||||
for named_source in [false, true] {
|
||||
let (mut s, sid) = story("alpha beta gamma");
|
||||
s.execute("swatch.create", &json!({"name": "Ink", "color": "#ff0000"})).unwrap();
|
||||
select(&mut s, sid, 0, 5);
|
||||
if named_source {
|
||||
s.execute("style.character.create", &json!({"name": "Base", "chars": {"fill": "Ink"}})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Source", "basedOn": "Base"})).unwrap();
|
||||
s.execute("style.character.apply", &json!({"name": "Source"})).unwrap();
|
||||
} else {
|
||||
s.execute("type.char", &json!({"fill": "Ink"})).unwrap();
|
||||
}
|
||||
s.execute("swatch.delete", &json!({"name": "Ink"})).unwrap();
|
||||
s.doc().unwrap().doc.check().unwrap();
|
||||
let before = document(&s);
|
||||
let selection = s.doc().unwrap().selection.clone();
|
||||
let undo_len = s.doc().unwrap().history.undo.len();
|
||||
s.execute("style.character.create", &json!({"name": "Captured", "fromSelection": true})).unwrap();
|
||||
assert_eq!(chars(&s, "Captured"), CharAttrs { fill: Some("Ink".into()), ..Default::default() });
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo_len + 1);
|
||||
assert_eq!(s.doc().unwrap().selection, selection);
|
||||
let after = document(&s);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(document(&s), before);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(document(&s), after);
|
||||
// Non-fill overrides do not turn an existing captured reference into explicit input.
|
||||
s.execute("style.character.create", &json!({"name": "Tweaked", "fromSelection": true, "chars": {"underline": false, "tracking": 0}}))
|
||||
.unwrap();
|
||||
assert_eq!(chars(&s, "Tweaked"), CharAttrs { fill: Some("Ink".into()), underline: Some(false), tracking: Some(0.0), ..Default::default() });
|
||||
s.execute("style.character.create", &json!({"name": "Paper", "fromSelection": true, "chars": {"fill": "[Paper]"}})).unwrap();
|
||||
assert_eq!(chars(&s, "Paper"), CharAttrs { fill: Some("[Paper]".into()), ..Default::default() });
|
||||
s.execute("style.character.create", &json!({"name": "Cleared fill", "fromSelection": true, "chars": {"fill": null}})).unwrap();
|
||||
assert!(chars(&s, "Cleared fill").is_empty());
|
||||
select(&mut s, sid, 6, 10);
|
||||
s.execute("style.character.apply", &json!({"name": "Captured"})).unwrap();
|
||||
assert_eq!(s.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["fill"], "Ink");
|
||||
s.doc().unwrap().doc.check().unwrap();
|
||||
|
||||
let saved = s.execute("file.serialize", &json!({})).unwrap();
|
||||
let mut reopened = Session::new();
|
||||
reopened.execute("file.openBytes", &json!({"name": "deleted-fill.designcraft", "base64": saved["base64"]})).unwrap();
|
||||
reopened.doc().unwrap().doc.check().unwrap();
|
||||
for name in ["Captured", "Tweaked", "Paper", "Cleared fill"] {
|
||||
assert_eq!(chars(&reopened, name), chars(&s, name));
|
||||
}
|
||||
select(&mut reopened, sid, 6, 10);
|
||||
assert_eq!(reopened.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["fill"], "Ink");
|
||||
select(&mut reopened, sid, 11, 16);
|
||||
assert_eq!(reopened.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["fill"], "[Black]");
|
||||
reopened.execute("style.character.apply", &json!({"name": "Captured"})).unwrap();
|
||||
assert_eq!(reopened.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["fill"], "Ink");
|
||||
reopened.execute("style.character.apply", &json!({"name": "Cleared fill"})).unwrap();
|
||||
assert_eq!(reopened.execute("type.selectionAttrs", &json!({})).unwrap()["chars"]["fill"], "[Black]");
|
||||
assert_eq!(reopened.doc().unwrap().doc.story(StoryId(sid)).unwrap().text, "alpha beta gamma");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_capture_rejects_explicit_missing_fills_without_losing_redo() {
|
||||
let (mut s, sid) = story("alpha");
|
||||
s.execute("swatch.create", &json!({"name": "Ink", "color": "#ff0000"})).unwrap();
|
||||
select(&mut s, sid, 0, 5);
|
||||
s.execute("type.char", &json!({"fill": "Ink"})).unwrap();
|
||||
s.execute("swatch.delete", &json!({"name": "Ink"})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Redo sentinel"})).unwrap();
|
||||
let redo_doc = s.doc().unwrap().doc.clone();
|
||||
let redo_selection = s.doc().unwrap().selection.clone();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let before = s.doc().unwrap().clone();
|
||||
let journal = s.journal.clone();
|
||||
for (capture, fill) in [(true, "Missing"), (true, "Ink"), (false, "Missing")] {
|
||||
let error = s.execute("style.character.create", &json!({"name": "Invalid", "fromSelection": capture, "chars": {"fill": fill}})).unwrap_err();
|
||||
assert!(error.to_string().contains(fill));
|
||||
assert!(std::sync::Arc::ptr_eq(&s.doc().unwrap().doc, &before.doc));
|
||||
assert_eq!(s.doc().unwrap().selection, before.selection);
|
||||
assert_eq!(s.doc().unwrap().revision, before.revision);
|
||||
assert_eq!(format!("{:?}", s.doc().unwrap().history), format!("{:?}", before.history));
|
||||
assert_eq!(s.journal, journal);
|
||||
}
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert!(std::sync::Arc::ptr_eq(&s.doc().unwrap().doc, &redo_doc));
|
||||
assert_eq!(s.doc().unwrap().selection, redo_selection);
|
||||
assert!(s.doc().unwrap().doc.styles.char_style("Redo sentinel").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_capture_explicit_null_restores_live_paragraph_inheritance() {
|
||||
for named_source in [false, true] {
|
||||
let (mut s, sid) = story("alpha\nbeta");
|
||||
select(&mut s, sid, 0, 5);
|
||||
let source = json!({"tracking": 20, "underline": true, "underlineWeight": 1.5, "fontStyle": "Italic"});
|
||||
if named_source {
|
||||
s.execute("style.character.create", &json!({"name": "Base", "chars": source})).unwrap();
|
||||
s.execute("style.character.create", &json!({"name": "Source", "basedOn": "Base"})).unwrap();
|
||||
s.execute("style.character.apply", &json!({"name": "Source"})).unwrap();
|
||||
} else {
|
||||
s.execute("type.char", &source).unwrap();
|
||||
}
|
||||
// Omitting chars still captures values, including a nullable attribute's numeric value.
|
||||
s.execute("style.character.create", &json!({"name": "Unchanged", "fromSelection": true})).unwrap();
|
||||
assert_eq!(chars(&s, "Unchanged").tracking, Some(20.0));
|
||||
assert_eq!(chars(&s, "Unchanged").underline_weight, Some(Some(1.5)));
|
||||
let before = document(&s);
|
||||
let undo_len = s.doc().unwrap().history.undo.len();
|
||||
s.execute(
|
||||
"style.character.create",
|
||||
&json!({"name": "Cleared", "fromSelection": true,
|
||||
"chars": {"tracking": null, "underlineWeight": null, "underline": false, "baselineShift": 0}}),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
chars(&s, "Cleared"),
|
||||
CharAttrs { font_style: Some("Italic".into()), underline: Some(false), baseline_shift: Some(0.0), ..Default::default() }
|
||||
);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo_len + 1);
|
||||
let after = document(&s);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(document(&s), before);
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(document(&s), after);
|
||||
s.execute("style.paragraph.create", &json!({"name": "Destination", "chars": {"tracking": 30, "underlineWeight": 2}})).unwrap();
|
||||
select(&mut s, sid, 6, 10);
|
||||
s.execute("style.paragraph.apply", &json!({"name": "Destination"})).unwrap();
|
||||
s.execute("style.character.apply", &json!({"name": "Cleared"})).unwrap();
|
||||
let effective = s.execute("type.selectionAttrs", &json!({})).unwrap();
|
||||
assert_eq!(effective["chars"]["tracking"], 30.0);
|
||||
assert_eq!(effective["chars"]["underlineWeight"], 2.0);
|
||||
assert_eq!(effective["chars"]["underline"], false);
|
||||
s.execute("style.paragraph.edit", &json!({"name": "Destination", "chars": {"tracking": 40, "underlineWeight": 3}})).unwrap();
|
||||
let effective = s.execute("type.selectionAttrs", &json!({})).unwrap();
|
||||
assert_eq!(effective["chars"]["tracking"], 40.0);
|
||||
assert_eq!(effective["chars"]["underlineWeight"], 3.0);
|
||||
}
|
||||
}
|
||||
@@ -150,3 +150,44 @@ fn style_row(app: &mut DesignApp, ui: &mut egui::Ui, para: bool, n: &str, curren
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui_kittest::{Harness, kittest::Queryable};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn character_style_plus_captures_selection_for_reuse() {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let frame = app.run("frame.create", json!({"rect": [36, 36, 300, 200], "content": "text", "text": "alpha beta"})).unwrap();
|
||||
app.run("text.select", json!({"story": frame["story"], "anchor": 0, "focus": 5})).unwrap();
|
||||
app.run("type.char", json!({"underline": true})).unwrap();
|
||||
let selection = app.session.doc().unwrap().selection.clone();
|
||||
let undo_len = app.session.doc().unwrap().history.undo.len();
|
||||
let mut h = Harness::builder().with_size(vec2(360.0, 240.0)).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let id = egui::Id::new("test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(id)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(id, true));
|
||||
return;
|
||||
}
|
||||
character(app, ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Create New Style from Selection").click();
|
||||
h.run_steps(3);
|
||||
let app = h.state_mut();
|
||||
let style = app.session.doc().unwrap().doc.styles.char_style("Character Style 1").unwrap();
|
||||
assert_eq!(style.chars, designcraft_doc::CharAttrs { underline: Some(true), ..Default::default() });
|
||||
assert_eq!(app.session.doc().unwrap().selection, selection);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo_len + 1);
|
||||
app.run("text.select", json!({"story": frame["story"], "anchor": 6, "focus": 10})).unwrap();
|
||||
app.run("style.character.apply", json!({"name": "Character Style 1"})).unwrap();
|
||||
assert_eq!(app.run("type.selectionAttrs", json!({})).unwrap()["chars"]["underline"], true);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user