cranpose_ui/text/
paragraph.rs1use icu_properties::{CodePointMapData, props::BidiClass};
2
3use crate::text::unit::TextUnit;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
6pub enum TextAlign {
7 #[default]
8 Unspecified,
9 Left,
10 Right,
11 Center,
12 Justify,
13 Start,
14 End,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
18pub enum TextDirection {
19 #[default]
20 Unspecified,
21 Ltr,
22 Rtl,
23 Content,
24 ContentOrLtr,
25 ContentOrRtl,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
29pub enum LineBreak {
30 #[default]
31 Unspecified,
32 Simple,
33 Paragraph,
34 Heading,
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
38pub enum Hyphens {
39 #[default]
40 Unspecified,
41 None,
42 Auto,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq)]
46pub struct TextIndent {
47 pub first_line: TextUnit,
48 pub rest_line: TextUnit,
49}
50
51impl Default for TextIndent {
52 fn default() -> Self {
53 Self {
54 first_line: TextUnit::Unspecified,
55 rest_line: TextUnit::Unspecified,
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
61pub enum ResolvedTextDirection {
62 #[default]
63 Ltr,
64 Rtl,
65}
66
67impl TextDirection {
68 pub fn resolve(self, text: &str) -> ResolvedTextDirection {
72 match self {
73 TextDirection::Ltr => ResolvedTextDirection::Ltr,
74 TextDirection::Rtl => ResolvedTextDirection::Rtl,
75 TextDirection::Content | TextDirection::ContentOrLtr => {
76 resolve_content_direction(text, ResolvedTextDirection::Ltr)
77 }
78 TextDirection::ContentOrRtl => {
79 resolve_content_direction(text, ResolvedTextDirection::Rtl)
80 }
81 TextDirection::Unspecified => {
82 resolve_content_direction(text, ResolvedTextDirection::Ltr)
83 }
84 }
85 }
86}
87
88impl LineBreak {
89 pub fn is_specified(self) -> bool {
90 !matches!(self, Self::Unspecified)
91 }
92
93 pub fn take_or_else(self, fallback: impl FnOnce() -> LineBreak) -> LineBreak {
94 if self.is_specified() {
95 self
96 } else {
97 fallback()
98 }
99 }
100}
101
102impl Hyphens {
103 pub fn is_specified(self) -> bool {
104 !matches!(self, Self::Unspecified)
105 }
106
107 pub fn take_or_else(self, fallback: impl FnOnce() -> Hyphens) -> Hyphens {
108 if self.is_specified() {
109 self
110 } else {
111 fallback()
112 }
113 }
114}
115
116pub fn resolve_text_direction(
119 text: &str,
120 text_direction: Option<TextDirection>,
121) -> ResolvedTextDirection {
122 text_direction.unwrap_or_default().resolve(text)
123}
124
125fn resolve_content_direction(text: &str, fallback: ResolvedTextDirection) -> ResolvedTextDirection {
126 for (index, byte) in text.bytes().enumerate() {
127 if byte.is_ascii_alphabetic() {
128 return ResolvedTextDirection::Ltr;
129 }
130 if !byte.is_ascii() {
131 return resolve_unicode_content_direction(&text[index..], fallback);
132 }
133 }
134 fallback
135}
136
137fn resolve_unicode_content_direction(
138 text: &str,
139 fallback: ResolvedTextDirection,
140) -> ResolvedTextDirection {
141 let classes = CodePointMapData::<BidiClass>::new();
142 let mut isolate_depth = 0usize;
143 for ch in text.chars() {
144 match ch {
145 '\u{2066}'..='\u{2068}' => isolate_depth += 1,
146 '\u{2069}' => isolate_depth = isolate_depth.saturating_sub(1),
147 _ if isolate_depth == 0 => match classes.get(ch) {
148 BidiClass::LeftToRight => return ResolvedTextDirection::Ltr,
149 BidiClass::RightToLeft | BidiClass::ArabicLetter => {
150 return ResolvedTextDirection::Rtl;
151 }
152 _ => {}
153 },
154 _ => {}
155 }
156 }
157 fallback
158}
159
160#[cfg(test)]
161#[path = "tests/paragraph_tests.rs"]
162mod tests;