diff --git a/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp b/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp index 3766eb7d12..a3557b836b 100644 --- a/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp +++ b/Userland/Libraries/LibWeb/Painting/InlinePaintable.cpp @@ -69,7 +69,17 @@ void InlinePaintable::paint(PaintContext& context, PaintPhase phase) const layer.spread_distance.to_px(layout_node()), layer.placement == CSS::ShadowPlacement::Outer ? ShadowPlacement::Outer : ShadowPlacement::Inner); } - paint_box_shadow(context, absolute_fragment_rect, border_radii_data, resolved_box_shadow_data); + auto borders_data = BordersData { + .top = computed_values().border_top(), + .right = computed_values().border_right(), + .bottom = computed_values().border_bottom(), + .left = computed_values().border_left(), + }; + auto absolute_fragment_rect_bordered = absolute_fragment_rect.inflated( + borders_data.top.width, borders_data.right.width, + borders_data.bottom.width, borders_data.left.width); + paint_box_shadow(context, absolute_fragment_rect_bordered, absolute_fragment_rect, + borders_data, border_radii_data, resolved_box_shadow_data); } return IterationDecision::Continue; diff --git a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp index 423d96fac7..57a1cb0800 100644 --- a/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Userland/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -296,7 +296,14 @@ void PaintableBox::paint_box_shadow(PaintContext& context) const auto resolved_box_shadow_data = resolve_box_shadow_data(); if (resolved_box_shadow_data.is_empty()) return; - Painting::paint_box_shadow(context, absolute_border_box_rect(), normalized_border_radii_data(), resolved_box_shadow_data); + auto borders_data = BordersData { + .top = computed_values().border_top(), + .right = computed_values().border_right(), + .bottom = computed_values().border_bottom(), + .left = computed_values().border_left(), + }; + Painting::paint_box_shadow(context, absolute_border_box_rect(), absolute_padding_box_rect(), + borders_data, normalized_border_radii_data(), resolved_box_shadow_data); } BorderRadiiData PaintableBox::normalized_border_radii_data(ShrinkRadiiForBorders shrink) const diff --git a/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp b/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp index 74564a4cd4..ad8ed6498d 100644 --- a/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp +++ b/Userland/Libraries/LibWeb/Painting/ShadowPainting.cpp @@ -19,11 +19,57 @@ namespace Web::Painting { -void paint_box_shadow(PaintContext& context, CSSPixelRect const& content_rect, BorderRadiiData const& border_radii, Vector const& box_shadow_layers) +static void paint_inner_box_shadow(PaintContext& context, CSSPixelRect const& content_rect, + BordersData const& borders_data, BorderRadiiData const& border_radii, ShadowData const& box_shadow_data) { - if (box_shadow_layers.is_empty()) - return; + auto& painter = context.painter(); + auto device_content_rect = context.rounded_device_rect(content_rect); + auto border_radii_shrunken = border_radii; + border_radii_shrunken.shrink(borders_data.top.width, borders_data.right.width, borders_data.bottom.width, borders_data.left.width); + ScopedCornerRadiusClip corner_clipper { context, painter, device_content_rect, border_radii_shrunken, CornerClip::Outside }; + DevicePixels offset_x = context.rounded_device_pixels(box_shadow_data.offset_x); + DevicePixels offset_y = context.rounded_device_pixels(box_shadow_data.offset_y); + DevicePixels blur_radius = context.rounded_device_pixels(box_shadow_data.blur_radius); + DevicePixels spread_distance = context.rounded_device_pixels(box_shadow_data.spread_distance); + auto spread_distance_value = spread_distance.value(); + auto shadows_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, device_content_rect.size().to_type()); + if (shadows_bitmap.is_error()) { + dbgln("Unable to allocate temporary bitmap {} for box-shadow rendering: {}", device_content_rect, shadows_bitmap.error()); + return; + } + auto shadow_bitmap = shadows_bitmap.release_value(); + Gfx::Painter shadow_painter { *shadow_bitmap }; + Gfx::AntiAliasingPainter shadow_aa_painter { shadow_painter }; + auto device_content_rect_int = device_content_rect.to_type(); + auto outer_shadow_rect = device_content_rect_int.translated(-device_content_rect_int.x(), -device_content_rect_int.y()); + auto inner_shadow_rect = outer_shadow_rect.translated({ offset_x, offset_y }); + inner_shadow_rect.inflate(-spread_distance_value, -spread_distance_value, -spread_distance_value, -spread_distance_value); + outer_shadow_rect = outer_shadow_rect.translated(-blur_radius.value(), -blur_radius.value()); + outer_shadow_rect.set_size(outer_shadow_rect.width() + blur_radius.value(), outer_shadow_rect.height() + blur_radius.value()); + auto top_left_corner = border_radii_shrunken.top_left.as_corner(context); + auto top_right_corner = border_radii_shrunken.top_right.as_corner(context); + auto bottom_right_corner = border_radii_shrunken.bottom_right.as_corner(context); + auto bottom_left_corner = border_radii_shrunken.bottom_left.as_corner(context); + shadow_painter.fill_rect(outer_shadow_rect, box_shadow_data.color.with_alpha(0xff)); + if (border_radii_shrunken.has_any_radius()) { + shadow_aa_painter.fill_rect_with_rounded_corners(inner_shadow_rect, box_shadow_data.color.with_alpha(0xff), + top_left_corner, top_right_corner, bottom_right_corner, bottom_left_corner, + Gfx::AntiAliasingPainter::BlendMode::AlphaSubtract); + } else { + shadow_painter.clear_rect(inner_shadow_rect, Color::Transparent); + } + Gfx::StackBlurFilter filter(*shadow_bitmap); + filter.process_rgba(blur_radius.value(), box_shadow_data.color); + Gfx::PainterStateSaver save { painter }; + painter.add_clip_rect(device_content_rect_int); + painter.blit({ device_content_rect_int.left(), device_content_rect_int.top() }, + *shadow_bitmap, shadow_bitmap->rect(), box_shadow_data.color.alpha() / 255.); +} + +static void paint_outer_box_shadow(PaintContext& context, CSSPixelRect const& content_rect, + BorderRadiiData const& border_radii, ShadowData const& box_shadow_data) +{ auto& painter = context.painter(); auto device_content_rect = context.rounded_device_rect(content_rect); @@ -34,301 +80,313 @@ void paint_box_shadow(PaintContext& context, CSSPixelRect const& content_rect, B ScopedCornerRadiusClip corner_clipper { context, painter, device_content_rect, border_radii, CornerClip::Inside }; + DevicePixels offset_x = context.rounded_device_pixels(box_shadow_data.offset_x); + DevicePixels offset_y = context.rounded_device_pixels(box_shadow_data.offset_y); + DevicePixels blur_radius = context.rounded_device_pixels(box_shadow_data.blur_radius); + DevicePixels spread_distance = context.rounded_device_pixels(box_shadow_data.spread_distance); + + auto fill_rect_masked = [](auto& painter, auto fill_rect, auto mask_rect, auto color) { + Gfx::DisjointRectSet rect_set; + rect_set.add(fill_rect); + auto shattered = rect_set.shatter(mask_rect); + for (auto& rect : shattered.rects()) + painter.fill_rect(rect.template to_type(), color); + }; + + // Our blur cannot handle radii over 255 so there's no point trying (255 is silly big anyway) + blur_radius = clamp(blur_radius, 0, 255); + + // If there's no blurring, nor rounded corners, we can save a lot of effort. + auto non_blurred_shadow_rect = device_content_rect.inflated(spread_distance, spread_distance, spread_distance, spread_distance); + if (blur_radius == 0 && !border_radii.has_any_radius()) { + fill_rect_masked(painter, non_blurred_shadow_rect.translated(offset_x, offset_y), device_content_rect, box_shadow_data.color); + return; + } + + 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 += spread_distance.value(); + corner.vertical_radius += spread_distance.value(); + } + }; + + 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 = spread_distance - (blur_radius * 2); + DevicePixelRect inner_bounding_rect = { + device_content_rect.x() + offset_x - expansion, + device_content_rect.y() + offset_y - expansion, + device_content_rect.width() + 2 * expansion, + device_content_rect.height() + 2 * expansion + }; + + // 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 = blur_radius * 2; + auto blurred_edge_thickness = blur_radius * 4; + + 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).to_type(); + auto top_right_corner_size = (top_right_shadow_corner ? top_right_shadow_corner.as_rect().size() : default_corner_size).to_type(); + auto bottom_left_corner_size = (bottom_left_shadow_corner ? bottom_left_shadow_corner.as_rect().size() : default_corner_size).to_type(); + auto bottom_right_corner_size = (bottom_right_shadow_corner ? bottom_right_shadow_corner.as_rect().size() : default_corner_size).to_type(); + + auto max_edge_width = non_blurred_shadow_rect.width() / 2; + auto max_edge_height = non_blurred_shadow_rect.height() / 2; + auto extra_edge_width = non_blurred_shadow_rect.width() % 2; + auto extra_edge_height = non_blurred_shadow_rect.height() % 2; + + auto clip_corner_size = [&](auto& size, auto const& corner, DevicePixels x_bonus = 0, DevicePixels y_bonus = 0) { + auto max_x = (max_edge_width + x_bonus).value(); + auto max_y = (max_edge_height + y_bonus).value(); + auto min_x = max(corner.horizontal_radius, min(double_radius, max_x).value()); + auto min_y = max(corner.vertical_radius, min(double_radius, max_y).value()); + if (min_x <= max_x) + size.set_width(clamp(size.width(), min_x, max_x)); + if (min_y <= max_y) + size.set_height(clamp(size.height(), min_y, max_y)); + }; + + clip_corner_size(top_left_corner_size, top_left_corner, extra_edge_width, extra_edge_height); + clip_corner_size(top_right_corner_size, top_right_corner, 0, extra_edge_height); + clip_corner_size(bottom_left_corner_size, bottom_left_corner, extra_edge_width); + clip_corner_size(bottom_right_corner_size, bottom_right_corner); + + auto shadow_bitmap_rect = DevicePixelRect( + 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 = DevicePixelRect { + 0, 0, + top_left_corner_size.width() + double_radius, + top_left_corner_size.height() + double_radius + }; + auto top_right_corner_rect = DevicePixelRect { + 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 = DevicePixelRect { + 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 = DevicePixelRect { + 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 + }; + + auto horizontal_edge_width = min(max_edge_height, double_radius) + double_radius; + auto vertical_edge_width = min(max_edge_width, double_radius) + double_radius; + auto horizontal_top_edge_width = min(max_edge_height + extra_edge_height, double_radius) + double_radius; + auto vertical_left_edge_width = min(max_edge_width + extra_edge_width, double_radius) + double_radius; + + DevicePixelRect left_edge_rect { 0, top_left_corner_rect.height(), vertical_left_edge_width, 1 }; + DevicePixelRect right_edge_rect { shadow_bitmap_rect.width() - vertical_edge_width, top_right_corner_rect.height(), vertical_edge_width, 1 }; + DevicePixelRect top_edge_rect { top_left_corner_rect.width(), 0, 1, horizontal_top_edge_width }; + DevicePixelRect bottom_edge_rect { bottom_left_corner_rect.width(), shadow_bitmap_rect.height() - horizontal_edge_width, 1, horizontal_edge_width }; + + auto shadows_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, shadow_bitmap_rect.size().to_type()); + if (shadows_bitmap.is_error()) { + dbgln("Unable to allocate temporary bitmap {} for box-shadow rendering: {}", shadow_bitmap_rect, shadows_bitmap.error()); + return; + } + auto shadow_bitmap = shadows_bitmap.release_value(); + Gfx::Painter corner_painter { *shadow_bitmap }; + 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).to_type(), + box_shadow_data.color, top_left_shadow_corner, top_right_shadow_corner, bottom_right_shadow_corner, bottom_left_shadow_corner); + Gfx::StackBlurFilter filter(*shadow_bitmap); + filter.process_rgba(blur_radius.value(), box_shadow_data.color); + + auto paint_shadow_infill = [&] { + if (!border_radii.has_any_radius()) + return painter.fill_rect(inner_bounding_rect.to_type(), 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; + + DevicePixelRect 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 + }; + DevicePixelRect 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 + }; + DevicePixelRect 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 + }; + DevicePixelRect 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 + }; + DevicePixelRect 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.to_type(), box_shadow_data.color); + painter.fill_rect(right_rect.to_type(), box_shadow_data.color); + painter.fill_rect(bottom_rect.to_type(), box_shadow_data.color); + painter.fill_rect(left_rect.to_type(), box_shadow_data.color); + painter.fill_rect(inner.to_type(), 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() + (blurred_edge_thickness - vertical_edge_width); + auto top_start = inner_bounding_rect.top() - blurred_edge_thickness; + auto bottom_start = inner_bounding_rect.top() + inner_bounding_rect.height() + (blurred_edge_thickness - horizontal_edge_width); + + 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() + 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() + double_radius); + auto bottom_right_corner_blit_pos = inner_bounding_rect.bottom_right().translated(-bottom_right_corner_size.width() + double_radius, -bottom_right_corner_size.height() + double_radius); + + auto paint_shadow = [&](DevicePixelRect clip_rect) { + Gfx::PainterStateSaver save { painter }; + painter.add_clip_rect(clip_rect.to_type()); + + paint_shadow_infill(); + + // Corners + painter.blit(top_left_corner_blit_pos.to_type(), shadow_bitmap, top_left_corner_rect.to_type()); + painter.blit(top_right_corner_blit_pos.to_type(), shadow_bitmap, top_right_corner_rect.to_type()); + painter.blit(bottom_left_corner_blit_pos.to_type(), shadow_bitmap, bottom_left_corner_rect.to_type()); + painter.blit(bottom_right_corner_blit_pos.to_type(), shadow_bitmap, bottom_right_corner_rect.to_type()); + + // Horizontal edges + 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.to_type()); + 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.to_type()); + + // Vertical edges + 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.to_type()); + 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.to_type()); + }; + + // 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%. + + // FIXME: We use this since we want the clip rect to include everything after a certain x or y. + // Note: Using painter.target()->width() or height() does not work, when the painter is a small + // translated bitmap rather than full screen, as the clip rect may not intersect. + constexpr auto really_large_number = NumericLimits::max() / 2; + + // Everything above content_rect, including sides + paint_shadow({ 0, 0, really_large_number, device_content_rect.top() }); + + // Everything below content_rect, including sides + paint_shadow({ 0, device_content_rect.bottom(), really_large_number, really_large_number }); + + // Everything directly to the left of content_rect + paint_shadow({ 0, device_content_rect.top(), device_content_rect.left(), device_content_rect.height() }); + + // Everything directly to the right of content_rect + paint_shadow({ device_content_rect.right(), device_content_rect.top(), really_large_number, device_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().to_type().translated(device_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().to_type().translated(device_content_rect.top_right().translated(-top_right_corner.horizontal_radius, 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().to_type().translated(device_content_rect.bottom_right().translated(-bottom_right_corner.horizontal_radius, -bottom_right_corner.vertical_radius)); + 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().to_type().translated(device_content_rect.bottom_left().translated(0, -bottom_left_corner.vertical_radius)); + paint_shadow(bottom_left); + } +} + +void paint_box_shadow(PaintContext& context, + CSSPixelRect const& bordered_content_rect, + CSSPixelRect const& borderless_content_rect, + BordersData const& borders_data, + BorderRadiiData const& border_radii, + Vector const& box_shadow_layers) +{ // Note: Box-shadow layers are ordered front-to-back, so we paint them in reverse for (auto& box_shadow_data : box_shadow_layers.in_reverse()) { - // FIXME: Paint inset shadows. - if (box_shadow_data.placement != ShadowPlacement::Outer) - continue; - - DevicePixels offset_x = context.rounded_device_pixels(box_shadow_data.offset_x); - DevicePixels offset_y = context.rounded_device_pixels(box_shadow_data.offset_y); - DevicePixels blur_radius = context.rounded_device_pixels(box_shadow_data.blur_radius); - DevicePixels spread_distance = context.rounded_device_pixels(box_shadow_data.spread_distance); - - auto fill_rect_masked = [](auto& painter, auto fill_rect, auto mask_rect, auto color) { - Gfx::DisjointRectSet rect_set; - rect_set.add(fill_rect); - auto shattered = rect_set.shatter(mask_rect); - for (auto& rect : shattered.rects()) - painter.fill_rect(rect.template to_type(), color); - }; - - // Our blur cannot handle radii over 255 so there's no point trying (255 is silly big anyway) - blur_radius = clamp(blur_radius, 0, 255); - - // If there's no blurring, nor rounded corners, we can save a lot of effort. - auto non_blurred_shadow_rect = device_content_rect.inflated(spread_distance, spread_distance, spread_distance, spread_distance); - if (blur_radius == 0 && !border_radii.has_any_radius()) { - fill_rect_masked(painter, non_blurred_shadow_rect.translated(offset_x, offset_y), device_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 += spread_distance.value(); - corner.vertical_radius += spread_distance.value(); - } - }; - - 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 = spread_distance - (blur_radius * 2); - DevicePixelRect inner_bounding_rect = { - device_content_rect.x() + offset_x - expansion, - device_content_rect.y() + offset_y - expansion, - device_content_rect.width() + 2 * expansion, - device_content_rect.height() + 2 * expansion - }; - - // 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 = blur_radius * 2; - auto blurred_edge_thickness = blur_radius * 4; - - 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).to_type(); - auto top_right_corner_size = (top_right_shadow_corner ? top_right_shadow_corner.as_rect().size() : default_corner_size).to_type(); - auto bottom_left_corner_size = (bottom_left_shadow_corner ? bottom_left_shadow_corner.as_rect().size() : default_corner_size).to_type(); - auto bottom_right_corner_size = (bottom_right_shadow_corner ? bottom_right_shadow_corner.as_rect().size() : default_corner_size).to_type(); - - auto max_edge_width = non_blurred_shadow_rect.width() / 2; - auto max_edge_height = non_blurred_shadow_rect.height() / 2; - auto extra_edge_width = non_blurred_shadow_rect.width() % 2; - auto extra_edge_height = non_blurred_shadow_rect.height() % 2; - - auto clip_corner_size = [&](auto& size, auto const& corner, DevicePixels x_bonus = 0, DevicePixels y_bonus = 0) { - auto max_x = (max_edge_width + x_bonus).value(); - auto max_y = (max_edge_height + y_bonus).value(); - auto min_x = max(corner.horizontal_radius, min(double_radius, max_x).value()); - auto min_y = max(corner.vertical_radius, min(double_radius, max_y).value()); - if (min_x <= max_x) - size.set_width(clamp(size.width(), min_x, max_x)); - if (min_y <= max_y) - size.set_height(clamp(size.height(), min_y, max_y)); - }; - - clip_corner_size(top_left_corner_size, top_left_corner, extra_edge_width, extra_edge_height); - clip_corner_size(top_right_corner_size, top_right_corner, 0, extra_edge_height); - clip_corner_size(bottom_left_corner_size, bottom_left_corner, extra_edge_width); - clip_corner_size(bottom_right_corner_size, bottom_right_corner); - - auto shadow_bitmap_rect = DevicePixelRect( - 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 = DevicePixelRect { - 0, 0, - top_left_corner_size.width() + double_radius, - top_left_corner_size.height() + double_radius - }; - auto top_right_corner_rect = DevicePixelRect { - 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 = DevicePixelRect { - 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 = DevicePixelRect { - 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 - }; - - auto horizontal_edge_width = min(max_edge_height, double_radius) + double_radius; - auto vertical_edge_width = min(max_edge_width, double_radius) + double_radius; - auto horizontal_top_edge_width = min(max_edge_height + extra_edge_height, double_radius) + double_radius; - auto vertical_left_edge_width = min(max_edge_width + extra_edge_width, double_radius) + double_radius; - - DevicePixelRect left_edge_rect { 0, top_left_corner_rect.height(), vertical_left_edge_width, 1 }; - DevicePixelRect right_edge_rect { shadow_bitmap_rect.width() - vertical_edge_width, top_right_corner_rect.height(), vertical_edge_width, 1 }; - DevicePixelRect top_edge_rect { top_left_corner_rect.width(), 0, 1, horizontal_top_edge_width }; - DevicePixelRect bottom_edge_rect { bottom_left_corner_rect.width(), shadow_bitmap_rect.height() - horizontal_edge_width, 1, horizontal_edge_width }; - - auto shadows_bitmap = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, shadow_bitmap_rect.size().to_type()); - if (shadows_bitmap.is_error()) { - dbgln("Unable to allocate temporary bitmap {} for box-shadow rendering: {}", shadow_bitmap_rect, shadows_bitmap.error()); - return; - } - auto shadow_bitmap = shadows_bitmap.release_value(); - Gfx::Painter corner_painter { *shadow_bitmap }; - 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).to_type(), box_shadow_data.color, top_left_shadow_corner, top_right_shadow_corner, bottom_right_shadow_corner, bottom_left_shadow_corner); - Gfx::StackBlurFilter filter(*shadow_bitmap); - filter.process_rgba(blur_radius.value(), box_shadow_data.color); - - auto paint_shadow_infill = [&] { - if (!border_radii.has_any_radius()) - return painter.fill_rect(inner_bounding_rect.to_type(), 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; - - DevicePixelRect 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 - }; - DevicePixelRect 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 - }; - DevicePixelRect 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 - }; - DevicePixelRect 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 - }; - DevicePixelRect 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.to_type(), box_shadow_data.color); - painter.fill_rect(right_rect.to_type(), box_shadow_data.color); - painter.fill_rect(bottom_rect.to_type(), box_shadow_data.color); - painter.fill_rect(left_rect.to_type(), box_shadow_data.color); - painter.fill_rect(inner.to_type(), 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() + (blurred_edge_thickness - vertical_edge_width); - auto top_start = inner_bounding_rect.top() - blurred_edge_thickness; - auto bottom_start = inner_bounding_rect.top() + inner_bounding_rect.height() + (blurred_edge_thickness - horizontal_edge_width); - - 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() + 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() + double_radius); - auto bottom_right_corner_blit_pos = inner_bounding_rect.bottom_right().translated(-bottom_right_corner_size.width() + double_radius, -bottom_right_corner_size.height() + double_radius); - - auto paint_shadow = [&](DevicePixelRect clip_rect) { - Gfx::PainterStateSaver save { painter }; - painter.add_clip_rect(clip_rect.to_type()); - - paint_shadow_infill(); - - // Corners - painter.blit(top_left_corner_blit_pos.to_type(), shadow_bitmap, top_left_corner_rect.to_type()); - painter.blit(top_right_corner_blit_pos.to_type(), shadow_bitmap, top_right_corner_rect.to_type()); - painter.blit(bottom_left_corner_blit_pos.to_type(), shadow_bitmap, bottom_left_corner_rect.to_type()); - painter.blit(bottom_right_corner_blit_pos.to_type(), shadow_bitmap, bottom_right_corner_rect.to_type()); - - // Horizontal edges - 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.to_type()); - 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.to_type()); - - // Vertical edges - 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.to_type()); - 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.to_type()); - }; - - // 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%. - - // FIXME: We use this since we want the clip rect to include everything after a certain x or y. - // Note: Using painter.target()->width() or height() does not work, when the painter is a small - // translated bitmap rather than full screen, as the clip rect may not intersect. - constexpr auto really_large_number = NumericLimits::max() / 2; - - // Everything above content_rect, including sides - paint_shadow({ 0, 0, really_large_number, device_content_rect.top() }); - - // Everything below content_rect, including sides - paint_shadow({ 0, device_content_rect.bottom(), really_large_number, really_large_number }); - - // Everything directly to the left of content_rect - paint_shadow({ 0, device_content_rect.top(), device_content_rect.left(), device_content_rect.height() }); - - // Everything directly to the right of content_rect - paint_shadow({ device_content_rect.right(), device_content_rect.top(), really_large_number, device_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().to_type().translated(device_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().to_type().translated(device_content_rect.top_right().translated(-top_right_corner.horizontal_radius, 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().to_type().translated(device_content_rect.bottom_right().translated(-bottom_right_corner.horizontal_radius, -bottom_right_corner.vertical_radius)); - 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().to_type().translated(device_content_rect.bottom_left().translated(0, -bottom_left_corner.vertical_radius)); - paint_shadow(bottom_left); + if (box_shadow_data.placement == ShadowPlacement::Inner) { + paint_inner_box_shadow(context, borderless_content_rect, borders_data, border_radii, box_shadow_data); + } else { + paint_outer_box_shadow(context, bordered_content_rect, border_radii, box_shadow_data); } } } diff --git a/Userland/Libraries/LibWeb/Painting/ShadowPainting.h b/Userland/Libraries/LibWeb/Painting/ShadowPainting.h index 099ff8100a..7a387b7c90 100644 --- a/Userland/Libraries/LibWeb/Painting/ShadowPainting.h +++ b/Userland/Libraries/LibWeb/Painting/ShadowPainting.h @@ -26,7 +26,13 @@ struct ShadowData { ShadowPlacement placement; }; -void paint_box_shadow(PaintContext&, CSSPixelRect const&, BorderRadiiData const&, Vector const&); +void paint_box_shadow( + PaintContext&, + CSSPixelRect const& bordered_content_rect, + CSSPixelRect const& borderless_content_rect, + BordersData const& borders_data, + BorderRadiiData const&, + Vector const&); void paint_text_shadow(PaintContext&, Layout::LineBoxFragment const&, Vector const&); }