blob: b53f18097fd24f88cd0eced3153f46c234b13554 [file] [log] [blame]
/* Classes for printing labelled rulers.
Copyright (C) 2023-2025 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,
("|~+~| |~+~|\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 */