use crate::bidi::{
apply_mirroring, bidi_class, process_paragraph_classes_with_brackets, reorder_combining_marks,
reorder_line, reset_trailing_levels, BidiClass,
};
use crate::face::Face;
use crate::shaper::{PositionedGlyph, Shaper};
use crate::Error;
pub fn run_width(glyphs: &[PositionedGlyph]) -> f32 {
let mut w = 0.0;
for g in glyphs {
w += g.x_advance + g.x_offset;
}
w
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VisualLine {
pub visual: Vec<char>,
pub logical_to_visual: Vec<usize>,
pub visual_to_logical: Vec<usize>,
pub base_level: u8,
}
impl VisualLine {
#[must_use]
pub fn to_visual_string(&self) -> String {
self.visual.iter().collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.visual.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.visual.is_empty()
}
}
#[must_use]
pub fn reorder_line_visual(text: &str, base_level: Option<u8>) -> VisualLine {
let chars: Vec<char> = text.chars().collect();
let classes: Vec<BidiClass> = chars.iter().copied().map(bidi_class).collect();
let carrier = process_paragraph_classes_with_brackets(&classes, &chars, base_level);
let mut line_levels = carrier.levels.clone();
reset_trailing_levels(&carrier.classes, &mut line_levels, carrier.paragraph_level);
let mut logical_to_visual = reorder_line(&line_levels);
reorder_combining_marks(&carrier.classes, &line_levels, &mut logical_to_visual);
let mut mirrored = chars.clone();
apply_mirroring(&mut mirrored, &carrier.levels);
let n = chars.len();
let mut visual = Vec::with_capacity(n);
let mut visual_to_logical = vec![0usize; n];
for (vis_pos, &log_idx) in logical_to_visual.iter().enumerate() {
visual.push(mirrored[log_idx]);
visual_to_logical[log_idx] = vis_pos;
}
VisualLine {
visual,
logical_to_visual,
visual_to_logical,
base_level: carrier.paragraph_level,
}
}
pub fn wrap_lines(
face: &Face,
text: &str,
size_px: f32,
max_width: f32,
) -> Result<Vec<String>, Error> {
if text.is_empty() {
return Ok(Vec::new());
}
if max_width <= 0.0 {
return Ok(text.split('\n').map(|s| s.to_string()).collect());
}
let mut lines: Vec<String> = Vec::new();
for paragraph in text.split('\n') {
wrap_paragraph(face, paragraph, size_px, max_width, &mut lines)?;
}
Ok(lines)
}
fn wrap_paragraph(
face: &Face,
text: &str,
size_px: f32,
max_width: f32,
lines: &mut Vec<String>,
) -> Result<(), Error> {
if text.is_empty() {
lines.push(String::new());
return Ok(());
}
let words: Vec<String> = split_keeping_whitespace(text);
if words.is_empty() {
lines.push(text.to_string());
return Ok(());
}
let mut current = String::new();
for word in words {
let candidate = if current.is_empty() {
word.trim_start().to_string()
} else {
format!("{current}{word}")
};
let glyphs = Shaper::shape(face, &candidate, size_px)?;
if run_width(&glyphs) <= max_width || current.is_empty() {
current = candidate;
let cur_glyphs = Shaper::shape(face, ¤t, size_px)?;
if run_width(&cur_glyphs) > max_width {
let (head, tail) = hard_break(face, ¤t, size_px, max_width)?;
lines.push(head);
current = tail;
}
} else {
lines.push(current.clone());
current = word.trim_start().to_string();
}
}
if !current.is_empty() {
lines.push(current);
}
Ok(())
}
fn split_keeping_whitespace(s: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
let mut buf = String::new();
let mut in_word = false;
for ch in s.chars() {
if ch.is_whitespace() {
if in_word {
out.push(std::mem::take(&mut buf));
in_word = false;
}
buf.push(ch);
} else {
in_word = true;
buf.push(ch);
}
}
if !buf.is_empty() {
out.push(buf);
}
out
}
fn hard_break(
face: &Face,
text: &str,
size_px: f32,
max_width: f32,
) -> Result<(String, String), Error> {
let chars: Vec<char> = text.chars().collect();
let mut last_good = 0usize;
for n in 1..=chars.len() {
let candidate: String = chars[..n].iter().collect();
let glyphs = Shaper::shape(face, &candidate, size_px)?;
if run_width(&glyphs) > max_width {
break;
}
last_good = n;
}
if last_good == 0 {
last_good = 1.min(chars.len());
}
let head: String = chars[..last_good].iter().collect();
let tail: String = chars[last_good..].iter().collect();
Ok((head, tail))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_keeping_whitespace_basic() {
let v = split_keeping_whitespace("hello world foo");
assert_eq!(v, vec!["hello", " world", " foo"]);
}
#[test]
fn split_keeping_whitespace_leading_trailing() {
let v = split_keeping_whitespace(" hi");
assert_eq!(v, vec![" hi"]);
}
#[test]
fn empty_text_is_empty_lines() {
}
#[test]
fn visual_ltr_is_identity() {
let line = reorder_line_visual("abc", None);
assert_eq!(line.base_level, 0);
assert_eq!(line.to_visual_string(), "abc");
assert_eq!(line.logical_to_visual, vec![0, 1, 2]);
assert_eq!(line.visual_to_logical, vec![0, 1, 2]);
assert_eq!(line.len(), 3);
assert!(!line.is_empty());
}
#[test]
fn visual_empty_line() {
let line = reorder_line_visual("", None);
assert!(line.is_empty());
assert_eq!(line.len(), 0);
assert_eq!(line.to_visual_string(), "");
assert!(line.logical_to_visual.is_empty());
assert!(line.visual_to_logical.is_empty());
}
#[test]
fn visual_pure_rtl_reverses() {
let line = reorder_line_visual("\u{05D0}\u{05D1}\u{05D2}", None);
assert_eq!(line.base_level, 1);
assert_eq!(line.logical_to_visual, vec![2, 1, 0]);
assert_eq!(line.to_visual_string(), "\u{05D2}\u{05D1}\u{05D0}");
for (vis_pos, &log_idx) in line.logical_to_visual.iter().enumerate() {
assert_eq!(line.visual_to_logical[log_idx], vis_pos);
}
}
#[test]
fn visual_permutation_is_a_bijection() {
let line = reorder_line_visual("ab \u{05D0}\u{05D1}12", None);
let n = line.len();
let mut seen = vec![false; n];
for &log_idx in &line.logical_to_visual {
assert!(log_idx < n);
assert!(!seen[log_idx], "permutation repeats index {log_idx}");
seen[log_idx] = true;
}
assert!(seen.iter().all(|&b| b));
for (vis_pos, &log_idx) in line.logical_to_visual.iter().enumerate() {
assert_eq!(line.visual_to_logical[log_idx], vis_pos);
}
}
#[test]
fn visual_base_level_override() {
let ltr = reorder_line_visual("abc", Some(0));
assert_eq!(ltr.base_level, 0);
assert_eq!(ltr.to_visual_string(), "abc");
let rtl = reorder_line_visual("abc", Some(1));
assert_eq!(rtl.base_level, 1);
assert_eq!(rtl.to_visual_string(), "abc");
let rtl2 = reorder_line_visual("abc", Some(3));
assert_eq!(rtl2.base_level, 1);
}
#[test]
fn visual_l4_mirrors_rtl_bracket() {
let line = reorder_line_visual("\u{05D0}(\u{05D1}", None);
assert_eq!(line.base_level, 1);
let s = line.to_visual_string();
assert!(
s.contains(')'),
"expected mirrored ')' in RTL line, got {s:?}"
);
assert!(!s.contains('('), "original '(' should have been mirrored");
}
#[test]
fn visual_ltr_bracket_not_mirrored() {
let line = reorder_line_visual("a(b)", None);
assert_eq!(line.base_level, 0);
assert_eq!(line.to_visual_string(), "a(b)");
}
}