use crate::bidi;
use crate::types::{Direction, RunSpan, ShapedGlyph, ShapedLine};
use super::shape::ShapedWord;
pub fn wrap_shaped_words(
items: &[ShapedWord],
space_width: f32,
line_height: f32,
max_width_lpx: f32,
) -> Vec<ShapedLine> {
let tab_width = 4.0 * space_width;
let mut lines: Vec<ShapedLine> = Vec::new();
let mut cur: Vec<&ShapedWord> = Vec::new();
let mut cur_width = 0.0f32;
let mut cur_ascent = 0.0f32;
let mut cur_descent = 0.0f32;
let mut y_offset = 0.0f32;
let mut pending: Option<&ShapedWord> = None;
for item in items {
if item.is_space_run {
pending = Some(item);
continue;
}
let pending_w = pending.map(|s| s.advance_width_lpx).unwrap_or(0.0);
let item_w = fit_advance(item, cur_width + pending_w, tab_width);
let candidate = if cur.is_empty() {
item_w
} else {
cur_width + pending_w + item_w
};
if candidate > max_width_lpx && !cur.is_empty() && item.break_before {
lines.push(assemble_visual_line(
&std::mem::take(&mut cur),
cur_ascent,
cur_descent,
y_offset,
false,
tab_width,
));
y_offset += line_height;
cur_width = 0.0;
pending = None;
} else if let Some(sp) = pending.take() {
commit_item(
sp,
sp.advance_width_lpx,
&mut cur,
&mut cur_width,
&mut cur_ascent,
&mut cur_descent,
);
}
let placed_w = fit_advance(item, cur_width, tab_width);
commit_item(
item,
placed_w,
&mut cur,
&mut cur_width,
&mut cur_ascent,
&mut cur_descent,
);
}
if let Some(sp) = pending.take() {
commit_item(
sp,
sp.advance_width_lpx,
&mut cur,
&mut cur_width,
&mut cur_ascent,
&mut cur_descent,
);
}
if !cur.is_empty() {
lines.push(assemble_visual_line(
&cur,
cur_ascent,
cur_descent,
y_offset,
false,
tab_width,
));
}
lines
}
#[inline]
pub(crate) fn fit_advance(item: &ShapedWord, pen: f32, tab_width: f32) -> f32 {
if item.is_tab && tab_width > 0.0 {
advance_to_tab_stop(pen, tab_width) - pen
} else {
item.advance_width_lpx
}
}
fn commit_item<'a>(
word: &'a ShapedWord,
advance: f32,
cur: &mut Vec<&'a ShapedWord>,
cur_width: &mut f32,
cur_ascent: &mut f32,
cur_descent: &mut f32,
) {
if cur.is_empty() {
*cur_ascent = word.ascent_lpx;
*cur_descent = word.descent_lpx;
}
cur.push(word);
*cur_width += advance;
}
pub(crate) fn assemble_visual_line(
items: &[&ShapedWord],
ascent_lpx: f32,
descent_lpx: f32,
y_offset_lpx: f32,
rewrite_clusters_absolute: bool,
tab_width: f32,
) -> ShapedLine {
let place = |word: &ShapedWord, pen_x: f32, out: &mut Vec<ShapedGlyph>| -> f32 {
let advance = if word.is_tab && tab_width > 0.0 {
advance_to_tab_stop(pen_x, tab_width) - pen_x
} else {
word.advance_width_lpx
};
for (k, g) in word.glyphs.iter().enumerate() {
let mut a = *g;
a.position_lpx[0] += pen_x;
if rewrite_clusters_absolute {
a.cluster = (word.source_byte_range.start + g.cluster as usize) as u32;
}
if word.is_tab {
a.x_advance_lpx = if k == 0 { advance } else { 0.0 };
}
out.push(a);
}
advance
};
if items.iter().all(|w| w.level == 0) {
let mut glyphs = Vec::new();
let mut pen = 0.0f32;
for w in items {
pen += place(w, pen, &mut glyphs);
}
return ShapedLine {
glyphs,
width_lpx: pen,
ascent_lpx,
descent_lpx,
y_offset_lpx,
base_direction: Direction::Ltr,
runs: Vec::new(),
};
}
let levels: Vec<u8> = items.iter().map(|w| w.level).collect();
let order = bidi::visual_order(&levels);
let mut glyphs = Vec::new();
let mut pen = 0.0f32;
for &idx in &order {
pen += place(items[idx], pen, &mut glyphs);
}
let base_level = levels.iter().copied().min().unwrap_or(0);
ShapedLine {
glyphs,
width_lpx: pen,
ascent_lpx,
descent_lpx,
y_offset_lpx,
base_direction: dir_from_level(base_level),
runs: build_visual_runs(items, &levels),
}
}
#[inline]
pub(crate) fn advance_to_tab_stop(pen: f32, tab_width: f32) -> f32 {
const EPSILON: f32 = 0.01;
((pen + EPSILON) / tab_width).ceil() * tab_width
}
#[inline]
pub(crate) fn dir_from_level(level: u8) -> Direction {
if level.is_multiple_of(2) {
Direction::Ltr
} else {
Direction::Rtl
}
}
fn build_visual_runs(items: &[&ShapedWord], levels: &[u8]) -> Vec<RunSpan> {
let mut logical: Vec<(usize, usize, u8)> = Vec::new();
for (w, &lvl) in items.iter().zip(levels) {
let (s, e) = (w.source_byte_range.start, w.source_byte_range.end);
match logical.last_mut() {
Some(last) if last.2 == lvl => {
debug_assert!(
s >= last.1,
"merged level-run items must be source-contiguous",
);
last.1 = e;
}
_ => logical.push((s, e, lvl)),
}
}
let run_levels: Vec<u8> = logical.iter().map(|r| r.2).collect();
bidi::visual_order(&run_levels)
.into_iter()
.map(|i| {
let (s, e, lvl) = logical[i];
RunSpan {
byte_range: s..e,
level: lvl,
direction: dir_from_level(lvl),
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tab_stop_arithmetic_at_boundaries() {
let tw = 40.0; assert_eq!(advance_to_tab_stop(0.0, tw), 40.0);
assert_eq!(advance_to_tab_stop(15.0, tw), 40.0);
assert_eq!(advance_to_tab_stop(40.0, tw), 80.0);
assert_eq!(advance_to_tab_stop(80.0, tw), 120.0);
}
}