use std::collections::HashMap;
use std::ops::Range;
use unicode_bidi::BidiInfo;
use valo_geometry::{Color, Rect};
use crate::font::{FaceSet, FontCollection, FontDemand, FontId};
use crate::shape::{shape_runs, ShapedRun};
use crate::style::{ParagraphStyle, TextDirection, TextStyle};
use crate::wrap::{place_lines, wrap_lines, Wrapped};
#[derive(Clone, Copy, Debug)]
pub struct PlacedGlyph {
pub id: u32,
pub x: f32,
pub y: f32,
pub cluster: usize,
pub advance: f32,
}
#[derive(Clone, Debug)]
pub struct PlacedRun {
pub font: FontId,
pub size: f32,
pub color: Color,
pub decoration: Option<crate::style::Decoration>,
pub shadows: Vec<crate::style::Shadow>,
pub glyphs: Vec<PlacedGlyph>,
pub rtl: bool,
pub bounds: Rect,
pub ink: Rect,
}
#[derive(Clone, Debug)]
pub struct Line {
pub runs: Vec<PlacedRun>,
pub baseline: f32,
pub ascent: f32,
pub descent: f32,
pub left: f32,
pub width: f32,
pub range: Range<usize>,
}
#[derive(Clone, Debug)]
pub(crate) struct Layout {
pub max_width: f32,
pub lines: Vec<Line>,
pub width: f32,
pub height: f32,
pub truncated: bool,
pub wrapped: Wrapped,
}
#[derive(Clone)]
pub struct Paragraph {
faces: FaceSet,
text: String,
style: ParagraphStyle,
spans: Vec<(Range<usize>, TextStyle)>,
shaped: Vec<ShapedRun>,
layout: Option<Layout>,
empty_metrics: Option<(f32, f32, f32)>,
demand: FontDemand,
}
impl Paragraph {
pub fn layout(&mut self, max_width: f32) {
if self
.layout
.as_ref()
.is_some_and(|l| l.max_width == max_width)
{
return;
}
let bidi = BidiInfo::new(&self.text, base_level(&self.style));
let wrapped = wrap_lines(
&self.text,
&self.shaped,
max_width,
self.style.max_lines,
self.style.preserve_trailing_whitespace,
);
self.layout = Some(self.place(&bidi, wrapped, max_width));
}
pub fn update_color(&mut self, span: usize, color: Color) {
let Some((range, style)) = self.spans.get_mut(span) else {
return;
};
style.color = color;
let range = range.clone();
for run in &mut self.shaped {
if run.range.start >= range.start && run.range.end <= range.end {
run.color = color;
}
}
if let Some(prior) = self.layout.take() {
let bidi = BidiInfo::new(&self.text, base_level(&self.style));
self.layout = Some(self.place(&bidi, prior.wrapped, prior.max_width));
}
}
fn place(&self, bidi: &BidiInfo, wrapped: Wrapped, max_width: f32) -> Layout {
place_lines(
&self.faces,
&self.text,
&self.shaped,
bidi,
wrapped,
max_width,
&self.style,
self.empty_line_metrics(),
)
}
fn empty_line_metrics(&self) -> Option<(f32, f32, f32)> {
self.empty_metrics
}
pub fn lines(&self) -> &[Line] {
self.layout.as_ref().map_or(&[], |l| &l.lines)
}
pub fn width(&self) -> f32 {
self.layout.as_ref().map_or(0.0, |l| l.width)
}
pub fn advance(&self) -> f32 {
self.lines()
.iter()
.map(|line| line.width)
.reduce(f32::max)
.unwrap_or(0.0)
}
pub fn last_glyph_origin(&self) -> Option<f32> {
self.lines()
.iter()
.flat_map(|line| &line.runs)
.flat_map(|run| &run.glyphs)
.next_back()
.map(|glyph| glyph.x)
}
pub fn height(&self) -> f32 {
self.layout.as_ref().map_or(0.0, |l| l.height)
}
pub fn ink_bounds(&self) -> Option<Rect> {
let mut result: Option<Rect> = None;
let mut rasterizer = crate::raster::Rasterizer::new();
let mut color_bounds = HashMap::<(FontId, u32, u32), Option<Rect>>::new();
for run in self.lines().iter().flat_map(|line| &line.runs) {
let font = self.faces.get(run.font);
for glyph in &run.glyphs {
let key = (run.font, glyph.id, run.size.to_bits());
let color = *color_bounds
.entry(key)
.or_insert_with(|| rasterizer.color_bounds(font, glyph.id, run.size));
let bounds = if let Some(bounds) = color {
bounds
} else if let Some(path) = crate::raster::glyph_path(font, glyph.id, run.size) {
path.tight_bounds()
} else {
continue;
};
let placed = Rect::new(
bounds.x + glyph.x,
bounds.y + glyph.y,
bounds.width,
bounds.height,
);
result = Some(result.map_or(placed, |current| current.union(&placed)));
}
}
result
}
pub fn primary_font(&self) -> Option<(&crate::font::Font, f32)> {
if let Some(run) = self.lines().first().and_then(|line| line.runs.first()) {
return Some((self.faces.get(run.font), run.size));
}
let (_, style) = self.spans.first()?;
if self.faces.is_empty() {
return None;
}
let attributes = style.font_attrs();
let identifier = self.faces.resolve(&style.families, attributes, ' ');
Some((self.faces.get(identifier), style.size))
}
pub fn bounds(&self) -> Rect {
Rect::new(0.0, 0.0, self.width(), self.height())
}
pub fn truncated(&self) -> bool {
self.layout.as_ref().is_some_and(|l| l.truncated)
}
pub fn min_intrinsic_width(&self) -> f32 {
self.layout
.as_ref()
.map_or(0.0, |l| l.wrapped.min_intrinsic)
}
pub fn max_intrinsic_width(&self) -> f32 {
self.layout
.as_ref()
.map_or(0.0, |l| l.wrapped.max_intrinsic)
}
pub fn longest_line(&self) -> f32 {
self.width()
}
}
pub struct ParagraphBuilder<'a> {
fonts: &'a mut FontCollection,
style: ParagraphStyle,
text: String,
spans: Vec<(Range<usize>, TextStyle)>,
}
impl<'a> ParagraphBuilder<'a> {
pub fn new(fonts: &'a mut FontCollection) -> Self {
Self {
fonts,
style: ParagraphStyle::default(),
text: String::new(),
spans: Vec::new(),
}
}
pub fn style(&mut self, style: ParagraphStyle) -> &mut Self {
self.style = style;
self
}
pub fn add_text(&mut self, text: &str, style: &TextStyle) -> &mut Self {
let start = self.text.len();
self.text.push_str(text);
self.spans.push((start..self.text.len(), style.clone()));
self
}
pub fn build(&mut self) -> Paragraph {
let bidi = BidiInfo::new(&self.text, base_level(&self.style));
let mut demand = FontDemand::default();
let shaped = shape_runs(self.fonts, &self.text, &self.spans, &bidi, &mut demand);
let faces = self.fonts.faces().clone();
let empty_metrics = empty_line_metrics(&faces, self.spans.first());
Paragraph {
faces,
text: std::mem::take(&mut self.text),
style: std::mem::take(&mut self.style),
spans: std::mem::take(&mut self.spans),
shaped,
layout: None,
empty_metrics,
demand,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PositionWithAffinity {
pub offset: usize,
pub downstream: bool,
}
#[derive(Clone, Debug)]
pub struct LineMetrics {
pub range: Range<usize>,
pub baseline: f32,
pub ascent: f32,
pub descent: f32,
pub left: f32,
pub width: f32,
}
impl Paragraph {
pub fn text(&self) -> &str {
&self.text
}
pub fn demand(&self) -> &FontDemand {
&self.demand
}
pub fn faces(&self) -> &FaceSet {
&self.faces
}
pub fn line_metrics(&self) -> Vec<LineMetrics> {
self.lines()
.iter()
.map(|line| LineMetrics {
range: line.range.clone(),
baseline: line.baseline,
ascent: line.ascent,
descent: line.descent,
left: line.left,
width: line.width,
})
.collect()
}
pub fn caret_for_offset(&self, offset: usize) -> Rect {
let Some(line) = self.line_for_offset(offset) else {
return Rect::default();
};
let x = caret_x(line, offset);
Rect::new(
x,
line.baseline - line.ascent,
0.0,
line.ascent + line.descent,
)
}
pub fn glyph_position_at(&self, p: valo_geometry::Point) -> PositionWithAffinity {
let Some(line) = self.line_at_y(p.y) else {
return PositionWithAffinity {
offset: 0,
downstream: true,
};
};
let mut best = PositionWithAffinity {
offset: line.range.start,
downstream: true,
};
let mut best_dx = f32::MAX;
for run in &line.runs {
for g in &run.glyphs {
let (lead_x, trail_x) = if run.rtl {
(g.x + g.advance, g.x)
} else {
(g.x, g.x + g.advance)
};
let leading = (p.x - lead_x).abs();
if leading < best_dx {
best_dx = leading;
best = PositionWithAffinity {
offset: g.cluster,
downstream: true,
};
}
let trailing = (p.x - trail_x).abs();
if trailing < best_dx {
best_dx = trailing;
best = PositionWithAffinity {
offset: self.cluster_end(g.cluster),
downstream: false,
};
}
}
}
best
}
pub fn rects_for_range(&self, range: Range<usize>) -> Vec<Rect> {
let mut out = Vec::new();
for line in self.lines() {
if range.end <= line.range.start || range.start >= line.range.end {
continue;
}
for run in &line.runs {
let cells: Vec<&PlacedGlyph> = run
.glyphs
.iter()
.filter(|g| g.cluster >= range.start && g.cluster < range.end)
.collect();
let Some(first) = cells.first() else {
continue;
};
let x0 = cells.iter().map(|g| g.x).fold(first.x, f32::min);
let x1 = cells
.iter()
.map(|g| g.x + g.advance)
.fold(first.x + first.advance, f32::max);
out.push(Rect::from_ltrb(
x0,
line.baseline - line.ascent,
x1,
line.baseline + line.descent,
));
}
}
out
}
pub fn word_boundary(&self, offset: usize) -> Range<usize> {
use unicode_segmentation::UnicodeSegmentation;
for (start, word) in self.text.split_word_bound_indices() {
if offset < start + word.len() {
return start..start + word.len();
}
}
self.text.len()..self.text.len()
}
fn line_for_offset(&self, offset: usize) -> Option<&Line> {
let lines = self.lines();
lines
.iter()
.find(|l| l.range.contains(&offset))
.or(lines.last())
}
fn line_at_y(&self, y: f32) -> Option<&Line> {
let lines = self.lines();
lines
.iter()
.find(|l| y <= l.baseline + l.descent)
.or(lines.last())
}
fn cluster_end(&self, cluster: usize) -> usize {
let next_on_line = self
.line_for_offset(cluster)
.into_iter()
.flat_map(|l| l.runs.iter())
.flat_map(|r| r.glyphs.iter())
.map(|g| g.cluster)
.filter(|&c| c > cluster)
.min();
next_on_line.unwrap_or_else(|| self.next_grapheme(cluster))
}
fn next_grapheme(&self, offset: usize) -> usize {
use unicode_segmentation::UnicodeSegmentation;
self.text[offset..]
.graphemes(true)
.next()
.map_or(self.text.len(), |g| offset + g.len())
}
}
fn caret_x(line: &Line, offset: usize) -> f32 {
let mut before: Option<(usize, f32)> = None;
for run in &line.runs {
for g in &run.glyphs {
let (lead_x, trail_x) = if run.rtl {
(g.x + g.advance, g.x)
} else {
(g.x, g.x + g.advance)
};
if g.cluster == offset {
return lead_x;
}
if g.cluster < offset && before.is_none_or(|(c, _)| g.cluster > c) {
before = Some((g.cluster, trail_x));
}
}
}
before.map_or(line.left, |(_, x)| x)
}
fn base_level(style: &ParagraphStyle) -> Option<unicode_bidi::Level> {
style.direction.map(|direction| match direction {
TextDirection::Ltr => unicode_bidi::Level::ltr(),
TextDirection::Rtl => unicode_bidi::Level::rtl(),
})
}
fn empty_line_metrics(
faces: &FaceSet,
first_span: Option<&(std::ops::Range<usize>, TextStyle)>,
) -> Option<(f32, f32, f32)> {
let (_, style) = first_span?;
if faces.is_empty() {
return None;
}
let attrs = style.font_attrs();
let id = faces.resolve(&style.families, attrs, ' ');
Some(crate::wrap::style_heights(
faces.get(id),
style.size,
style.height,
))
}