1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-18 02:35:07 +00:00
serenity/Userland/Libraries/LibWeb/Painting/TableBordersPainting.cpp
Andi Gallo a7166eb103 LibWeb: Complete table border conflict resolution
Add the element type and grid position to the algorithm and change the
table borders painting to apply the new criteria to corners as well.
2023-07-25 15:21:04 +02:00

325 lines
18 KiB
C++

/*
* Copyright (c) 2023, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/HashMap.h>
#include <AK/QuickSort.h>
#include <AK/Traits.h>
#include <LibWeb/Layout/TableFormattingContext.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/TableBordersPainting.h>
struct CellCoordinates {
size_t row_index;
size_t column_index;
bool operator==(CellCoordinates const& other) const
{
return row_index == other.row_index && column_index == other.column_index;
}
};
namespace AK {
template<>
struct Traits<CellCoordinates> : public GenericTraits<CellCoordinates> {
static unsigned hash(CellCoordinates const& key) { return pair_int_hash(key.row_index, key.column_index); }
};
}
namespace Web::Painting {
static void collect_cell_boxes_with_collapsed_borders(Vector<PaintableBox const*>& cell_boxes, Layout::Node const& box)
{
box.for_each_child([&](auto& child) {
if (child.display().is_table_cell() && child.computed_values().border_collapse() == CSS::BorderCollapse::Collapse) {
VERIFY(is<Layout::Box>(child) && child.paintable());
cell_boxes.append(static_cast<Layout::Box const&>(child).paintable_box());
} else {
collect_cell_boxes_with_collapsed_borders(cell_boxes, child);
}
});
}
enum class EdgeDirection {
Horizontal,
Vertical,
};
struct BorderEdgePaintingInfo {
DevicePixelRect rect;
PaintableBox::BorderDataWithElementKind border_data_with_element_kind;
EdgeDirection direction;
Optional<size_t> row;
Optional<size_t> column;
};
static Optional<size_t> row_index_for_element_kind(size_t index, Painting::PaintableBox::ConflictingElementKind element_kind)
{
switch (element_kind) {
case Painting::PaintableBox::ConflictingElementKind::Cell:
case Painting::PaintableBox::ConflictingElementKind::Row:
case Painting::PaintableBox::ConflictingElementKind::RowGroup: {
return index;
}
default:
return {};
}
}
static Optional<size_t> column_index_for_element_kind(size_t index, Painting::PaintableBox::ConflictingElementKind element_kind)
{
switch (element_kind) {
case Painting::PaintableBox::ConflictingElementKind::Cell:
case Painting::PaintableBox::ConflictingElementKind::Column:
case Painting::PaintableBox::ConflictingElementKind::ColumnGroup: {
return index;
}
default:
return {};
}
}
static BorderEdgePaintingInfo make_right_cell_edge(
PaintContext& context,
CSSPixelRect const& right_cell_rect,
CSSPixelRect const& cell_rect,
PaintableBox::BordersDataWithElementKind const& borders_data,
CellCoordinates const& coordinates)
{
DevicePixelRect right_border_rect = {
context.rounded_device_pixels(right_cell_rect.x() - round(borders_data.right.border_data.width / 2)),
context.rounded_device_pixels(cell_rect.y() - round(borders_data.top.border_data.width / 2)),
context.rounded_device_pixels(borders_data.right.border_data.width),
context.rounded_device_pixels(max(cell_rect.height(), right_cell_rect.height()) + round(borders_data.top.border_data.width / 2) + round(borders_data.bottom.border_data.width / 2)),
};
return BorderEdgePaintingInfo {
.rect = right_border_rect,
.border_data_with_element_kind = borders_data.right,
.direction = EdgeDirection::Vertical,
.row = row_index_for_element_kind(coordinates.row_index, borders_data.right.element_kind),
.column = column_index_for_element_kind(coordinates.column_index, borders_data.right.element_kind),
};
}
static BorderEdgePaintingInfo make_down_cell_edge(
PaintContext& context,
CSSPixelRect const& down_cell_rect,
CSSPixelRect const& cell_rect,
PaintableBox::BordersDataWithElementKind const& borders_data,
CellCoordinates const& coordinates)
{
DevicePixelRect down_border_rect = {
context.rounded_device_pixels(cell_rect.x() - round(borders_data.left.border_data.width / 2)),
context.rounded_device_pixels(down_cell_rect.y() - round(borders_data.bottom.border_data.width / 2)),
context.rounded_device_pixels(max(cell_rect.width(), down_cell_rect.width()) + round(borders_data.left.border_data.width / 2) + round(borders_data.right.border_data.width / 2)),
context.rounded_device_pixels(borders_data.bottom.border_data.width),
};
return BorderEdgePaintingInfo {
.rect = down_border_rect,
.border_data_with_element_kind = borders_data.bottom,
.direction = EdgeDirection::Horizontal,
.row = row_index_for_element_kind(coordinates.row_index, borders_data.bottom.element_kind),
.column = column_index_for_element_kind(coordinates.column_index, borders_data.bottom.element_kind),
};
}
static BorderEdgePaintingInfo make_first_row_top_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates)
{
DevicePixelRect top_border_rect = {
context.rounded_device_pixels(cell_rect.x() - round(borders_data.left.border_data.width / 2)),
context.rounded_device_pixels(cell_rect.y() - round(borders_data.top.border_data.width / 2)),
context.rounded_device_pixels(cell_rect.width()),
context.rounded_device_pixels(borders_data.top.border_data.width),
};
return BorderEdgePaintingInfo {
.rect = top_border_rect,
.border_data_with_element_kind = borders_data.top,
.direction = EdgeDirection::Horizontal,
.row = row_index_for_element_kind(coordinates.row_index, borders_data.top.element_kind),
.column = column_index_for_element_kind(coordinates.column_index, borders_data.top.element_kind),
};
}
static BorderEdgePaintingInfo make_last_row_bottom_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates)
{
DevicePixelRect bottom_border_rect = {
context.rounded_device_pixels(cell_rect.x() - round(borders_data.left.border_data.width / 2)),
context.rounded_device_pixels(cell_rect.y() + cell_rect.height() - round(borders_data.bottom.border_data.width / 2)),
context.rounded_device_pixels(cell_rect.width() + round(borders_data.left.border_data.width / 2) + round(borders_data.right.border_data.width / 2)),
context.rounded_device_pixels(borders_data.bottom.border_data.width),
};
return BorderEdgePaintingInfo {
.rect = bottom_border_rect,
.border_data_with_element_kind = borders_data.bottom,
.direction = EdgeDirection::Horizontal,
.row = row_index_for_element_kind(coordinates.row_index, borders_data.bottom.element_kind),
.column = column_index_for_element_kind(coordinates.column_index, borders_data.bottom.element_kind),
};
}
static BorderEdgePaintingInfo make_first_column_left_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates)
{
DevicePixelRect left_border_rect = {
context.rounded_device_pixels(cell_rect.x() - round(borders_data.left.border_data.width / 2)),
context.rounded_device_pixels(cell_rect.y() - round(borders_data.top.border_data.width / 2)),
context.rounded_device_pixels(borders_data.left.border_data.width),
context.rounded_device_pixels(cell_rect.height() + round(borders_data.top.border_data.width / 2)),
};
return BorderEdgePaintingInfo {
.rect = left_border_rect,
.border_data_with_element_kind = borders_data.left,
.direction = EdgeDirection::Vertical,
.row = row_index_for_element_kind(coordinates.row_index, borders_data.left.element_kind),
.column = column_index_for_element_kind(coordinates.column_index, borders_data.left.element_kind),
};
}
static BorderEdgePaintingInfo make_last_column_right_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates)
{
DevicePixelRect right_border_rect = {
context.rounded_device_pixels(cell_rect.x() + cell_rect.width() - round(borders_data.right.border_data.width / 2)),
context.rounded_device_pixels(cell_rect.y() - round(borders_data.top.border_data.width / 2)),
context.rounded_device_pixels(borders_data.right.border_data.width),
context.rounded_device_pixels(cell_rect.height() + round(borders_data.top.border_data.width / 2) + round(borders_data.bottom.border_data.width / 2)),
};
return BorderEdgePaintingInfo {
.rect = right_border_rect,
.border_data_with_element_kind = borders_data.right,
.direction = EdgeDirection::Vertical,
.row = row_index_for_element_kind(coordinates.row_index, borders_data.right.element_kind),
.column = column_index_for_element_kind(coordinates.column_index, borders_data.right.element_kind),
};
}
static void paint_collected_edges(PaintContext& context, Vector<BorderEdgePaintingInfo>& border_edge_painting_info_list)
{
// This sorting step isn't part of the specification, but it matches the behavior of other browsers at border intersections, which aren't
// part of border conflict resolution in the specification but it's still desirable to handle them in a way which is consistent with it.
// See https://www.w3.org/TR/CSS22/tables.html#border-conflict-resolution for reference.
quick_sort(border_edge_painting_info_list, [](auto const& a, auto const& b) {
auto const& a_border_data = a.border_data_with_element_kind.border_data;
auto const& b_border_data = b.border_data_with_element_kind.border_data;
if (a_border_data.line_style == b_border_data.line_style && a_border_data.width == b_border_data.width) {
if (b.border_data_with_element_kind.element_kind < a.border_data_with_element_kind.element_kind) {
return true;
} else if (b.border_data_with_element_kind.element_kind > a.border_data_with_element_kind.element_kind) {
return false;
}
// Here the element kind is the same, thus the coordinates are either both set or not set.
VERIFY(a.column.has_value() == b.column.has_value());
VERIFY(a.row.has_value() == b.row.has_value());
if (a.column.has_value()) {
if (b.column.value() < a.column.value()) {
return true;
} else if (b.column.value() > a.column.value()) {
return false;
}
}
return a.row.has_value() ? b.row.value() < a.row.value() : false;
}
return Layout::TableFormattingContext::border_is_less_specific(a_border_data, b_border_data);
});
for (auto const& border_edge_painting_info : border_edge_painting_info_list) {
auto const& border_data_with_element_kind = border_edge_painting_info.border_data_with_element_kind;
CSSPixels width = border_data_with_element_kind.border_data.width;
if (width <= 0)
continue;
auto color = border_data_with_element_kind.border_data.color;
auto border_style = border_data_with_element_kind.border_data.line_style;
auto p1 = border_edge_painting_info.rect.top_left();
auto p2 = border_edge_painting_info.direction == EdgeDirection::Horizontal
? border_edge_painting_info.rect.top_right()
: border_edge_painting_info.rect.bottom_left();
if (border_style == CSS::LineStyle::Dotted) {
Gfx::AntiAliasingPainter aa_painter { context.painter() };
aa_painter.draw_line(p1.to_type<int>(), p2.to_type<int>(), color, width.to_double(), Gfx::Painter::LineStyle::Dotted);
} else if (border_style == CSS::LineStyle::Dashed) {
context.painter().draw_line(p1.to_type<int>(), p2.to_type<int>(), color, width.to_double(), Gfx::Painter::LineStyle::Dashed);
} else {
// FIXME: Support the remaining line styles instead of rendering them as solid.
context.painter().fill_rect(Gfx::IntRect(border_edge_painting_info.rect.location(), border_edge_painting_info.rect.size()), color);
}
}
}
void paint_table_collapsed_borders(PaintContext& context, Layout::Node const& box)
{
// Partial implementation of painting according to the collapsing border model:
// https://www.w3.org/TR/CSS22/tables.html#collapsing-borders
Vector<PaintableBox const*> cell_boxes;
collect_cell_boxes_with_collapsed_borders(cell_boxes, box);
Vector<BorderEdgePaintingInfo> border_edge_painting_info_list;
HashMap<CellCoordinates, PaintableBox const*> cell_coordinates_to_box;
size_t row_count = 0;
size_t column_count = 0;
for (auto const cell_box : cell_boxes) {
cell_coordinates_to_box.set(CellCoordinates {
.row_index = cell_box->table_cell_coordinates()->row_index,
.column_index = cell_box->table_cell_coordinates()->column_index },
cell_box);
row_count = max(row_count, cell_box->table_cell_coordinates()->row_index + cell_box->table_cell_coordinates()->row_span);
column_count = max(column_count, cell_box->table_cell_coordinates()->column_index + cell_box->table_cell_coordinates()->column_span);
}
for (auto const cell_box : cell_boxes) {
auto borders_data = cell_box->override_borders_data().has_value() ? cell_box->override_borders_data().value() : PaintableBox::BordersDataWithElementKind {
.top = { .border_data = cell_box->box_model().border.top == 0 ? CSS::BorderData() : cell_box->computed_values().border_top(), .element_kind = PaintableBox::ConflictingElementKind::Cell },
.right = { .border_data = cell_box->box_model().border.right == 0 ? CSS::BorderData() : cell_box->computed_values().border_right(), .element_kind = PaintableBox::ConflictingElementKind::Cell },
.bottom = { .border_data = cell_box->box_model().border.bottom == 0 ? CSS::BorderData() : cell_box->computed_values().border_bottom(), .element_kind = PaintableBox::ConflictingElementKind::Cell },
.left = { .border_data = cell_box->box_model().border.left == 0 ? CSS::BorderData() : cell_box->computed_values().border_left(), .element_kind = PaintableBox::ConflictingElementKind::Cell },
};
auto cell_rect = cell_box->absolute_border_box_rect();
CellCoordinates right_cell_coordinates {
.row_index = cell_box->table_cell_coordinates()->row_index,
.column_index = cell_box->table_cell_coordinates()->column_index + cell_box->table_cell_coordinates()->column_span
};
auto maybe_right_cell = cell_coordinates_to_box.get(right_cell_coordinates);
CellCoordinates down_cell_coordinates {
.row_index = cell_box->table_cell_coordinates()->row_index + cell_box->table_cell_coordinates()->row_span,
.column_index = cell_box->table_cell_coordinates()->column_index
};
auto maybe_down_cell = cell_coordinates_to_box.get(down_cell_coordinates);
if (maybe_right_cell.has_value())
border_edge_painting_info_list.append(make_right_cell_edge(context, maybe_right_cell.value()->absolute_border_box_rect(), cell_rect, borders_data, right_cell_coordinates));
if (maybe_down_cell.has_value())
border_edge_painting_info_list.append(make_down_cell_edge(context, maybe_down_cell.value()->absolute_border_box_rect(), cell_rect, borders_data, down_cell_coordinates));
if (cell_box->table_cell_coordinates()->row_index == 0)
border_edge_painting_info_list.append(make_first_row_top_cell_edge(context, cell_rect, borders_data,
{ .row_index = 0, .column_index = cell_box->table_cell_coordinates()->column_index }));
if (cell_box->table_cell_coordinates()->row_index + cell_box->table_cell_coordinates()->row_span == row_count)
border_edge_painting_info_list.append(make_last_row_bottom_cell_edge(context, cell_rect, borders_data,
{ .row_index = row_count - 1, .column_index = cell_box->table_cell_coordinates()->column_index }));
if (cell_box->table_cell_coordinates()->column_index == 0)
border_edge_painting_info_list.append(make_first_column_left_cell_edge(context, cell_rect, borders_data,
{ .row_index = cell_box->table_cell_coordinates()->row_index, .column_index = 0 }));
if (cell_box->table_cell_coordinates()->column_index + cell_box->table_cell_coordinates()->column_span == column_count)
border_edge_painting_info_list.append(make_last_column_right_cell_edge(context, cell_rect, borders_data,
{ .row_index = cell_box->table_cell_coordinates()->row_index, .column_index = column_count - 1 }));
}
paint_collected_edges(context, border_edge_painting_info_list);
for (auto const cell_box : cell_boxes) {
auto const& border_radii_data = cell_box->normalized_border_radii_data();
auto top_left = border_radii_data.top_left.as_corner(context);
auto top_right = border_radii_data.top_right.as_corner(context);
auto bottom_right = border_radii_data.bottom_right.as_corner(context);
auto bottom_left = border_radii_data.bottom_left.as_corner(context);
if (!top_left && !top_right && !bottom_left && !bottom_right) {
continue;
} else {
auto borders_data = cell_box->override_borders_data().has_value() ? PaintableBox::remove_element_kind_from_borders_data(cell_box->override_borders_data().value()) : BordersData {
.top = cell_box->box_model().border.top == 0 ? CSS::BorderData() : cell_box->computed_values().border_top(),
.right = cell_box->box_model().border.right == 0 ? CSS::BorderData() : cell_box->computed_values().border_right(),
.bottom = cell_box->box_model().border.bottom == 0 ? CSS::BorderData() : cell_box->computed_values().border_bottom(),
.left = cell_box->box_model().border.left == 0 ? CSS::BorderData() : cell_box->computed_values().border_left(),
};
paint_all_borders(context, cell_box->absolute_border_box_rect(), cell_box->normalized_border_radii_data(), borders_data);
}
}
}
}