Skip to main content

cranpose_ui/text/
paragraph.rs

1use icu_properties::{CodePointMapData, props::BidiClass};
2
3use crate::text::unit::TextUnit;
4
5/// Horizontal alignment of the lines of a paragraph within its width.
6/// Compose's `TextAlign`.
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
8pub enum TextAlign {
9    /// No alignment is set. The value comes from an enclosing style and acts like `Start`
10    /// when no style sets it.
11    #[default]
12    Unspecified,
13    /// Aligns lines to the left edge in both text directions.
14    Left,
15    /// Aligns lines to the right edge in both text directions.
16    Right,
17    /// Centers each line within the paragraph width.
18    Center,
19    /// Justified alignment. Lines are not stretched yet; they align like `Start`.
20    Justify,
21    /// Aligns lines to the start edge: left for left-to-right text, right otherwise.
22    Start,
23    /// Aligns lines to the end edge: right for left-to-right text, left otherwise.
24    End,
25}
26
27/// Base direction of a paragraph. Compose's `TextDirection`.
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
29pub enum TextDirection {
30    /// No direction is set. The value comes from an enclosing style and acts like `Content`
31    /// when no style sets it.
32    #[default]
33    Unspecified,
34    /// Left-to-right.
35    Ltr,
36    /// Right-to-left.
37    Rtl,
38    /// Taken from the first strong directional character of the text, left-to-right if the
39    /// text has none.
40    Content,
41    /// Like `Content`, with left-to-right as the fallback.
42    ContentOrLtr,
43    /// Like `Content`, with right-to-left as the fallback.
44    ContentOrRtl,
45}
46
47/// How a paragraph is broken into lines. Compose's `LineBreak`.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
49pub enum LineBreak {
50    /// No strategy is set. The value comes from an enclosing style and acts like `Simple`
51    /// when no style sets it.
52    #[default]
53    Unspecified,
54    /// Fills each line with as many words as fit. Fast, and the wrap changes little while
55    /// the user edits text.
56    Simple,
57    /// Spreads the words over the lines to even out their lengths, leaving the last line
58    /// free to stay short. Costs more than `Simple`.
59    Paragraph,
60    /// Spreads the words over all lines, the last one included, for short text such as
61    /// titles. Costs more than `Simple`.
62    Heading,
63}
64
65/// Whether words may be split with a hyphen at the end of a line. Compose's `Hyphens`.
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
67pub enum Hyphens {
68    /// No setting. The value comes from an enclosing style and acts like `None` when no
69    /// style sets it.
70    #[default]
71    Unspecified,
72    /// Words are never split with a hyphen.
73    None,
74    /// Words are split at hyphenation points of the text's locale when it makes a line fit.
75    Auto,
76}
77
78/// Indentation of the lines of a paragraph. Compose's `TextIndent`. Not yet applied by
79/// text layout.
80#[derive(Clone, Copy, Debug, PartialEq)]
81pub struct TextIndent {
82    /// Indent of the first line, as a `TextUnit`.
83    pub first_line: TextUnit,
84    /// Indent of every line after the first, as a `TextUnit`.
85    pub rest_line: TextUnit,
86}
87
88impl Default for TextIndent {
89    fn default() -> Self {
90        Self {
91            first_line: TextUnit::Unspecified,
92            rest_line: TextUnit::Unspecified,
93        }
94    }
95}
96
97/// A text direction with the content-based choices resolved, as used for layout.
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
99pub enum ResolvedTextDirection {
100    /// Left-to-right.
101    #[default]
102    Ltr,
103    /// Right-to-left.
104    Rtl,
105}
106
107impl TextDirection {
108    /// Resolves content-based directions from the first strong Unicode bidi
109    /// character outside directional isolates. Text without a strong character
110    /// uses the variant's fallback, which is LTR except for `ContentOrRtl`.
111    pub fn resolve(self, text: &str) -> ResolvedTextDirection {
112        match self {
113            TextDirection::Ltr => ResolvedTextDirection::Ltr,
114            TextDirection::Rtl => ResolvedTextDirection::Rtl,
115            TextDirection::Content | TextDirection::ContentOrLtr => {
116                resolve_content_direction(text, ResolvedTextDirection::Ltr)
117            }
118            TextDirection::ContentOrRtl => {
119                resolve_content_direction(text, ResolvedTextDirection::Rtl)
120            }
121            TextDirection::Unspecified => {
122                resolve_content_direction(text, ResolvedTextDirection::Ltr)
123            }
124        }
125    }
126}
127
128impl LineBreak {
129    /// Returns true unless the value is `Unspecified`.
130    pub fn is_specified(self) -> bool {
131        !matches!(self, Self::Unspecified)
132    }
133
134    /// Returns `self` when it is specified, otherwise the value returned by `fallback`.
135    pub fn take_or_else(self, fallback: impl FnOnce() -> LineBreak) -> LineBreak {
136        if self.is_specified() {
137            self
138        } else {
139            fallback()
140        }
141    }
142}
143
144impl Hyphens {
145    /// Returns true unless the value is `Unspecified`.
146    pub fn is_specified(self) -> bool {
147        !matches!(self, Self::Unspecified)
148    }
149
150    /// Returns `self` when it is specified, otherwise the value returned by `fallback`.
151    pub fn take_or_else(self, fallback: impl FnOnce() -> Hyphens) -> Hyphens {
152        if self.is_specified() {
153            self
154        } else {
155            fallback()
156        }
157    }
158}
159
160/// Resolves a paragraph's direction, using content with an LTR fallback when
161/// no direction is specified.
162pub fn resolve_text_direction(
163    text: &str,
164    text_direction: Option<TextDirection>,
165) -> ResolvedTextDirection {
166    text_direction.unwrap_or_default().resolve(text)
167}
168
169fn resolve_content_direction(text: &str, fallback: ResolvedTextDirection) -> ResolvedTextDirection {
170    for (index, byte) in text.bytes().enumerate() {
171        if byte.is_ascii_alphabetic() {
172            return ResolvedTextDirection::Ltr;
173        }
174        if !byte.is_ascii() {
175            return resolve_unicode_content_direction(&text[index..], fallback);
176        }
177    }
178    fallback
179}
180
181fn resolve_unicode_content_direction(
182    text: &str,
183    fallback: ResolvedTextDirection,
184) -> ResolvedTextDirection {
185    let classes = CodePointMapData::<BidiClass>::new();
186    let mut isolate_depth = 0usize;
187    for ch in text.chars() {
188        match ch {
189            '\u{2066}'..='\u{2068}' => isolate_depth += 1,
190            '\u{2069}' => isolate_depth = isolate_depth.saturating_sub(1),
191            _ if isolate_depth == 0 => match classes.get(ch) {
192                BidiClass::LeftToRight => return ResolvedTextDirection::Ltr,
193                BidiClass::RightToLeft | BidiClass::ArabicLetter => {
194                    return ResolvedTextDirection::Rtl;
195                }
196                _ => {}
197            },
198            _ => {}
199        }
200    }
201    fallback
202}
203
204#[cfg(test)]
205#[path = "tests/paragraph_tests.rs"]
206mod tests;