1use unicode_segmentation::UnicodeSegmentation;
5
6use crate::{Cell, Glyph, Pos, Shape, Size, Surface};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct Text {
18 lines: Vec<Vec<Glyph>>,
20}
21
22impl Text {
23 #[must_use]
28 pub fn new(content: &str) -> Option<Self> {
29 let lines = content
30 .split('\n')
31 .map(|line| line.graphemes(true).map(Glyph::new).collect())
32 .collect::<Option<Vec<_>>>()?;
33
34 Some(Self { lines })
35 }
36
37 #[must_use]
39 pub fn lines(&self) -> &[Vec<Glyph>] {
40 &self.lines
41 }
42
43 #[must_use]
48 pub fn width(&self) -> u16 {
49 let longest = self.lines.iter().map(Vec::len).max().unwrap_or_default();
50 u16::try_from(longest).unwrap_or(u16::MAX)
51 }
52
53 #[must_use]
55 pub fn height(&self) -> u16 {
56 u16::try_from(self.lines.len()).unwrap_or(u16::MAX)
57 }
58}
59
60#[derive(Debug)]
72pub struct TextShape {
73 pub at: Pos,
75 pub area: Size,
77 pub text: Text,
79}
80
81impl Shape for TextShape {
82 fn draw(&self, surface: &mut dyn Surface) {
83 let dy = (i32::from(self.area.height) - i32::from(self.text.height())) / 2;
84
85 for (row, line) in self.text.lines().iter().enumerate() {
86 let width = i32::try_from(line.len()).unwrap_or(i32::MAX);
87 let dx = (i32::from(self.area.width) - width) / 2;
88 let Some(y) = coordinate(self.at.y, dy, row) else {
89 continue;
90 };
91
92 for (column, glyph) in line.iter().enumerate() {
93 let Some(x) = coordinate(self.at.x, dx, column) else {
94 continue;
95 };
96 surface.stamp(Pos { x, y }, Cell::Literal(glyph.clone()));
97 }
98 }
99 }
100}
101
102fn coordinate(base: i16, offset: i32, step: usize) -> Option<i16> {
105 let step = i32::try_from(step).ok()?;
106 i16::try_from(i32::from(base).checked_add(offset)?.checked_add(step)?).ok()
107}
108
109#[cfg(test)]
110mod tests {
111 use super::{Text, TextShape};
112 use crate::shape::counting::CountingSurface;
113 use crate::{Buffer, FigureLayer, GlyphCatalog, Pos, Shape, Size, StampMode, Window, render};
114
115 fn text(content: &str) -> Text {
116 Text::new(content).unwrap_or_else(|| panic!("{content:?} has no control character"))
117 }
118
119 fn light() -> GlyphCatalog {
120 GlyphCatalog::light()
121 }
122
123 fn origin() -> Pos {
124 Pos { x: 0, y: 0 }
125 }
126
127 fn size(width: u16, height: u16) -> Size {
128 Size { width, height }
129 }
130
131 #[test]
134 fn a_text_splits_on_newlines_and_refuses_other_control_characters() {
135 let text = Text::new("one\ntwo").expect("\"one\\ntwo\" is two lines");
136
137 assert_eq!(text.lines().len(), 2);
138 assert_eq!(text.lines()[0][0].as_str(), "o");
139 assert_eq!(text.lines()[1][2].as_str(), "o");
140
141 for refused in ["a\tb", "a\rb", "a\r\nb", "a\u{7}b"] {
142 assert!(
143 Text::new(refused).is_none(),
144 "{refused:?} holds a control character"
145 );
146 }
147 }
148
149 #[test]
152 fn an_empty_text_is_one_empty_line_and_a_trailing_newline_adds_one() {
153 for (content, lines, width, height) in [
154 ("", 1, 0, 1),
155 ("one", 1, 3, 1),
156 ("one\n", 2, 3, 2),
157 ("one\n\n", 3, 3, 3),
158 ] {
159 let text = Text::new(content)
160 .unwrap_or_else(|| panic!("{content:?} has no control character"));
161
162 assert_eq!(text.lines().len(), lines, "{content:?}");
163 assert_eq!(text.width(), width, "{content:?}");
164 assert_eq!(text.height(), height, "{content:?}");
165 }
166 }
167
168 #[test]
172 fn a_text_shape_centres_each_line_in_its_area() {
173 let mut buffer = Buffer::new(Window {
174 origin: origin(),
175 size: size(8, 5),
176 });
177 TextShape {
178 at: origin(),
179 area: size(8, 5),
180 text: text("one\ntwo"),
181 }
182 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
183
184 assert_eq!(
185 render(&buffer, &light(), buffer.window()),
186 " \n one \n two \n \n \n"
187 );
188 }
189
190 #[test]
193 fn a_text_shape_writes_a_line_at_the_computed_position() {
194 let mut buffer = Buffer::new(Window {
195 origin: origin(),
196 size: size(8, 5),
197 });
198 TextShape {
199 at: origin(),
200 area: size(8, 5),
201 text: text("hi"),
202 }
203 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
204
205 assert_eq!(
206 render(&buffer, &light(), buffer.window()),
207 " \n \n hi \n \n \n"
208 );
209 }
210
211 #[test]
214 fn a_zero_area_places_the_block_around_the_position() {
215 let mut buffer = Buffer::new(Window {
216 origin: Pos { x: 8, y: 4 },
217 size: size(6, 3),
218 });
219 TextShape {
220 at: Pos { x: 10, y: 5 },
221 area: size(0, 0),
222 text: text("abc"),
223 }
224 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
225
226 assert_eq!(
227 render(&buffer, &light(), buffer.window()),
228 " \n abc \n \n"
229 );
230 }
231
232 #[test]
235 fn a_text_shape_does_not_clip_to_its_area() {
236 let mut buffer = Buffer::new(Window {
237 origin: origin(),
238 size: size(8, 3),
239 });
240 TextShape {
241 at: Pos { x: 5, y: 1 },
242 area: size(1, 1),
243 text: text("abc"),
244 }
245 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
246
247 assert_eq!(
248 render(&buffer, &light(), buffer.window()),
249 " \n abc \n \n"
250 );
251 }
252
253 #[test]
256 fn an_empty_line_writes_nothing_and_a_short_line_writes_no_spaces() {
257 let mut buffer = Buffer::new(Window {
258 origin: origin(),
259 size: size(4, 3),
260 });
261 TextShape {
262 at: origin(),
263 area: size(4, 3),
264 text: text("a\n\nbc"),
265 }
266 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
267
268 assert_eq!(
269 render(&buffer, &light(), buffer.window()),
270 " a \n \n bc \n"
271 );
272 }
273
274 #[test]
276 fn no_text_shape_writes_any_position_more_than_once() {
277 for (area, content) in [
278 (size(8, 5), "one\ntwo"),
279 (size(0, 0), "abc"),
280 (size(1, 1), "abc"),
281 (size(4, 3), "a\n\nbc"),
282 (size(4, 3), ""),
283 ] {
284 let mut surface = CountingSurface::default();
285 TextShape {
286 at: Pos { x: 0, y: 0 },
287 area,
288 text: text(content),
289 }
290 .draw(&mut surface);
291
292 assert!(
293 surface.max_writes() <= 1,
294 "{content:?} in {area:?} wrote a position more than once"
295 );
296 }
297 }
298
299 #[test]
302 fn a_three_line_block_at_a_zero_area_sits_around_the_point() {
303 let mut buffer = Buffer::new(Window {
304 origin: origin(),
305 size: size(10, 10),
306 });
307 TextShape {
308 at: Pos { x: 5, y: 5 },
309 area: size(0, 0),
310 text: text("ABC\nD\nEFG"),
311 }
312 .draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
313
314 assert_eq!(
315 render(&buffer, &light(), buffer.window()),
316 concat!(
317 " \n",
318 " \n",
319 " \n",
320 " \n",
321 " ABC \n",
322 " D \n",
323 " EFG \n",
324 " \n",
325 " \n",
326 " \n",
327 )
328 );
329 }
330
331 #[test]
334 fn a_second_text_shape_overwrites_the_first_where_they_meet() {
335 let first = || TextShape {
336 at: Pos { x: 5, y: 5 },
337 area: size(0, 0),
338 text: text("ABC\nD\nEFG"),
339 };
340 let second = || TextShape {
341 at: Pos { x: 5, y: 5 },
342 area: size(6, 6),
343 text: text("123\n4\n567"),
344 };
345
346 let drawn = |lower: &TextShape, upper: &TextShape| {
347 let mut buffer = Buffer::new(Window {
348 origin: origin(),
349 size: size(10, 10),
350 });
351 lower.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
352 upper.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
353 render(&buffer, &light(), buffer.window())
354 };
355
356 let first_then_second = drawn(&first(), &second());
357 assert_eq!(
358 first_then_second,
359 concat!(
360 " \n",
361 " \n",
362 " \n",
363 " \n",
364 " ABC \n",
365 " D \n",
366 " EF123 \n",
367 " 4 \n",
368 " 567 \n",
369 " \n",
370 )
371 );
372
373 let second_then_first = drawn(&second(), &first());
374 assert!(
375 second_then_first.lines().nth(6) == Some(" EFG23 "),
376 "the other order leaves `EFG23` on that row: {second_then_first:?}"
377 );
378 assert_ne!(first_then_second, second_then_first);
379 }
380}