Skip to content

Commit bdba71b

Browse files
committed
add notes
1 parent 596ac8e commit bdba71b

File tree

3 files changed

+190
-9
lines changed

3 files changed

+190
-9
lines changed

sugarloaf/src/font/cjk_metrics_tests.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,25 @@ mod tests {
114114
},
115115
}
116116
}
117+
118+
fn noto_color_emoji() -> Self {
119+
Self {
120+
name: "Noto Color Emoji",
121+
face: FaceMetrics {
122+
cell_width: 20.0, // Emoji are typically double-width
123+
ascent: 14.0,
124+
descent: 3.5,
125+
line_gap: 1.0,
126+
underline_position: Some(-2.0),
127+
underline_thickness: Some(1.2),
128+
strikethrough_position: Some(7.0),
129+
strikethrough_thickness: Some(1.2),
130+
cap_height: Some(10.5),
131+
ex_height: Some(7.0),
132+
ic_width: None,
133+
},
134+
}
135+
}
117136
}
118137

119138
#[test]
@@ -866,4 +885,130 @@ mod tests {
866885
"Mixed content must not affect total height calculations"
867886
);
868887
}
888+
889+
/// Test edge cases for font metrics calculations
890+
#[test]
891+
fn test_edge_cases() {
892+
// Test extremely small font size
893+
let tiny_font = FaceMetrics {
894+
cell_width: 0.1,
895+
ascent: 0.1,
896+
descent: 0.05,
897+
line_gap: 0.01,
898+
underline_position: Some(-0.01),
899+
underline_thickness: Some(0.01),
900+
strikethrough_position: Some(0.05),
901+
strikethrough_thickness: Some(0.01),
902+
cap_height: Some(0.08),
903+
ex_height: Some(0.05),
904+
ic_width: None,
905+
};
906+
907+
let tiny_metrics = Metrics::calc(tiny_font);
908+
assert!(
909+
tiny_metrics.cell_width >= 1,
910+
"Cell width must be at least 1 pixel"
911+
);
912+
assert!(
913+
tiny_metrics.cell_height >= 1,
914+
"Cell height must be at least 1 pixel"
915+
);
916+
assert!(
917+
tiny_metrics.underline_thickness >= 1,
918+
"Line thickness must be at least 1 pixel"
919+
);
920+
921+
// Test extremely large font size
922+
let huge_font = FaceMetrics {
923+
cell_width: 1000.0,
924+
ascent: 1200.0,
925+
descent: 300.0,
926+
line_gap: 100.0,
927+
underline_position: Some(-100.0),
928+
underline_thickness: Some(100.0),
929+
strikethrough_position: Some(600.0),
930+
strikethrough_thickness: Some(100.0),
931+
cap_height: Some(900.0),
932+
ex_height: Some(600.0),
933+
ic_width: None,
934+
};
935+
936+
let huge_metrics = Metrics::calc(huge_font);
937+
assert_eq!(
938+
huge_metrics.cell_width, 1000,
939+
"Large font metrics should be preserved"
940+
);
941+
assert_eq!(
942+
huge_metrics.cell_height, 1700,
943+
"Large font height calculation"
944+
);
945+
946+
// Test font with missing optional metrics
947+
let minimal_font = FaceMetrics {
948+
cell_width: 10.0,
949+
ascent: 12.0,
950+
descent: 3.0,
951+
line_gap: 1.0,
952+
underline_position: None,
953+
underline_thickness: None,
954+
strikethrough_position: None,
955+
strikethrough_thickness: None,
956+
cap_height: None,
957+
ex_height: None,
958+
ic_width: None,
959+
};
960+
961+
let minimal_metrics = Metrics::calc(minimal_font);
962+
assert!(
963+
minimal_metrics.underline_thickness >= 1,
964+
"Default underline thickness"
965+
);
966+
assert!(
967+
minimal_metrics.strikethrough_thickness >= 1,
968+
"Default strikethrough thickness"
969+
);
970+
assert!(
971+
minimal_metrics.underline_position > 0,
972+
"Default underline position"
973+
);
974+
assert!(
975+
minimal_metrics.strikethrough_position > 0,
976+
"Default strikethrough position"
977+
);
978+
}
979+
980+
/// Test mixed script rendering (Latin + CJK + Emoji)
981+
#[test]
982+
fn test_mixed_script_rendering() {
983+
let latin_font = TestFontData::cascadia_code();
984+
let cjk_font = TestFontData::noto_sans_cjk();
985+
let emoji_font = TestFontData::noto_color_emoji();
986+
987+
let latin_metrics = Metrics::calc(latin_font.face);
988+
let cjk_metrics =
989+
Metrics::calc_with_primary_cell_dimensions(cjk_font.face, &latin_metrics);
990+
let emoji_metrics =
991+
Metrics::calc_with_primary_cell_dimensions(emoji_font.face, &latin_metrics);
992+
993+
// All fonts should have the same cell height for consistent rendering
994+
assert_eq!(latin_metrics.cell_height, cjk_metrics.cell_height);
995+
assert_eq!(latin_metrics.cell_height, emoji_metrics.cell_height);
996+
997+
// All fonts should have the same baseline
998+
assert_eq!(latin_metrics.cell_baseline, cjk_metrics.cell_baseline);
999+
assert_eq!(latin_metrics.cell_baseline, emoji_metrics.cell_baseline);
1000+
1001+
// Verify that a line containing all three scripts renders consistently
1002+
let mixed_line_height = latin_metrics.cell_height;
1003+
let latin_only_height = latin_metrics.cell_height;
1004+
let cjk_only_height = cjk_metrics.cell_height;
1005+
let emoji_only_height = emoji_metrics.cell_height;
1006+
1007+
assert_eq!(
1008+
mixed_line_height, latin_only_height,
1009+
"Mixed script lines must have same height as single script lines"
1010+
);
1011+
assert_eq!(mixed_line_height, cjk_only_height);
1012+
assert_eq!(mixed_line_height, emoji_only_height);
1013+
}
8691014
}

sugarloaf/src/font/metrics.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ pub struct FaceMetrics {
5757
/// if present. This is used for font size adjustment, to normalize
5858
/// the width of CJK fonts mixed with latin fonts.
5959
///
60+
/// Why "水" (water)?
61+
/// - It's a common CJK ideograph present in most CJK fonts
62+
/// - Has typical width characteristics of CJK characters
63+
/// - Simple structure makes it reliable for measurement
64+
/// - Part of the CJK Unified Ideographs block (U+4E00-U+9FFF)
65+
/// - Used as a standard reference in many font metrics systems
66+
///
6067
/// NOTE: IC = Ideograph Character
6168
pub ic_width: Option<f64>,
6269
}
@@ -100,6 +107,12 @@ impl FaceMetrics {
100107
///
101108
/// This measurement is used for font size adjustment to normalize
102109
/// CJK fonts mixed with Latin fonts in the `calculate_cjk_font_size_adjustment` function.
110+
///
111+
/// The water ideograph is chosen because:
112+
/// - It has consistent width across different CJK fonts
113+
/// - It's present in virtually all CJK fonts (basic kanji/hanzi)
114+
/// - Its width is representative of typical CJK character width
115+
/// - It avoids edge cases like punctuation or rare characters
103116
fn measure_cjk_character_width(
104117
font_ref: &crate::font_introspector::FontRef,
105118
) -> Option<f64> {
@@ -142,6 +155,12 @@ impl Metrics {
142155

143156
// Calculate baseline position from bottom of cell (adjusted for Rio's positive descent)
144157
// Rio uses positive descent format, baseline = half_line_gap + descent
158+
//
159+
// Baseline Adjustment Strategy:
160+
// - The baseline is positioned consistently for all fonts (primary and secondary)
161+
// - This ensures CJK characters align properly with Latin text
162+
// - The half_line_gap provides breathing room above and below text
163+
// - Using descent ensures proper positioning for characters with descenders
145164
let cell_baseline = (half_line_gap + face.descent).round();
146165

147166
// Calculate top-to-baseline for other calculations

sugarloaf/src/font/mod.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,22 @@ impl FontLibraryData {
241241
}
242242

243243
/// Get font metrics for rich text rendering (consistent metrics approach)
244-
/// Primary font determines cell dimensions for all fonts
244+
///
245+
/// Primary font determines cell dimensions for all fonts to ensure consistent
246+
/// baseline alignment across different scripts (Latin, CJK, emoji, etc.).
247+
///
248+
/// # Arguments
249+
/// * `font_id` - The font to get metrics for
250+
/// * `font_size` - The font size in pixels
251+
///
252+
/// # Returns
253+
/// A tuple of (width, height, line_height) for the font, or None if the font
254+
/// cannot be found or metrics cannot be calculated.
255+
///
256+
/// # Implementation Notes
257+
/// - Primary font metrics are cached for performance
258+
/// - Secondary fonts inherit cell dimensions from primary font
259+
/// - This ensures CJK characters don't appear "higher" than Latin text
245260
pub fn get_font_metrics(
246261
&mut self,
247262
font_id: &usize,
@@ -254,20 +269,22 @@ impl FontLibraryData {
254269
if let Some(cached) = self.primary_metrics_cache.get(&size_key) {
255270
*cached
256271
} else {
257-
// Calculate primary font metrics and cache them
258272
let primary_font = self.inner.get_mut(&FONT_ID_REGULAR)?;
259273
let primary_metrics = primary_font.get_metrics(font_size, None)?;
260274
self.primary_metrics_cache.insert(size_key, primary_metrics);
261275
primary_metrics
262276
};
263277

264-
if font_id == &FONT_ID_REGULAR {
265-
// Primary font uses its own metrics
266-
Some(primary_metrics.for_rich_text())
267-
} else {
268-
// Secondary fonts use primary font's cell dimensions
269-
let font = self.inner.get_mut(font_id)?;
270-
font.get_rich_text_metrics(font_size, Some(&primary_metrics))
278+
match font_id {
279+
&FONT_ID_REGULAR => {
280+
// Primary font uses its own metrics
281+
Some(primary_metrics.for_rich_text())
282+
}
283+
_ => {
284+
// Secondary fonts use primary font's cell dimensions
285+
let font = self.inner.get_mut(font_id)?;
286+
font.get_rich_text_metrics(font_size, Some(&primary_metrics))
287+
}
271288
}
272289
}
273290

0 commit comments

Comments
 (0)