1 | /* Canvas for random-access procedural text art. |
2 | Copyright (C) 2023-2024 Free Software Foundation, Inc. |
3 | Contributed by David Malcolm <dmalcolm@redhat.com>. |
4 | |
5 | This file is part of GCC. |
6 | |
7 | GCC is free software; you can redistribute it and/or modify it under |
8 | the terms of the GNU General Public License as published by the Free |
9 | Software Foundation; either version 3, or (at your option) any later |
10 | version. |
11 | |
12 | GCC is distributed in the hope that it will be useful, but WITHOUT ANY |
13 | WARRANTY; without even the implied warranty of MERCHANTABILITY or |
14 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
15 | for more details. |
16 | |
17 | You should have received a copy of the GNU General Public License |
18 | along with GCC; see the file COPYING3. If not see |
19 | <http://www.gnu.org/licenses/>. */ |
20 | |
21 | #include "config.h" |
22 | #define INCLUDE_VECTOR |
23 | #include "system.h" |
24 | #include "coretypes.h" |
25 | #include "pretty-print.h" |
26 | #include "selftest.h" |
27 | #include "text-art/selftests.h" |
28 | #include "text-art/canvas.h" |
29 | |
30 | using namespace text_art; |
31 | |
32 | canvas::canvas (size_t size, const style_manager &style_mgr) |
33 | : m_cells (size_t (size.w, size.h)), |
34 | m_style_mgr (style_mgr) |
35 | { |
36 | m_cells.fill (element: cell_t (' ')); |
37 | } |
38 | |
39 | void |
40 | canvas::paint (coord_t coord, styled_unichar ch) |
41 | { |
42 | m_cells.set (coord, element: std::move (ch)); |
43 | } |
44 | |
45 | void |
46 | canvas::paint_text (coord_t coord, const styled_string &text) |
47 | { |
48 | for (auto ch : text) |
49 | { |
50 | paint (coord, ch); |
51 | if (ch.double_width_p ()) |
52 | coord.x += 2; |
53 | else |
54 | coord.x++; |
55 | } |
56 | } |
57 | |
58 | void |
59 | canvas::fill (rect_t rect, cell_t c) |
60 | { |
61 | for (int y = rect.get_min_y (); y < rect.get_next_y (); y++) |
62 | for (int x = rect.get_min_x (); x < rect.get_next_x (); x++) |
63 | paint(coord: coord_t (x, y), ch: c); |
64 | } |
65 | |
66 | void |
67 | canvas::debug_fill () |
68 | { |
69 | fill (rect: rect_t (coord_t (0, 0), get_size ()), c: cell_t ('*')); |
70 | } |
71 | |
72 | void |
73 | canvas::print_to_pp (pretty_printer *pp, |
74 | const char *per_line_prefix) const |
75 | { |
76 | for (int y = 0; y < m_cells.get_size ().h; y++) |
77 | { |
78 | style::id_t curr_style_id = 0; |
79 | if (per_line_prefix) |
80 | pp_string (pp, per_line_prefix); |
81 | |
82 | pretty_printer line_pp; |
83 | line_pp.show_color = pp->show_color; |
84 | line_pp.url_format = pp->url_format; |
85 | const int final_x_in_row = get_final_x_in_row (y); |
86 | for (int x = 0; x <= final_x_in_row; x++) |
87 | { |
88 | if (x > 0) |
89 | { |
90 | const cell_t prev_cell = m_cells.get (coord: coord_t (x - 1, y)); |
91 | if (prev_cell.double_width_p ()) |
92 | /* This cell is just a placeholder for the |
93 | 2nd column of a double width cell; skip it. */ |
94 | continue; |
95 | } |
96 | const cell_t cell = m_cells.get (coord: coord_t (x, y)); |
97 | if (cell.get_style_id () != curr_style_id) |
98 | { |
99 | m_style_mgr.print_any_style_changes (pp: &line_pp, |
100 | old_id: curr_style_id, |
101 | new_id: cell.get_style_id ()); |
102 | curr_style_id = cell.get_style_id (); |
103 | } |
104 | pp_unicode_character (&line_pp, cell.get_code ()); |
105 | if (cell.emoji_variant_p ()) |
106 | /* Append U+FE0F VARIATION SELECTOR-16 to select the emoji |
107 | variation of the char. */ |
108 | pp_unicode_character (&line_pp, 0xFE0F); |
109 | } |
110 | /* Reset the style at the end of each line. */ |
111 | m_style_mgr.print_any_style_changes (pp: &line_pp, old_id: curr_style_id, new_id: 0); |
112 | |
113 | /* Print from line_pp to pp, stripping trailing whitespace from |
114 | the line. */ |
115 | const char *line_buf = pp_formatted_text (&line_pp); |
116 | ::size_t len = strlen (s: line_buf); |
117 | while (len > 0) |
118 | { |
119 | if (line_buf[len - 1] == ' ') |
120 | len--; |
121 | else |
122 | break; |
123 | } |
124 | pp_append_text (pp, line_buf, line_buf + len); |
125 | pp_newline (pp); |
126 | } |
127 | } |
128 | |
129 | DEBUG_FUNCTION void |
130 | canvas::debug (bool styled) const |
131 | { |
132 | pretty_printer pp; |
133 | if (styled) |
134 | { |
135 | pp_show_color (&pp) = true; |
136 | pp.url_format = determine_url_format (DIAGNOSTICS_URL_AUTO); |
137 | } |
138 | print_to_pp (pp: &pp); |
139 | fprintf (stderr, format: "%s\n" , pp_formatted_text (&pp)); |
140 | } |
141 | |
142 | /* Find right-most non-default cell in this row, |
143 | or -1 if all are default. */ |
144 | |
145 | int |
146 | canvas::get_final_x_in_row (int y) const |
147 | { |
148 | for (int x = m_cells.get_size ().w - 1; x >= 0; x--) |
149 | { |
150 | cell_t cell = m_cells.get (coord: coord_t (x, y)); |
151 | if (cell.get_code () != ' ' |
152 | || cell.get_style_id () != style::id_plain) |
153 | return x; |
154 | } |
155 | return -1; |
156 | } |
157 | |
158 | #if CHECKING_P |
159 | |
160 | namespace selftest { |
161 | |
162 | static void |
163 | test_blank () |
164 | { |
165 | style_manager sm; |
166 | canvas c (canvas::size_t (5, 5), sm); |
167 | ASSERT_CANVAS_STREQ (c, false, |
168 | ("\n" |
169 | "\n" |
170 | "\n" |
171 | "\n" |
172 | "\n" )); |
173 | } |
174 | |
175 | static void |
176 | test_abc () |
177 | { |
178 | style_manager sm; |
179 | canvas c (canvas::size_t (3, 3), sm); |
180 | c.paint (coord: canvas::coord_t (0, 0), ch: styled_unichar ('A')); |
181 | c.paint (coord: canvas::coord_t (1, 1), ch: styled_unichar ('B')); |
182 | c.paint (coord: canvas::coord_t (2, 2), ch: styled_unichar ('C')); |
183 | |
184 | ASSERT_CANVAS_STREQ (c, false, |
185 | "A\n B\n C\n" ); |
186 | } |
187 | |
188 | static void |
189 | test_debug_fill () |
190 | { |
191 | style_manager sm; |
192 | canvas c (canvas::size_t (5, 3), sm); |
193 | c.debug_fill(); |
194 | ASSERT_CANVAS_STREQ (c, false, |
195 | ("*****\n" |
196 | "*****\n" |
197 | "*****\n" )); |
198 | } |
199 | |
200 | static void |
201 | test_text () |
202 | { |
203 | style_manager sm; |
204 | canvas c (canvas::size_t (6, 1), sm); |
205 | c.paint_text (coord: canvas::coord_t (0, 0), text: styled_string (sm, "012345" )); |
206 | ASSERT_CANVAS_STREQ (c, false, |
207 | ("012345\n" )); |
208 | |
209 | /* Paint an emoji character that should occupy two canvas columns when |
210 | printed. */ |
211 | c.paint_text (coord: canvas::coord_t (2, 0), text: styled_string ((cppchar_t)0x1f642)); |
212 | ASSERT_CANVAS_STREQ (c, false, |
213 | ("01🙂45\n" )); |
214 | } |
215 | |
216 | static void |
217 | test_circle () |
218 | { |
219 | canvas::size_t sz (30, 30); |
220 | style_manager sm; |
221 | canvas canvas (sz, sm); |
222 | canvas::coord_t center (sz.w / 2, sz.h / 2); |
223 | const int radius = 12; |
224 | const int radius_squared = radius * radius; |
225 | for (int x = 0; x < sz.w; x++) |
226 | for (int y = 0; y < sz.h; y++) |
227 | { |
228 | int dx = x - center.x; |
229 | int dy = y - center.y; |
230 | char ch = "AB" [(x + y) % 2]; |
231 | if (dx * dx + dy * dy < radius_squared) |
232 | canvas.paint (coord: canvas::coord_t (x, y), ch: styled_unichar (ch)); |
233 | } |
234 | ASSERT_CANVAS_STREQ |
235 | (canvas, false, |
236 | ("\n" |
237 | "\n" |
238 | "\n" |
239 | "\n" |
240 | " BABABABAB\n" |
241 | " ABABABABABABA\n" |
242 | " ABABABABABABABA\n" |
243 | " ABABABABABABABABA\n" |
244 | " ABABABABABABABABABA\n" |
245 | " ABABABABABABABABABABA\n" |
246 | " BABABABABABABABABABAB\n" |
247 | " BABABABABABABABABABABAB\n" |
248 | " ABABABABABABABABABABABA\n" |
249 | " BABABABABABABABABABABAB\n" |
250 | " ABABABABABABABABABABABA\n" |
251 | " BABABABABABABABABABABAB\n" |
252 | " ABABABABABABABABABABABA\n" |
253 | " BABABABABABABABABABABAB\n" |
254 | " ABABABABABABABABABABABA\n" |
255 | " BABABABABABABABABABABAB\n" |
256 | " BABABABABABABABABABAB\n" |
257 | " ABABABABABABABABABABA\n" |
258 | " ABABABABABABABABABA\n" |
259 | " ABABABABABABABABA\n" |
260 | " ABABABABABABABA\n" |
261 | " ABABABABABABA\n" |
262 | " BABABABAB\n" |
263 | "\n" |
264 | "\n" |
265 | "\n" )); |
266 | } |
267 | |
268 | static void |
269 | test_color_circle () |
270 | { |
271 | const canvas::size_t sz (10, 10); |
272 | const canvas::coord_t center (sz.w / 2, sz.h / 2); |
273 | const int outer_r2 = 25; |
274 | const int inner_r2 = 10; |
275 | style_manager sm; |
276 | canvas c (sz, sm); |
277 | for (int x = 0; x < sz.w; x++) |
278 | for (int y = 0; y < sz.h; y++) |
279 | { |
280 | const int dist_from_center_squared |
281 | = ((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y)); |
282 | if (dist_from_center_squared < outer_r2) |
283 | { |
284 | style s; |
285 | if (dist_from_center_squared < inner_r2) |
286 | s.m_fg_color = style::named_color::RED; |
287 | else |
288 | s.m_fg_color = style::named_color::GREEN; |
289 | c.paint (coord: canvas::coord_t (x, y), |
290 | ch: styled_unichar ('*', false, sm.get_or_create_id (style: s))); |
291 | } |
292 | } |
293 | ASSERT_EQ (sm.get_num_styles (), 3); |
294 | ASSERT_CANVAS_STREQ |
295 | (c, false, |
296 | ("\n" |
297 | " *****\n" |
298 | " *******\n" |
299 | " *********\n" |
300 | " *********\n" |
301 | " *********\n" |
302 | " *********\n" |
303 | " *********\n" |
304 | " *******\n" |
305 | " *****\n" )); |
306 | ASSERT_CANVAS_STREQ |
307 | (c, true, |
308 | ("\n" |
309 | " [32m[K*****[m[K\n" |
310 | " [32m[K***[31m[K*[32m[K***[m[K\n" |
311 | " [32m[K**[31m[K*****[32m[K**[m[K\n" |
312 | " [32m[K**[31m[K*****[32m[K**[m[K\n" |
313 | " [32m[K*[31m[K*******[32m[K*[m[K\n" |
314 | " [32m[K**[31m[K*****[32m[K**[m[K\n" |
315 | " [32m[K**[31m[K*****[32m[K**[m[K\n" |
316 | " [32m[K***[31m[K*[32m[K***[m[K\n" |
317 | " [32m[K*****[m[K\n" )); |
318 | } |
319 | |
320 | static void |
321 | test_bold () |
322 | { |
323 | auto_fix_quotes fix_quotes; |
324 | style_manager sm; |
325 | styled_string s (styled_string::from_fmt (sm, format_decoder: nullptr, |
326 | fmt: "before %qs after" , "foo" )); |
327 | canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm); |
328 | c.paint_text (coord: canvas::coord_t (0, 0), text: s); |
329 | ASSERT_CANVAS_STREQ (c, false, |
330 | "before `foo' after\n" ); |
331 | ASSERT_CANVAS_STREQ (c, true, |
332 | "before `[00;01m[Kfoo[00m[K' after\n" ); |
333 | } |
334 | |
335 | static void |
336 | test_emoji () |
337 | { |
338 | style_manager sm; |
339 | styled_string s (0x26A0, /* U+26A0 WARNING SIGN. */ |
340 | true); |
341 | canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm); |
342 | c.paint_text (coord: canvas::coord_t (0, 0), text: s); |
343 | ASSERT_CANVAS_STREQ (c, false, "⚠️\n" ); |
344 | ASSERT_CANVAS_STREQ (c, true, "⚠️\n" ); |
345 | } |
346 | |
347 | static void |
348 | test_emoji_2 () |
349 | { |
350 | style_manager sm; |
351 | styled_string s; |
352 | s.append (suffix: styled_string (0x26A0, /* U+26A0 WARNING SIGN. */ |
353 | true)); |
354 | s.append (suffix: styled_string (sm, "test" )); |
355 | ASSERT_EQ (s.size (), 5); |
356 | ASSERT_EQ (s.calc_canvas_width (), 5); |
357 | canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm); |
358 | c.paint_text (coord: canvas::coord_t (0, 0), text: s); |
359 | ASSERT_CANVAS_STREQ (c, false, |
360 | /* U+26A0 WARNING SIGN, as UTF-8: 0xE2 0x9A 0xA0. */ |
361 | "\xE2\x9A\xA0" |
362 | /* U+FE0F VARIATION SELECTOR-16, as UTF-8: 0xEF 0xB8 0x8F. */ |
363 | "\xEF\xB8\x8F" |
364 | "test\n" ); |
365 | } |
366 | |
367 | static void |
368 | test_canvas_urls () |
369 | { |
370 | style_manager sm; |
371 | canvas canvas (canvas::size_t (9, 3), sm); |
372 | styled_string foo_ss (sm, "foo" ); |
373 | foo_ss.set_url (sm, url: "https://www.example.com/foo" ); |
374 | styled_string bar_ss (sm, "bar" ); |
375 | bar_ss.set_url (sm, url: "https://www.example.com/bar" ); |
376 | canvas.paint_text(coord: canvas::coord_t (1, 1), text: foo_ss); |
377 | canvas.paint_text(coord: canvas::coord_t (5, 1), text: bar_ss); |
378 | |
379 | ASSERT_CANVAS_STREQ (canvas, false, |
380 | ("\n" |
381 | " foo bar\n" |
382 | "\n" )); |
383 | { |
384 | pretty_printer pp; |
385 | pp_show_color (&pp) = true; |
386 | pp.url_format = URL_FORMAT_ST; |
387 | assert_canvas_streq (SELFTEST_LOCATION, canvas, pp: &pp, |
388 | expected_str: (/* Line 1. */ |
389 | "\n" |
390 | /* Line 2. */ |
391 | " " |
392 | "\33]8;;https://www.example.com/foo\33\\foo\33]8;;\33\\" |
393 | " " |
394 | "\33]8;;https://www.example.com/bar\33\\bar\33]8;;\33\\" |
395 | "\n" |
396 | /* Line 3. */ |
397 | "\n" )); |
398 | } |
399 | |
400 | { |
401 | pretty_printer pp; |
402 | pp_show_color (&pp) = true; |
403 | pp.url_format = URL_FORMAT_BEL; |
404 | assert_canvas_streq (SELFTEST_LOCATION, canvas, pp: &pp, |
405 | expected_str: (/* Line 1. */ |
406 | "\n" |
407 | /* Line 2. */ |
408 | " " |
409 | "\33]8;;https://www.example.com/foo\afoo\33]8;;\a" |
410 | " " |
411 | "\33]8;;https://www.example.com/bar\abar\33]8;;\a" |
412 | "\n" |
413 | /* Line 3. */ |
414 | "\n" )); |
415 | } |
416 | } |
417 | |
418 | /* Run all selftests in this file. */ |
419 | |
420 | void |
421 | text_art_canvas_cc_tests () |
422 | { |
423 | test_blank (); |
424 | test_abc (); |
425 | test_debug_fill (); |
426 | test_text (); |
427 | test_circle (); |
428 | test_color_circle (); |
429 | test_bold (); |
430 | test_emoji (); |
431 | test_emoji_2 (); |
432 | test_canvas_urls (); |
433 | } |
434 | |
435 | } // namespace selftest |
436 | |
437 | |
438 | #endif /* #if CHECKING_P */ |
439 | |