use crate::compat::{String, Vec};
use crate::core::Font;
use crate::render::{ShapedText, TextCluster};
pub(crate) use crate::render::grapheme::{is_combining_mark, is_variation_selector};
pub(crate) fn for_each_cluster(text: &str, mut visit: impl FnMut(&str, (usize, usize))) {
let mut buffer = String::new();
let mut start = 0usize;
for (index, scalar) in text.char_indices() {
let joins = !buffer.is_empty()
&& (buffer.ends_with('\u{200D}')
|| scalar == '\u{200D}'
|| is_combining_mark(scalar)
|| is_variation_selector(scalar));
if joins {
buffer.push(scalar);
} else {
if !buffer.is_empty() {
visit(&buffer, (start, index));
buffer.clear();
}
start = index;
buffer.push(scalar);
}
}
if !buffer.is_empty() {
visit(&buffer, (start, text.len()));
}
}
pub(crate) fn shape_line(text: &str, font: &Font, scale: f32) -> ShapedText {
let mut clusters: Vec<TextCluster> = Vec::new();
let mut ranges: Vec<(usize, usize)> = Vec::new();
for_each_cluster(text, |cluster_text, range| {
clusters.push(TextCluster {
text: cluster_text.into(),
advance: estimate_cluster_advance(cluster_text, font.size(), scale),
});
ranges.push(range);
});
let tracking = font.letter_spacing() * scale;
#[cfg(feature = "text-shaping")]
let real_advances = crate::render::text::shaping::cluster_advances(text, &ranges, font, scale);
#[cfg(not(feature = "text-shaping"))]
let real_advances: Option<Vec<f32>> = None;
let count = clusters.len();
for (index, cluster) in clusters.iter_mut().enumerate() {
cluster.advance = match &real_advances {
Some(advances) if advances.len() == count => advances[index].max(0.0),
_ => estimate_cluster_advance(&cluster.text, font.size(), scale),
};
}
let mut total_advance: f32 = clusters.iter().map(|cluster| cluster.advance).sum();
if tracking != 0.0 && !clusters.is_empty() {
total_advance += tracking * (clusters.len() - 1) as f32;
}
let order = crate::render::text::bidi::visual_order(text, &ranges);
if order.iter().enumerate().any(|(visual, &logical)| visual != logical) {
let mut reordered: Vec<TextCluster> = Vec::with_capacity(clusters.len());
for &logical in &order {
reordered.push(clusters[logical].clone());
}
clusters = reordered;
}
ShapedText { clusters, advance: total_advance }
}
pub fn is_wide_scalar(scalar: char) -> bool {
matches!(
scalar as u32,
0x1100..=0x115F | 0x2329..=0x232A | 0x2E80..=0xA4CF | 0xAC00..=0xD7A3 | 0xF900..=0xFAFF | 0xFE10..=0xFE19 | 0xFE30..=0xFE6F | 0xFF00..=0xFF60 | 0xFFE0..=0xFFE6 | 0x1F300..=0x1FAFF | 0x20000..=0x3FFFD )
}
pub fn estimate_cluster_advance(cluster: &str, font_size: f32, scale: f32) -> f32 {
if cluster.trim().is_empty() {
return (font_size * 0.33 * scale).max(1.0);
}
let has_wide = cluster.chars().any(is_wide_scalar);
let factor = if has_wide { 1.0 } else { 0.6 };
(font_size * factor * scale).max(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Font;
fn font() -> Font {
Font::new("sans", 10.0, false, false)
}
#[test]
fn a_left_to_right_line_keeps_its_logical_order() {
let shaped = shape_line("abc", &font(), 1.0);
let texts: Vec<&str> = shaped.clusters().iter().map(|c| c.text.as_str()).collect();
assert_eq!(texts, ["a", "b", "c"]);
}
#[test]
fn an_arabic_line_is_reordered_for_painting() {
let shaped = shape_line("\u{05e9}\u{05dc}\u{05d5}\u{05dd}", &font(), 1.0);
let texts: Vec<&str> = shaped.clusters().iter().map(|c| c.text.as_str()).collect();
assert_eq!(texts, ["\u{05dd}", "\u{05d5}", "\u{05dc}", "\u{05e9}"]);
}
#[test]
fn reordering_does_not_change_the_measured_width() {
let text = "\u{05e9}\u{05dc}\u{05d5}\u{05dd} abc";
let shaped = shape_line(text, &font(), 1.0);
let summed: f32 = shaped.clusters().iter().map(|c| c.advance).sum();
assert!((shaped.advance() - summed).abs() < 0.01, "the advance is the sum, in any order");
}
#[test]
fn a_combining_mark_merges_into_its_base_cluster() {
let shaped = shape_line("a\u{0301}b", &font(), 1.0);
let texts: Vec<&str> = shaped.clusters().iter().map(|c| c.text.as_str()).collect();
assert_eq!(texts, ["a\u{0301}", "b"], "the accent must not become its own cluster");
}
#[test]
fn a_wide_cluster_is_charged_a_full_em() {
assert!(is_wide_scalar('\u{4e2d}'), "a CJK ideograph");
assert!(is_wide_scalar('\u{ac00}'), "a Hangul syllable");
assert!(is_wide_scalar('\u{ff21}'), "a fullwidth form");
assert!(is_wide_scalar('\u{1f600}'), "an emoji");
assert!(is_wide_scalar('\u{20000}'), "a CJK extension-B ideograph");
assert!(!is_wide_scalar('A'));
assert!(!is_wide_scalar('\u{0416}'), "Cyrillic is narrow");
}
}