use crate::glyphs::ellipsis_glyph;
use crate::style::TextStyle;
use crate::system::TextSystem;
use parley::PositionedLayoutItem;
use parley::StyleProperty;
use parley::style::LineHeight;
use smudge_core::geometry::Size;
pub struct TextLayout {
pub(crate) layout: parley::Layout<()>,
pub(crate) truncation: Option<Truncation>,
fallback_ellipsis: Option<Option<Ellipsis>>,
}
#[derive(Clone, Debug)]
pub(crate) struct Truncation {
pub(crate) kept_lines: usize,
pub(crate) cut: Option<Cut>,
pub(crate) width: f32,
pub(crate) height: f32,
}
#[derive(Clone, Debug)]
pub(crate) struct Cut {
pub(crate) run: usize,
pub(crate) glyphs: usize,
pub(crate) x: f32,
pub(crate) baseline: f32,
pub(crate) ellipsis: Option<Ellipsis>,
}
#[derive(Clone, Debug)]
pub(crate) struct Ellipsis {
pub(crate) font: parley::FontData,
pub(crate) size: f32,
pub(crate) glyph: u32,
pub(crate) dots: u32,
pub(crate) advance: f32,
}
impl Ellipsis {
pub(crate) fn width(&self) -> f32 {
self.advance * self.dots as f32
}
}
struct Candidate {
run: usize,
glyphs: usize,
x: f32,
end: f32,
}
impl TextLayout {
pub fn new(text: &str, style: &TextStyle, max_width: Option<f32>) -> TextLayout {
TextSystem::with(|system| {
Self::build(
text,
style,
max_width,
&mut system.fonts,
&mut system.layouts,
)
})
}
pub(crate) fn build(
text: &str,
style: &TextStyle,
max_width: Option<f32>,
fonts: &mut parley::FontContext,
layouts: &mut parley::LayoutContext<()>,
) -> TextLayout {
#[cfg(test)]
SHAPES.with(|shapes| shapes.set(shapes.get() + 1));
let mut builder = layouts.ranged_builder(fonts, text, 1.0, false);
builder.push_default(StyleProperty::FontFamily(style.family.clone()));
builder.push_default(StyleProperty::FontSize(style.size));
builder.push_default(StyleProperty::FontWeight(style.weight));
builder.push_default(StyleProperty::LineHeight(LineHeight::FontSizeRelative(
style.line_height,
)));
let mut layout = builder.build(text);
layout.break_all_lines(max_width);
TextLayout {
layout,
truncation: None,
fallback_ellipsis: None,
}
}
pub(crate) fn rebreak(&mut self, max_width: Option<f32>) {
self.layout.break_all_lines(max_width);
self.truncation = None;
}
pub(crate) fn truncate(
&mut self,
max_lines: Option<u32>,
width: Option<f32>,
style: &TextStyle,
) {
self.truncate_resolving(max_lines, width, style, &mut ellipsis_glyph);
}
pub(crate) fn truncate_resolving(
&mut self,
max_lines: Option<u32>,
width: Option<f32>,
style: &TextStyle,
own: &mut dyn FnMut(&parley::FontData, f32) -> Option<Ellipsis>,
) {
let mut fallback = self.fallback_ellipsis.take();
self.truncate_with(max_lines, width, &mut |font, size| {
own(font, size).or_else(|| {
fallback
.get_or_insert_with(|| style_ellipsis(style))
.clone()
})
});
self.fallback_ellipsis = fallback;
}
pub(crate) fn truncate_with(
&mut self,
max_lines: Option<u32>,
width: Option<f32>,
ellipsis_for: &mut dyn FnMut(&parley::FontData, f32) -> Option<Ellipsis>,
) {
self.truncation = None;
let Some(max_lines) = max_lines else {
return;
};
let kept_lines = (max_lines as usize).min(self.layout.len());
if kept_lines == 0 {
if !self.layout.is_empty() {
self.truncation = Some(Truncation {
kept_lines: 0,
cut: None,
width: 0.0,
height: 0.0,
});
}
return;
}
let Some(line) = self.layout.get(kept_lines - 1) else {
return;
};
let overflows = width.is_some_and(|width| line_width(&line) > width);
if self.layout.len() == kept_lines && !overflows {
return;
}
let height = self.kept_height(kept_lines);
if self.layout.is_rtl() {
self.truncation = Some(Truncation {
kept_lines,
cut: None,
width: self.kept_width(kept_lines),
height,
});
return;
}
let Some((cut, end)) = cut_line(&line, width.unwrap_or(f32::INFINITY), ellipsis_for) else {
self.truncation = Some(Truncation {
kept_lines,
cut: None,
width: self.kept_width(kept_lines),
height,
});
return;
};
self.truncation = Some(Truncation {
kept_lines,
cut: Some(cut),
width: self.kept_width(kept_lines - 1).max(end),
height,
});
}
fn kept_width(&self, kept_lines: usize) -> f32 {
self.layout
.lines()
.take(kept_lines)
.map(|line| line_width(&line))
.fold(0.0f32, f32::max)
}
fn kept_height(&self, kept_lines: usize) -> f32 {
self.layout
.lines()
.take(kept_lines)
.map(|line| line.metrics().line_height)
.sum()
}
pub fn size(&self) -> Size {
match &self.truncation {
Some(truncation) => Size {
width: truncation.width,
height: truncation.height,
},
None => Size {
width: self.layout.width(),
height: self.layout.height(),
},
}
}
pub fn line_count(&self) -> usize {
match &self.truncation {
Some(truncation) => truncation.kept_lines,
None => self.layout.len(),
}
}
pub fn glyph_runs(&self) -> impl Iterator<Item = parley::GlyphRun<'_, ()>> + Clone + '_ {
let kept_lines = self
.truncation
.as_ref()
.map_or(usize::MAX, |truncation| truncation.kept_lines);
self.layout.lines().take(kept_lines).flat_map(|line| {
line.items().filter_map(|item| match item {
PositionedLayoutItem::GlyphRun(run) => Some(run),
PositionedLayoutItem::InlineBox(_) => None,
})
})
}
}
fn line_width(line: &parley::Line<'_, ()>) -> f32 {
let metrics = line.metrics();
metrics.advance - metrics.hanging_advance
}
fn cut_line(
line: &parley::Line<'_, ()>,
width: f32,
ellipsis_for: &mut dyn FnMut(&parley::FontData, f32) -> Option<Ellipsis>,
) -> Option<(Cut, f32)> {
let mut best: Option<Candidate> = None;
let mut best_ellipsis: Option<Ellipsis> = None;
let mut spent = 0.0f32;
let mut run_index = 0usize;
'line: for item in line.items() {
let PositionedLayoutItem::GlyphRun(glyph_run) = item else {
continue;
};
let run = glyph_run.run();
let ellipsis = ellipsis_for(run.font(), run.font_size());
let dots = ellipsis.as_ref().map_or(0.0, Ellipsis::width);
if best.is_none() {
best = Some(Candidate {
run: run_index,
glyphs: 0,
x: glyph_run.offset(),
end: dots,
});
best_ellipsis.clone_from(&ellipsis);
}
let run_start = spent;
let rtl = run.is_rtl();
let mut glyphs = 0usize;
let mut unit_glyphs = 0usize;
let mut unit_advance = 0.0f32;
let mut unit_blank = true;
let mut clusters = run.visual_clusters().peekable();
while let Some(cluster) = clusters.next() {
unit_glyphs += cluster.glyphs().count();
unit_advance += cluster.advance();
unit_blank &= cluster.is_space_or_nbsp() || cluster.is_hard_line_break();
let complete = if rtl {
!cluster.is_ligature_continuation()
} else {
clusters
.peek()
.is_none_or(|next| !next.is_ligature_continuation())
};
if !complete {
continue;
}
if spent + unit_advance > width {
break 'line;
}
spent += unit_advance;
glyphs += unit_glyphs;
if !unit_blank && spent + dots <= width {
if best.as_ref().is_some_and(|best| best.run != run_index) {
best_ellipsis.clone_from(&ellipsis);
}
best = Some(Candidate {
run: run_index,
glyphs,
x: glyph_run.offset() + (spent - run_start),
end: spent + dots,
});
}
unit_glyphs = 0;
unit_advance = 0.0;
unit_blank = true;
}
run_index += 1;
}
let best = best?;
Some((
Cut {
run: best.run,
glyphs: best.glyphs,
x: best.x,
baseline: line.metrics().baseline,
ellipsis: best_ellipsis,
},
best.end,
))
}
pub(crate) fn style_ellipsis(style: &TextStyle) -> Option<Ellipsis> {
let layout = TextLayout::new("\u{2026}", style, None);
let run = layout.glyph_runs().next()?;
ellipsis_glyph(run.run().font(), run.run().font_size())
}
#[cfg(test)]
thread_local! {
pub(crate) static SHAPES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
#[path = "layout_tests.rs"]
mod tests;