diff options
Diffstat (limited to 'gcc/text-art/ruler.cc')
-rw-r--r-- | gcc/text-art/ruler.cc | 724 |
1 files changed, 724 insertions, 0 deletions
diff --git a/gcc/text-art/ruler.cc b/gcc/text-art/ruler.cc new file mode 100644 index 0000000..3323a05 --- /dev/null +++ b/gcc/text-art/ruler.cc @@ -0,0 +1,724 @@ +/* Classes for printing labelled rulers. + Copyright (C) 2023 Free Software Foundation, Inc. + Contributed by David Malcolm <dmalcolm@redhat.com>. + +This file is part of GCC. + +GCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +GCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with GCC; see the file COPYING3. If not see +<http://www.gnu.org/licenses/>. */ + +#include "config.h" +#define INCLUDE_ALGORITHM +#define INCLUDE_VECTOR +#include "system.h" +#include "coretypes.h" +#include "pretty-print.h" +#include "selftest.h" +#include "text-art/selftests.h" +#include "text-art/ruler.h" +#include "text-art/theme.h" + +using namespace text_art; + +void +x_ruler::add_label (const canvas::range_t &r, + styled_string text, + style::id_t style_id, + label_kind kind) +{ + m_labels.push_back (label (r, std::move (text), style_id, kind)); + m_has_layout = false; +} + +int +x_ruler::get_canvas_y (int rel_y) const +{ + gcc_assert (rel_y >= 0); + gcc_assert (rel_y < m_size.h); + switch (m_label_dir) + { + default: + gcc_unreachable (); + case label_dir::ABOVE: + return m_size.h - (rel_y + 1); + case label_dir::BELOW: + return rel_y; + } +} + +void +x_ruler::paint_to_canvas (canvas &canvas, + canvas::coord_t offset, + const theme &theme) +{ + ensure_layout (); + + if (0) + canvas.fill (canvas::rect_t (offset, m_size), + canvas::cell_t ('*')); + + for (size_t idx = 0; idx < m_labels.size (); idx++) + { + const label &iter_label = m_labels[idx]; + + /* Paint the ruler itself. */ + const int ruler_rel_y = get_canvas_y (0); + for (int rel_x = iter_label.m_range.start; + rel_x < iter_label.m_range.next; + rel_x++) + { + enum theme::cell_kind kind = theme::cell_kind::X_RULER_MIDDLE; + + if (rel_x == iter_label.m_range.start) + { + kind = theme::cell_kind::X_RULER_LEFT_EDGE; + if (idx > 0) + { + const label &prev_label = m_labels[idx - 1]; + if (prev_label.m_range.get_max () == iter_label.m_range.start) + kind = theme::cell_kind::X_RULER_INTERNAL_EDGE; + } + } + else if (rel_x == iter_label.m_range.get_max ()) + kind = theme::cell_kind::X_RULER_RIGHT_EDGE; + else if (rel_x == iter_label.m_connector_x) + { + switch (m_label_dir) + { + default: + gcc_unreachable (); + case label_dir::ABOVE: + kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE; + break; + case label_dir::BELOW: + kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW; + break; + } + } + canvas.paint (canvas::coord_t (rel_x, ruler_rel_y) + offset, + theme.get_cell (kind, iter_label.m_style_id)); + } + + /* Paint the connector to the text. */ + for (int connector_rel_y = 1; + connector_rel_y < iter_label.m_text_rect.get_min_y (); + connector_rel_y++) + { + canvas.paint + ((canvas::coord_t (iter_label.m_connector_x, + get_canvas_y (connector_rel_y)) + + offset), + theme.get_cell (theme::cell_kind::X_RULER_VERTICAL_CONNECTOR, + iter_label.m_style_id)); + } + + /* Paint the text. */ + switch (iter_label.m_kind) + { + default: + gcc_unreachable (); + case x_ruler::label_kind::TEXT: + canvas.paint_text + ((canvas::coord_t (iter_label.m_text_rect.get_min_x (), + get_canvas_y (iter_label.m_text_rect.get_min_y ())) + + offset), + iter_label.m_text); + break; + + case x_ruler::label_kind::TEXT_WITH_BORDER: + { + const canvas::range_t rel_x_range + (iter_label.m_text_rect.get_x_range ()); + + enum theme::cell_kind inner_left_kind; + enum theme::cell_kind inner_connector_kind; + enum theme::cell_kind inner_right_kind; + enum theme::cell_kind outer_left_kind; + enum theme::cell_kind outer_right_kind; + + switch (m_label_dir) + { + default: + gcc_unreachable (); + case label_dir::ABOVE: + outer_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT; + outer_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT; + inner_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT; + inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW; + inner_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT; + break; + case label_dir::BELOW: + inner_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT; + inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE; + inner_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT; + outer_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT; + outer_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT; + break; + } + /* Inner border. */ + { + const int rel_canvas_y + = get_canvas_y (iter_label.m_text_rect.get_min_y ()); + /* Left corner. */ + canvas.paint ((canvas::coord_t (rel_x_range.get_min (), + rel_canvas_y) + + offset), + theme.get_cell (inner_left_kind, + iter_label.m_style_id)); + /* Edge. */ + const canvas::cell_t edge_border_cell + = theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL, + iter_label.m_style_id); + const canvas::cell_t connector_border_cell + = theme.get_cell (inner_connector_kind, + iter_label.m_style_id); + for (int rel_x = rel_x_range.get_min () + 1; + rel_x < rel_x_range.get_max (); + rel_x++) + if (rel_x == iter_label.m_connector_x) + canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y) + + offset), + connector_border_cell); + else + canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y) + + offset), + edge_border_cell); + + /* Right corner. */ + canvas.paint ((canvas::coord_t (rel_x_range.get_max (), + rel_canvas_y) + + offset), + theme.get_cell (inner_right_kind, + iter_label.m_style_id)); + } + + { + const int rel_canvas_y + = get_canvas_y (iter_label.m_text_rect.get_min_y () + 1); + const canvas::cell_t border_cell + = theme.get_cell (theme::cell_kind::TEXT_BORDER_VERTICAL, + iter_label.m_style_id); + + /* Left border. */ + canvas.paint ((canvas::coord_t (rel_x_range.get_min (), + rel_canvas_y) + + offset), + border_cell); + /* Text. */ + canvas.paint_text ((canvas::coord_t (rel_x_range.get_min () + 1, + rel_canvas_y) + + offset), + iter_label.m_text); + /* Right border. */ + canvas.paint ((canvas::coord_t (rel_x_range.get_max (), + rel_canvas_y) + + offset), + border_cell); + } + + /* Outer border. */ + { + const int rel_canvas_y + = get_canvas_y (iter_label.m_text_rect.get_max_y ()); + /* Left corner. */ + canvas.paint ((canvas::coord_t (rel_x_range.get_min (), + rel_canvas_y) + + offset), + theme.get_cell (outer_left_kind, + iter_label.m_style_id)); + /* Edge. */ + const canvas::cell_t border_cell + = theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL, + iter_label.m_style_id); + for (int rel_x = rel_x_range.get_min () + 1; + rel_x < rel_x_range.get_max (); + rel_x++) + canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y) + + offset), + border_cell); + + /* Right corner. */ + canvas.paint ((canvas::coord_t (rel_x_range.get_max (), + rel_canvas_y) + + offset), + theme.get_cell (outer_right_kind, + iter_label.m_style_id)); + } + } + break; + } + } +} + +DEBUG_FUNCTION void +x_ruler::debug (const style_manager &sm) +{ + canvas c (get_size (), sm); + paint_to_canvas (c, canvas::coord_t (0, 0), unicode_theme ()); + c.debug (true); +} + +x_ruler::label::label (const canvas::range_t &range, + styled_string text, + style::id_t style_id, + label_kind kind) +: m_range (range), + m_text (std::move (text)), + m_style_id (style_id), + m_kind (kind), + m_text_rect (canvas::coord_t (0, 0), + canvas::size_t (m_text.calc_canvas_width (), 1)), + m_connector_x ((m_range.get_min () + m_range.get_max ()) / 2) +{ + if (kind == label_kind::TEXT_WITH_BORDER) + { + m_text_rect.m_size.w += 2; + m_text_rect.m_size.h += 2; + } +} + +bool +x_ruler::label::operator< (const label &other) const +{ + int cmp = m_range.start - other.m_range.start; + if (cmp) + return cmp < 0; + return m_range.next < other.m_range.next; +} + +void +x_ruler::ensure_layout () +{ + if (m_has_layout) + return; + update_layout (); + m_has_layout = true; +} + +void +x_ruler::update_layout () +{ + if (m_labels.empty ()) + return; + + std::sort (m_labels.begin (), m_labels.end ()); + + /* Place labels. */ + int ruler_width = m_labels.back ().m_range.get_next (); + int width_with_labels = ruler_width; + + /* Get x coordinates of text parts of each label + (m_text_rect.m_top_left.x for each label). */ + for (size_t idx = 0; idx < m_labels.size (); idx++) + { + label &iter_label = m_labels[idx]; + /* Attempt to center the text label. */ + int min_x; + if (idx > 0) + { + /* ...but don't overlap with the connector to the left. */ + int left_neighbor_connector_x = m_labels[idx - 1].m_connector_x; + min_x = left_neighbor_connector_x + 1; + } + else + { + /* ...or go beyond the leftmost column. */ + min_x = 0; + } + int connector_x = iter_label.m_connector_x; + int centered_x + = connector_x - ((int)iter_label.m_text_rect.get_width () / 2); + int text_x = std::max (min_x, centered_x); + iter_label.m_text_rect.m_top_left.x = text_x; + } + + /* Now walk backwards trying to place them vertically, + setting m_text_rect.m_top_left.y for each label, + consolidating the rows where possible. + The y cooordinates are stored with respect to label_dir::BELOW. */ + int label_y = 2; + for (int idx = m_labels.size () - 1; idx >= 0; idx--) + { + label &iter_label = m_labels[idx]; + /* Does it fit on the same row as the text label to the right? */ + size_t text_len = iter_label.m_text_rect.get_width (); + /* Get the x-coord of immediately beyond iter_label's text. */ + int next_x = iter_label.m_text_rect.get_min_x () + text_len; + if (idx < (int)m_labels.size () - 1) + { + if (next_x >= m_labels[idx + 1].m_text_rect.get_min_x ()) + { + /* If not, start a new row. */ + label_y += m_labels[idx + 1].m_text_rect.get_height (); + } + } + iter_label.m_text_rect.m_top_left.y = label_y; + width_with_labels = std::max (width_with_labels, next_x); + } + + m_size = canvas::size_t (width_with_labels, + label_y + m_labels[0].m_text_rect.get_height ()); +} + +#if CHECKING_P + +namespace selftest { + +static void +assert_x_ruler_streq (const location &loc, + x_ruler &ruler, + const theme &theme, + const style_manager &sm, + bool styled, + const char *expected_str) +{ + canvas c (ruler.get_size (), sm); + ruler.paint_to_canvas (c, canvas::coord_t (0, 0), theme); + if (0) + c.debug (styled); + assert_canvas_streq (loc, c, styled, expected_str); +} + +#define ASSERT_X_RULER_STREQ(RULER, THEME, SM, STYLED, EXPECTED_STR) \ + SELFTEST_BEGIN_STMT \ + assert_x_ruler_streq ((SELFTEST_LOCATION), \ + (RULER), \ + (THEME), \ + (SM), \ + (STYLED), \ + (EXPECTED_STR)); \ + SELFTEST_END_STMT + +static void +test_single () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"), + style::id_plain, x_ruler::label_kind::TEXT); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + ("|~~~~+~~~~|\n" + " |\n" + " foo\n")); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("├────┬────┤\n" + " │\n" + " foo\n")); +} + +static void +test_single_above () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::ABOVE); + r.add_label (canvas::range_t (0, 11), styled_string (sm, "hello world"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + ("hello world\n" + " |\n" + "|~~~~+~~~~|\n")); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("hello world\n" + " │\n" + "├────┴────┤\n")); +} + +static void +test_multiple_contiguous () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"), + style::id_plain); + r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + ("|~~~~+~~~~|~+~~|\n" + " | |\n" + " foo bar\n")); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("├────┬────┼─┬──┤\n" + " │ │\n" + " foo bar\n")); +} + +static void +test_multiple_contiguous_above () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::ABOVE); + r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"), + style::id_plain); + r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + (" foo bar\n" + " | |\n" + "|~~~~+~~~~|~+~~|\n")); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + (" foo bar\n" + " │ │\n" + "├────┴────┼─┴──┤\n")); +} + +static void +test_multiple_contiguous_abutting_labels () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 11), styled_string (sm, "12345678"), + style::id_plain); + r.add_label (canvas::range_t (10, 16), styled_string (sm, "1234678"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("├────┬────┼─┬──┤\n" + " │ │\n" + " │ 1234678\n" + " 12345678\n")); +} + +static void +test_multiple_contiguous_overlapping_labels () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 11), styled_string (sm, "123456789"), + style::id_plain); + r.add_label (canvas::range_t (10, 16), styled_string (sm, "12346789"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("├────┬────┼─┬──┤\n" + " │ │\n" + " │ 12346789\n" + " 123456789\n")); +} +static void +test_abutting_left_border () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 6), + styled_string (sm, "this is a long label"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("├─┬──┤\n" + " │\n" + "this is a long label\n")); +} + +static void +test_too_long_to_consolidate_vertically () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 11), + styled_string (sm, "long string A"), + style::id_plain); + r.add_label (canvas::range_t (10, 16), + styled_string (sm, "long string B"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("├────┬────┼─┬──┤\n" + " │ │\n" + " │long string B\n" + "long string A\n")); +} + +static void +test_abutting_neighbor () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 11), + styled_string (sm, "very long string A"), + style::id_plain); + r.add_label (canvas::range_t (10, 16), + styled_string (sm, "very long string B"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + ("├────┬────┼─┬──┤\n" + " │ │\n" + " │very long string B\n" + "very long string A\n")); +} + +static void +test_gaps () +{ + style_manager sm; + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 5), + styled_string (sm, "foo"), + style::id_plain); + r.add_label (canvas::range_t (10, 15), + styled_string (sm, "bar"), + style::id_plain); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + ("|~+~| |~+~|\n" + " | |\n" + " foo bar\n")); +} + +static void +test_styled () +{ + style_manager sm; + style s1, s2; + s1.m_bold = true; + s1.m_fg_color = style::named_color::YELLOW; + s2.m_bold = true; + s2.m_fg_color = style::named_color::BLUE; + style::id_t sid1 = sm.get_or_create_id (s1); + style::id_t sid2 = sm.get_or_create_id (s2); + + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 5), styled_string (sm, "foo"), sid1); + r.add_label (canvas::range_t (10, 15), styled_string (sm, "bar"), sid2); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + ("[00;01;33m[K|~+~|[00m[K [00;01;34m[K|~+~|[00m[K\n" + " [00;01;33m[K|[00m[K [00;01;34m[K|[00m[K\n" + " foo bar\n")); +} + +static void +test_borders () +{ + style_manager sm; + { + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 5), + styled_string (sm, "label 1"), + style::id_plain, + x_ruler::label_kind::TEXT_WITH_BORDER); + r.add_label (canvas::range_t (10, 15), + styled_string (sm, "label 2"), + style::id_plain); + r.add_label (canvas::range_t (20, 25), + styled_string (sm, "label 3"), + style::id_plain, + x_ruler::label_kind::TEXT_WITH_BORDER); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + "|~+~| |~+~| |~+~|\n" + " | | |\n" + " | label 2 +---+---+\n" + "+-+-----+ |label 3|\n" + "|label 1| +-------+\n" + "+-------+\n"); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + "├─┬─┤ ├─┬─┤ ├─┬─┤\n" + " │ │ │\n" + " │ label 2 ╭───┴───╮\n" + "╭─┴─────╮ │label 3│\n" + "│label 1│ ╰───────╯\n" + "╰───────╯\n"); + } + { + x_ruler r (x_ruler::label_dir::ABOVE); + r.add_label (canvas::range_t (0, 5), + styled_string (sm, "label 1"), + style::id_plain, + x_ruler::label_kind::TEXT_WITH_BORDER); + r.add_label (canvas::range_t (10, 15), + styled_string (sm, "label 2"), + style::id_plain); + r.add_label (canvas::range_t (20, 25), + styled_string (sm, "label 3"), + style::id_plain, + x_ruler::label_kind::TEXT_WITH_BORDER); + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + "+-------+\n" + "|label 1| +-------+\n" + "+-+-----+ |label 3|\n" + " | label 2 +---+---+\n" + " | | |\n" + "|~+~| |~+~| |~+~|\n"); + ASSERT_X_RULER_STREQ + (r, unicode_theme (), sm, true, + "╭───────╮\n" + "│label 1│ ╭───────╮\n" + "╰─┬─────╯ │label 3│\n" + " │ label 2 ╰───┬───╯\n" + " │ │ │\n" + "├─┴─┤ ├─┴─┤ ├─┴─┤\n"); + } +} + +static void +test_emoji () +{ + style_manager sm; + + styled_string s; + s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN. */ + true)); + s.append (styled_string (sm, " ")); + s.append (styled_string (sm, "this is a warning")); + + x_ruler r (x_ruler::label_dir::BELOW); + r.add_label (canvas::range_t (0, 5), + std::move (s), + style::id_plain, + x_ruler::label_kind::TEXT_WITH_BORDER); + + ASSERT_X_RULER_STREQ + (r, ascii_theme (), sm, true, + "|~+~|\n" + " |\n" + "+-+------------------+\n" + "|⚠️ this is a warning|\n" + "+--------------------+\n"); +} + +/* Run all selftests in this file. */ + +void +text_art_ruler_cc_tests () +{ + test_single (); + test_single_above (); + test_multiple_contiguous (); + test_multiple_contiguous_above (); + test_multiple_contiguous_abutting_labels (); + test_multiple_contiguous_overlapping_labels (); + test_abutting_left_border (); + test_too_long_to_consolidate_vertically (); + test_abutting_neighbor (); + test_gaps (); + test_styled (); + test_borders (); + test_emoji (); +} + +} // namespace selftest + + +#endif /* #if CHECKING_P */ |