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
5This file is part of GCC.
6
7GCC is free software; you can redistribute it and/or modify it under
8the terms of the GNU General Public License as published by the Free
9Software Foundation; either version 3, or (at your option) any later
10version.
11
12GCC is distributed in the hope that it will be useful, but WITHOUT ANY
13WARRANTY; without even the implied warranty of MERCHANTABILITY or
14FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15for more details.
16
17You should have received a copy of the GNU General Public License
18along 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
30using namespace text_art;
31
32canvas::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
39void
40canvas::paint (coord_t coord, styled_unichar ch)
41{
42 m_cells.set (coord, element: std::move (ch));
43}
44
45void
46canvas::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
58void
59canvas::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
66void
67canvas::debug_fill ()
68{
69 fill (rect: rect_t (coord_t (0, 0), get_size ()), c: cell_t ('*'));
70}
71
72void
73canvas::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
129DEBUG_FUNCTION void
130canvas::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
145int
146canvas::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
160namespace selftest {
161
162static void
163test_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
175static void
176test_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
188static void
189test_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
200static void
201test_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
216static void
217test_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
268static void
269test_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 " *****\n"
310 " *******\n"
311 " *********\n"
312 " *********\n"
313 " *********\n"
314 " *********\n"
315 " *********\n"
316 " *******\n"
317 " *****\n"));
318}
319
320static void
321test_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 `foo' after\n");
333}
334
335static void
336test_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
347static void
348test_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
367static void
368test_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
420void
421text_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

source code of gcc/text-art/canvas.cc