From ea6e079e074ece2685f7df252323c7553f330ee7 Mon Sep 17 00:00:00 2001 From: Andi Gallo Date: Mon, 31 Jul 2023 00:46:50 +0000 Subject: [PATCH] LibWeb: Snap table grid to device pixels in collapsed borders mode Before painting the borders, build a grid snapped to device pixels and use it to construct the rectangles for the cell edges. Also adjust their starting coordinate and size such that they join correctly without overhangs. This approach works at all zoom levels. --- .../LibWeb/Painting/TableBordersPainting.cpp | 203 +++++++++++++----- 1 file changed, 151 insertions(+), 52 deletions(-) diff --git a/Userland/Libraries/LibWeb/Painting/TableBordersPainting.cpp b/Userland/Libraries/LibWeb/Painting/TableBordersPainting.cpp index 28e1bfd674b..df85b0d6c69 100644 --- a/Userland/Libraries/LibWeb/Painting/TableBordersPainting.cpp +++ b/Userland/Libraries/LibWeb/Painting/TableBordersPainting.cpp @@ -47,9 +47,27 @@ enum class EdgeDirection { Vertical, }; +struct DeviceBorderData { + Color color { Color::Transparent }; + CSS::LineStyle line_style { CSS::LineStyle::None }; + DevicePixels width { 0 }; +}; + +struct DeviceBorderDataWithElementKind { + DeviceBorderData border_data; + Painting::PaintableBox::ConflictingElementKind element_kind { Painting::PaintableBox::ConflictingElementKind::Cell }; +}; + +struct DeviceBordersDataWithElementKind { + DeviceBorderDataWithElementKind top; + DeviceBorderDataWithElementKind right; + DeviceBorderDataWithElementKind bottom; + DeviceBorderDataWithElementKind left; +}; + struct BorderEdgePaintingInfo { DevicePixelRect rect; - PaintableBox::BorderDataWithElementKind border_data_with_element_kind; + DeviceBorderDataWithElementKind border_data_with_element_kind; EdgeDirection direction; Optional row; Optional column; @@ -81,18 +99,29 @@ static Optional column_index_for_element_kind(size_t index, Painting::Pa } } +static DevicePixels half_ceil(DevicePixels width) +{ + return ceil(static_cast(width.value()) / 2); +} + +static DevicePixels half_floor(DevicePixels width) +{ + return floor(static_cast(width.value()) / 2); +} + static BorderEdgePaintingInfo make_right_cell_edge( - PaintContext& context, - CSSPixelRect const& right_cell_rect, - CSSPixelRect const& cell_rect, - PaintableBox::BordersDataWithElementKind const& borders_data, + DevicePixelRect const& right_cell_rect, + DevicePixelRect const& cell_rect, + DeviceBordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) { + auto connect_top_offset = half_ceil(borders_data.top.border_data.width); + auto connect_excess_height = connect_top_offset + half_floor(borders_data.bottom.border_data.width); 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)), + right_cell_rect.x() - half_ceil(borders_data.right.border_data.width), + cell_rect.y() - connect_top_offset, + borders_data.right.border_data.width, + max(cell_rect.height(), right_cell_rect.height()) + connect_excess_height, }; return BorderEdgePaintingInfo { .rect = right_border_rect, @@ -104,17 +133,18 @@ static BorderEdgePaintingInfo make_right_cell_edge( } static BorderEdgePaintingInfo make_down_cell_edge( - PaintContext& context, - CSSPixelRect const& down_cell_rect, - CSSPixelRect const& cell_rect, - PaintableBox::BordersDataWithElementKind const& borders_data, + DevicePixelRect const& down_cell_rect, + DevicePixelRect const& cell_rect, + DeviceBordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) { + auto connect_left_offset = half_ceil(borders_data.left.border_data.width); + auto connect_excess_width = connect_left_offset + half_floor(borders_data.right.border_data.width); 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), + cell_rect.x() - connect_left_offset, + down_cell_rect.y() - half_ceil(borders_data.bottom.border_data.width), + max(cell_rect.width(), down_cell_rect.width()) + connect_excess_width, + borders_data.bottom.border_data.width, }; return BorderEdgePaintingInfo { .rect = down_border_rect, @@ -125,13 +155,15 @@ static BorderEdgePaintingInfo make_down_cell_edge( }; } -static BorderEdgePaintingInfo make_first_row_top_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) +static BorderEdgePaintingInfo make_first_row_top_cell_edge(DevicePixelRect const& cell_rect, DeviceBordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) { + auto connect_left_offset = half_ceil(borders_data.left.border_data.width.value()); + auto connect_excess_width = connect_left_offset + half_floor(borders_data.right.border_data.width.value()); 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), + cell_rect.x() - connect_left_offset, + cell_rect.y() - half_ceil(borders_data.top.border_data.width.value()), + cell_rect.width() + connect_excess_width, + borders_data.top.border_data.width, }; return BorderEdgePaintingInfo { .rect = top_border_rect, @@ -142,13 +174,15 @@ static BorderEdgePaintingInfo make_first_row_top_cell_edge(PaintContext& context }; } -static BorderEdgePaintingInfo make_last_row_bottom_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) +static BorderEdgePaintingInfo make_last_row_bottom_cell_edge(DevicePixelRect const& cell_rect, DeviceBordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) { + auto connect_left_offset = half_ceil(borders_data.left.border_data.width); + auto connect_excess_width = connect_left_offset + half_floor(borders_data.right.border_data.width); 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), + cell_rect.x() - connect_left_offset, + cell_rect.y() + cell_rect.height() - half_ceil(borders_data.bottom.border_data.width), + cell_rect.width() + connect_excess_width, + borders_data.bottom.border_data.width, }; return BorderEdgePaintingInfo { .rect = bottom_border_rect, @@ -159,13 +193,15 @@ static BorderEdgePaintingInfo make_last_row_bottom_cell_edge(PaintContext& conte }; } -static BorderEdgePaintingInfo make_first_column_left_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) +static BorderEdgePaintingInfo make_first_column_left_cell_edge(DevicePixelRect const& cell_rect, DeviceBordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) { + auto connect_top_offset = half_ceil(borders_data.top.border_data.width); + auto connect_excess_height = connect_top_offset + half_floor(borders_data.bottom.border_data.width); 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)), + cell_rect.x() - half_ceil(borders_data.left.border_data.width), + cell_rect.y() - connect_top_offset, + borders_data.left.border_data.width, + cell_rect.height() + connect_excess_height, }; return BorderEdgePaintingInfo { .rect = left_border_rect, @@ -176,13 +212,15 @@ static BorderEdgePaintingInfo make_first_column_left_cell_edge(PaintContext& con }; } -static BorderEdgePaintingInfo make_last_column_right_cell_edge(PaintContext& context, CSSPixelRect const& cell_rect, PaintableBox::BordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) +static BorderEdgePaintingInfo make_last_column_right_cell_edge(DevicePixelRect const& cell_rect, DeviceBordersDataWithElementKind const& borders_data, CellCoordinates const& coordinates) { + auto connect_top_offset = half_ceil(borders_data.top.border_data.width); + auto connect_excess_height = connect_top_offset + half_floor(borders_data.bottom.border_data.width); 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)), + cell_rect.x() + cell_rect.width() - half_ceil(borders_data.right.border_data.width), + cell_rect.y() - connect_top_offset, + borders_data.right.border_data.width, + cell_rect.height() + connect_excess_height, }; return BorderEdgePaintingInfo { .rect = right_border_rect, @@ -193,6 +231,15 @@ static BorderEdgePaintingInfo make_last_column_right_cell_edge(PaintContext& con }; } +static CSS::BorderData css_border_data_from_device_border_data(DeviceBorderData const& device_border_data) +{ + return CSS::BorderData { + .color = device_border_data.color, + .line_style = device_border_data.line_style, + .width = device_border_data.width.value(), + }; +} + static void paint_collected_edges(PaintContext& context, Vector& 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 @@ -219,12 +266,14 @@ static void paint_collected_edges(PaintContext& context, Vector(), p2.to_type(), color, width.to_double(), Gfx::Painter::LineStyle::Dotted); + aa_painter.draw_line(p1.to_type(), p2.to_type(), color, width.value(), Gfx::Painter::LineStyle::Dotted); } else if (border_style == CSS::LineStyle::Dashed) { - context.painter().draw_line(p1.to_type(), p2.to_type(), color, width.to_double(), Gfx::Painter::LineStyle::Dashed); + context.painter().draw_line(p1.to_type(), p2.to_type(), color, width.value(), 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); @@ -246,6 +295,50 @@ static void paint_collected_edges(PaintContext& context, Vector snap_cells_to_device_coordinates(HashMap const& cell_coordinates_to_box, size_t row_count, size_t column_count, PaintContext const& context) +{ + Vector y_line_coordinates; + y_line_coordinates.resize(row_count + 1); + Vector x_line_coordinates; + x_line_coordinates.resize(column_count + 1); + for (auto const& kv : cell_coordinates_to_box) { + auto const& cell_box = kv.value; + auto start_row_index = cell_box->table_cell_coordinates()->row_index; + auto end_row_index = start_row_index + cell_box->table_cell_coordinates()->row_span; + auto cell_rect = cell_box->absolute_border_box_rect(); + y_line_coordinates[start_row_index] = max(context.rounded_device_pixels(cell_rect.y()), y_line_coordinates[start_row_index]); + y_line_coordinates[end_row_index] = max(context.rounded_device_pixels(cell_rect.y() + cell_rect.height()), y_line_coordinates[end_row_index]); + auto start_column_index = cell_box->table_cell_coordinates()->column_index; + auto end_column_index = start_column_index + cell_box->table_cell_coordinates()->column_span; + x_line_coordinates[start_column_index] = max(context.rounded_device_pixels(cell_rect.x()), x_line_coordinates[start_column_index]); + x_line_coordinates[end_column_index] = max(context.rounded_device_pixels(cell_rect.x() + cell_rect.width()), x_line_coordinates[end_column_index]); + } + HashMap cell_coordinates_to_device_rect; + for (auto const& kv : cell_coordinates_to_box) { + auto const& cell_box = kv.value; + auto start_row_index = cell_box->table_cell_coordinates()->row_index; + auto end_row_index = start_row_index + cell_box->table_cell_coordinates()->row_span; + auto height = y_line_coordinates[end_row_index] - y_line_coordinates[start_row_index]; + auto start_column_index = cell_box->table_cell_coordinates()->column_index; + auto end_column_index = start_column_index + cell_box->table_cell_coordinates()->column_span; + auto width = x_line_coordinates[end_column_index] - x_line_coordinates[start_column_index]; + cell_coordinates_to_device_rect.set(kv.key, DevicePixelRect { x_line_coordinates[start_column_index], y_line_coordinates[start_row_index], width, height }); + } + return cell_coordinates_to_device_rect; +} + +static DeviceBorderDataWithElementKind device_border_data_from_css_border_data(Painting::PaintableBox::BorderDataWithElementKind const& border_data_with_element_kind, PaintContext const& context) +{ + return DeviceBorderDataWithElementKind { + .border_data = { + .color = border_data_with_element_kind.border_data.color, + .line_style = border_data_with_element_kind.border_data.line_style, + .width = context.rounded_device_pixels(border_data_with_element_kind.border_data.width), + }, + .element_kind = border_data_with_element_kind.element_kind, + }; +} + void paint_table_collapsed_borders(PaintContext& context, Layout::Node const& box) { // Partial implementation of painting according to the collapsing border model: @@ -264,39 +357,46 @@ void paint_table_collapsed_borders(PaintContext& context, Layout::Node const& bo 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); } + auto cell_coordinates_to_device_rect = snap_cells_to_device_coordinates(cell_coordinates_to_box, row_count, column_count, context); 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 { + auto css_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(); + DeviceBordersDataWithElementKind borders_data = { + .top = device_border_data_from_css_border_data(css_borders_data.top, context), + .right = device_border_data_from_css_border_data(css_borders_data.right, context), + .bottom = device_border_data_from_css_border_data(css_borders_data.bottom, context), + .left = device_border_data_from_css_border_data(css_borders_data.left, context), + }; + auto cell_rect = cell_coordinates_to_device_rect.get({ cell_box->table_cell_coordinates()->row_index, cell_box->table_cell_coordinates()->column_index }).value(); 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); + auto maybe_right_cell = cell_coordinates_to_device_rect.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); + auto maybe_down_cell = cell_coordinates_to_device_rect.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)); + border_edge_painting_info_list.append(make_right_cell_edge(maybe_right_cell.value(), 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)); + border_edge_painting_info_list.append(make_down_cell_edge(maybe_down_cell.value(), 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, + border_edge_painting_info_list.append(make_first_row_top_cell_edge(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, + border_edge_painting_info_list.append(make_last_row_bottom_cell_edge(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, + border_edge_painting_info_list.append(make_first_column_left_cell_edge(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, + border_edge_painting_info_list.append(make_last_column_right_cell_edge(cell_rect, borders_data, { .row_index = cell_box->table_cell_coordinates()->row_index, .column_index = column_count - 1 })); } @@ -321,5 +421,4 @@ void paint_table_collapsed_borders(PaintContext& context, Layout::Node const& bo } } } - }