1#![forbid(unsafe_code)]
2
3use crate::{Alignment, Sides};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
26pub enum FlowDirection {
27 #[default]
29 Ltr,
30 Rtl,
32}
33
34impl FlowDirection {
35 pub const fn is_rtl(self) -> bool {
37 matches!(self, FlowDirection::Rtl)
38 }
39
40 pub const fn is_ltr(self) -> bool {
42 matches!(self, FlowDirection::Ltr)
43 }
44
45 pub fn locale_is_rtl(locale: &str) -> bool {
48 let lang = locale
49 .split(['-', '_'])
50 .next()
51 .unwrap_or("")
52 .to_ascii_lowercase();
53 matches!(
54 lang.as_str(),
55 "ar" | "he"
56 | "fa"
57 | "ur"
58 | "ps"
59 | "sd"
60 | "yi"
61 | "ku"
62 | "dv"
63 | "ks"
64 | "ckb"
65 | "syr"
66 | "arc"
67 | "nqo"
68 | "man"
69 | "sam"
70 )
71 }
72
73 pub fn from_locale(locale: &str) -> Self {
75 if Self::locale_is_rtl(locale) {
76 FlowDirection::Rtl
77 } else {
78 FlowDirection::Ltr
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
92pub enum LogicalAlignment {
93 #[default]
95 Start,
96 End,
98 Center,
100}
101
102impl LogicalAlignment {
103 pub const fn resolve(self, flow: FlowDirection) -> Alignment {
110 match (self, flow) {
111 (LogicalAlignment::Start, FlowDirection::Ltr) => Alignment::Start,
112 (LogicalAlignment::Start, FlowDirection::Rtl) => Alignment::End,
113 (LogicalAlignment::End, FlowDirection::Ltr) => Alignment::End,
114 (LogicalAlignment::End, FlowDirection::Rtl) => Alignment::Start,
115 (LogicalAlignment::Center, _) => Alignment::Center,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
129pub struct LogicalSides {
130 pub top: u16,
131 pub bottom: u16,
132 pub start: u16,
134 pub end: u16,
136}
137
138impl LogicalSides {
139 pub const fn all(val: u16) -> Self {
141 Self {
142 top: val,
143 bottom: val,
144 start: val,
145 end: val,
146 }
147 }
148
149 pub const fn symmetric(block: u16, inline: u16) -> Self {
151 Self {
152 top: block,
153 bottom: block,
154 start: inline,
155 end: inline,
156 }
157 }
158
159 pub const fn inline(start: u16, end: u16) -> Self {
161 Self {
162 top: 0,
163 bottom: 0,
164 start,
165 end,
166 }
167 }
168
169 pub const fn block(top: u16, bottom: u16) -> Self {
171 Self {
172 top,
173 bottom,
174 start: 0,
175 end: 0,
176 }
177 }
178
179 pub const fn resolve(self, flow: FlowDirection) -> Sides {
184 match flow {
185 FlowDirection::Ltr => Sides {
186 top: self.top,
187 right: self.end,
188 bottom: self.bottom,
189 left: self.start,
190 },
191 FlowDirection::Rtl => Sides {
192 top: self.top,
193 right: self.start,
194 bottom: self.bottom,
195 left: self.end,
196 },
197 }
198 }
199
200 pub const fn inline_sum(self) -> u16 {
202 self.start + self.end
203 }
204
205 pub const fn block_sum(self) -> u16 {
207 self.top + self.bottom
208 }
209}
210
211impl LogicalSides {
215 pub const fn from_physical(sides: Sides, flow: FlowDirection) -> Self {
216 match flow {
217 FlowDirection::Ltr => Self {
218 top: sides.top,
219 bottom: sides.bottom,
220 start: sides.left,
221 end: sides.right,
222 },
223 FlowDirection::Rtl => Self {
224 top: sides.top,
225 bottom: sides.bottom,
226 start: sides.right,
227 end: sides.left,
228 },
229 }
230 }
231}
232
233pub fn mirror_rects_horizontal(
243 rects: &mut [ftui_core::geometry::Rect],
244 area: ftui_core::geometry::Rect,
245) {
246 for rect in rects.iter_mut() {
247 let offset_from_left = rect.x.saturating_sub(area.x);
253 let new_offset = area
254 .width
255 .saturating_sub(offset_from_left)
256 .saturating_sub(rect.width);
257 rect.x = area.x.saturating_add(new_offset);
258 }
259}
260
261#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
272 fn flow_direction_default_is_ltr() {
273 assert_eq!(FlowDirection::default(), FlowDirection::Ltr);
274 assert!(FlowDirection::Ltr.is_ltr());
275 assert!(!FlowDirection::Ltr.is_rtl());
276 assert!(FlowDirection::Rtl.is_rtl());
277 assert!(!FlowDirection::Rtl.is_ltr());
278 }
279
280 #[test]
281 fn flow_direction_from_locale() {
282 assert_eq!(FlowDirection::from_locale("en"), FlowDirection::Ltr);
283 assert_eq!(FlowDirection::from_locale("en-US"), FlowDirection::Ltr);
284 assert_eq!(FlowDirection::from_locale("fr"), FlowDirection::Ltr);
285 assert_eq!(FlowDirection::from_locale("ja"), FlowDirection::Ltr);
286 assert_eq!(FlowDirection::from_locale("ar"), FlowDirection::Rtl);
287 assert_eq!(FlowDirection::from_locale("ar-SA"), FlowDirection::Rtl);
288 assert_eq!(FlowDirection::from_locale("he"), FlowDirection::Rtl);
289 assert_eq!(FlowDirection::from_locale("fa"), FlowDirection::Rtl);
290 assert_eq!(FlowDirection::from_locale("ur"), FlowDirection::Rtl);
291 assert_eq!(FlowDirection::from_locale("yi"), FlowDirection::Rtl);
292 }
293
294 #[test]
295 fn flow_direction_locale_case_insensitive() {
296 assert_eq!(FlowDirection::from_locale("AR"), FlowDirection::Rtl);
297 assert_eq!(FlowDirection::from_locale("He"), FlowDirection::Rtl);
298 assert_eq!(FlowDirection::from_locale("EN"), FlowDirection::Ltr);
299 }
300
301 #[test]
304 fn logical_alignment_ltr_resolution() {
305 assert_eq!(
306 LogicalAlignment::Start.resolve(FlowDirection::Ltr),
307 Alignment::Start
308 );
309 assert_eq!(
310 LogicalAlignment::End.resolve(FlowDirection::Ltr),
311 Alignment::End
312 );
313 assert_eq!(
314 LogicalAlignment::Center.resolve(FlowDirection::Ltr),
315 Alignment::Center
316 );
317 }
318
319 #[test]
320 fn logical_alignment_rtl_resolution() {
321 assert_eq!(
322 LogicalAlignment::Start.resolve(FlowDirection::Rtl),
323 Alignment::End
324 );
325 assert_eq!(
326 LogicalAlignment::End.resolve(FlowDirection::Rtl),
327 Alignment::Start
328 );
329 assert_eq!(
330 LogicalAlignment::Center.resolve(FlowDirection::Rtl),
331 Alignment::Center
332 );
333 }
334
335 #[test]
338 fn logical_sides_ltr_resolution() {
339 let logical = LogicalSides {
340 top: 1,
341 bottom: 2,
342 start: 3,
343 end: 4,
344 };
345 let physical = logical.resolve(FlowDirection::Ltr);
346 assert_eq!(physical.top, 1);
347 assert_eq!(physical.bottom, 2);
348 assert_eq!(physical.left, 3); assert_eq!(physical.right, 4); }
351
352 #[test]
353 fn logical_sides_rtl_resolution() {
354 let logical = LogicalSides {
355 top: 1,
356 bottom: 2,
357 start: 3,
358 end: 4,
359 };
360 let physical = logical.resolve(FlowDirection::Rtl);
361 assert_eq!(physical.top, 1);
362 assert_eq!(physical.bottom, 2);
363 assert_eq!(physical.left, 4); assert_eq!(physical.right, 3); }
366
367 #[test]
368 fn logical_sides_symmetry() {
369 let logical = LogicalSides::all(5);
371 let ltr = logical.resolve(FlowDirection::Ltr);
372 let rtl = logical.resolve(FlowDirection::Rtl);
373 assert_eq!(ltr, rtl);
374 }
375
376 #[test]
377 fn logical_sides_roundtrip() {
378 let original = LogicalSides {
380 top: 1,
381 bottom: 2,
382 start: 3,
383 end: 4,
384 };
385
386 let ltr_physical = original.resolve(FlowDirection::Ltr);
387 let roundtrip = LogicalSides::from_physical(ltr_physical, FlowDirection::Ltr);
388 assert_eq!(original, roundtrip);
389
390 let rtl_physical = original.resolve(FlowDirection::Rtl);
391 let roundtrip = LogicalSides::from_physical(rtl_physical, FlowDirection::Rtl);
392 assert_eq!(original, roundtrip);
393 }
394
395 #[test]
396 fn logical_sides_constructors() {
397 let all = LogicalSides::all(5);
398 assert_eq!(all.top, 5);
399 assert_eq!(all.bottom, 5);
400 assert_eq!(all.start, 5);
401 assert_eq!(all.end, 5);
402
403 let sym = LogicalSides::symmetric(2, 4);
404 assert_eq!(sym.top, 2);
405 assert_eq!(sym.bottom, 2);
406 assert_eq!(sym.start, 4);
407 assert_eq!(sym.end, 4);
408
409 let inline = LogicalSides::inline(3, 7);
410 assert_eq!(inline.top, 0);
411 assert_eq!(inline.bottom, 0);
412 assert_eq!(inline.start, 3);
413 assert_eq!(inline.end, 7);
414
415 let block = LogicalSides::block(1, 9);
416 assert_eq!(block.top, 1);
417 assert_eq!(block.bottom, 9);
418 assert_eq!(block.start, 0);
419 assert_eq!(block.end, 0);
420 }
421
422 #[test]
423 fn logical_sides_sums() {
424 let s = LogicalSides {
425 top: 1,
426 bottom: 2,
427 start: 3,
428 end: 4,
429 };
430 assert_eq!(s.inline_sum(), 7);
431 assert_eq!(s.block_sum(), 3);
432 }
433
434 #[test]
437 fn mirror_rects_simple() {
438 use ftui_core::geometry::Rect;
439
440 let area = Rect::new(0, 0, 100, 20);
441 let mut rects = vec![
442 Rect::new(0, 0, 30, 20),
443 Rect::new(30, 0, 40, 20),
444 Rect::new(70, 0, 30, 20),
445 ];
446
447 mirror_rects_horizontal(&mut rects, area);
448
449 assert_eq!(rects[0].x, 70);
451 assert_eq!(rects[0].width, 30);
452 assert_eq!(rects[1].x, 30);
453 assert_eq!(rects[1].width, 40);
454 assert_eq!(rects[2].x, 0);
455 assert_eq!(rects[2].width, 30);
456 }
457
458 #[test]
459 fn mirror_rects_with_offset() {
460 use ftui_core::geometry::Rect;
461
462 let area = Rect::new(10, 5, 80, 20);
463 let mut rects = vec![
464 Rect::new(10, 5, 20, 20), Rect::new(30, 5, 60, 20), ];
467
468 mirror_rects_horizontal(&mut rects, area);
469
470 assert_eq!(rects[0].x, 70);
473 assert_eq!(rects[0].width, 20);
474 assert_eq!(rects[1].x, 10);
475 assert_eq!(rects[1].width, 60);
476 }
477
478 #[test]
479 fn mirror_rects_empty() {
480 use ftui_core::geometry::Rect;
481
482 let area = Rect::new(0, 0, 100, 20);
483 let mut rects: Vec<Rect> = vec![];
484 mirror_rects_horizontal(&mut rects, area); assert!(rects.is_empty());
486 }
487
488 #[test]
489 fn mirror_rects_idempotent_double_mirror() {
490 use ftui_core::geometry::Rect;
491
492 let area = Rect::new(5, 0, 90, 20);
493 let original = vec![
494 Rect::new(5, 0, 30, 20),
495 Rect::new(35, 0, 25, 20),
496 Rect::new(60, 0, 35, 20),
497 ];
498
499 let mut rects = original.clone();
500 mirror_rects_horizontal(&mut rects, area);
501 mirror_rects_horizontal(&mut rects, area);
502
503 assert_eq!(rects, original);
505 }
506
507 #[test]
510 fn flex_horizontal_rtl_reverses_order() {
511 use crate::{Constraint, Flex};
512 use ftui_core::geometry::Rect;
513
514 let area = Rect::new(0, 0, 100, 10);
515
516 let ltr_rects = Flex::horizontal()
517 .constraints([Constraint::Fixed(30), Constraint::Fixed(70)])
518 .split(area);
519
520 let rtl_rects = Flex::horizontal()
521 .constraints([Constraint::Fixed(30), Constraint::Fixed(70)])
522 .flow_direction(FlowDirection::Rtl)
523 .split(area);
524
525 assert_eq!(ltr_rects[0].x, 0);
527 assert_eq!(ltr_rects[1].x, 30);
528
529 assert_eq!(rtl_rects[0].x, 70);
531 assert_eq!(rtl_rects[0].width, 30);
532 assert_eq!(rtl_rects[1].x, 0);
533 assert_eq!(rtl_rects[1].width, 70);
534 }
535
536 #[test]
537 fn flex_vertical_rtl_no_change() {
538 use crate::{Constraint, Flex};
539 use ftui_core::geometry::Rect;
540
541 let area = Rect::new(0, 0, 80, 40);
542
543 let ltr_rects = Flex::vertical()
544 .constraints([Constraint::Fixed(10), Constraint::Fixed(30)])
545 .split(area);
546
547 let rtl_rects = Flex::vertical()
548 .constraints([Constraint::Fixed(10), Constraint::Fixed(30)])
549 .flow_direction(FlowDirection::Rtl)
550 .split(area);
551
552 assert_eq!(ltr_rects, rtl_rects);
554 }
555
556 #[test]
557 fn flex_horizontal_rtl_with_gap() {
558 use crate::{Constraint, Flex};
559 use ftui_core::geometry::Rect;
560
561 let area = Rect::new(0, 0, 100, 10);
562
563 let rtl_rects = Flex::horizontal()
564 .constraints([
565 Constraint::Fixed(20),
566 Constraint::Fixed(30),
567 Constraint::Fixed(40),
568 ])
569 .gap(5)
570 .flow_direction(FlowDirection::Rtl)
571 .split(area);
572
573 assert_eq!(rtl_rects[0].x, 80);
577 assert_eq!(rtl_rects[0].width, 20);
578 assert_eq!(rtl_rects[1].x, 45);
579 assert_eq!(rtl_rects[1].width, 30);
580 assert_eq!(rtl_rects[2].x, 0);
581 assert_eq!(rtl_rects[2].width, 40);
582 }
583
584 #[test]
585 fn flex_ltr_default_unchanged() {
586 use crate::{Constraint, Flex};
587 use ftui_core::geometry::Rect;
588
589 let area = Rect::new(0, 0, 100, 10);
590
591 let default_rects = Flex::horizontal()
593 .constraints([Constraint::Fixed(30), Constraint::Fixed(70)])
594 .split(area);
595
596 let explicit_ltr = Flex::horizontal()
597 .constraints([Constraint::Fixed(30), Constraint::Fixed(70)])
598 .flow_direction(FlowDirection::Ltr)
599 .split(area);
600
601 assert_eq!(default_rects, explicit_ltr);
602 }
603
604 #[test]
605 fn flex_mixed_direction_nested() {
606 use crate::{Constraint, Flex};
607 use ftui_core::geometry::Rect;
608
609 let outer = Rect::new(0, 0, 100, 20);
611
612 let rtl_cols = Flex::horizontal()
613 .constraints([Constraint::Fixed(40), Constraint::Fixed(60)])
614 .flow_direction(FlowDirection::Rtl)
615 .split(outer);
616
617 assert_eq!(rtl_cols[0].x, 60);
619 assert_eq!(rtl_cols[0].width, 40);
620 assert_eq!(rtl_cols[1].x, 0);
621 assert_eq!(rtl_cols[1].width, 60);
622
623 let inner_ltr = Flex::vertical()
625 .constraints([Constraint::Fixed(10), Constraint::Fill])
626 .split(rtl_cols[0]);
627
628 assert_eq!(inner_ltr[0].x, rtl_cols[0].x);
630 assert_eq!(inner_ltr[0].y, rtl_cols[0].y);
631 assert_eq!(inner_ltr[0].height, 10);
632 }
633
634 #[test]
635 fn logical_alignment_in_flex() {
636 use crate::{Constraint, Flex};
637 use ftui_core::geometry::Rect;
638
639 let area = Rect::new(0, 0, 100, 10);
640
641 let alignment = LogicalAlignment::Start.resolve(FlowDirection::Rtl);
643 let rects = Flex::horizontal()
644 .constraints([Constraint::Fixed(20)])
645 .alignment(alignment)
646 .split(area);
647
648 assert_eq!(rects[0].x, 80);
650 assert_eq!(rects[0].width, 20);
651 }
652}