/* Classes for printing labelled rulers. Copyright (C) 2023-2024 Free Software Foundation, Inc. Contributed by David Malcolm . 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 . */ #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, ("|~+~| |~+~|\n" " | |\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 */