use std::ops::Range;
use unicode_bidi::BidiInfo;
use crate::font::FaceSet;
use crate::paragraph::{Layout, Line, PlacedGlyph, PlacedRun};
use crate::shape::{shape_isolated, ShapedRun};
use crate::style::{ParagraphStyle, TextAlign};
#[derive(Clone, Debug)]
pub struct Wrapped {
pub ranges: Vec<Range<usize>>,
pub truncated: bool,
pub min_intrinsic: f32,
pub max_intrinsic: f32,
}
pub(crate) struct Measure {
bytes: Vec<usize>,
prefix: Vec<f32>,
}
impl Measure {
pub(crate) fn new(runs: &[ShapedRun]) -> Self {
let mut widths: Vec<(usize, f32)> = runs
.iter()
.flat_map(|run| run.glyphs.iter())
.map(|g| (g.cluster, g.x_advance))
.collect();
widths.sort_by_key(|&(cluster, _)| cluster);
let mut bytes = Vec::with_capacity(widths.len());
let mut prefix = vec![0.0f32];
for (cluster, advance) in widths {
if bytes.last() == Some(&cluster) {
*prefix.last_mut().expect("nonempty") += advance;
} else {
bytes.push(cluster);
let total = prefix.last().copied().expect("nonempty") + advance;
prefix.push(total);
}
}
Self { bytes, prefix }
}
pub(crate) fn width(&self, start: usize, end: usize) -> f32 {
let a = self.bytes.partition_point(|&b| b < start);
let b = self.bytes.partition_point(|&b| b < end);
self.prefix[b] - self.prefix[a]
}
}
pub fn wrap_lines(
text: &str,
runs: &[ShapedRun],
max_width: f32,
max_lines: Option<u32>,
preserve_trailing_whitespace: bool,
) -> Wrapped {
let measure = Measure::new(runs);
let cap = max_lines.map_or(usize::MAX, |n| n.max(1) as usize);
let mut ranges = Vec::new();
let mut start = 0usize;
let mut committed = 0usize; let mut intrinsics = Intrinsics::default();
for (at, kind) in unicode_linebreak::linebreaks(text) {
intrinsics.segment(
text,
&measure,
committed,
at,
kind,
preserve_trailing_whitespace,
);
if ranges.len() >= cap {
return Wrapped {
ranges,
truncated: true,
min_intrinsic: intrinsics.min,
max_intrinsic: intrinsics.max,
};
}
if measured_width(text, &measure, start..at, preserve_trailing_whitespace) > max_width
&& committed > start
{
ranges.push(start..committed);
start = committed;
}
committed = at;
if kind == unicode_linebreak::BreakOpportunity::Mandatory && ranges.len() < cap {
ranges.push(start..at);
start = at;
}
}
if start < text.len() && ranges.len() < cap {
ranges.push(start..text.len());
}
if ranges.len() < cap
&& text.chars().last().is_some_and(is_hard_break)
&& ranges.last().is_some_and(|r| r.end == text.len())
{
ranges.push(text.len()..text.len());
}
if ranges.is_empty() {
ranges.push(0..0);
}
Wrapped {
truncated: start < text.len()
&& ranges.len() >= cap
&& ranges.last().is_none_or(|l| l.end < text.len()),
ranges,
min_intrinsic: intrinsics.min,
max_intrinsic: intrinsics.max,
}
}
#[derive(Default)]
struct Intrinsics {
min: f32,
max: f32,
line: f32,
line_start: usize,
}
impl Intrinsics {
fn segment(
&mut self,
text: &str,
measure: &Measure,
from: usize,
to: usize,
kind: unicode_linebreak::BreakOpportunity,
preserve_trailing_whitespace: bool,
) {
self.min = self.min.max(measured_width(
text,
measure,
from..to,
preserve_trailing_whitespace,
));
if kind == unicode_linebreak::BreakOpportunity::Mandatory {
self.line = measured_width(
text,
measure,
self.line_start..to,
preserve_trailing_whitespace,
);
self.max = self.max.max(self.line);
self.line_start = to;
}
}
}
fn measured_width(
text: &str,
measure: &Measure,
range: Range<usize>,
preserve_trailing_whitespace: bool,
) -> f32 {
let content_end = content_end(text, &range, preserve_trailing_whitespace);
measure.width(range.start, content_end)
}
fn content_end(text: &str, range: &Range<usize>, preserve_trailing_whitespace: bool) -> usize {
if preserve_trailing_whitespace {
range.end
} else {
trimmed_end(text, range)
}
}
fn trimmed_end(text: &str, range: &Range<usize>) -> usize {
text[range.clone()]
.trim_end()
.len()
.saturating_add(range.start)
}
#[allow(clippy::too_many_arguments)] pub fn place_lines(
collection: &FaceSet,
text: &str,
runs: &[ShapedRun],
bidi: &BidiInfo,
wrapped: Wrapped,
max_width: f32,
style: &ParagraphStyle,
empty_metrics: Option<(f32, f32, f32)>,
) -> Layout {
let mut lines = Vec::new();
let mut y = 0.0f32;
let mut para_width = 0.0f32;
let mut last_heights = empty_metrics.unwrap_or((16.0f32, 4.0, 24.0));
let count = wrapped.ranges.len();
for (index, range) in wrapped.ranges.iter().cloned().enumerate() {
let overlapping: Vec<&ShapedRun> = runs
.iter()
.filter(|r| r.range.start < range.end && r.range.end > range.start)
.collect();
if let Some(h) = line_heights(collection, &overlapping) {
last_heights = h;
}
let (ascent, _descent, height) = last_heights;
let baseline = y + ascent;
let ellipsis = ellipsis_for(
collection,
style,
&overlapping,
wrapped.truncated,
index,
count,
);
let line = LineSpec {
range,
baseline,
max_width,
is_last: index + 1 == count,
align: style.align,
preserve_trailing_whitespace: style.preserve_trailing_whitespace,
};
let placed = place_line(collection, text, bidi, &overlapping, &line, ellipsis);
para_width = para_width.max(placed.width);
lines.push(Line {
runs: placed.runs,
baseline,
ascent,
descent: _descent,
left: placed.left,
width: placed.width,
range: line.range,
});
y += height;
}
Layout {
max_width,
width: para_width,
height: y,
truncated: wrapped.truncated,
wrapped,
lines,
}
}
struct LineSpec {
range: Range<usize>,
baseline: f32,
max_width: f32,
is_last: bool,
align: TextAlign,
preserve_trailing_whitespace: bool,
}
struct PlacedLine {
runs: Vec<PlacedRun>,
width: f32,
left: f32,
}
fn ellipsis_for(
collection: &FaceSet,
style: &ParagraphStyle,
overlapping: &[&ShapedRun],
truncated: bool,
index: usize,
count: usize,
) -> Option<ShapedRun> {
if !truncated || index + 1 != count {
return None;
}
let text = style.ellipsis.as_deref()?;
let tail = overlapping.last()?;
Some(shape_isolated(
collection, tail.font, tail.size, tail.color, text,
))
}
fn place_line(
collection: &FaceSet,
text: &str,
bidi: &BidiInfo,
runs: &[&ShapedRun],
line: &LineSpec,
ellipsis: Option<ShapedRun>,
) -> PlacedLine {
let Some(para) = bidi
.paragraphs
.iter()
.find(|p| p.range.contains(&line.range.start))
else {
return PlacedLine {
runs: Vec::new(),
width: 0.0,
left: 0.0,
};
};
let mut end = content_end(
text,
&(line.range.start..line.range.end.min(para.range.end)),
line.preserve_trailing_whitespace,
);
let ellipsis_width: f32 = ellipsis
.as_ref()
.map(|e| e.glyphs.iter().map(|g| g.x_advance).sum())
.unwrap_or(0.0);
if ellipsis.is_some() {
end = fit_for_ellipsis(runs, line, end, ellipsis_width);
}
let content = line.range.start..end;
let width = advance_between_refs(runs, &content) + ellipsis_width;
let extra_per_space = justify_extra(text, line, para, &content, width);
let x0 = align_shift(line, width);
let rtl_base = para.level.is_rtl();
let mut x = x0;
let mut placed = Vec::new();
if rtl_base {
if let Some(e) = &ellipsis {
place_isolated(collection, e, line.baseline, end, &mut x, &mut placed);
}
}
if !content.is_empty() {
place_visual_runs(
collection,
text,
bidi,
runs,
para,
&content,
line.baseline,
extra_per_space,
&mut x,
&mut placed,
);
}
if !rtl_base {
if let Some(e) = &ellipsis {
place_isolated(collection, e, line.baseline, end, &mut x, &mut placed);
}
}
PlacedLine {
runs: placed,
width,
left: x0,
}
}
fn advance_between_refs(runs: &[&ShapedRun], range: &Range<usize>) -> f32 {
runs.iter()
.flat_map(|run| run.glyphs.iter())
.filter(|g| g.cluster >= range.start && g.cluster < range.end)
.map(|g| g.x_advance)
.sum()
}
fn fit_for_ellipsis(
runs: &[&ShapedRun],
line: &LineSpec,
end: usize,
ellipsis_width: f32,
) -> usize {
if !line.max_width.is_finite() {
return end;
}
let budget = line.max_width - ellipsis_width;
let mut clusters: Vec<(usize, f32)> = runs
.iter()
.flat_map(|run| run.glyphs.iter())
.filter(|g| g.cluster >= line.range.start && g.cluster < end)
.map(|g| (g.cluster, g.x_advance))
.collect();
clusters.sort_by_key(|&(cluster, _)| cluster);
let mut total = 0.0;
for (cluster, advance) in clusters {
if total + advance > budget {
return cluster;
}
total += advance;
}
end
}
fn justify_extra(
text: &str,
line: &LineSpec,
para: &unicode_bidi::ParagraphInfo,
content: &Range<usize>,
width: f32,
) -> f32 {
if line.align != TextAlign::Justify || !line.max_width.is_finite() {
return 0.0;
}
let para_final = line.is_last || line.range.end >= para.range.end;
if para_final || width >= line.max_width {
return 0.0;
}
let spaces = text[content.clone()].bytes().filter(|&b| b == b' ').count();
if spaces == 0 {
return 0.0;
}
(line.max_width - width) / spaces as f32
}
fn align_shift(line: &LineSpec, width: f32) -> f32 {
if !line.max_width.is_finite() {
return 0.0;
}
match line.align {
TextAlign::Left | TextAlign::Justify => 0.0,
TextAlign::Center => (line.max_width - width) * 0.5,
TextAlign::Right => line.max_width - width,
}
}
#[allow(clippy::too_many_arguments)] fn place_visual_runs(
collection: &FaceSet,
text: &str,
bidi: &BidiInfo,
runs: &[&ShapedRun],
para: &unicode_bidi::ParagraphInfo,
content: &Range<usize>,
baseline: f32,
extra_per_space: f32,
x: &mut f32,
placed: &mut Vec<PlacedRun>,
) {
let (levels, visual) = bidi.visual_runs(para, content.clone());
for segment in visual {
let mut overlapping: Vec<&&ShapedRun> = runs
.iter()
.filter(|run| run.range.start < segment.end && run.range.end > segment.start)
.collect();
let rtl = levels[segment.start].is_rtl();
if rtl {
overlapping.reverse();
}
for run in overlapping {
let slice = Slice {
range: segment.start.max(content.start)..segment.end.min(content.end),
baseline,
extra_per_space,
rtl,
};
if let Some(p) = place_slice(collection, text, run, &slice, x) {
placed.push(p);
}
}
}
}
struct Slice {
range: Range<usize>,
baseline: f32,
extra_per_space: f32,
rtl: bool,
}
fn place_slice(
collection: &FaceSet,
text: &str,
run: &ShapedRun,
slice: &Slice,
x: &mut f32,
) -> Option<PlacedRun> {
let font = collection.get(run.font);
let start_x = *x;
let mut last_origin = *x;
let mut glyphs = Vec::new();
for g in &run.glyphs {
if g.cluster < slice.range.start || g.cluster >= slice.range.end {
continue;
}
let mut advance = g.x_advance;
if text.as_bytes().get(g.cluster) == Some(&b' ') {
advance += slice.extra_per_space;
}
last_origin = *x;
glyphs.push(PlacedGlyph {
id: g.id,
x: *x + g.x_offset,
y: slice.baseline - g.y_offset,
cluster: g.cluster,
advance,
});
*x += advance;
}
if glyphs.is_empty() {
return None;
}
let bounds = valo_geometry::Rect::from_ltrb(
start_x,
slice.baseline - font.ascent_px(run.size),
*x,
slice.baseline + font.descent_px(run.size),
);
Some(PlacedRun {
font: run.font,
size: run.size,
color: run.color,
decoration: run.decoration,
shadows: run.shadows.clone(),
glyphs,
rtl: slice.rtl,
bounds,
ink: ink_bounds(font, run.size, start_x, last_origin, *x, slice.baseline),
})
}
fn ink_bounds(
font: &crate::font::Font,
size: f32,
start_x: f32,
last_origin: f32,
end_x: f32,
baseline: f32,
) -> valo_geometry::Rect {
let (ascent, descent) = (font.ascent_px(size), font.descent_px(size));
match font.ink_box_px(size) {
Some((x_min, y_min, x_max, y_max)) => valo_geometry::Rect::from_ltrb(
start_x + x_min.min(0.0),
baseline - ascent.max(y_max),
end_x.max(last_origin + x_max),
baseline + descent.max(-y_min),
),
None => {
valo_geometry::Rect::from_ltrb(start_x, baseline - ascent, end_x, baseline + descent)
}
}
}
fn place_isolated(
collection: &FaceSet,
run: &ShapedRun,
baseline: f32,
cluster: usize,
x: &mut f32,
placed: &mut Vec<PlacedRun>,
) {
let font = collection.get(run.font);
let start_x = *x;
let mut last_origin = *x;
let mut glyphs = Vec::new();
for g in &run.glyphs {
last_origin = *x;
glyphs.push(PlacedGlyph {
id: g.id,
x: *x + g.x_offset,
y: baseline - g.y_offset,
cluster,
advance: g.x_advance,
});
*x += g.x_advance;
}
if glyphs.is_empty() {
return;
}
placed.push(PlacedRun {
font: run.font,
size: run.size,
color: run.color,
decoration: run.decoration,
shadows: run.shadows.clone(),
glyphs,
rtl: false,
bounds: valo_geometry::Rect::from_ltrb(
start_x,
baseline - font.ascent_px(run.size),
*x,
baseline + font.descent_px(run.size),
),
ink: ink_bounds(font, run.size, start_x, last_origin, *x, baseline),
});
}
fn line_heights(collection: &FaceSet, runs: &[&ShapedRun]) -> Option<(f32, f32, f32)> {
let mut out: Option<(f32, f32, f32)> = None;
for run in runs {
let font = collection.get(run.font);
let candidate = style_heights(font, run.size, run.height);
out = Some(match out {
None => candidate,
Some(cur) => (
cur.0.max(candidate.0),
cur.1.max(candidate.1),
cur.2.max(candidate.2),
),
});
}
out
}
pub(crate) fn style_heights(
font: &crate::font::Font,
size: f32,
height: Option<f32>,
) -> (f32, f32, f32) {
let mut candidate = (
font.ascent_px(size),
font.descent_px(size),
font.line_height_px(size),
);
if let Some(multiplier) = height {
let target = multiplier * size;
let k = target / (candidate.0 + candidate.1).max(1e-3);
candidate = (candidate.0 * k, candidate.1 * k, target);
}
candidate
}
fn is_hard_break(c: char) -> bool {
matches!(
c,
'\n' | '\r' | '\u{0B}' | '\u{0C}' | '\u{85}' | '\u{2028}' | '\u{2029}'
)
}