use std::ops::Range;
use densa_text::{Font, GlyphId, GlyphRun, PositionedGlyph};
use parley::{Alignment, AlignmentOptions, FontData, Layout, PositionedLayoutItem};
use crate::style::{check_width, ParagraphError, TextAlign};
use densa_core::Color;
#[derive(Debug, Clone, PartialEq)]
pub struct LineBox {
pub baseline: f32,
pub top: f32,
pub bottom: f32,
pub left: f32,
pub width: f32,
pub text_range: Range<usize>,
}
pub(crate) type FaceKey = (usize, usize, u32);
pub(crate) fn bytes_key(bytes: &[u8], index: u32) -> FaceKey {
(bytes.as_ptr() as usize, bytes.len(), index)
}
pub(crate) fn face_key(data: &FontData) -> FaceKey {
bytes_key(data.data.data(), data.index)
}
#[derive(Debug)]
pub(crate) struct Face {
pub(crate) key: FaceKey,
pub(crate) font: Font,
}
#[derive(Debug, Clone, Copy)]
struct RunRecord {
face: usize,
px: f32,
color: Color,
glyphs: (u32, u32),
}
pub struct Paragraph {
layout: Layout<Color>,
text_len: usize,
align: TextAlign,
max_width: Option<f32>,
faces: Vec<Face>,
glyphs: Vec<PositionedGlyph>,
runs: Vec<RunRecord>,
lines: Vec<LineBox>,
}
impl std::fmt::Debug for Paragraph {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Paragraph")
.field("align", &self.align)
.field("max_width", &self.max_width)
.field("faces", &self.faces.len())
.field("glyphs", &self.glyphs.len())
.field("runs", &self.runs.len())
.field("lines", &self.lines)
.finish_non_exhaustive()
}
}
impl Paragraph {
pub(crate) fn new(
layout: Layout<Color>,
text_len: usize,
align: TextAlign,
max_width: Option<f32>,
resolve: &mut dyn FnMut(&FontData) -> Option<Font>,
) -> Result<Self, ParagraphError> {
let mut paragraph = Self {
layout,
text_len,
align,
max_width,
faces: Vec::new(),
glyphs: Vec::new(),
runs: Vec::new(),
lines: Vec::new(),
};
paragraph.break_and_flatten(resolve)?;
Ok(paragraph)
}
pub(crate) fn layout(&self) -> &Layout<Color> {
&self.layout
}
pub(crate) fn text_len(&self) -> usize {
self.text_len
}
pub fn width(&self) -> f32 {
self.layout.width()
}
pub fn height(&self) -> f32 {
self.lines.last().map_or(0.0, |line| line.bottom)
}
pub fn max_width(&self) -> Option<f32> {
self.max_width
}
pub fn lines(&self) -> &[LineBox] {
&self.lines
}
pub fn glyph_count(&self) -> usize {
self.glyphs.len()
}
pub fn runs(&self) -> impl ExactSizeIterator<Item = GlyphRun<'_>> + '_ {
self.runs.iter().map(|run| {
let (start, end) = run.glyphs;
GlyphRun {
font: &self.faces[run.face].font,
px: run.px,
color: run.color,
glyphs: &self.glyphs[start as usize..end as usize],
}
})
}
pub fn reflow(&mut self, max_width: Option<f32>) -> Result<(), ParagraphError> {
self.max_width = check_width(max_width)?;
self.break_and_flatten(&mut |_| None)
}
fn break_and_flatten(
&mut self,
resolve: &mut dyn FnMut(&FontData) -> Option<Font>,
) -> Result<(), ParagraphError> {
self.layout.break_all_lines(self.max_width);
let options = AlignmentOptions::default();
self.layout.align(alignment(self.align), options);
self.glyphs.clear();
self.runs.clear();
self.lines.clear();
for line in self.layout.lines() {
let metrics = line.metrics();
self.lines.push(LineBox {
baseline: metrics.baseline,
top: metrics.block_min_coord,
bottom: metrics.block_max_coord,
left: metrics.offset,
width: metrics.advance,
text_range: clamp(line.text_range(), self.text_len),
});
for item in line.items() {
let PositionedLayoutItem::GlyphRun(run) = item else {
continue; };
let data = run.run().font();
let key = face_key(data);
let face = match self.faces.iter().position(|f| f.key == key) {
Some(face) => face,
None => {
let font = resolve(data).ok_or(ParagraphError::UnreadableFont)?;
self.faces.push(Face { key, font });
self.faces.len() - 1
}
};
let start = self.glyphs.len() as u32;
self.glyphs
.extend(run.positioned_glyphs().map(|g| PositionedGlyph {
id: GlyphId(g.id),
x: g.x,
y: g.y,
}));
let end = self.glyphs.len() as u32;
self.runs.push(RunRecord {
face,
px: run.run().font_size(),
color: run.style().brush,
glyphs: (start, end),
});
}
}
Ok(())
}
}
fn clamp(range: Range<usize>, len: usize) -> Range<usize> {
range.start.min(len)..range.end.min(len)
}
fn alignment(align: TextAlign) -> Alignment {
match align {
TextAlign::Start => Alignment::Start,
TextAlign::End => Alignment::End,
TextAlign::Left => Alignment::Left,
TextAlign::Center => Alignment::Center,
TextAlign::Right => Alignment::Right,
TextAlign::Justify => Alignment::Justify,
}
}