cranpose_text/paragraph.rs
1use icu_properties::{CodePointMapData, props::BidiClass};
2
3use crate::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 Self::Ltr => ResolvedTextDirection::Ltr,
114 Self::Rtl => ResolvedTextDirection::Rtl,
115 Self::Content | Self::ContentOrLtr => {
116 resolve_content_direction(text, ResolvedTextDirection::Ltr)
117 }
118 Self::ContentOrRtl => resolve_content_direction(text, ResolvedTextDirection::Rtl),
119 Self::Unspecified => resolve_content_direction(text, ResolvedTextDirection::Ltr),
120 }
121 }
122}
123
124macro_rules! impl_unspecified_fallback {
125 ($($choice:ty),*) => {$(
126 impl $choice {
127 /// Returns true unless the value is `Unspecified`.
128 pub fn is_specified(self) -> bool {
129 !matches!(self, Self::Unspecified)
130 }
131
132 /// Returns `self` when it is specified, otherwise the value returned by `fallback`.
133 pub fn take_or_else(self, fallback: impl FnOnce() -> Self) -> Self {
134 if self.is_specified() {
135 self
136 } else {
137 fallback()
138 }
139 }
140 }
141 )*};
142}
143
144impl_unspecified_fallback!(LineBreak, Hyphens);
145
146/// Resolves a paragraph's direction, using content with an LTR fallback when
147/// no direction is specified.
148pub fn resolve_text_direction(
149 text: &str,
150 text_direction: Option<TextDirection>,
151) -> ResolvedTextDirection {
152 text_direction.unwrap_or_default().resolve(text)
153}
154
155fn resolve_content_direction(text: &str, fallback: ResolvedTextDirection) -> ResolvedTextDirection {
156 for (index, byte) in text.bytes().enumerate() {
157 if byte.is_ascii_alphabetic() {
158 return ResolvedTextDirection::Ltr;
159 }
160 if !byte.is_ascii() {
161 return resolve_unicode_content_direction(&text[index..], fallback);
162 }
163 }
164 fallback
165}
166
167fn resolve_unicode_content_direction(
168 text: &str,
169 fallback: ResolvedTextDirection,
170) -> ResolvedTextDirection {
171 let classes = CodePointMapData::<BidiClass>::new();
172 let mut isolate_depth = 0usize;
173 for ch in text.chars() {
174 match ch {
175 '\u{2066}'..='\u{2068}' => isolate_depth += 1,
176 '\u{2069}' => isolate_depth = isolate_depth.saturating_sub(1),
177 _ if isolate_depth == 0 => match classes.get(ch) {
178 BidiClass::LeftToRight => return ResolvedTextDirection::Ltr,
179 BidiClass::RightToLeft | BidiClass::ArabicLetter => {
180 return ResolvedTextDirection::Rtl;
181 }
182 _ => {}
183 },
184 _ => {}
185 }
186 }
187 fallback
188}
189
190#[cfg(test)]
191#[path = "tests/paragraph_tests.rs"]
192mod tests;