From ae19df8ef2cb1f524ca85d0aed4f610000901a29 Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 15 Oct 2026 06:30:20 +0000 Subject: [PATCH] fix(typography): Standard Ligatures switches on over a feature-list -liga --- crates/engine/src/cmd/text.rs | 8 ++++++-- crates/ui-egui/src/panels/typography.rs | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index adf297311..af35bc833 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -1501,8 +1501,9 @@ fn open_type_features(s: &mut Session, _p: &Value) -> Result { let measure = |tag: &str| { let (mut applied, mut available) = (0, 0); for seg in &segments { - // Standard ligatures are the `ligatures` attribute, which shaping reads. - let on = if tag == "liga" { seg.ligatures } else { designcraft_doc::otf::is_on(&seg.applied, tag) }; + // Standard ligatures are the `ligatures` attribute, which shaping reads, unless the + // feature list turns `liga` off. + let on = designcraft_doc::otf::is_on(&seg.applied, tag) && (tag != "liga" || seg.ligatures); if on { applied += seg.len; } @@ -2844,6 +2845,9 @@ mod open_type_features_tests { s.execute("type.char", &json!({"attrs": {"ligatures": true}})).unwrap(); let v = s.execute("type.openTypeFeatures", &json!({})).unwrap(); assert_eq!(feature(&v, "liga")["applied"], "all", "{v}"); + s.execute("type.feature", &json!({"features": {"liga": false}})).unwrap(); + let v = s.execute("type.openTypeFeatures", &json!({})).unwrap(); + assert_eq!(feature(&v, "liga")["applied"], "none", "-liga in the feature list: {v}"); } } diff --git a/crates/ui-egui/src/panels/typography.rs b/crates/ui-egui/src/panels/typography.rs index 5cd0814f8..4206459d0 100644 --- a/crates/ui-egui/src/panels/typography.rs +++ b/crates/ui-egui/src/panels/typography.rs @@ -92,10 +92,12 @@ struct Changes { impl Changes { /// Standard Ligatures is the `ligatures` attribute: shaping turns `liga` off through it, and - /// IDML carries it as `Ligatures`, so `liga` stays out of the feature list. + /// IDML carries it as `Ligatures`, so `liga` stays out of the feature list. The tag is also + /// cleared from the selection's features, since an earlier `-liga` would keep shaping it off. fn set(&mut self, tag: &str, on: bool) { if tag == "liga" { self.ligatures = Some(on); + self.features.insert("liga".to_string(), true); } else { self.features.insert(tag.to_string(), on); } @@ -396,4 +398,16 @@ mod tests { click(&mut h, "Standard Ligatures"); assert_eq!(chars(&mut h)["ligatures"], true, "the switch turns ligatures back on"); } + + #[test] + fn standard_ligatures_switch_on_clears_a_feature_list_liga_off() { + let mut h = harness(); + h.state_mut().run("type.feature", json!({"features": {"liga": false}})).unwrap(); + assert_eq!(chars(&mut h)["otfFeatures"], json!(["-liga"])); + h.run_steps(2); + click(&mut h, "Standard Ligatures"); + let c = chars(&mut h); + assert_eq!(c["ligatures"], true, "{c}"); + assert!(!c["otfFeatures"].as_array().is_some_and(|l| l.iter().any(|t| t.as_str().is_some_and(|t| t.ends_with("liga")))), "{c}"); + } }