cranpose_ui/text/
paragraph.rs1use crate::text::unit::TextUnit;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
4pub enum TextAlign {
5 #[default]
6 Unspecified, 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 | 0xFB1D..=0xFDFF | 0xFE70..=0xFEFF | 0x10800..=0x10FFF | 0x1E800..=0x1EEFF )
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}