use crate::layout::{DEFAULT_TEXT_FAMILY, FontMetrics, LayoutNode, Rect};
use crate::style::{TextOverflow, WhiteSpace};
pub struct TextLine {
pub text: String,
pub width: i32,
pub height: i32,
}
#[inline]
fn snapped_size(font_size_px: f32, scale: f32) -> f32 {
if scale.is_finite() && scale > 0.0 && scale != 1.0 {
(font_size_px * scale).round() / scale
} else {
font_size_px
}
}
fn measure_heuristic(text: &str, font_size_px: f32, scale: f32) -> f32 {
let snapped = snapped_size(font_size_px, scale);
snapped * 0.5 * text.chars().count() as f32
}
use std::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MeasuredText {
pub width: f32,
pub ascent: f32,
pub descent: f32,
}
impl MeasuredText {
#[inline]
pub fn line_extent(&self) -> f32 {
self.ascent + self.descent
}
}
pub type TextMeasurer = fn(&str, f32, &str, f32) -> MeasuredText;
static SKIA_MEASURER: RwLock<Option<TextMeasurer>> = RwLock::new(None);
static CURRENT_SCALE: RwLock<f32> = RwLock::new(1.0);
pub fn set_current_scale(scale: f32) {
if let Ok(mut g) = CURRENT_SCALE.write() {
*g = if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
};
}
}
pub fn current_scale() -> f32 {
CURRENT_SCALE.read().ok().map(|g| *g).unwrap_or(1.0)
}
pub fn set_skia_measurer(f: TextMeasurer) {
if let Ok(mut g) = SKIA_MEASURER.write() {
*g = Some(f);
}
}
fn measure_text_internal(text: &str, font_size_px: f32, font_family: &str, scale: f32) -> f32 {
if let Ok(g) = SKIA_MEASURER.read()
&& let Some(f) = *g
{
return f(text, font_size_px, font_family, scale).width;
}
measure_heuristic(text, font_size_px, scale)
}
pub fn measure_text_metrics(
text: &str,
font_size_px: f32,
font_family: &str,
scale: f32,
) -> MeasuredText {
let (ascent, descent) = FontMetrics::heuristic_vertical(snapped_size(font_size_px, scale));
if let Ok(g) = SKIA_MEASURER.read()
&& let Some(f) = *g
{
let m = f(text, font_size_px, font_family, scale);
if m.ascent.is_finite() && m.descent.is_finite() && m.line_extent() > 0.0 {
return m;
}
return MeasuredText {
width: m.width,
ascent,
descent,
};
}
MeasuredText {
width: measure_heuristic(text, font_size_px, scale),
ascent,
descent,
}
}
pub fn measure_text(text: &str, font_size_px: f32, font_family: &str, scale: f32) -> f32 {
measure_text_internal(text, font_size_px, font_family, scale)
}
pub(crate) fn line_box_height(m: &MeasuredText, strut: &FontMetrics) -> i32 {
m.line_extent().max(strut.ascent + strut.descent).round() as i32
}
#[allow(dead_code)]
fn measure_heuristic_old(text: &str, font_size_px: f32) -> f32 {
font_size_px * 0.6 * text.chars().count() as f32
}
pub(crate) const ELLIPSIS: &str = "\u{2026}";
pub(crate) struct MeasurableFragment {
pub text: String,
pub width: f32,
pub font_size: f32,
pub font_family: String,
}
pub(crate) fn truncate_fragments_with_ellipsis(
fragments: &[MeasurableFragment],
max_width: f32,
scale: f32,
) -> Option<Vec<usize>> {
if fragments.is_empty() {
return None;
}
if max_width <= 0.0 {
return Some(vec![0; fragments.len()]);
}
if fragments.iter().map(|f| f.width).sum::<f32>() <= max_width {
return None;
}
let mut kept = vec![0usize; fragments.len()];
let mut done = 0.0f32;
for (fi, fragment) in fragments.iter().enumerate() {
let snapped = snapped_size(fragment.font_size, scale);
let ellipsis_w =
measure_text_internal(ELLIPSIS, snapped, fragment.font_family.as_str(), scale);
let mut cur = String::new();
let mut width = 0.0f32;
for ch in fragment.text.chars() {
cur.push(ch);
let next =
measure_text_internal(cur.as_str(), snapped, fragment.font_family.as_str(), scale);
if done + next + ellipsis_w > max_width {
break;
}
kept[fi] += 1;
width = next;
}
done += width;
if kept[fi] < fragment.text.chars().count() {
break;
}
}
Some(kept)
}
fn truncate_with_ellipsis(
text: &str,
max_width: f32,
font_size_px: f32,
font_family: &str,
scale: f32,
) -> String {
if max_width <= 0.0 || text.is_empty() {
return String::new();
}
let snapped = snapped_size(font_size_px, scale);
let fragment = MeasurableFragment {
text: text.to_string(),
width: measure_text_internal(text, snapped, font_family, scale),
font_size: font_size_px,
font_family: font_family.to_string(),
};
let Some(kept) =
truncate_fragments_with_ellipsis(std::slice::from_ref(&fragment), max_width, scale)
else {
return text.to_string();
};
let kept: String = text
.chars()
.take(kept.first().copied().unwrap_or(0))
.collect();
format!("{kept}{ELLIPSIS}")
}
pub fn wrap_text_measured(
text: &str,
max_width: f32,
font_size_px: f32,
font_family: &str,
scale: f32,
) -> Vec<(String, f32)> {
wrap_text_with_style(
text,
max_width,
font_size_px,
font_family,
scale,
WhiteSpace::Normal,
TextOverflow::Clip,
)
}
pub fn wrap_text_with_style(
text: &str,
max_width: f32,
font_size_px: f32,
font_family: &str,
scale: f32,
white_space: WhiteSpace,
text_overflow: TextOverflow,
) -> Vec<(String, f32)> {
let snapped = snapped_size(font_size_px, scale);
let ellipsis = text_overflow == TextOverflow::Ellipsis;
let limit = if max_width <= 0.0 {
f32::INFINITY
} else {
max_width
};
let m = |s: &str| measure_text_internal(s, snapped, font_family, scale);
match white_space {
WhiteSpace::Nowrap => {
if ellipsis && m(text) > limit {
let truncated =
truncate_with_ellipsis(text, limit, font_size_px, font_family, scale);
let w = m(truncated.as_str());
vec![(truncated, w)]
} else {
vec![(text.to_string(), m(text))]
}
}
WhiteSpace::Pre => {
let mut out = Vec::new();
for para in text.split('\n') {
if ellipsis && m(para) > limit {
let tr = truncate_with_ellipsis(para, limit, font_size_px, font_family, scale);
let w = m(tr.as_str());
out.push((tr, w));
} else {
out.push((para.to_string(), m(para)));
}
}
if out.is_empty() {
out.push((String::new(), 0.0));
}
out
}
WhiteSpace::PreWrap => {
let mut out = Vec::new();
for para in text.split('\n') {
if para.is_empty() {
out.push((String::new(), 0.0));
continue;
}
let words: Vec<&str> = para.split_whitespace().collect();
if words.is_empty() {
out.push((para.to_string(), m(para)));
continue;
}
let mut current = String::new();
let mut current_w = 0.0;
for word in words {
let candidate = if current.is_empty() {
word.to_string()
} else {
format!("{} {}", current, word)
};
let candidate_w = m(candidate.as_str());
if candidate_w <= limit || current.is_empty() {
current = candidate;
current_w = candidate_w;
} else {
out.push((current.clone(), current_w));
current = word.to_string();
current_w = m(current.as_str());
}
}
if !current.is_empty() {
out.push((current, current_w));
}
}
if out.is_empty() {
out.push((String::new(), 0.0));
}
if ellipsis && out.len() == 1 && m(out[0].0.as_str()) > limit {
let tr = truncate_with_ellipsis(
out[0].0.as_str(),
limit,
font_size_px,
font_family,
scale,
);
let w = m(tr.as_str());
out[0] = (tr, w);
}
out
}
WhiteSpace::Normal | WhiteSpace::PreLine => {
let mut lines: Vec<(String, f32)> = Vec::new();
for para in text.split('\n') {
if para.trim().is_empty() {
if text.contains('\n') {
lines.push((String::new(), 0.0));
}
continue;
}
let mut current = String::new();
let mut current_w = 0.0;
for word in para.split_whitespace() {
let candidate = if current.is_empty() {
word.to_string()
} else {
format!("{} {}", current, word)
};
let candidate_w = m(candidate.as_str());
if candidate_w <= limit || current.is_empty() {
current = candidate;
current_w = candidate_w;
} else {
lines.push((current, current_w));
current = word.to_string();
current_w = m(current.as_str());
}
}
if !current.is_empty() {
lines.push((current, current_w));
}
}
if lines.is_empty() {
lines.push((String::new(), 0.0));
}
if ellipsis && lines.len() == 1 && m(lines[0].0.as_str()) > limit {
let tr = truncate_with_ellipsis(
lines[0].0.as_str(),
limit,
font_size_px,
font_family,
scale,
);
let w = m(tr.as_str());
lines[0] = (tr, w);
} else if ellipsis && lines.len() > 1 {
}
lines
}
}
}
pub fn wrap_text(text: &str, max_width: i32, font_size_px: f32) -> Vec<TextLine> {
let max_w = max_width as f32;
let scale = current_scale();
let measured = wrap_text_measured(text, max_w, font_size_px, DEFAULT_TEXT_FAMILY, scale);
let metrics = FontMetrics::from_font_size(font_size_px);
let line_height = metrics.line_height.round() as i32;
if measured.is_empty() {
return vec![TextLine {
text: String::new(),
width: 0,
height: line_height,
}];
}
let snapped = snapped_size(font_size_px, scale);
measured
.into_iter()
.map(|(s, w)| {
let run = measure_text_metrics(s.as_str(), snapped, DEFAULT_TEXT_FAMILY, scale);
TextLine {
text: s,
width: w.round() as i32,
height: line_box_height(&run, &metrics),
}
})
.collect()
}
pub fn wrap_text_with_options(
text: &str,
max_width: i32,
font_size_px: f32,
font_family: &str,
scale: f32,
white_space: WhiteSpace,
text_overflow: TextOverflow,
) -> Vec<TextLine> {
let measured = wrap_text_with_style(
text,
max_width as f32,
font_size_px,
font_family,
scale,
white_space,
text_overflow,
);
let metrics = FontMetrics::from_font_size(snapped_size(font_size_px, scale));
let line_height = metrics.line_height.round() as i32;
let snapped = snapped_size(font_size_px, scale);
if measured.is_empty() {
return vec![TextLine {
text: String::new(),
width: 0,
height: line_height,
}];
}
measured
.into_iter()
.map(|(s, w)| {
let run = measure_text_metrics(s.as_str(), snapped, font_family, scale);
TextLine {
text: s,
width: w.round() as i32,
height: line_box_height(&run, &metrics),
}
})
.collect()
}
pub fn create_text_nodes(
text: &str,
x: i32,
y: i32,
max_width: i32,
font_size_px: f32,
source_index: Option<usize>,
) -> Vec<LayoutNode> {
let lines = wrap_text(text, max_width, font_size_px);
let mut nodes = Vec::new();
let mut cur_y = y;
for line in lines {
nodes.push(LayoutNode {
rect: Rect {
x,
y: cur_y,
w: line.width,
h: line.height,
},
z_index: 0,
display_none: false,
source_index,
scroll_x: 0,
scroll_y: 0,
clip: None,
stacking_context: false,
scroll_height: line.height,
max_scroll_y: 0,
scrollable: false,
children: vec![],
});
cur_y += line.height;
}
nodes
}