use crate::app_state::with_app_state;
use crate::layout::Size;
use crate::widgets::font::{FontFamily, FontWeight};
use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping};
use rustc_hash::FxHashMap;
const SMALLEST_SHAPEABLE: f32 = 0.01;
pub(crate) fn shapeable_metrics(font_size: f32) -> (f32, f32) {
let size = if font_size.is_finite() && font_size >= SMALLEST_SHAPEABLE {
font_size
} else {
SMALLEST_SHAPEABLE
};
(size, size * LINE_HEIGHT_RATIO)
}
const LINE_HEIGHT_RATIO: f32 = 1.2;
use std::hash::{Hash, Hasher};
#[derive(Hash, Eq, PartialEq, Clone, Copy)]
struct MeasureCacheKey {
content_hash: u64,
text_len: u32,
font_size_bits: u32,
font_weight: FontWeight,
max_width_bits: Option<u32>,
}
impl MeasureCacheKey {
fn new(
text: &str,
font_size: f32,
max_width: Option<f32>,
font_family: FontFamily,
font_weight: FontWeight,
) -> Self {
let mut hasher = rustc_hash::FxHasher::default();
text.hash(&mut hasher);
font_family.hash(&mut hasher);
Self {
content_hash: hasher.finish(),
text_len: text.len() as u32,
font_size_bits: font_size.to_bits(),
font_weight,
max_width_bits: max_width.map(|w| w.to_bits()),
}
}
}
const MEASURE_CACHE_CAP: usize = 8192;
#[derive(Clone, Copy, Debug)]
pub struct Measured {
pub size: Size,
pub baseline: f32,
}
pub struct TextMeasurer {
font_system: FontSystem,
measure_cache: FxHashMap<MeasureCacheKey, Measured>,
}
impl TextMeasurer {
pub fn new() -> Self {
let mut font_system = FontSystem::new();
for data in crate::get_registered_fonts() {
font_system
.db_mut()
.load_font_source(cosmic_text::fontdb::Source::Binary(data));
}
Self {
font_system,
measure_cache: FxHashMap::default(),
}
}
pub fn measure(&mut self, text: &str, font_size: f32, max_width: Option<f32>) -> Size {
self.measure_styled(
text,
font_size,
max_width,
FontFamily::default(),
FontWeight::NORMAL,
)
}
pub fn measure_styled(
&mut self,
text: &str,
font_size: f32,
max_width: Option<f32>,
font_family: FontFamily,
font_weight: FontWeight,
) -> Size {
self.measure_full(text, font_size, max_width, font_family, font_weight)
.size
}
pub fn measure_full(
&mut self,
text: &str,
font_size: f32,
max_width: Option<f32>,
font_family: FontFamily,
font_weight: FontWeight,
) -> Measured {
let cache_key = MeasureCacheKey::new(text, font_size, max_width, font_family, font_weight);
if let Some(&cached) = self.measure_cache.get(&cache_key) {
return cached;
}
let measured = {
let buffer = self.shape(text, font_size, max_width, font_family, font_weight);
let mut width = 0.0f32;
let mut height = 0.0f32;
let mut baseline = None;
for run in buffer.layout_runs() {
width = width.max(run.line_w);
height += run.line_height;
baseline.get_or_insert(run.line_y);
}
if height == 0.0 {
height = font_size * 1.2;
}
Measured {
size: Size::new(width, height),
baseline: baseline.unwrap_or(font_size),
}
};
if self.measure_cache.len() >= MEASURE_CACHE_CAP {
self.measure_cache.clear();
}
self.measure_cache.insert(cache_key, measured);
measured
}
fn shape(
&mut self,
text: &str,
font_size: f32,
max_width: Option<f32>,
font_family: FontFamily,
font_weight: FontWeight,
) -> Buffer {
let (size, line_height) = shapeable_metrics(font_size);
let metrics = Metrics::new(size, line_height);
let mut buffer = Buffer::new(&mut self.font_system, metrics);
buffer.set_size(&mut self.font_system, max_width, None);
buffer.set_text(
&mut self.font_system,
text,
&Attrs::new()
.family(font_family.to_cosmic())
.weight(font_weight.to_cosmic()),
Shaping::Advanced,
None,
);
buffer.shape_until_scroll(&mut self.font_system, true);
buffer
}
pub fn char_positions_styled(
&mut self,
text: &str,
font_size: f32,
font_family: FontFamily,
font_weight: FontWeight,
) -> Vec<f32> {
let char_count = text.chars().count();
let mut positions = vec![0.0f32; char_count + 1];
if text.is_empty() {
return positions;
}
let mut char_index_at_byte = vec![usize::MAX; text.len() + 1];
for (char_idx, (byte_idx, _)) in text.char_indices().enumerate() {
char_index_at_byte[byte_idx] = char_idx;
}
char_index_at_byte[text.len()] = char_count;
let buffer = self.shape(text, font_size, None, font_family, font_weight);
let mut total_width = 0.0f32;
let mut any_glyphs = false;
for run in buffer.layout_runs() {
for glyph in run.glyphs {
if let Some(&char_idx) = char_index_at_byte.get(glyph.start)
&& char_idx != usize::MAX
{
positions[char_idx] = glyph.x;
any_glyphs = true;
}
}
total_width = total_width.max(run.line_w);
}
positions[char_count] = total_width;
if any_glyphs {
for i in 1..char_count {
if positions[i] == 0.0 && positions[i - 1] > 0.0 {
positions[i] = positions[i - 1];
}
}
}
positions
}
pub fn measure_to_char(&mut self, text: &str, font_size: f32, char_index: usize) -> f32 {
self.measure_to_char_styled(
text,
font_size,
char_index,
FontFamily::default(),
FontWeight::NORMAL,
)
}
pub fn measure_to_char_styled(
&mut self,
text: &str,
font_size: f32,
char_index: usize,
font_family: FontFamily,
font_weight: FontWeight,
) -> f32 {
if char_index == 0 || text.is_empty() {
return 0.0;
}
let byte_pos = text
.char_indices()
.nth(char_index)
.map(|(i, _)| i)
.unwrap_or(text.len());
let prefix = &text[..byte_pos];
self.measure_styled(prefix, font_size, None, font_family, font_weight)
.width
}
pub fn char_from_x(&mut self, text: &str, font_size: f32, x: f32) -> usize {
self.char_from_x_styled(
text,
font_size,
x,
FontFamily::default(),
FontWeight::NORMAL,
)
}
pub fn char_from_x_styled(
&mut self,
text: &str,
font_size: f32,
x: f32,
font_family: FontFamily,
font_weight: FontWeight,
) -> usize {
if text.is_empty() || x <= 0.0 {
return 0;
}
let char_count = text.chars().count();
let total_width = self
.measure_styled(text, font_size, None, font_family, font_weight)
.width;
if x >= total_width {
return char_count;
}
let mut left = 0;
let mut right = char_count;
while left < right {
let mid = (left + right) / 2;
let width = self.measure_to_char_styled(text, font_size, mid, font_family, font_weight);
if width < x {
left = mid + 1;
} else {
right = mid;
}
}
if left > 0 {
let prev_width =
self.measure_to_char_styled(text, font_size, left - 1, font_family, font_weight);
let curr_width =
self.measure_to_char_styled(text, font_size, left, font_family, font_weight);
if (x - prev_width) < (curr_width - x) {
return left - 1;
}
}
left.min(char_count)
}
}
fn with_measurer<R>(f: impl FnOnce(&mut TextMeasurer) -> R) -> R {
with_app_state(|app| {
let mut measurer = app.text_measurer.borrow_mut();
f(measurer.get_or_insert_with(TextMeasurer::new))
})
}
pub fn measure_text(text: &str, font_size: f32, max_width: Option<f32>) -> Size {
with_measurer(|m| m.measure(text, font_size, max_width))
}
pub fn measure_text_styled(
text: &str,
font_size: f32,
max_width: Option<f32>,
font_family: FontFamily,
font_weight: FontWeight,
) -> Size {
with_measurer(|m| m.measure_styled(text, font_size, max_width, font_family, font_weight))
}
pub fn measure_text_full(
text: &str,
font_size: f32,
max_width: Option<f32>,
font_family: FontFamily,
font_weight: FontWeight,
) -> Measured {
with_measurer(|m| m.measure_full(text, font_size, max_width, font_family, font_weight))
}
pub fn measure_text_to_char(text: &str, font_size: f32, char_index: usize) -> f32 {
with_measurer(|m| m.measure_to_char(text, font_size, char_index))
}
pub fn measure_text_to_char_styled(
text: &str,
font_size: f32,
char_index: usize,
font_family: FontFamily,
font_weight: FontWeight,
) -> f32 {
with_measurer(|m| {
m.measure_to_char_styled(text, font_size, char_index, font_family, font_weight)
})
}
pub fn measure_char_positions_styled(
text: &str,
font_size: f32,
font_family: FontFamily,
font_weight: FontWeight,
) -> Vec<f32> {
with_measurer(|m| m.char_positions_styled(text, font_size, font_family, font_weight))
}
pub fn char_index_from_x(text: &str, font_size: f32, x: f32) -> usize {
with_measurer(|m| m.char_from_x(text, font_size, x))
}
pub fn char_index_from_x_styled(
text: &str,
font_size: f32,
x: f32,
font_family: FontFamily,
font_weight: FontWeight,
) -> usize {
with_measurer(|m| m.char_from_x_styled(text, font_size, x, font_family, font_weight))
}
#[cfg(test)]
mod baseline_tests {
use super::*;
#[test]
fn a_size_that_cannot_be_shaped_still_answers() {
let (done, waiting) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for font_size in [-1.0f32, 0.0, -0.0, f32::NAN, f32::NEG_INFINITY] {
let m = measure_text_full(
"a line of words",
font_size,
Some(100.0),
FontFamily::default(),
FontWeight::NORMAL,
);
assert!(
m.size.width.is_finite() && m.size.height.is_finite(),
"a {font_size} size measured as {:?}",
m.size
);
}
let _ = done.send(());
});
waiting
.recv_timeout(std::time::Duration::from_secs(10))
.expect("a font size that cannot be shaped has to come back, not spin");
}
#[test]
fn a_baseline_sits_inside_the_line_box() {
for font_size in [12.0f32, 16.0, 24.0, 48.0] {
let m = measure_text_full(
"Hxgjp",
font_size,
None,
FontFamily::default(),
FontWeight::NORMAL,
);
assert!(
m.baseline > m.size.height * 0.5 && m.baseline < m.size.height,
"at {font_size}px the baseline is {} of a {} line — \
that is not a baseline",
m.baseline,
m.size.height
);
}
}
}