Skip to main content

telar_layout_core/
direction.rs

1/// The writing direction the layout resolves logical edges against.
2///
3/// Layout is authored in *logical* terms — start/end rather than left/right — and resolved to physical edges
4/// when a style is handed to the engine. One build therefore serves both directions: flipping [`Direction`]
5/// re-resolves the tree in place instead of rebuilding it, which is why the intent is kept alongside the
6/// resolved style rather than baked into it.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
8pub enum Direction {
9    #[default]
10    Ltr,
11    Rtl,
12}
13
14impl Direction {
15    pub fn is_rtl(self) -> bool {
16        matches!(self, Direction::Rtl)
17    }
18
19    /// The direction conventionally written with `locale`'s script, keyed by language subtag: Arabic, Hebrew,
20    /// Persian, Urdu and the other right-to-left languages, matched against the tag's primary subtag so
21    /// `ar-EG` resolves like `ar`.
22    pub fn for_locale(locale: &str) -> Self {
23        let lang = locale
24            .split(['-', '_'])
25            .next()
26            .unwrap_or(locale)
27            .to_ascii_lowercase();
28        const RTL: &[&str] = &[
29            "ar", "arc", "ckb", "dv", "fa", "ha", "he", "khw", "ks", "ps", "sd", "ur", "uz", "yi",
30        ];
31        if RTL.contains(&lang.as_str()) {
32            Direction::Rtl
33        } else {
34            Direction::Ltr
35        }
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn rtl_languages_are_recognised_with_and_without_a_region() {
45        for tag in ["ar", "ar-EG", "he_IL", "fa", "ur-PK", "HE"] {
46            assert_eq!(Direction::for_locale(tag), Direction::Rtl, "{tag}");
47        }
48    }
49
50    #[test]
51    fn everything_else_is_left_to_right() {
52        for tag in ["en", "es-AR", "ja", "", "zz"] {
53            assert_eq!(Direction::for_locale(tag), Direction::Ltr, "{tag}");
54        }
55    }
56}