Skip to main content

cranpose_ui/text/
paragraph.rs

1use crate::text::unit::TextUnit;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
4pub enum TextAlign {
5    #[default]
6    Unspecified, // Kotlin distinction
7    Left,
8    Right,
9    Center,
10    Justify,
11    Start,
12    End,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
16pub enum TextDirection {
17    #[default]
18    Unspecified,
19    Ltr,
20    Rtl,
21    Content,
22    ContentOrLtr,
23    ContentOrRtl,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
27pub enum LineBreak {
28    #[default]
29    Unspecified,
30    Simple,
31    Paragraph,
32    Heading,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
36pub enum Hyphens {
37    #[default]
38    Unspecified,
39    None,
40    Auto,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct TextIndent {
45    pub first_line: TextUnit,
46    pub rest_line: TextUnit,
47}
48
49impl Default for TextIndent {
50    fn default() -> Self {
51        Self {
52            first_line: TextUnit::Unspecified,
53            rest_line: TextUnit::Unspecified,
54        }
55    }
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
59pub enum ResolvedTextDirection {
60    #[default]
61    Ltr,
62    Rtl,
63}
64
65impl TextDirection {
66    pub fn resolve(self, text: &str) -> ResolvedTextDirection {
67        match self {
68            TextDirection::Ltr => ResolvedTextDirection::Ltr,
69            TextDirection::Rtl => ResolvedTextDirection::Rtl,
70            TextDirection::Content | TextDirection::ContentOrLtr => {
71                resolve_content_direction(text, ResolvedTextDirection::Ltr)
72            }
73            TextDirection::ContentOrRtl => {
74                resolve_content_direction(text, ResolvedTextDirection::Rtl)
75            }
76            TextDirection::Unspecified => {
77                resolve_content_direction(text, ResolvedTextDirection::Ltr)
78            }
79        }
80    }
81}
82
83impl LineBreak {
84    pub fn is_specified(self) -> bool {
85        !matches!(self, Self::Unspecified)
86    }
87
88    pub fn take_or_else(self, fallback: impl FnOnce() -> LineBreak) -> LineBreak {
89        if self.is_specified() {
90            self
91        } else {
92            fallback()
93        }
94    }
95}
96
97impl Hyphens {
98    pub fn is_specified(self) -> bool {
99        !matches!(self, Self::Unspecified)
100    }
101
102    pub fn take_or_else(self, fallback: impl FnOnce() -> Hyphens) -> Hyphens {
103        if self.is_specified() {
104            self
105        } else {
106            fallback()
107        }
108    }
109}
110
111pub fn resolve_text_direction(
112    text: &str,
113    text_direction: Option<TextDirection>,
114) -> ResolvedTextDirection {
115    text_direction.unwrap_or_default().resolve(text)
116}
117
118fn resolve_content_direction(text: &str, fallback: ResolvedTextDirection) -> ResolvedTextDirection {
119    for ch in text.chars() {
120        if is_rtl_char(ch) {
121            return ResolvedTextDirection::Rtl;
122        }
123        if ch.is_alphabetic() {
124            return ResolvedTextDirection::Ltr;
125        }
126    }
127    fallback
128}
129
130fn is_rtl_char(ch: char) -> bool {
131    matches!(
132        ch as u32,
133        0x0590..=0x08FF | // Hebrew, Arabic, Syriac, Thaana, NKo, Samaritan, Mandaic, Arabic ext
134        0xFB1D..=0xFDFF | // Hebrew/Arabic presentation forms
135        0xFE70..=0xFEFF | // Arabic presentation forms B
136        0x10800..=0x10FFF | // Cypriot, Imperial Aramaic and other RTL historical scripts
137        0x1E800..=0x1EEFF // Adlam and Arabic mathematical alphabetic symbols
138    )
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn resolve_content_direction_detects_rtl_script() {
147        assert_eq!(
148            TextDirection::Content.resolve("שלום"),
149            ResolvedTextDirection::Rtl
150        );
151    }
152
153    #[test]
154    fn resolve_content_direction_detects_ltr_script() {
155        assert_eq!(
156            TextDirection::Content.resolve("Compose"),
157            ResolvedTextDirection::Ltr
158        );
159    }
160
161    #[test]
162    fn resolve_content_or_rtl_falls_back_to_rtl() {
163        assert_eq!(
164            TextDirection::ContentOrRtl.resolve("12345"),
165            ResolvedTextDirection::Rtl
166        );
167    }
168
169    #[test]
170    fn resolve_text_direction_defaults_to_ltr_for_unspecified() {
171        assert_eq!(
172            resolve_text_direction("12345", None),
173            ResolvedTextDirection::Ltr
174        );
175    }
176
177    #[test]
178    fn resolve_text_direction_uses_content_for_unspecified() {
179        assert_eq!(
180            resolve_text_direction("שלום", Some(TextDirection::Unspecified)),
181            ResolvedTextDirection::Rtl
182        );
183    }
184
185    #[test]
186    fn line_break_take_or_else_uses_fallback_for_unspecified() {
187        let value = LineBreak::Unspecified.take_or_else(|| LineBreak::Simple);
188        assert_eq!(value, LineBreak::Simple);
189        assert!(LineBreak::Simple.is_specified());
190    }
191
192    #[test]
193    fn hyphens_take_or_else_uses_fallback_for_unspecified() {
194        let value = Hyphens::Unspecified.take_or_else(|| Hyphens::None);
195        assert_eq!(value, Hyphens::None);
196        assert!(Hyphens::Auto.is_specified());
197    }
198}