use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping};
use crate::font::{FontId, FontManager};
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub struct TextMetrics {
pub width: f32,
pub height: f32,
pub line_count: usize,
}
impl TextMetrics {
#[inline(always)]
pub fn zero() -> Self {
Self::default()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.width <= 0.0 || self.height <= 0.0
}
}
#[derive(Copy, Clone, Debug)]
pub struct ShapedGlyph {
pub start: usize,
pub end: usize,
pub font_id: FontId,
pub glyph_id: u16,
pub x: f32,
pub y: f32,
pub w: f32,
pub font_size: f32,
pub bidi_level: u8,
}
#[derive(Clone, Debug)]
pub struct ShapedLine {
pub text: String,
pub rtl: bool,
pub line_y: f32,
pub line_top: f32,
pub line_height: f32,
pub line_w: f32,
pub glyphs: Vec<ShapedGlyph>,
}
pub struct Shaper {
buffer: Buffer,
}
impl Shaper {
pub fn new(font_system: &mut FontSystem, metrics: Metrics) -> Self {
Self {
buffer: Buffer::new(font_system, metrics),
}
}
pub fn new_empty(metrics: Metrics) -> Self {
Self {
buffer: Buffer::new_empty(metrics),
}
}
pub fn set_metrics(&mut self, font_size: f32, line_height: f32) {
let metrics = Metrics::new(font_size, line_height);
self.buffer.set_metrics(metrics);
}
pub fn set_size(&mut self, width: Option<f32>, height: Option<f32>) {
self.buffer.set_size(width, height);
}
pub fn set_text(&mut self, text: &str, attrs: &Attrs) {
self.buffer.set_text(text, attrs, Shaping::Advanced, None);
}
pub fn shape(&mut self, font_system: &mut FontSystem) {
self.buffer.shape_until_scroll(font_system, false);
}
pub fn measure(&self) -> TextMetrics {
let mut max_width = 0.0f32;
let mut total_height = 0.0f32;
let mut line_count = 0usize;
for run in self.buffer.layout_runs() {
let run_w = run.line_w.max(
run.glyphs
.iter()
.map(|g| g.x + g.w)
.max_by(f32::total_cmp)
.unwrap_or(0.0),
);
max_width = max_width.max(run_w);
total_height += run.line_height;
line_count += 1;
}
TextMetrics {
width: max_width,
height: total_height,
line_count,
}
}
pub fn lines(&self) -> Vec<ShapedLine> {
self.buffer
.layout_runs()
.map(|run| ShapedLine {
text: run.text.to_string(),
rtl: run.rtl,
line_y: run.line_y,
line_top: run.line_top,
line_height: run.line_height,
line_w: run.line_w,
glyphs: run
.glyphs
.iter()
.map(|g| ShapedGlyph {
start: g.start,
end: g.end,
font_id: FontId(g.font_id),
glyph_id: g.glyph_id,
x: g.x,
y: g.y,
w: g.w,
font_size: g.font_size,
bidi_level: g.level.number(),
})
.collect(),
})
.collect()
}
#[inline(always)]
pub fn buffer(&self) -> &Buffer {
&self.buffer
}
#[inline(always)]
pub fn buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffer
}
pub fn measure_text(
&mut self,
font_system: &mut FontSystem,
text: &str,
attrs: &Attrs,
font_size: f32,
line_height: f32,
max_width: Option<f32>,
) -> TextMetrics {
self.set_metrics(font_size, line_height);
self.set_size(max_width, None);
self.set_text(text, attrs);
self.shape(font_system);
self.measure()
}
}
pub fn measure_text(
manager: &mut FontManager,
text: &str,
font_size: f32,
line_height: f32,
max_width: Option<f32>,
) -> TextMetrics {
measure_text_with_attrs(
manager,
text,
&Attrs::new(),
font_size,
line_height,
max_width,
)
}
pub fn measure_text_with_attrs(
manager: &mut FontManager,
text: &str,
attrs: &Attrs,
font_size: f32,
line_height: f32,
max_width: Option<f32>,
) -> TextMetrics {
let mut shaper = Shaper::new_empty(Metrics::new(font_size, line_height));
shaper.set_size(max_width, None);
shaper.set_text(text, attrs);
shaper.shape(manager.system_mut());
shaper.measure()
}
pub fn shape_text(
manager: &mut FontManager,
text: &str,
font_size: f32,
line_height: f32,
max_width: Option<f32>,
) -> Vec<ShapedLine> {
let mut shaper = Shaper::new_empty(Metrics::new(font_size, line_height));
shaper.set_size(max_width, None);
shaper.set_text(text, &Attrs::new());
shaper.shape(manager.system_mut());
shaper.lines()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_metrics_zero() {
let m = TextMetrics::zero();
assert_eq!(m.width, 0.0);
assert_eq!(m.height, 0.0);
assert_eq!(m.line_count, 0);
assert!(m.is_empty());
}
#[test]
fn text_metrics_non_empty() {
let m = TextMetrics {
width: 100.0,
height: 20.0,
line_count: 1,
};
assert!(!m.is_empty());
}
#[test]
fn shaper_new_empty() {
let shaper = Shaper::new_empty(Metrics::new(16.0, 20.0));
let m = shaper.measure();
assert_eq!(m.line_count, 0);
}
#[test]
fn shaper_measure_empty_text() {
let mut manager = FontManager::with_fonts(std::iter::empty());
let metrics = measure_text(&mut manager, "", 16.0, 20.0, None);
assert!(
metrics.line_count <= 1,
"empty text should have 0 or 1 lines"
);
assert_eq!(metrics.width, 0.0);
}
#[test]
fn shaper_measure_simple_text() {
let mut manager = FontManager::new();
let metrics = measure_text(&mut manager, "Hello", 16.0, 20.0, None);
assert!(
metrics.line_count >= 1,
"should have at least 1 line, got {}",
metrics.line_count
);
if metrics.width > 0.0 {
assert!(metrics.height > 0.0);
}
}
#[test]
fn shaper_measure_with_wrapping() {
let mut manager = FontManager::new();
let metrics = measure_text(
&mut manager,
"The quick brown fox jumps over the lazy dog repeatedly",
16.0,
20.0,
Some(50.0),
);
if metrics.width > 0.0 {
assert!(
metrics.line_count > 1,
"expected wrapping, got {} lines",
metrics.line_count
);
}
}
#[test]
fn shaper_measure_multilingual() {
let mut manager = FontManager::new();
let texts = ["Hello", "مرحبا", "你好", "Привет", "こんにちは"];
for text in &texts {
let metrics = measure_text(&mut manager, text, 16.0, 20.0, None);
let _ = metrics;
}
}
#[test]
fn shape_text_returns_lines() {
let mut manager = FontManager::new();
let lines = shape_text(&mut manager, "Hello World", 16.0, 20.0, None);
if !lines.is_empty() {
let first = &lines[0];
assert!(!first.text.is_empty());
}
}
#[test]
fn shaped_glyph_bidi_level() {
let mut manager = FontManager::new();
let lines = shape_text(&mut manager, "Hello", 16.0, 20.0, None);
for line in &lines {
for glyph in &line.glyphs {
assert_eq!(
glyph.bidi_level % 2,
0,
"LTR text should have even bidi level"
);
}
}
}
#[test]
fn shaper_reuse() {
let mut manager = FontManager::new();
let mut shaper = Shaper::new(manager.system_mut(), Metrics::new(16.0, 20.0));
shaper.set_text("First", &Attrs::new());
shaper.shape(manager.system_mut());
let m1 = shaper.measure();
shaper.set_text("Second text that is longer", &Attrs::new());
shaper.shape(manager.system_mut());
let m2 = shaper.measure();
let _ = (m1, m2);
}
#[test]
fn shaper_set_size_affects_wrapping() {
let mut manager = FontManager::new();
let mut shaper = Shaper::new(manager.system_mut(), Metrics::new(16.0, 20.0));
shaper.set_size(None, None);
shaper.set_text("The quick brown fox", &Attrs::new());
shaper.shape(manager.system_mut());
let unbounded = shaper.measure();
shaper.set_size(Some(30.0), None);
shaper.set_text("The quick brown fox", &Attrs::new());
shaper.shape(manager.system_mut());
let bounded = shaper.measure();
if unbounded.line_count > 0 && bounded.line_count > 0 {
assert!(
bounded.line_count >= unbounded.line_count,
"bounded width should have >= lines than unbounded"
);
}
}
}