diff --git a/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp b/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp index 6d494d9339..38792e0d98 100644 --- a/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp +++ b/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2021-2022, Sam Atkins + * Copyright (c) 2022, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ @@ -10,6 +11,7 @@ #include #include #include +#include #include #include @@ -17,22 +19,33 @@ namespace Web::Painting { void paint_box_shadow(PaintContext& context, Gfx::IntRect const& content_rect, BorderRadiiData const& border_radii, Vector const& box_shadow_layers) { - (void) border_radii; - if (box_shadow_layers.is_empty()) return; auto& painter = context.painter(); + auto top_left_corner = border_radii.top_left.as_corner(); + auto top_right_corner = border_radii.top_right.as_corner(); + auto bottom_right_corner = border_radii.bottom_right.as_corner(); + auto bottom_left_corner = border_radii.bottom_left.as_corner(); + + Optional corner_radius_clipper {}; + + if (border_radii.has_any_radius()) { + auto clipper = BorderRadiusCornerClipper::create(content_rect, border_radii, CornerClip::Inside); + if (!clipper.is_error()) + corner_radius_clipper = clipper.release_value(); + } + + if (corner_radius_clipper.has_value()) + corner_radius_clipper->sample_under_corners(painter); + // Note: Box-shadow layers are ordered front-to-back, so we paint them in reverse - for (int layer_index = box_shadow_layers.size() - 1; layer_index >= 0; layer_index--) { - auto& box_shadow_data = box_shadow_layers[layer_index]; + for (auto& box_shadow_data : box_shadow_layers.in_reverse()) { // FIXME: Paint inset shadows. if (box_shadow_data.placement != ShadowPlacement::Outer) continue; - // FIXME: Account for rounded corners. - auto fill_rect_masked = [](auto& painter, auto fill_rect, auto mask_rect, auto color) { Gfx::DisjointRectSet rect_set; rect_set.add(fill_rect); @@ -41,79 +54,215 @@ void paint_box_shadow(PaintContext& context, Gfx::IntRect const& content_rect, B painter.fill_rect(rect, color); }; - // If there's no blurring, we can save a lot of effort. - if (box_shadow_data.blur_radius == 0) { + // If there's no blurring, nor rounded corners, we can save a lot of effort. + if (box_shadow_data.blur_radius == 0 && !border_radii.has_any_radius()) { fill_rect_masked(painter, content_rect.inflated(box_shadow_data.spread_distance, box_shadow_data.spread_distance, box_shadow_data.spread_distance, box_shadow_data.spread_distance).translated(box_shadow_data.offset_x, box_shadow_data.offset_y), content_rect, box_shadow_data.color); continue; } + auto top_left_shadow_corner = top_left_corner; + auto top_right_shadow_corner = top_right_corner; + auto bottom_right_shadow_corner = bottom_right_corner; + auto bottom_left_shadow_corner = bottom_left_corner; + + auto spread_corner = [&](auto& corner) { + if (corner) { + corner.horizontal_radius += box_shadow_data.spread_distance; + corner.vertical_radius += box_shadow_data.spread_distance; + } + }; + + spread_corner(top_left_shadow_corner); + spread_corner(top_right_shadow_corner); + spread_corner(bottom_right_shadow_corner); + spread_corner(bottom_left_shadow_corner); + auto expansion = box_shadow_data.spread_distance - (box_shadow_data.blur_radius * 2); - Gfx::IntRect solid_rect = { + Gfx::IntRect inner_bounding_rect = { content_rect.x() + box_shadow_data.offset_x - expansion, content_rect.y() + box_shadow_data.offset_y - expansion, content_rect.width() + 2 * expansion, content_rect.height() + 2 * expansion }; - fill_rect_masked(painter, solid_rect, content_rect, box_shadow_data.color); // Calculating and blurring the box-shadow full size is expensive, and wasteful - aside from the corners, // all vertical strips of the shadow are identical, and the same goes for horizontal ones. // So instead, we generate a shadow bitmap that is just large enough to include the corners and 1px of // non-corner, and then we repeatedly blit sections of it. This is similar to a NinePatch on Android. + auto double_radius = box_shadow_data.blur_radius * 2; + auto blurred_edge_thickness = box_shadow_data.blur_radius * 4; - auto corner_size = box_shadow_data.blur_radius * 4; - Gfx::IntRect corner_rect { 0, 0, corner_size, corner_size }; - Gfx::IntRect all_corners_rect { 0, 0, corner_size * 2 + 1, corner_size * 2 + 1 }; - Gfx::IntRect left_edge_rect { 0, corner_size, corner_size, 1 }; - Gfx::IntRect right_edge_rect { all_corners_rect.width() - corner_size, corner_size, corner_size, 1 }; - Gfx::IntRect top_edge_rect { corner_size, 0, 1, corner_size }; - Gfx::IntRect bottom_edge_rect { corner_size, all_corners_rect.height() - corner_size, 1, corner_size }; + auto default_corner_size = Gfx::IntSize { double_radius, double_radius }; + auto top_left_corner_size = top_left_shadow_corner ? top_left_shadow_corner.as_rect().size() : default_corner_size; + auto top_right_corner_size = top_right_shadow_corner ? top_right_shadow_corner.as_rect().size() : default_corner_size; + auto bottom_left_corner_size = bottom_left_shadow_corner ? bottom_left_shadow_corner.as_rect().size() : default_corner_size; + auto bottom_right_corner_size = bottom_right_shadow_corner ? bottom_right_shadow_corner.as_rect().size() : default_corner_size; - auto shadows_bitmap = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, all_corners_rect.size()); + auto shadow_bitmap_rect = Gfx::IntRect( + 0, 0, + max( + top_left_corner_size.width() + top_right_corner_size.width(), + bottom_left_corner_size.width() + bottom_right_corner_size.width()) + + 1 + blurred_edge_thickness, + max( + top_left_corner_size.height() + bottom_left_corner_size.height(), + top_right_corner_size.height() + bottom_right_corner_size.height()) + + 1 + blurred_edge_thickness); + + auto top_left_corner_rect = Gfx::IntRect { + 0, 0, + top_left_corner_size.width() + double_radius, + top_left_corner_size.height() + double_radius + }; + auto top_right_corner_rect = Gfx::IntRect { + shadow_bitmap_rect.width() - (top_right_corner_size.width() + double_radius), 0, + top_right_corner_size.width() + double_radius, + top_right_corner_size.height() + double_radius + }; + auto bottom_right_corner_rect = Gfx::IntRect { + shadow_bitmap_rect.width() - (bottom_right_corner_size.width() + double_radius), + shadow_bitmap_rect.height() - (bottom_right_corner_size.height() + double_radius), + bottom_right_corner_size.width() + double_radius, + bottom_right_corner_size.height() + double_radius + }; + auto bottom_left_corner_rect = Gfx::IntRect { + 0, shadow_bitmap_rect.height() - (bottom_left_corner_size.height() + double_radius), + bottom_left_corner_size.width() + double_radius, + bottom_left_corner_size.height() + double_radius + }; + + Gfx::IntRect left_edge_rect { 0, top_left_corner_rect.height(), blurred_edge_thickness, 1 }; + Gfx::IntRect right_edge_rect { shadow_bitmap_rect.width() - blurred_edge_thickness, top_right_corner_rect.height(), blurred_edge_thickness, 1 }; + Gfx::IntRect top_edge_rect { top_left_corner_rect.width(), 0, 1, blurred_edge_thickness }; + Gfx::IntRect bottom_edge_rect { bottom_left_corner_rect.width(), shadow_bitmap_rect.height() - blurred_edge_thickness, 1, blurred_edge_thickness }; + + auto shadows_bitmap = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, shadow_bitmap_rect.size()); if (shadows_bitmap.is_error()) { dbgln("Unable to allocate temporary bitmap for box-shadow rendering: {}", shadows_bitmap.error()); return; } auto shadow_bitmap = shadows_bitmap.release_value(); Gfx::Painter corner_painter { *shadow_bitmap }; - auto double_radius = box_shadow_data.blur_radius * 2; - corner_painter.fill_rect(all_corners_rect.shrunken(double_radius, double_radius, double_radius, double_radius), box_shadow_data.color); + Gfx::AntiAliasingPainter aa_corner_painter { corner_painter }; + + aa_corner_painter.fill_rect_with_rounded_corners(shadow_bitmap_rect.shrunken(double_radius, double_radius, double_radius, double_radius), box_shadow_data.color, top_left_shadow_corner, top_right_shadow_corner, bottom_right_shadow_corner, bottom_left_shadow_corner); + // FIXME: Make fast box blur faster Gfx::FastBoxBlurFilter filter(*shadow_bitmap); filter.apply_three_passes(box_shadow_data.blur_radius); - auto left_start = solid_rect.left() - corner_size; - auto right_start = solid_rect.left() + solid_rect.width(); - auto top_start = solid_rect.top() - corner_size; - auto bottom_start = solid_rect.top() + solid_rect.height(); + auto paint_shadow_infill = [&] { + if (!border_radii.has_any_radius()) + return painter.fill_rect(inner_bounding_rect, box_shadow_data.color); + + auto top_left_inner_width = top_left_corner_rect.width() - blurred_edge_thickness; + auto top_left_inner_height = top_left_corner_rect.height() - blurred_edge_thickness; + auto top_right_inner_width = top_right_corner_rect.width() - blurred_edge_thickness; + auto top_right_inner_height = top_right_corner_rect.height() - blurred_edge_thickness; + auto bottom_right_inner_width = bottom_right_corner_rect.width() - blurred_edge_thickness; + auto bottom_right_inner_height = bottom_right_corner_rect.height() - blurred_edge_thickness; + auto bottom_left_inner_width = bottom_left_corner_rect.width() - blurred_edge_thickness; + auto bottom_left_inner_height = bottom_left_corner_rect.height() - blurred_edge_thickness; + + Gfx::IntRect top_rect { + inner_bounding_rect.x() + top_left_inner_width, + inner_bounding_rect.y(), + inner_bounding_rect.width() - top_left_inner_width - top_right_inner_width, + top_left_inner_height + }; + Gfx::IntRect right_rect { + inner_bounding_rect.x() + inner_bounding_rect.width() - top_right_inner_width, + inner_bounding_rect.y() + top_right_inner_height, + top_right_inner_width, + inner_bounding_rect.height() - top_right_inner_height - bottom_right_inner_height + }; + Gfx::IntRect bottom_rect { + inner_bounding_rect.x() + bottom_left_inner_width, + inner_bounding_rect.y() + inner_bounding_rect.height() - bottom_right_inner_height, + inner_bounding_rect.width() - bottom_left_inner_width - bottom_right_inner_width, + bottom_right_inner_height + }; + Gfx::IntRect left_rect { + inner_bounding_rect.x(), + inner_bounding_rect.y() + top_left_inner_height, + bottom_left_inner_width, + inner_bounding_rect.height() - top_left_inner_height - bottom_left_inner_height + }; + Gfx::IntRect inner = { + left_rect.x() + left_rect.width(), + left_rect.y(), + inner_bounding_rect.width() - left_rect.width() - right_rect.width(), + inner_bounding_rect.height() - top_rect.height() - bottom_rect.height() + }; + + painter.fill_rect(top_rect, box_shadow_data.color); + painter.fill_rect(right_rect, box_shadow_data.color); + painter.fill_rect(bottom_rect, box_shadow_data.color); + painter.fill_rect(left_rect, box_shadow_data.color); + painter.fill_rect(inner, box_shadow_data.color); + }; + + auto left_start = inner_bounding_rect.left() - blurred_edge_thickness; + auto right_start = inner_bounding_rect.left() + inner_bounding_rect.width(); + auto top_start = inner_bounding_rect.top() - blurred_edge_thickness; + auto bottom_start = inner_bounding_rect.top() + inner_bounding_rect.height(); + + // Note: The +1s in a few of the following translations are due to the -1s Gfx::Rect::right() and Gfx::Rect::bottom(). + auto top_left_corner_blit_pos = inner_bounding_rect.top_left().translated(-blurred_edge_thickness, -blurred_edge_thickness); + auto top_right_corner_blit_pos = inner_bounding_rect.top_right().translated(-top_right_corner_size.width() + 1 + double_radius, -blurred_edge_thickness); + auto bottom_left_corner_blit_pos = inner_bounding_rect.bottom_left().translated(-blurred_edge_thickness, -bottom_left_corner_size.height() + 1 + double_radius); + auto bottom_right_corner_blit_pos = inner_bounding_rect.bottom_right().translated(-bottom_right_corner_size.width() + 1 + double_radius, -bottom_right_corner_size.height() + 1 + double_radius); - // FIXME: Painter only lets us define a clip-rect which discards drawing outside of it, whereas here we want - // a rect which discards drawing inside it. So, we run the draw operations 4 times with clip-rects - // covering each side of the content_rect exactly once. auto paint_shadow = [&](Gfx::IntRect clip_rect) { - painter.save(); + Gfx::PainterStateSaver save { painter }; painter.add_clip_rect(clip_rect); - // Paint corners - painter.blit({ left_start, top_start }, shadow_bitmap, corner_rect); - painter.blit({ right_start, top_start }, shadow_bitmap, corner_rect.translated(corner_rect.width() + 1, 0)); - painter.blit({ left_start, bottom_start }, shadow_bitmap, corner_rect.translated(0, corner_rect.height() + 1)); - painter.blit({ right_start, bottom_start }, shadow_bitmap, corner_rect.translated(corner_rect.width() + 1, corner_rect.height() + 1)); + paint_shadow_infill(); + + // Corners + painter.blit(top_left_corner_blit_pos, shadow_bitmap, top_left_corner_rect); + painter.blit(top_right_corner_blit_pos, shadow_bitmap, top_right_corner_rect); + painter.blit(bottom_left_corner_blit_pos, shadow_bitmap, bottom_left_corner_rect); + painter.blit(bottom_right_corner_blit_pos, shadow_bitmap, bottom_right_corner_rect); // Horizontal edges - for (auto y = solid_rect.top(); y <= solid_rect.bottom(); ++y) { - painter.blit({ left_start, y }, shadow_bitmap, left_edge_rect); - painter.blit({ right_start, y }, shadow_bitmap, right_edge_rect); - } + for (auto x = inner_bounding_rect.left() + (bottom_left_corner_size.width() - double_radius); x <= inner_bounding_rect.right() - (bottom_right_corner_size.width() - double_radius); ++x) + painter.blit({ x, bottom_start }, shadow_bitmap, bottom_edge_rect); + for (auto x = inner_bounding_rect.left() + (top_left_corner_size.width() - double_radius); x <= inner_bounding_rect.right() - (top_right_corner_size.width() - double_radius); ++x) + painter.blit({ x, top_start }, shadow_bitmap, top_edge_rect); // Vertical edges - for (auto x = solid_rect.left(); x <= solid_rect.right(); ++x) { - painter.blit({ x, top_start }, shadow_bitmap, top_edge_rect); - painter.blit({ x, bottom_start }, shadow_bitmap, bottom_edge_rect); - } - - painter.restore(); + for (auto y = inner_bounding_rect.top() + (top_right_corner_size.height() - double_radius); y <= inner_bounding_rect.bottom() - (bottom_right_corner_size.height() - double_radius); ++y) + painter.blit({ right_start, y }, shadow_bitmap, right_edge_rect); + for (auto y = inner_bounding_rect.top() + (top_left_corner_size.height() - double_radius); y <= inner_bounding_rect.bottom() - (bottom_left_corner_size.height() - double_radius); ++y) + painter.blit({ left_start, y }, shadow_bitmap, left_edge_rect); }; + // FIXME: Painter only lets us define a clip-rect which discards drawing outside of it, whereas here we want + // a rect which discards drawing inside it. So, we run the draw operations 4 to 8 times with clip-rects + // covering each side of the content_rect exactly once. + + // If we were painting a shadow without a border radius we'd want to clip everything inside the box below. + // If painting a shadow with rounded corners (but still rectangular) we want to clip everything inside + // the box except the corners. This gives us an upper bound of 8 shadow paints now :^(. + // (However, this does not seem to be the costly part in profiling). + // + // ┌───┬────────┬───┐ + // │ │xxxxxxxx│ │ + // ├───┼────────┼───┤ + // │xxx│xxxxxxxx│xxx│ + // │xxx│xxxxxxxx│xxx│ + // │xxx│xxxxxxxx│xxx│ + // │xxx│xxxxxxxx│xxx│ + // │xxx│xxxxxxxx│xxx│ + // ├───┼────────┼───┤ + // │ │ xxxxxx │ │ + // └───┴────────┴───┘ + + // How many times would you like to paint the shadow sir? + // Yes. + + // FIXME: Could reduce the shadow paints from 8 to 4 for shadows with all corner radii 50%. + // Everything above content_rect, including sides paint_shadow({ 0, 0, painter.target()->width(), content_rect.top() }); @@ -125,7 +274,34 @@ void paint_box_shadow(PaintContext& context, Gfx::IntRect const& content_rect, B // Everything directly to the right of content_rect paint_shadow({ content_rect.right() + 1, content_rect.top(), painter.target()->width(), content_rect.height() }); + + if (top_left_corner) { + // Inside the top left corner (the part outside the border radius) + auto top_left = top_left_corner.as_rect().translated(content_rect.top_left()); + paint_shadow(top_left); + } + + if (top_right_corner) { + // Inside the top right corner (the part outside the border radius) + auto top_right = top_right_corner.as_rect().translated(content_rect.top_right().translated(-top_right_corner.horizontal_radius + 1, 0)); + paint_shadow(top_right); + } + + if (bottom_right_corner) { + // Inside the bottom right corner (the part outside the border radius) + auto bottom_right = bottom_right_corner.as_rect().translated(content_rect.bottom_right().translated(-bottom_right_corner.horizontal_radius + 1, -bottom_right_corner.vertical_radius + 1)); + paint_shadow(bottom_right); + } + + if (bottom_left_corner) { + // Inside the bottom left corner (the part outside the border radius) + auto bottom_left = bottom_left_corner.as_rect().translated(content_rect.bottom_left().translated(0, -bottom_left_corner.vertical_radius + 1)); + paint_shadow(bottom_left); + } } + + if (corner_radius_clipper.has_value()) + corner_radius_clipper->blit_corner_clipping(painter); } void paint_text_shadow(PaintContext& context, Layout::LineBoxFragment const& fragment, Vector const& shadow_layers)