1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum Direction {
16 #[default]
18 Auto,
19 Ltr,
21 Rtl,
23}
24
25impl Direction {
26 fn level(self) -> Option<unicode_bidi::Level> {
28 match self {
29 Direction::Auto => None,
30 Direction::Ltr => Some(unicode_bidi::Level::ltr()),
31 Direction::Rtl => Some(unicode_bidi::Level::rtl()),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct Run {
40 pub start: usize,
42 pub length: usize,
44 pub is_rtl: bool,
46}
47
48#[derive(Debug)]
50pub struct Resolver {
51 text: String,
52 offsets: Vec<usize>,
55 direction: Direction,
56}
57
58impl Resolver {
59 #[must_use]
61 pub fn new(chars: &[u32], direction: Direction) -> Resolver {
62 let mut text = String::new();
63 let mut offsets = Vec::with_capacity(chars.len() + 1);
64 for code in chars {
65 offsets.push(text.len());
66 text.push(char::from_u32(*code).unwrap_or(char::REPLACEMENT_CHARACTER));
67 }
68 offsets.push(text.len());
69 Resolver {
70 text,
71 offsets,
72 direction,
73 }
74 }
75
76 #[must_use]
82 pub fn visual_runs(&self, start: usize, length: usize) -> Vec<Run> {
83 if length == 0 || start >= self.offsets.len().saturating_sub(1) {
84 return Vec::new();
85 }
86 let end = (start + length).min(self.offsets.len() - 1);
87 let (Some(from), Some(to)) = (self.offsets.get(start), self.offsets.get(end)) else {
88 return Vec::new();
89 };
90
91 let info = unicode_bidi::BidiInfo::new(&self.text, self.direction.level());
92 let Some(paragraph) = info.paragraphs.first() else {
93 return Vec::new();
94 };
95 let (levels, ranges) = info.visual_runs(paragraph, *from..*to);
96
97 ranges
98 .into_iter()
99 .filter_map(|range| {
100 let run_start = self.char_index(range.start)?;
101 let run_end = self.char_index(range.end)?;
102 let is_rtl = levels
103 .get(range.start)
104 .is_some_and(unicode_bidi::Level::is_rtl);
105 (run_end > run_start).then_some(Run {
106 start: run_start,
107 length: run_end - run_start,
108 is_rtl,
109 })
110 })
111 .collect()
112 }
113
114 fn char_index(&self, byte: usize) -> Option<usize> {
116 self.offsets.iter().position(|offset| *offset == byte)
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::{Direction, Resolver};
123
124 fn order(text: &str, direction: Direction) -> Vec<usize> {
127 let chars: Vec<u32> = text.chars().map(|c| c as u32).collect();
128 let resolver = Resolver::new(&chars, direction);
129 let mut out = Vec::new();
130 for run in resolver.visual_runs(0, chars.len()) {
131 let span = run.start..run.start + run.length;
132 if run.is_rtl {
133 out.extend(span.rev());
134 } else {
135 out.extend(span);
136 }
137 }
138 out
139 }
140
141 const LATIN_FIRST: &str = "A_B_C_\u{5D0}_\u{5D1}";
144 const HEBREW_FIRST: &str = "\u{5D0}_\u{5D1}_\u{5D2}_A_B";
145
146 #[test]
147 fn latin_first_reads_left_to_right_until_the_hebrew_run() {
148 assert_eq!(
149 order(LATIN_FIRST, Direction::Auto),
150 [0, 1, 2, 3, 4, 5, 8, 7, 6]
151 );
152 assert_eq!(
153 order(LATIN_FIRST, Direction::Ltr),
154 [0, 1, 2, 3, 4, 5, 8, 7, 6]
155 );
156 assert_eq!(
157 order(LATIN_FIRST, Direction::Rtl),
158 [8, 7, 6, 5, 0, 1, 2, 3, 4]
159 );
160 }
161
162 #[test]
163 fn hebrew_first_puts_the_whole_paragraph_the_other_way_round() {
164 assert_eq!(
165 order(HEBREW_FIRST, Direction::Auto),
166 [6, 7, 8, 5, 4, 3, 2, 1, 0]
167 );
168 assert_eq!(
169 order(HEBREW_FIRST, Direction::Ltr),
170 [4, 3, 2, 1, 0, 5, 6, 7, 8]
171 );
172 assert_eq!(
173 order(HEBREW_FIRST, Direction::Rtl),
174 [6, 7, 8, 5, 4, 3, 2, 1, 0]
175 );
176 }
177
178 #[test]
179 fn an_empty_line_reports_no_runs_at_all() {
180 let resolver = Resolver::new(&[], Direction::Auto);
181 assert!(resolver.visual_runs(0, 0).is_empty());
182 let resolver = Resolver::new(&[65], Direction::Auto);
183 assert!(resolver.visual_runs(0, 0).is_empty());
184 assert!(resolver.visual_runs(4, 2).is_empty());
185 }
186}