ladybird/Userland/Libraries/LibWeb/Painting/StackingContext.cpp
MacDue 3fad64dfbc LibWeb: Sample the destination when painting element opacity/transform
This now copies the area under the destination to a new bitmap, that
is then scaled to the size of the source. The element is then painted
into that bitmap, which is then scaled and painted back to
the destination. This is done as many effects such as shadows, border
radii, filters, etc require being able to read pixels from the painter.

This does work (and is not that noticeable in many cases), but it does
mean there may be a few scaling artifacts in the background
around transformed elements. Though that was already the case before
anyway for the elements (since it is just a bitmap scale).

What we really want is to (where possible) just scale the paintable
and its descendants, then paint things normally, which would give
much nicer results (but is much more tricky to achieve).

This also now makes it so only a bitmap of the size of the paintable is
copied/created, rather than the whole page.
2022-09-25 18:37:31 +02:00

475 lines
20 KiB
C++

/*
* Copyright (c) 2020-2022, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/ExtraMathConstants.h>
#include <AK/QuickSort.h>
#include <AK/StringBuilder.h>
#include <LibGfx/AffineTransform.h>
#include <LibGfx/Matrix4x4.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Rect.h>
#include <LibWeb/Layout/Box.h>
#include <LibWeb/Layout/InitialContainingBlock.h>
#include <LibWeb/Layout/ReplacedBox.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/StackingContext.h>
namespace Web::Painting {
static void paint_node(Layout::Node const& layout_node, PaintContext& context, PaintPhase phase)
{
if (auto const* paintable = layout_node.paintable())
paintable->paint(context, phase);
}
StackingContext::StackingContext(Layout::Box& box, StackingContext* parent)
: m_box(box)
, m_transform(combine_transformations(m_box.computed_values().transformations()))
, m_transform_origin(compute_transform_origin())
, m_parent(parent)
{
VERIFY(m_parent != this);
if (m_parent)
m_parent->m_children.append(this);
}
void StackingContext::sort()
{
quick_sort(m_children, [](auto& a, auto& b) {
auto a_z_index = a->m_box.computed_values().z_index().value_or(0);
auto b_z_index = b->m_box.computed_values().z_index().value_or(0);
if (a_z_index == b_z_index)
return a->m_box.is_before(b->m_box);
return a_z_index < b_z_index;
});
for (auto* child : m_children)
child->sort();
}
static PaintPhase to_paint_phase(StackingContext::StackingContextPaintPhase phase)
{
// There are not a fully correct mapping since some stacking context phases are combined.
switch (phase) {
case StackingContext::StackingContextPaintPhase::Floats:
case StackingContext::StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced:
case StackingContext::StackingContextPaintPhase::BackgroundAndBorders:
return PaintPhase::Background;
case StackingContext::StackingContextPaintPhase::Foreground:
return PaintPhase::Foreground;
case StackingContext::StackingContextPaintPhase::FocusAndOverlay:
return PaintPhase::Overlay;
default:
VERIFY_NOT_REACHED();
}
}
void StackingContext::paint_descendants(PaintContext& context, Layout::Node& box, StackingContextPaintPhase phase) const
{
if (auto* paintable = box.paintable())
paintable->before_children_paint(context, to_paint_phase(phase), Paintable::ShouldClipOverflow::Yes);
box.for_each_child([&](auto& child) {
// If `child` establishes its own stacking context, skip over it.
if (is<Layout::Box>(child) && child.paintable() && static_cast<Layout::Box const&>(child).paint_box()->stacking_context())
return;
bool child_is_inline_or_replaced = child.is_inline() || is<Layout::ReplacedBox>(child);
switch (phase) {
case StackingContextPaintPhase::BackgroundAndBorders:
if (!child_is_inline_or_replaced && !child.is_floating() && !child.is_positioned()) {
paint_node(child, context, PaintPhase::Background);
paint_node(child, context, PaintPhase::Border);
paint_descendants(context, child, phase);
}
break;
case StackingContextPaintPhase::Floats:
if (!child.is_positioned()) {
if (child.is_floating()) {
paint_node(child, context, PaintPhase::Background);
paint_node(child, context, PaintPhase::Border);
paint_descendants(context, child, StackingContextPaintPhase::BackgroundAndBorders);
}
paint_descendants(context, child, phase);
}
break;
case StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced:
if (!child.is_positioned()) {
if (child_is_inline_or_replaced) {
paint_node(child, context, PaintPhase::Background);
paint_node(child, context, PaintPhase::Border);
paint_descendants(context, child, StackingContextPaintPhase::BackgroundAndBorders);
}
paint_descendants(context, child, phase);
}
break;
case StackingContextPaintPhase::Foreground:
if (!child.is_positioned()) {
paint_node(child, context, PaintPhase::Foreground);
paint_descendants(context, child, phase);
}
break;
case StackingContextPaintPhase::FocusAndOverlay:
if (context.has_focus()) {
paint_node(child, context, PaintPhase::FocusOutline);
}
paint_node(child, context, PaintPhase::Overlay);
paint_descendants(context, child, phase);
break;
}
});
if (auto* paintable = box.paintable())
paintable->after_children_paint(context, to_paint_phase(phase), Paintable::ShouldClipOverflow::Yes);
}
void StackingContext::paint_internal(PaintContext& context) const
{
// For a more elaborate description of the algorithm, see CSS 2.1 Appendix E
// Draw the background and borders for the context root (steps 1, 2)
paint_node(m_box, context, PaintPhase::Background);
paint_node(m_box, context, PaintPhase::Border);
auto paint_child = [&](auto* child) {
auto parent = child->m_box.parent();
auto should_clip_overflow = child->m_box.is_absolutely_positioned() ? Paintable::ShouldClipOverflow::No : Paintable::ShouldClipOverflow::Yes;
auto* paintable = parent ? parent->paintable() : nullptr;
if (paintable)
paintable->before_children_paint(context, PaintPhase::Foreground, should_clip_overflow);
child->paint(context);
if (paintable)
paintable->after_children_paint(context, PaintPhase::Foreground, should_clip_overflow);
};
// Draw positioned descendants with negative z-indices (step 3)
for (auto* child : m_children) {
if (child->m_box.computed_values().z_index().has_value() && child->m_box.computed_values().z_index().value() < 0)
paint_child(child);
}
// Draw the background and borders for block-level children (step 4)
paint_descendants(context, m_box, StackingContextPaintPhase::BackgroundAndBorders);
// Draw the non-positioned floats (step 5)
paint_descendants(context, m_box, StackingContextPaintPhase::Floats);
// Draw inline content, replaced content, etc. (steps 6, 7)
paint_descendants(context, m_box, StackingContextPaintPhase::BackgroundAndBordersForInlineLevelAndReplaced);
paint_node(m_box, context, PaintPhase::Foreground);
paint_descendants(context, m_box, StackingContextPaintPhase::Foreground);
// Draw other positioned descendants (steps 8, 9)
for (auto* child : m_children) {
if (child->m_box.computed_values().z_index().has_value() && child->m_box.computed_values().z_index().value() < 0)
continue;
paint_child(child);
}
paint_node(m_box, context, PaintPhase::FocusOutline);
paint_node(m_box, context, PaintPhase::Overlay);
paint_descendants(context, m_box, StackingContextPaintPhase::FocusAndOverlay);
}
Gfx::FloatMatrix4x4 StackingContext::get_transformation_matrix(CSS::Transformation const& transformation) const
{
auto count = transformation.values.size();
auto value = [this, transformation](size_t index, Optional<CSS::Length const&> reference_length = {}) -> float {
return transformation.values[index].visit(
[this, reference_length](CSS::LengthPercentage const& value) {
return value.resolved(m_box, reference_length.value()).to_px(m_box);
},
[](CSS::Angle const& value) {
return value.to_degrees() * static_cast<float>(M_DEG2RAD);
},
[](float value) {
return value;
});
};
auto reference_box = paintable().absolute_rect();
auto width = CSS::Length::make_px(reference_box.width());
auto height = CSS::Length::make_px(reference_box.height());
switch (transformation.function) {
case CSS::TransformFunction::Matrix:
if (count == 6)
return Gfx::FloatMatrix4x4(value(0), value(2), 0, value(4),
value(1), value(3), 0, value(5),
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::Translate:
if (count == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, value(0, width),
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
if (count == 2)
return Gfx::FloatMatrix4x4(1, 0, 0, value(0, width),
0, 1, 0, value(1, height),
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::TranslateX:
if (count == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, value(0, width),
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::TranslateY:
if (count == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, 0,
0, 1, 0, value(0, height),
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::Scale:
if (count == 1)
return Gfx::FloatMatrix4x4(value(0), 0, 0, 0,
0, value(0), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
if (count == 2)
return Gfx::FloatMatrix4x4(value(0), 0, 0, 0,
0, value(0), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::ScaleX:
if (count == 1)
return Gfx::FloatMatrix4x4(value(0), 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::ScaleY:
if (count == 1)
return Gfx::FloatMatrix4x4(1, 0, 0, 0,
0, value(0), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1);
break;
case CSS::TransformFunction::Rotate:
if (count == 1)
return Gfx::rotation_matrix({ 0.0f, 0.0f, 1.0f }, value(0));
break;
default:
dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Unhandled transformation function {}", CSS::TransformationStyleValue::create(transformation.function, {})->to_string());
}
return Gfx::FloatMatrix4x4::identity();
}
Gfx::FloatMatrix4x4 StackingContext::combine_transformations(Vector<CSS::Transformation> const& transformations) const
{
auto matrix = Gfx::FloatMatrix4x4::identity();
for (auto const& transform : transformations)
matrix = matrix * get_transformation_matrix(transform);
return matrix;
}
// FIXME: This extracts the affine 2D part of the full transformation matrix.
// Use the whole matrix when we get better transformation support in LibGfx or use LibGL for drawing the bitmap
Gfx::AffineTransform StackingContext::affine_transform_matrix() const
{
auto* m = m_transform.elements();
return Gfx::AffineTransform(m[0][0], m[1][0], m[0][1], m[1][1], m[0][3], m[1][3]);
}
void StackingContext::paint(PaintContext& context) const
{
Gfx::PainterStateSaver saver(context.painter());
if (m_box.is_fixed_position()) {
context.painter().translate(context.scroll_offset());
}
auto opacity = m_box.computed_values().opacity();
if (opacity == 0.0f)
return;
auto affine_transform = affine_transform_matrix();
if (opacity < 1.0f || !affine_transform.is_identity()) {
auto transform_origin = this->transform_origin();
auto source_rect = paintable().absolute_paint_rect().translated(-transform_origin);
auto transformed_destination_rect = affine_transform.map(source_rect).translated(transform_origin);
auto destination_rect = transformed_destination_rect.to_rounded<int>();
// FIXME: We should find a way to scale the paintable, rather than paint into a separate bitmap,
// then scale it. This snippet now copies the background at the destination, then scales it down/up
// to the size of the source (which could add some artefacts, though just scaling the bitmap already does that).
// We need to copy the background at the destination because a bunch of our rendering effects now rely on
// being able to sample the painter (see border radii, shadows, filters, etc).
auto try_get_scaled_destination_bitmap = [&]() -> ErrorOr<NonnullRefPtr<Gfx::Bitmap>> {
auto bitmap = TRY(context.painter().get_region_bitmap(destination_rect, Gfx::BitmapFormat::BGRA8888, destination_rect));
if (source_rect.size() != transformed_destination_rect.size()) {
bitmap = TRY(bitmap->scaled(
static_cast<float>(source_rect.width()) / transformed_destination_rect.width(),
static_cast<float>(source_rect.height()) / transformed_destination_rect.height()));
}
return bitmap;
};
auto bitmap_or_error = try_get_scaled_destination_bitmap();
if (bitmap_or_error.is_error())
return;
auto bitmap = bitmap_or_error.release_value_but_fixme_should_propagate_errors();
Gfx::Painter painter(bitmap);
painter.translate(-paintable().absolute_paint_rect().location().to_rounded<int>());
auto paint_context = context.clone(painter);
paint_internal(paint_context);
// NOTE: If the destination and source rects are the same size, we round the source rect to ensure that it's pixel-aligned.
if (transformed_destination_rect.size() == source_rect.size())
context.painter().draw_scaled_bitmap(destination_rect, *bitmap, bitmap->rect(), opacity);
else
context.painter().draw_scaled_bitmap(destination_rect, *bitmap, bitmap->rect(), opacity, Gfx::Painter::ScalingMode::BilinearBlend);
} else {
paint_internal(context);
}
}
Gfx::FloatPoint StackingContext::compute_transform_origin() const
{
auto style_value = m_box.computed_values().transform_origin();
// FIXME: respect transform-box property
auto reference_box = paintable().absolute_border_box_rect();
auto x = reference_box.left() + style_value.x.resolved(m_box, CSS::Length::make_px(reference_box.width())).to_px(m_box);
auto y = reference_box.top() + style_value.y.resolved(m_box, CSS::Length::make_px(reference_box.height())).to_px(m_box);
return { x, y };
}
Optional<HitTestResult> StackingContext::hit_test(Gfx::FloatPoint const& position, HitTestType type) const
{
if (!m_box.is_visible())
return {};
if (m_box.computed_values().z_index().value_or(0) < 0)
return {};
auto transform_origin = this->transform_origin();
auto transformed_position = affine_transform_matrix().inverse().value_or({}).map(position - transform_origin) + transform_origin;
// FIXME: Support more overflow variations.
if (paintable().computed_values().overflow_x() == CSS::Overflow::Hidden && paintable().computed_values().overflow_y() == CSS::Overflow::Hidden) {
if (!paintable().absolute_border_box_rect().contains(transformed_position.x(), transformed_position.y()))
return {};
}
// NOTE: Hit testing basically happens in reverse painting order.
// https://www.w3.org/TR/CSS22/visuren.html#z-index
// 7. the child stacking contexts with positive stack levels (least positive first).
for (ssize_t i = m_children.size() - 1; i >= 0; --i) {
auto const& child = *m_children[i];
auto result = child.hit_test(transformed_position, type);
if (result.has_value())
return result;
}
Optional<HitTestResult> result;
// 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
paintable().for_each_in_subtree_of_type<PaintableBox>([&](auto& paint_box) {
// FIXME: Support more overflow variations.
if (paint_box.computed_values().overflow_x() == CSS::Overflow::Hidden && paint_box.computed_values().overflow_y() == CSS::Overflow::Hidden) {
if (!paint_box.absolute_border_box_rect().contains(transformed_position.x(), transformed_position.y()))
return TraversalDecision::SkipChildrenAndContinue;
}
auto& layout_box = paint_box.layout_box();
if (layout_box.is_positioned() && !paint_box.stacking_context()) {
if (auto candidate = paint_box.hit_test(transformed_position, type); candidate.has_value())
result = move(candidate);
}
return TraversalDecision::Continue;
});
if (result.has_value())
return result;
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
if (m_box.children_are_inline() && is<Layout::BlockContainer>(m_box)) {
auto result = paintable().hit_test(transformed_position, type);
if (result.has_value())
return result;
}
// 4. the non-positioned floats.
paintable().for_each_in_subtree_of_type<PaintableBox>([&](auto const& paint_box) {
// FIXME: Support more overflow variations.
if (paint_box.computed_values().overflow_x() == CSS::Overflow::Hidden && paint_box.computed_values().overflow_y() == CSS::Overflow::Hidden) {
if (!paint_box.absolute_border_box_rect().contains(transformed_position.x(), transformed_position.y()))
return TraversalDecision::SkipChildrenAndContinue;
}
auto& layout_box = paint_box.layout_box();
if (layout_box.is_floating()) {
if (auto candidate = paint_box.hit_test(transformed_position, type); candidate.has_value())
result = move(candidate);
}
return TraversalDecision::Continue;
});
if (result.has_value())
return result;
// 3. the in-flow, non-inline-level, non-positioned descendants.
if (!m_box.children_are_inline()) {
paintable().for_each_in_subtree_of_type<PaintableBox>([&](auto const& paint_box) {
// FIXME: Support more overflow variations.
if (paint_box.computed_values().overflow_x() == CSS::Overflow::Hidden && paint_box.computed_values().overflow_y() == CSS::Overflow::Hidden) {
if (!paint_box.absolute_border_box_rect().contains(transformed_position.x(), transformed_position.y()))
return TraversalDecision::SkipChildrenAndContinue;
}
auto& layout_box = paint_box.layout_box();
if (!layout_box.is_absolutely_positioned() && !layout_box.is_floating()) {
if (auto candidate = paint_box.hit_test(transformed_position, type); candidate.has_value())
result = move(candidate);
}
return TraversalDecision::Continue;
});
if (result.has_value())
return result;
}
// 2. the child stacking contexts with negative stack levels (most negative first).
for (ssize_t i = m_children.size() - 1; i >= 0; --i) {
auto const& child = *m_children[i];
auto result = child.hit_test(transformed_position, type);
if (result.has_value())
return result;
}
// 1. the background and borders of the element forming the stacking context.
if (paintable().absolute_border_box_rect().contains(transformed_position)) {
return HitTestResult {
.paintable = paintable(),
};
}
return {};
}
void StackingContext::dump(int indent) const
{
StringBuilder builder;
for (int i = 0; i < indent; ++i)
builder.append(' ');
builder.appendff("SC for {} {} [children: {}] (z-index: ", m_box.debug_description(), paintable().absolute_rect(), m_children.size());
if (m_box.computed_values().z_index().has_value())
builder.appendff("{}", m_box.computed_values().z_index().value());
else
builder.append("auto"sv);
builder.append(')');
auto affine_transform = affine_transform_matrix();
if (!affine_transform.is_identity()) {
builder.appendff(", transform: {}", affine_transform);
}
dbgln("{}", builder.string_view());
for (auto& child : m_children)
child->dump(indent + 1);
}
}