1use std::hash::{Hash, Hasher};
2
3const MIN_SCALE_DOWN_FONT_SIZE_SP: f32 = 1.0;
4
5#[derive(Clone, Copy, Debug, Default)]
7pub enum TextOverflow {
8 #[default]
9 Clip,
10 Ellipsis,
11 Visible,
12 StartEllipsis,
13 MiddleEllipsis,
14 ScaleDown {
15 min_font_size_sp: f32,
16 },
17}
18
19impl TextOverflow {
20 pub fn normalized(self) -> Self {
21 match self {
22 Self::ScaleDown { min_font_size_sp } => Self::ScaleDown {
23 min_font_size_sp: normalize_scale_down_min_font_size_sp(min_font_size_sp),
24 },
25 other => other,
26 }
27 }
28
29 pub fn scale_down_min_font_size_sp(self) -> Option<f32> {
30 match self.normalized() {
31 Self::ScaleDown { min_font_size_sp } => Some(min_font_size_sp),
32 _ => None,
33 }
34 }
35}
36
37impl PartialEq for TextOverflow {
38 fn eq(&self, other: &Self) -> bool {
39 match ((*self).normalized(), (*other).normalized()) {
40 (Self::Clip, Self::Clip)
41 | (Self::Ellipsis, Self::Ellipsis)
42 | (Self::Visible, Self::Visible)
43 | (Self::StartEllipsis, Self::StartEllipsis)
44 | (Self::MiddleEllipsis, Self::MiddleEllipsis) => true,
45 (
46 Self::ScaleDown {
47 min_font_size_sp: left,
48 },
49 Self::ScaleDown {
50 min_font_size_sp: right,
51 },
52 ) => left.to_bits() == right.to_bits(),
53 _ => false,
54 }
55 }
56}
57
58impl Eq for TextOverflow {}
59
60impl Hash for TextOverflow {
61 fn hash<H: Hasher>(&self, state: &mut H) {
62 match (*self).normalized() {
63 Self::Clip => 0u8.hash(state),
64 Self::Ellipsis => 1u8.hash(state),
65 Self::Visible => 2u8.hash(state),
66 Self::StartEllipsis => 3u8.hash(state),
67 Self::MiddleEllipsis => 4u8.hash(state),
68 Self::ScaleDown { min_font_size_sp } => {
69 5u8.hash(state);
70 min_font_size_sp.to_bits().hash(state);
71 }
72 }
73 }
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78pub struct TextLayoutOptions {
79 pub overflow: TextOverflow,
80 pub soft_wrap: bool,
81 pub max_lines: usize,
82 pub min_lines: usize,
83}
84
85impl Default for TextLayoutOptions {
86 fn default() -> Self {
87 Self {
88 overflow: TextOverflow::Clip,
89 soft_wrap: true,
90 max_lines: usize::MAX,
91 min_lines: 1,
92 }
93 }
94}
95
96impl TextLayoutOptions {
97 pub fn normalized(self) -> Self {
98 let min_lines = self.min_lines.max(1);
99 let max_lines = self.max_lines.max(min_lines);
100 Self {
101 overflow: self.overflow.normalized(),
102 soft_wrap: self.soft_wrap,
103 max_lines,
104 min_lines,
105 }
106 }
107}
108
109fn normalize_scale_down_min_font_size_sp(value: f32) -> f32 {
110 if value.is_finite() && value >= MIN_SCALE_DOWN_FONT_SIZE_SP {
111 value
112 } else {
113 MIN_SCALE_DOWN_FONT_SIZE_SP
114 }
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
121pub struct TextOptions {
122 pub overflow: TextOverflow,
123 pub soft_wrap: bool,
124 pub max_lines: Option<usize>,
125 pub min_lines: usize,
126}
127
128impl Default for TextOptions {
129 fn default() -> Self {
130 Self {
131 overflow: TextOverflow::Clip,
132 soft_wrap: true,
133 max_lines: None,
134 min_lines: 1,
135 }
136 }
137}
138
139impl From<TextOptions> for TextLayoutOptions {
140 fn from(options: TextOptions) -> Self {
141 Self {
142 overflow: options.overflow,
143 soft_wrap: options.soft_wrap,
144 max_lines: options.max_lines.unwrap_or(usize::MAX),
145 min_lines: options.min_lines,
146 }
147 .normalized()
148 }
149}
150
151impl From<TextLayoutOptions> for TextOptions {
152 fn from(options: TextLayoutOptions) -> Self {
153 let options = options.normalized();
154 Self {
155 overflow: options.overflow,
156 soft_wrap: options.soft_wrap,
157 max_lines: (options.max_lines != usize::MAX).then_some(options.max_lines),
158 min_lines: options.min_lines,
159 }
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn normalized_enforces_minimum_one_line() {
169 let options = TextLayoutOptions {
170 min_lines: 0,
171 max_lines: 0,
172 ..Default::default()
173 }
174 .normalized();
175
176 assert_eq!(options.min_lines, 1);
177 assert_eq!(options.max_lines, 1);
178 }
179
180 #[test]
181 fn normalized_ensures_max_not_smaller_than_min() {
182 let options = TextLayoutOptions {
183 min_lines: 3,
184 max_lines: 1,
185 ..Default::default()
186 }
187 .normalized();
188
189 assert_eq!(options.min_lines, 3);
190 assert_eq!(options.max_lines, 3);
191 }
192
193 #[test]
194 fn normalized_sanitizes_scale_down_min_font_size() {
195 let options = TextLayoutOptions {
196 overflow: TextOverflow::ScaleDown {
197 min_font_size_sp: f32::NAN,
198 },
199 ..Default::default()
200 }
201 .normalized();
202
203 assert_eq!(
204 options.overflow,
205 TextOverflow::ScaleDown {
206 min_font_size_sp: 1.0
207 }
208 );
209 }
210
211 #[test]
212 fn text_options_default_maps_to_unlimited_layout() {
213 let layout = TextLayoutOptions::from(TextOptions::default());
214
215 assert_eq!(layout.overflow, TextOverflow::Clip);
216 assert!(layout.soft_wrap);
217 assert_eq!(layout.max_lines, usize::MAX);
218 assert_eq!(layout.min_lines, 1);
219 }
220
221 #[test]
222 fn text_options_maps_optional_max_lines_to_layout_limit() {
223 let layout = TextLayoutOptions::from(TextOptions {
224 overflow: TextOverflow::Ellipsis,
225 soft_wrap: false,
226 max_lines: Some(1),
227 min_lines: 1,
228 });
229
230 assert_eq!(layout.overflow, TextOverflow::Ellipsis);
231 assert!(!layout.soft_wrap);
232 assert_eq!(layout.max_lines, 1);
233 assert_eq!(layout.min_lines, 1);
234 }
235
236 #[test]
237 fn text_options_preserve_scale_down_overflow() {
238 let layout = TextLayoutOptions::from(TextOptions {
239 overflow: TextOverflow::ScaleDown {
240 min_font_size_sp: 9.0,
241 },
242 soft_wrap: false,
243 max_lines: Some(1),
244 min_lines: 1,
245 });
246
247 assert_eq!(
248 layout.overflow,
249 TextOverflow::ScaleDown {
250 min_font_size_sp: 9.0
251 }
252 );
253 assert!(!layout.soft_wrap);
254 assert_eq!(layout.max_lines, 1);
255 }
256}