use harfrust::{FontRef, GlyphBuffer, ShaperData, ShaperInstance, UnicodeBuffer};
use skrifa::{
prelude::{LocationRef, Size},
FontRef as SkrifaFontRef, MetadataProvider,
};
pub fn shape<'a>(text: &str, font: &FontRef, location: impl Into<LocationRef<'a>>) -> GlyphBuffer {
let instance = ShaperInstance::from_coords(font, location.into().coords().iter().copied());
let data = ShaperData::new(font);
let shaper = data.shaper(font).instance(Some(&instance)).build();
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.guess_segment_properties();
shaper.shape(buffer, &[])
}
fn get_text_width(
text: &str,
font: &FontRef,
skrifa_font: &SkrifaFontRef,
font_size: f32,
location: LocationRef,
) -> f32 {
let glyphs = shape(text, font, location);
let upem = skrifa_font.metrics(Size::unscaled(), location).units_per_em as f32;
let scale = font_size / upem;
glyphs
.glyph_positions()
.iter()
.map(|pos| pos.x_advance)
.sum::<i32>() as f32
* scale
}
pub fn measure_height_px(
text: String,
font_size: f32,
line_spacing: f32,
width: f32,
font_bytes: &[u8],
location: LocationRef,
) -> Result<f32, Box<dyn std::error::Error>> {
let harf_font_ref = FontRef::new(font_bytes).expect("For font files to be font files!");
let skrifa_font_ref = SkrifaFontRef::new(font_bytes).expect("Fonts to be fonts");
let metrics = skrifa_font_ref.metrics(Size::new(font_size), location);
let line_height = (metrics.ascent - metrics.descent + metrics.leading) * line_spacing;
let mut all_lines = Vec::new();
for text_line in text.lines() {
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text_line.split_whitespace() {
let potential_line = if current_line.is_empty() {
word.to_string()
} else {
format!("{} {}", current_line, word)
};
if get_text_width(
&potential_line,
&harf_font_ref,
&skrifa_font_ref,
font_size,
location,
) <= width
{
current_line = potential_line;
} else {
let should_break_word = current_line.is_empty() || potential_line.contains(" ");
if !current_line.is_empty() {
lines.push(current_line);
}
if should_break_word
&& get_text_width(word, &harf_font_ref, &skrifa_font_ref, font_size, location)
> width
{
let mut temp_word = String::new();
for c in word.chars() {
let next_temp_word = format!("{}{}", temp_word, c);
if !temp_word.is_empty()
&& get_text_width(
&next_temp_word,
&harf_font_ref,
&skrifa_font_ref,
font_size,
location,
) > width
{
lines.push(temp_word);
temp_word = c.to_string();
} else {
temp_word = next_temp_word;
}
}
current_line = temp_word;
} else {
current_line = word.to_string();
}
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
all_lines.extend(lines);
}
let total_height = all_lines.len() as f32 * line_height;
Ok(total_height)
}
#[cfg(test)]
mod tests {
use harfrust::GlyphPosition;
use skrifa::{prelude::LocationRef, FontRef, MetadataProvider};
use crate::{
assert_matches,
measure::{measure_height_px, shape},
testdata,
};
#[test]
fn single_line_height() {
let text = "Hello";
let font_size = 16.0;
let line_spacing = 1.33;
let width = 100.0;
let actual_height = measure_height_px(
text.to_string(),
font_size,
line_spacing,
width,
testdata::ICON_FONT,
LocationRef::default(),
)
.unwrap();
let expected_height = 25.536001f32;
assert_eq!(
actual_height, expected_height,
"Expected\n{expected_height}\n!= Actual\n{actual_height}",
);
}
#[test]
fn two_lines_height() {
let text = "Hello\nWorld!";
let font_size = 16.0;
let line_spacing = 1.33;
let width = 100.0;
let actual_height = measure_height_px(
text.to_string(),
font_size,
line_spacing,
width,
testdata::ICON_FONT,
LocationRef::default(),
)
.unwrap();
let expected_height = 51.072002f32;
assert_eq!(
actual_height, expected_height,
"Expected\n{expected_height}\n!= Actual\n{actual_height}",
);
}
#[test]
fn multiple_lines_with_word_breaking_height() {
let text = "Hello Looooooooooooooong World and some";
let font_size = 16.0;
let line_spacing = 1.33;
let width = 100.0;
let actual_height = measure_height_px(
text.to_string(),
font_size,
line_spacing,
width,
testdata::ICON_FONT,
LocationRef::default(),
)
.unwrap();
let expected_height = 178.75201f32;
assert_eq!(
actual_height, expected_height,
"Expected\n{expected_height}\n!= Actual\n{actual_height}",
);
}
#[test]
fn shaper_uses_location() {
let font = FontRef::new(testdata::INCONSOLATA_FONT).unwrap();
let narrow_shape = shape("A", &font, &font.axes().location([("wdth", 50.0)]));
assert_matches!(
narrow_shape.glyph_positions(),
[GlyphPosition { x_advance: 250, .. }]
);
let wide_shape = shape("A", &font, &font.axes().location([("wdth", 200.0)]));
assert_matches!(
wide_shape.glyph_positions(),
[GlyphPosition {
x_advance: 1000,
..
}]
);
}
}