1use crate::layout::PositionedGlyph;
13use crate::style::Direction;
14use unicode_bidi::{BidiInfo, Level};
15
16#[derive(Debug, Clone)]
18pub struct BidiRun {
19 pub char_start: usize,
21 pub char_end: usize,
23 pub level: Level,
25 pub is_rtl: bool,
27}
28
29pub fn analyze_bidi(text: &str, direction: Direction) -> Vec<BidiRun> {
37 if text.is_empty() {
38 return vec![];
39 }
40
41 let para_level = match direction {
42 Direction::Ltr => Some(Level::ltr()),
43 Direction::Rtl => Some(Level::rtl()),
44 Direction::Auto => None, };
46
47 let bidi_info = BidiInfo::new(text, para_level);
48
49 if bidi_info.paragraphs.is_empty() {
53 return vec![];
54 }
55
56 let paragraph = &bidi_info.paragraphs[0];
57 let levels = &bidi_info.levels;
58
59 let chars: Vec<char> = text.chars().collect();
61 let mut runs = Vec::new();
62 let mut run_start = 0;
63
64 let para_start = paragraph.range.start;
66 let para_end = paragraph.range.end;
67
68 let mut char_levels = Vec::with_capacity(chars.len());
70 for (byte_idx, _ch) in text.char_indices() {
71 if byte_idx >= para_start && byte_idx < para_end {
72 char_levels.push(levels[byte_idx]);
73 }
74 }
75
76 if char_levels.is_empty() {
77 return vec![];
78 }
79
80 for i in 1..char_levels.len() {
81 if char_levels[i] != char_levels[run_start] {
82 runs.push(BidiRun {
83 char_start: run_start,
84 char_end: i,
85 level: char_levels[run_start],
86 is_rtl: char_levels[run_start].is_rtl(),
87 });
88 run_start = i;
89 }
90 }
91 runs.push(BidiRun {
93 char_start: run_start,
94 char_end: char_levels.len(),
95 level: char_levels[run_start],
96 is_rtl: char_levels[run_start].is_rtl(),
97 });
98
99 runs
100}
101
102pub fn is_pure_ltr(text: &str, direction: Direction) -> bool {
105 if matches!(direction, Direction::Rtl) {
106 return false;
107 }
108
109 !text.chars().any(is_rtl_char)
111}
112
113fn is_rtl_char(ch: char) -> bool {
115 matches!(ch,
118 '\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}' )
137}
138
139pub fn reorder_line_glyphs(
144 mut glyphs: Vec<PositionedGlyph>,
145 levels: &[Level],
146) -> Vec<PositionedGlyph> {
147 if glyphs.is_empty() || levels.is_empty() {
148 return glyphs;
149 }
150
151 let min_level = levels.iter().copied().min().unwrap_or(Level::ltr());
155 let max_level = levels.iter().copied().max().unwrap_or(Level::ltr());
156
157 if !max_level.is_rtl() {
159 return glyphs;
160 }
161
162 let min_odd = if min_level.is_rtl() {
165 min_level
166 } else {
167 Level::rtl() };
169
170 let mut current_level = max_level;
171 while current_level >= min_odd {
172 let mut i = 0;
173 while i < glyphs.len() {
174 if levels.get(i).copied().unwrap_or(Level::ltr()) >= current_level {
175 let start = i;
177 while i < glyphs.len()
178 && levels.get(i).copied().unwrap_or(Level::ltr()) >= current_level
179 {
180 i += 1;
181 }
182 glyphs[start..i].reverse();
184 } else {
185 i += 1;
186 }
187 }
188 if current_level.number() == 0 {
190 break;
191 }
192 current_level = Level::new(current_level.number() - 1).unwrap_or(Level::ltr());
193 }
194
195 glyphs
196}
197
198pub fn reposition_after_reorder(glyphs: &mut [PositionedGlyph], start_x: f64) {
203 let mut x = start_x;
204 for g in glyphs.iter_mut() {
205 g.x_offset = x;
206 x += g.x_advance;
207 }
208}
209
210#[allow(dead_code)]
212fn build_byte_to_char_map(text: &str) -> Vec<usize> {
213 let mut map = vec![0usize; text.len() + 1];
214 let mut char_idx = 0;
215 for (byte_idx, _) in text.char_indices() {
216 map[byte_idx] = char_idx;
217 char_idx += 1;
218 }
219 map[text.len()] = char_idx;
220 map
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_pure_ltr() {
229 assert!(is_pure_ltr("Hello World", Direction::Ltr));
230 assert!(is_pure_ltr("Hello World", Direction::Auto));
231 assert!(!is_pure_ltr("Hello World", Direction::Rtl));
232 }
233
234 #[test]
235 fn test_rtl_detection() {
236 assert!(!is_pure_ltr("مرحبا", Direction::Ltr));
237 assert!(!is_pure_ltr("שלום", Direction::Ltr));
238 }
239
240 #[test]
241 fn test_analyze_bidi_pure_ltr() {
242 let runs = analyze_bidi("Hello World", Direction::Ltr);
243 assert_eq!(runs.len(), 1);
244 assert!(!runs[0].is_rtl);
245 assert_eq!(runs[0].char_start, 0);
246 assert_eq!(runs[0].char_end, 11);
247 }
248
249 #[test]
250 fn test_analyze_bidi_pure_rtl() {
251 let runs = analyze_bidi("مرحبا", Direction::Rtl);
252 assert_eq!(runs.len(), 1);
253 assert!(runs[0].is_rtl);
254 }
255
256 #[test]
257 fn test_analyze_bidi_mixed() {
258 let runs = analyze_bidi("Hello مرحبا World", Direction::Ltr);
260 assert!(
261 runs.len() >= 2,
262 "Expected at least 2 runs, got {}",
263 runs.len()
264 );
265 assert!(!runs[0].is_rtl);
267 assert!(runs.iter().any(|r| r.is_rtl), "Should have an RTL run");
269 }
270
271 #[test]
272 fn test_analyze_bidi_empty() {
273 let runs = analyze_bidi("", Direction::Ltr);
274 assert!(runs.is_empty());
275 }
276
277 #[test]
278 fn test_rtl_direction_defaults_right_align() {
279 use crate::style::{Style, TextAlign};
281 let style = Style {
282 direction: Some(Direction::Rtl),
283 ..Default::default()
284 };
285 let resolved = style.resolve(None, 500.0);
286 assert!(matches!(resolved.text_align, TextAlign::Right));
287 }
288
289 #[test]
290 fn test_ltr_direction_defaults_left_align() {
291 use crate::style::{Style, TextAlign};
292 let style = Style {
293 direction: Some(Direction::Ltr),
294 ..Default::default()
295 };
296 let resolved = style.resolve(None, 500.0);
297 assert!(matches!(resolved.text_align, TextAlign::Left));
298 }
299}