diff --git a/crates/idml/src/import/styles.rs b/crates/idml/src/import/styles.rs index e2e11e979..023ab2d40 100644 --- a/crates/idml/src/import/styles.rs +++ b/crates/idml/src/import/styles.rs @@ -199,8 +199,9 @@ impl<'r> Importer<'r> { .flatten(); let levels = based_on_levels(e, &objects_by_self); // Text Frame General Options: each attribute from the nearest style whose - // `TextFramePreference` sets it, among the styles that don't turn the category off. - let text_frame = if e.get("EnableTextFrameGeneralOptions") == Some("true") { + // `TextFramePreference` sets it, among the styles that don't turn the category off. A + // style without the category flag takes it from the style it is based on. + let text_frame = if category_on(&levels, "EnableTextFrameGeneralOptions", |_| false) { let prefs: Vec<&El> = levels .iter() .filter(|l| l.get("EnableTextFrameGeneralOptions") != Some("false")) @@ -211,7 +212,7 @@ impl<'r> Importer<'r> { None }; let effects = self.style_effects(e); - let corners = style_corners(e, &levels); + let corners = style_corners(&levels); let apply_next_style = paragraph_style.is_some() && e.get("ApplyNextParagraphStyle") == Some("true"); let s = ObjectStyle { name: name.clone(), based_on, fill, stroke, paragraph_style, apply_next_style, text_frame, effects, corners }; match self.styles.object.iter_mut().find(|o| o.name == name) { @@ -412,21 +413,34 @@ impl<'r> Importer<'r> { } } -/// Object style `e`'s corner options (Stroke & Corner Options), by name, when it sets them. A style -/// that sets some corners, or turns the category on, takes the others from the styles in `levels` -/// (`e` and the styles it is based on) that don't turn the category off. -fn style_corners(e: &El, levels: &[&El]) -> Option { - match e.get("EnableStrokeAndCornerOptions") { - Some("false") => return None, - Some("true") => {} - _ => { - corners(e, "")?; - } +/// The corner options (Stroke & Corner Options) of the object style that starts `levels` (the +/// style, then the styles it is based on), by name, when it sets them. A style that sets some +/// corners, or turns the category on (itself or through the style it is based on), takes the +/// others from the styles in `levels` that don't turn the category off. +fn style_corners(levels: &[&El]) -> Option { + if !category_on(levels, "EnableStrokeAndCornerOptions", |l| corners(l, "").is_some()) { + return None; } let on: Vec<&El> = levels.iter().copied().filter(|l| l.get("EnableStrokeAndCornerOptions") != Some("false")).collect(); corners_in(&on, "").map(|c| CornerOptions { corners: c }) } +/// Whether the style chain `levels` (a style, then the styles it is based on) turns the category +/// whose flag is `flag` on. The nearest style that gives the flag decides; a style without it +/// turns the category on when `own` finds settings of its own. A `[None]` after the first style +/// ends the chain, as a `BasedOn` of `[None]` names no style. +fn category_on(levels: &[&El], flag: &str, own: impl Fn(&El) -> bool) -> bool { + let none = |l: &&El| l.get("Self") == Some("ObjectStyle/$ID/[None]"); + for l in levels.iter().take(1).chain(levels.iter().skip(1).take_while(|l| !none(l))) { + match l.get(flag) { + Some(v) => return v == "true", + None if own(l) => return true, + None => {} + } + } + false +} + /// Object style `e`, then the styles it is based on, nearest first (cycle-safe, at most 16). A /// `BasedOn` may name a style by its `Self` id or by its bare name. fn based_on_levels<'e>(e: &'e El, objects_by_self: &HashMap<&str, &'e El>) -> Vec<&'e El> { diff --git a/crates/idml/src/tests/object_styles.rs b/crates/idml/src/tests/object_styles.rs index 6226d4a74..52cfd3e20 100644 --- a/crates/idml/src/tests/object_styles.rs +++ b/crates/idml/src/tests/object_styles.rs @@ -363,6 +363,43 @@ fn a_style_turning_corners_on_inherits_all_four() { assert_eq!((c.map(|c| c.shape), c.map(|c| c.size)), ([CornerShape::Rounded; 4], [17.0; 4])); } +/// A style with no Stroke & Corner Options or Text Frame General Options flag and no settings of +/// its own takes each category from the style it is based on: on, off, or (for a style based on +/// `[None]`) not set. +#[test] +fn a_style_without_category_flags_takes_them_from_its_based_on_style() { + let styles = r#" + $ID/[None] + + + + ObjectStyle/Round + + + + ObjectStyle/Round + + + ObjectStyle/Framed + + + ObjectStyle/Off + + + $ID/[None] + "#; + let d = fixture(&format!("{ROUND}{styles}"), &[]); + let style = |n: &str| d.styles.object_style(n).cloned().unwrap(); + let c = style("RoundKid").corners.expect("RoundKid inherits Round's corners").corners; + assert_eq!((c.map(|c| c.shape), c.map(|c| c.size)), ([CornerShape::Rounded; 4], [17.0; 4])); + assert_eq!(style("FramedKid").text_frame.map(|t| t.columns), Some(3), "FramedKid inherits Framed's text frame options"); + for n in ["OffKid", "Bare"] { + assert!(style(n).corners.is_none() && style(n).text_frame.is_none(), "{n}: {:?}", style(n)); + } + assert!(style(designcraft_doc::NO_OBJECT_STYLE).corners.is_some(), "[None] keeps the corners it sets itself"); +} + /// An object style giving its corners only with the legacy all-corners `CornerOption` and /// `CornerRadius` keeps them, so applying it to another frame rounds it. #[test]