From 5865e4cb51bc08573713a41941ad411720544c69 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 05:29:30 +0700 Subject: [PATCH] Type: Top-to-Top Leading follow-ups (review of #385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The em box top that Top-to-Top Leading asks of every glyph's line metrics parsed the font's cmap and vertical metrics per glyph on every layout: each face now reads it once. - The Paragraph panel menu shows Top-to-Top / Bottom-to-Bottom Leading with the East Asian options (Preferences › Type), as the Mojikumi Set does, and Reset Panel goes back to Bottom-to-Bottom Leading, the default. - Saved as `leading_model`, like the other paragraph attributes in files. - Simplified Chinese (landed since) translates the two menu items. Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/text.rs | 2 +- crates/engine/src/cmd/typecmd.rs | 4 ++-- crates/text/src/fontdb.rs | 9 +++++++-- crates/ui-egui/src/i18n/es.tsv | 4 ++++ crates/ui-egui/src/i18n/zh-hans.tsv | 2 ++ crates/ui-egui/src/panels/paragraph.rs | 21 ++++++++++++--------- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index c8c9d75f9..550e82163 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -327,7 +327,7 @@ struct ParaStyleFile { mojikumi: Mojikumi, #[serde(default, skip_serializing_if = "Option::is_none")] direction: Option, - #[serde(default, rename = "leadingModel", skip_serializing_if = "crate::skip::is_default")] + #[serde(default, skip_serializing_if = "crate::skip::is_default")] leading_model: LeadingModel, } diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index f79a53b1a..eefb38a1f 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -428,9 +428,9 @@ mod area_tests { let t = text(&s); assert_eq!(t.para.leading_model, LeadingModel::EmBoxTop); assert_ne!(t.cached_bounds, before, "the first line moves up to the frame's top"); - assert_eq!(serde_json::to_value(&t.para).unwrap()["leadingModel"], "emBoxTop"); + assert_eq!(serde_json::to_value(&t.para).unwrap()["leading_model"], "emBoxTop"); s.execute("edit.undo", &json!({})).unwrap(); - assert!(serde_json::to_value(&text(&s).para).unwrap().get("leadingModel").is_none()); + assert!(serde_json::to_value(&text(&s).para).unwrap().get("leading_model").is_none()); } #[test] diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 8f426fe8f..835f2008f 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -77,6 +77,8 @@ pub struct FontFace { location: Location, /// [`Self::variations`] for the shaper (advances, kerning, feature variations). pub(crate) instance: Option, + /// [`Self::ideographic_centre`], read once: layout asks for it per glyph. + ideographic_centre: std::sync::OnceLock, } impl std::fmt::Debug for FontFace { @@ -170,8 +172,10 @@ impl FontFace { /// vertical metrics (an ideograph's cell), else the usual 0.38 (the box running from 0.12 em /// below the baseline to 0.88 em above it). pub fn ideographic_centre(&self) -> f64 { - let gid = ['国', 'あ', '一'].into_iter().map(|c| self.glyph_for(c)).find(|g| *g != 0); - gid.and_then(|g| self.vertical_glyph(g)).map_or(0.38, |(advance, origin)| (origin - advance * 0.5) / self.upem) + *self.ideographic_centre.get_or_init(|| { + let gid = ['国', 'あ', '一'].into_iter().map(|c| self.glyph_for(c)).find(|g| *g != 0); + gid.and_then(|g| self.vertical_glyph(g)).map_or(0.38, |(advance, origin)| (origin - advance * 0.5) / self.upem) + }) } /// Glyph id for `c` (0 = .notdef). pub fn glyph_for(&self, c: char) -> u32 { @@ -813,6 +817,7 @@ fn make_face(bytes: FontBytes, index: u32, spec: FaceStyle, path: Option