use crate::layout::PositionedGlyph;
use crate::style::Direction;
use unicode_bidi::{BidiInfo, Level};
#[derive(Debug, Clone)]
pub struct BidiRun {
pub char_start: usize,
pub char_end: usize,
pub level: Level,
pub is_rtl: bool,
}
pub fn analyze_bidi(text: &str, direction: Direction) -> Vec<BidiRun> {
if text.is_empty() {
return vec![];
}
let para_level = match direction {
Direction::Ltr => Some(Level::ltr()),
Direction::Rtl => Some(Level::rtl()),
Direction::Auto => None, };
let bidi_info = BidiInfo::new(text, para_level);
if bidi_info.paragraphs.is_empty() {
return vec![];
}
let paragraph = &bidi_info.paragraphs[0];
let levels = &bidi_info.levels;
let chars: Vec<char> = text.chars().collect();
let mut runs = Vec::new();
let mut run_start = 0;
let para_start = paragraph.range.start;
let para_end = paragraph.range.end;
let mut char_levels = Vec::with_capacity(chars.len());
for (byte_idx, _ch) in text.char_indices() {
if byte_idx >= para_start && byte_idx < para_end {
char_levels.push(levels[byte_idx]);
}
}
if char_levels.is_empty() {
return vec![];
}
for i in 1..char_levels.len() {
if char_levels[i] != char_levels[run_start] {
runs.push(BidiRun {
char_start: run_start,
char_end: i,
level: char_levels[run_start],
is_rtl: char_levels[run_start].is_rtl(),
});
run_start = i;
}
}
runs.push(BidiRun {
char_start: run_start,
char_end: char_levels.len(),
level: char_levels[run_start],
is_rtl: char_levels[run_start].is_rtl(),
});
runs
}
pub fn is_pure_ltr(text: &str, direction: Direction) -> bool {
if matches!(direction, Direction::Rtl) {
return false;
}
!text.chars().any(is_rtl_char)
}
fn is_rtl_char(ch: char) -> bool {
matches!(ch,
'\u{0590}'..='\u{05FF}' | '\u{0600}'..='\u{06FF}' | '\u{0700}'..='\u{074F}' | '\u{0750}'..='\u{077F}' | '\u{0780}'..='\u{07BF}' | '\u{07C0}'..='\u{07FF}' | '\u{0800}'..='\u{083F}' | '\u{0840}'..='\u{085F}' | '\u{08A0}'..='\u{08FF}' | '\u{FB1D}'..='\u{FB4F}' | '\u{FB50}'..='\u{FDFF}' | '\u{FE70}'..='\u{FEFF}' | '\u{10800}'..='\u{10FFF}' | '\u{1E800}'..='\u{1EEFF}' | '\u{200F}' | '\u{202B}' | '\u{202E}' | '\u{2067}' )
}
pub fn reorder_line_glyphs(
mut glyphs: Vec<PositionedGlyph>,
levels: &[Level],
) -> Vec<PositionedGlyph> {
if glyphs.is_empty() || levels.is_empty() {
return glyphs;
}
let min_level = levels.iter().copied().min().unwrap_or(Level::ltr());
let max_level = levels.iter().copied().max().unwrap_or(Level::ltr());
if !max_level.is_rtl() {
return glyphs;
}
let min_odd = if min_level.is_rtl() {
min_level
} else {
Level::rtl() };
let mut current_level = max_level;
while current_level >= min_odd {
let mut i = 0;
while i < glyphs.len() {
if levels.get(i).copied().unwrap_or(Level::ltr()) >= current_level {
let start = i;
while i < glyphs.len()
&& levels.get(i).copied().unwrap_or(Level::ltr()) >= current_level
{
i += 1;
}
glyphs[start..i].reverse();
} else {
i += 1;
}
}
if current_level.number() == 0 {
break;
}
current_level = Level::new(current_level.number() - 1).unwrap_or(Level::ltr());
}
glyphs
}
pub fn reposition_after_reorder(glyphs: &mut [PositionedGlyph], start_x: f64) {
let mut x = start_x;
for g in glyphs.iter_mut() {
g.x_offset = x;
x += g.x_advance;
}
}
#[allow(dead_code)]
fn build_byte_to_char_map(text: &str) -> Vec<usize> {
let mut map = vec![0usize; text.len() + 1];
let mut char_idx = 0;
for (byte_idx, _) in text.char_indices() {
map[byte_idx] = char_idx;
char_idx += 1;
}
map[text.len()] = char_idx;
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pure_ltr() {
assert!(is_pure_ltr("Hello World", Direction::Ltr));
assert!(is_pure_ltr("Hello World", Direction::Auto));
assert!(!is_pure_ltr("Hello World", Direction::Rtl));
}
#[test]
fn test_rtl_detection() {
assert!(!is_pure_ltr("مرحبا", Direction::Ltr));
assert!(!is_pure_ltr("שלום", Direction::Ltr));
}
#[test]
fn test_analyze_bidi_pure_ltr() {
let runs = analyze_bidi("Hello World", Direction::Ltr);
assert_eq!(runs.len(), 1);
assert!(!runs[0].is_rtl);
assert_eq!(runs[0].char_start, 0);
assert_eq!(runs[0].char_end, 11);
}
#[test]
fn test_analyze_bidi_pure_rtl() {
let runs = analyze_bidi("مرحبا", Direction::Rtl);
assert_eq!(runs.len(), 1);
assert!(runs[0].is_rtl);
}
#[test]
fn test_analyze_bidi_mixed() {
let runs = analyze_bidi("Hello مرحبا World", Direction::Ltr);
assert!(
runs.len() >= 2,
"Expected at least 2 runs, got {}",
runs.len()
);
assert!(!runs[0].is_rtl);
assert!(runs.iter().any(|r| r.is_rtl), "Should have an RTL run");
}
#[test]
fn test_analyze_bidi_empty() {
let runs = analyze_bidi("", Direction::Ltr);
assert!(runs.is_empty());
}
#[test]
fn test_rtl_direction_defaults_right_align() {
use crate::style::{Style, TextAlign};
let style = Style {
direction: Some(Direction::Rtl),
..Default::default()
};
let resolved = style.resolve(None, 500.0);
assert!(matches!(resolved.text_align, TextAlign::Right));
}
#[test]
fn test_ltr_direction_defaults_left_align() {
use crate::style::{Style, TextAlign};
let style = Style {
direction: Some(Direction::Ltr),
..Default::default()
};
let resolved = style.resolve(None, 500.0);
assert!(matches!(resolved.text_align, TextAlign::Left));
}
}