Using the new perspective_matrix() helper resolves a FIXME in AccumulatedVisualContext as well.
844 lines
37 KiB
C++
844 lines
37 KiB
C++
/*
|
||
* Copyright (c) 2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||
* Copyright (c) 2026, Jelle Raaijmakers <jelle@ladybird.org>
|
||
*
|
||
* SPDX-License-Identifier: BSD-2-Clause
|
||
*/
|
||
|
||
#include <AK/Atomic.h>
|
||
#include <AK/StringBuilder.h>
|
||
#include <LibGfx/Matrix4x4.h>
|
||
#include <LibIPC/Decoder.h>
|
||
#include <LibIPC/Encoder.h>
|
||
#include <LibWeb/CSS/ComputedValues.h>
|
||
#include <LibWeb/CSS/StyleValues/TransformationStyleValue.h>
|
||
#include <LibWeb/CSS/VisualViewport.h>
|
||
#include <LibWeb/DOM/Document.h>
|
||
#include <LibWeb/HTML/HTMLHtmlElement.h>
|
||
#include <LibWeb/Page/Page.h>
|
||
#include <LibWeb/Painting/AccumulatedVisualContext.h>
|
||
#include <LibWeb/Painting/Blending.h>
|
||
#include <LibWeb/Painting/DevicePixelConverter.h>
|
||
#include <LibWeb/Painting/Paintable.h>
|
||
#include <LibWeb/Painting/PaintableBox.h>
|
||
#include <LibWeb/Painting/ResolvedCSSFilter.h>
|
||
#include <LibWeb/Painting/ScrollState.h>
|
||
#include <LibWeb/Painting/ViewportPaintable.h>
|
||
|
||
namespace Web::Painting {
|
||
|
||
AccumulatedVisualContextTree build_accumulated_visual_context_tree(ViewportPaintable&);
|
||
void update_visual_viewport_accumulated_visual_context(ViewportPaintable&);
|
||
|
||
bool ClipData::contains(DevicePixelPoint point) const
|
||
{
|
||
return corner_radii.contains(point.to_type<int>(), rect.to_type<int>());
|
||
}
|
||
|
||
static Atomic<u64> s_next_accumulated_visual_context_tree_version { 1 };
|
||
|
||
static TransformData identity_visual_viewport_transform()
|
||
{
|
||
return { Gfx::FloatMatrix4x4::identity(), { 0.f, 0.f } };
|
||
}
|
||
|
||
AccumulatedVisualContextTree AccumulatedVisualContextTree::create()
|
||
{
|
||
return create(identity_visual_viewport_transform());
|
||
}
|
||
|
||
AccumulatedVisualContextTree AccumulatedVisualContextTree::create(TransformData visual_viewport_transform)
|
||
{
|
||
Vector<AccumulatedVisualContextNode> nodes;
|
||
// Visual viewport transform root. This is identity for trees that are not attached to a document viewport.
|
||
nodes.append({ move(visual_viewport_transform), {}, 0, false });
|
||
return AccumulatedVisualContextTree {
|
||
s_next_accumulated_visual_context_tree_version.fetch_add(1, AK::MemoryOrder::memory_order_relaxed),
|
||
move(nodes)
|
||
};
|
||
}
|
||
|
||
static CSSPixelRect effective_css_clip_rect(CSSPixelRect const& css_clip)
|
||
{
|
||
if (css_clip.width() < 0 || css_clip.height() < 0)
|
||
return CSSPixelRect { 0, 0, 0, 0 };
|
||
return css_clip;
|
||
}
|
||
|
||
// Converts a CSS-pixel-space 4x4 matrix to device-pixel-space.
|
||
// - Translation column (column 3, rows 0-2) is scaled up by DPR
|
||
// - Perspective row (row 3, columns 0-2) is scaled down by DPR
|
||
// - All other elements are unaffected (the scale factors cancel out)
|
||
static FloatMatrix4x4 scale_matrix_for_device_pixels(FloatMatrix4x4 matrix, float scale)
|
||
{
|
||
matrix[0, 3] *= scale;
|
||
matrix[1, 3] *= scale;
|
||
matrix[2, 3] *= scale;
|
||
matrix[3, 0] /= scale;
|
||
matrix[3, 1] /= scale;
|
||
matrix[3, 2] /= scale;
|
||
return matrix;
|
||
}
|
||
|
||
static TransformData visual_viewport_transform_data(DOM::Document& document)
|
||
{
|
||
auto scale = static_cast<float>(document.page().client().device_pixels_per_css_pixel());
|
||
auto matrix = scale_matrix_for_device_pixels(document.visual_viewport()->transform().to_matrix(), scale);
|
||
return TransformData { matrix, { 0.f, 0.f } };
|
||
}
|
||
|
||
static Optional<TransformData> compute_transform(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values, double pixel_ratio)
|
||
{
|
||
if (!paintable_box.has_css_transform())
|
||
return {};
|
||
|
||
auto reference_box = paintable_box.transform_reference_box();
|
||
auto const& css_transform_origin = computed_values.transform_origin();
|
||
auto origin_x = css_transform_origin.x.to_px(paintable_box.layout_node(), reference_box.width());
|
||
auto origin_y = css_transform_origin.y.to_px(paintable_box.layout_node(), reference_box.height());
|
||
auto origin_z = css_transform_origin.z.to_px(paintable_box.layout_node(), 0).to_float();
|
||
|
||
auto matrix = Gfx::translation_matrix(Vector3 { 0.f, 0.f, origin_z });
|
||
if (auto const& translate = computed_values.translate())
|
||
matrix = matrix * translate->to_matrix(paintable_box);
|
||
if (auto const& rotate = computed_values.rotate())
|
||
matrix = matrix * rotate->to_matrix(paintable_box);
|
||
if (auto const& scale = computed_values.scale())
|
||
matrix = matrix * scale->to_matrix(paintable_box);
|
||
for (auto const& transform : computed_values.transformations())
|
||
matrix = matrix * transform->to_matrix(paintable_box);
|
||
matrix = matrix * Gfx::translation_matrix(Vector3 { 0.f, 0.f, -origin_z });
|
||
|
||
auto origin = reference_box.location() + CSSPixelPoint { origin_x, origin_y };
|
||
auto scale = static_cast<float>(pixel_ratio);
|
||
auto device_origin = origin.to_type<float>() * scale;
|
||
return TransformData { scale_matrix_for_device_pixels(matrix, scale), device_origin };
|
||
}
|
||
|
||
// https://drafts.csswg.org/css-transforms-2/#perspective-matrix
|
||
static Optional<Gfx::FloatMatrix4x4> compute_perspective_matrix(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values)
|
||
{
|
||
auto perspective = computed_values.perspective();
|
||
if (!perspective.has_value())
|
||
return {};
|
||
|
||
// The perspective matrix is computed as follows:
|
||
|
||
// 1. Start with the identity matrix.
|
||
// 2. Translate by the computed X and Y values of 'perspective-origin'
|
||
// https://drafts.csswg.org/css-transforms-2/#perspective-origin-property
|
||
// Percentages: refer to the size of the reference box
|
||
auto reference_box = paintable_box.transform_reference_box();
|
||
auto perspective_origin = computed_values.perspective_origin().resolved(paintable_box.layout_node(), reference_box);
|
||
auto computed_x = perspective_origin.x().to_float();
|
||
auto computed_y = perspective_origin.y().to_float();
|
||
auto perspective_matrix = Gfx::translation_matrix(Vector3<float>(computed_x, computed_y, 0));
|
||
|
||
// 3. Multiply by the matrix that would be obtained from the 'perspective()' transform function, where the
|
||
// length is provided by the value of the perspective property
|
||
// https://drafts.csswg.org/css-transforms-2/#funcdef-perspective
|
||
// If the depth value is less than '1px', it must be treated as '1px' for the purpose of rendering, [..]
|
||
auto distance = max(perspective->to_float(), 1.f);
|
||
perspective_matrix = perspective_matrix * Gfx::perspective_matrix(distance);
|
||
|
||
// 4. Translate by the negated computed X and Y values of 'perspective-origin'
|
||
perspective_matrix = perspective_matrix * Gfx::translation_matrix(Vector3<float>(-computed_x, -computed_y, 0));
|
||
return perspective_matrix;
|
||
}
|
||
|
||
static Optional<ClipData> compute_clip_data(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values, DevicePixelConverter const& converter)
|
||
{
|
||
auto overflow_x = computed_values.overflow_x();
|
||
auto overflow_y = computed_values.overflow_y();
|
||
|
||
// https://drafts.csswg.org/css-contain-2/#paint-containment
|
||
// 1. The contents of the element including any ink or scrollable overflow must be clipped to the overflow clip
|
||
// edge of the paint containment box, taking corner clipping into account. This does not include the creation of
|
||
// any mechanism to access or indicate the presence of the clipped content; nor does it inhibit the creation of
|
||
// any such mechanism through other properties, such as overflow, resize, or text-overflow.
|
||
// NOTE: This clipping shape respects overflow-clip-margin, allowing an element with paint containment
|
||
// to still slightly overflow its normal bounds.
|
||
if (paintable_box.layout_node().has_paint_containment()) {
|
||
// NOTE: Note: The behavior is described in this paragraph is equivalent to changing 'overflow-x: visible' into
|
||
// 'overflow-x: clip' and 'overflow-y: visible' into 'overflow-y: clip' at used value time, while leaving other
|
||
// values of 'overflow-x' and 'overflow-y' unchanged.
|
||
overflow_x = CSS::Overflow::Clip;
|
||
overflow_y = CSS::Overflow::Clip;
|
||
}
|
||
|
||
auto has_hidden_overflow = overflow_x != CSS::Overflow::Visible || overflow_y != CSS::Overflow::Visible;
|
||
|
||
if (has_hidden_overflow && paintable_box.overflow_property_applies()) {
|
||
auto clip_rect = paintable_box.absolute_padding_box_rect();
|
||
|
||
// https://drafts.csswg.org/css-overflow-3/#propdef-overflow
|
||
// 'clip'
|
||
// This value indicates that the box’s content is clipped to its overflow clip edge
|
||
auto overflow_clip_edge = paintable_box.overflow_clip_edge_rect();
|
||
if (overflow_x == CSS::Overflow::Visible) {
|
||
clip_rect.set_left(0);
|
||
clip_rect.set_right(CSSPixels::max_integer_value);
|
||
} else if (overflow_x == CSS::Overflow::Clip) {
|
||
clip_rect.set_left(overflow_clip_edge.left());
|
||
clip_rect.set_right(overflow_clip_edge.right());
|
||
}
|
||
if (overflow_y == CSS::Overflow::Visible) {
|
||
clip_rect.set_top(0);
|
||
clip_rect.set_bottom(CSSPixels::max_integer_value);
|
||
} else if (overflow_y == CSS::Overflow::Clip) {
|
||
clip_rect.set_top(overflow_clip_edge.top());
|
||
clip_rect.set_bottom(overflow_clip_edge.bottom());
|
||
}
|
||
|
||
// https://drafts.csswg.org/css-overflow-3/#corner-clipping
|
||
// As mentioned in CSS Backgrounds 3 § 4.3 Corner Clipping, the clipping region established by 'overflow' can be
|
||
// rounded:
|
||
// - When 'overflow-x' and 'overflow-y' compute to 'hidden', 'scroll', or 'auto', the clipping region is rounded
|
||
// based on the border radius, adjusted to the padding edge, as described in CSS Backgrounds 3 § 4.2 Corner
|
||
// Shaping.
|
||
// - When both 'overflow-x' and 'overflow-y' compute to 'clip', the clipping region is rounded as described in § 3.2
|
||
// Expanding Clipping Bounds: the 'overflow-clip-margin' property.
|
||
// - However, when one of 'overflow-x' or 'overflow-y' computes to 'clip' and the other computes to 'visible', the
|
||
// clipping region is not rounded.
|
||
// FIXME: Adjust the border radii for the overflow-clip-margin case. (see https://drafts.csswg.org/css-overflow-4/#valdef-overflow-clip-margin-length-0 )
|
||
auto radii = (overflow_x != CSS::Overflow::Visible && overflow_y != CSS::Overflow::Visible) ? paintable_box.normalized_border_radii_data(PaintableBox::ShrinkRadiiForBorders::Yes) : BorderRadiiData {};
|
||
return ClipData { converter.rounded_device_rect(clip_rect), radii.as_corners(converter) };
|
||
}
|
||
|
||
return {};
|
||
}
|
||
|
||
static Optional<ClipData> compute_css_clip_data(PaintableBox const& paintable_box, DevicePixelConverter const& converter)
|
||
{
|
||
if (auto css_clip = paintable_box.get_clip_rect(); css_clip.has_value()) {
|
||
auto effective_rect = effective_css_clip_rect(*css_clip);
|
||
return ClipData { converter.rounded_device_rect(effective_rect), {} };
|
||
}
|
||
return {};
|
||
}
|
||
|
||
static Optional<ClipPathData> compute_basic_shape_clip_path_data(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values, DevicePixelConverter const& converter, float scale)
|
||
{
|
||
// FIXME: Support other geometry boxes. See: https://drafts.fxtf.org/css-masking/#typedef-geometry-box
|
||
auto const& clip_path = computed_values.clip_path();
|
||
if (!clip_path.has_value() || !clip_path->is_basic_shape())
|
||
return {};
|
||
|
||
auto masking_area = paintable_box.absolute_border_box_rect();
|
||
auto reference_box = CSSPixelRect { {}, masking_area.size() };
|
||
auto const& basic_shape = clip_path->basic_shape();
|
||
auto path = basic_shape.to_path(reference_box, paintable_box.layout_node());
|
||
path.offset(masking_area.top_left().template to_type<float>());
|
||
auto fill_rule = basic_shape.basic_shape().visit(
|
||
[](CSS::Polygon const& polygon) { return polygon.fill_rule; },
|
||
[](CSS::Path const& path) { return path.fill_rule; },
|
||
[](auto const&) { return Gfx::WindingRule::Nonzero; });
|
||
auto device_path = path.copy_transformed(Gfx::AffineTransform {}.set_scale(scale, scale));
|
||
auto device_bounding_rect = converter.rounded_device_rect(masking_area);
|
||
return ClipPathData { move(device_path), device_bounding_rect, fill_rule };
|
||
}
|
||
|
||
AccumulatedVisualContextTree build_accumulated_visual_context_tree(ViewportPaintable& viewport_paintable)
|
||
{
|
||
auto& document = viewport_paintable.document();
|
||
auto visual_context_tree = AccumulatedVisualContextTree::create(visual_viewport_transform_data(document));
|
||
auto pixel_ratio = document.page().client().device_pixels_per_css_pixel();
|
||
DevicePixelConverter converter { pixel_ratio };
|
||
auto scale = static_cast<float>(pixel_ratio);
|
||
|
||
auto append_node = [&](VisualContextIndex parent_index, VisualContextData data) -> VisualContextIndex {
|
||
return visual_context_tree.append(move(data), parent_index);
|
||
};
|
||
|
||
auto make_effects_data = [&](PaintableBox const& box) -> Optional<EffectsData> {
|
||
auto const& computed_values = box.computed_values();
|
||
auto gfx_filter = to_gfx_filter(box.filter(), pixel_ratio);
|
||
EffectsData effects {
|
||
computed_values.opacity(),
|
||
mix_blend_mode_to_compositing_and_blending_operator(computed_values.mix_blend_mode()),
|
||
move(gfx_filter)
|
||
};
|
||
if (!effects.needs_layer())
|
||
return {};
|
||
return effects;
|
||
};
|
||
|
||
auto visual_viewport_context_index = VISUAL_VIEWPORT_NODE_INDEX;
|
||
|
||
VisualContextIndex viewport_state_for_descendants = visual_viewport_context_index;
|
||
if (viewport_paintable.own_scroll_frame_index().value())
|
||
viewport_state_for_descendants = append_node(visual_viewport_context_index, ScrollData { viewport_paintable.own_scroll_frame_index(), false });
|
||
viewport_paintable.set_accumulated_visual_context(VISUAL_VIEWPORT_NODE_INDEX);
|
||
viewport_paintable.set_accumulated_visual_context_for_descendants(viewport_state_for_descendants);
|
||
|
||
struct DescendantVisualContexts {
|
||
VisualContextIndex normal;
|
||
VisualContextIndex absolute_position;
|
||
VisualContextIndex fixed_position;
|
||
};
|
||
|
||
auto build_paintable_box = [&](auto& self, PaintableBox& paintable_box, DescendantVisualContexts inherited_contexts) -> void {
|
||
// Resolve filters before make_effects_data reads them.
|
||
auto const& paintable_box_computed_values = paintable_box.computed_values();
|
||
if (paintable_box_computed_values.filter().has_filters())
|
||
paintable_box.set_filter(resolve_css_filter(paintable_box_computed_values.filter(), paintable_box));
|
||
else
|
||
paintable_box.set_filter({});
|
||
|
||
VisualContextIndex inherited_state;
|
||
|
||
if (paintable_box.is_fixed_position()) {
|
||
inherited_state = inherited_contexts.fixed_position;
|
||
} else if (paintable_box.is_absolutely_positioned()) {
|
||
inherited_state = inherited_contexts.absolute_position;
|
||
} else {
|
||
// In-flow and relatively positioned boxes inherit the normal descendant context from their visual parent.
|
||
inherited_state = inherited_contexts.normal;
|
||
}
|
||
|
||
// Build this element's own state from inherited state.
|
||
VisualContextIndex own_state = inherited_state;
|
||
|
||
// Out-of-flow descendants can skip overflow and scroll clips from intermediate ancestors. Keep their visual
|
||
// contexts separate as we descend, and replace them with the normal descendant context only when this box
|
||
// establishes the relevant containing block.
|
||
VisualContextIndex state_for_absolute_position_descendants = inherited_contexts.absolute_position;
|
||
VisualContextIndex state_for_fixed_position_descendants = inherited_contexts.fixed_position;
|
||
|
||
auto append_to_own_and_positioned_descendant_contexts = [&](auto const& data) {
|
||
own_state = append_node(own_state, data);
|
||
state_for_absolute_position_descendants = append_node(state_for_absolute_position_descendants, data);
|
||
state_for_fixed_position_descendants = append_node(state_for_fixed_position_descendants, data);
|
||
};
|
||
|
||
if (paintable_box.is_sticky_position()) {
|
||
// For sticky elements, use enclosing_scroll_frame which holds the sticky frame.
|
||
// own_scroll_frame may be a different scroll frame if the sticky element also has scrollable overflow.
|
||
if (auto sticky_idx = paintable_box.enclosing_scroll_frame_index(); sticky_idx.value() && viewport_paintable.scroll_state().frame_at(sticky_idx).is_sticky())
|
||
own_state = append_node(own_state, ScrollData { sticky_idx, true });
|
||
}
|
||
|
||
auto const& computed_values = paintable_box.computed_values();
|
||
|
||
if (auto effects = make_effects_data(paintable_box); effects.has_value())
|
||
append_to_own_and_positioned_descendant_contexts(effects.value());
|
||
|
||
if (auto transform_data = compute_transform(paintable_box, computed_values, pixel_ratio); transform_data.has_value()) {
|
||
paintable_box.set_has_non_invertible_css_transform(!transform_data->matrix.is_invertible());
|
||
own_state = append_node(own_state, *transform_data);
|
||
} else {
|
||
paintable_box.set_has_non_invertible_css_transform(false);
|
||
}
|
||
|
||
if (auto css_clip = compute_css_clip_data(paintable_box, converter); css_clip.has_value())
|
||
append_to_own_and_positioned_descendant_contexts(css_clip.value());
|
||
|
||
if (auto clip_path_data = compute_basic_shape_clip_path_data(paintable_box, computed_values, converter, scale); clip_path_data.has_value())
|
||
append_to_own_and_positioned_descendant_contexts(clip_path_data.value());
|
||
|
||
paintable_box.set_accumulated_visual_context(own_state);
|
||
|
||
Vector<CSS::BackgroundLayerData> const* background_layers = &computed_values.background_layers();
|
||
if (paintable_box.layout_node_with_style_and_box_metrics().is_root_element()) {
|
||
if (auto* html_element = as_if<HTML::HTMLHtmlElement>(paintable_box.dom_node().ptr())) {
|
||
if (html_element->should_use_body_background_properties())
|
||
background_layers = paintable_box.document().background_layers();
|
||
}
|
||
}
|
||
|
||
if (background_layers) {
|
||
bool has_fixed_background = false;
|
||
for (auto const& layer : *background_layers) {
|
||
if (layer.attachment == CSS::BackgroundAttachment::Fixed) {
|
||
has_fixed_background = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (has_fixed_background) {
|
||
// https://drafts.csswg.org/css-transforms-1/#transform-rendering
|
||
// For elements that are effected by a transform (i.e. have a transform applied to them, or to any of
|
||
// their ancestor elements) and do not have their background propagated to the canvas, a value of fixed
|
||
// for the background-attachment property is treated as if it had a value of scroll.
|
||
auto has_transform_ancestor = false;
|
||
if (!paintable_box.layout_node_with_style_and_box_metrics().is_root_element()) {
|
||
for (auto const* node = &paintable_box.layout_node(); node && !node->is_viewport(); node = node->parent()) {
|
||
if (node->has_css_transform()) {
|
||
has_transform_ancestor = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!has_transform_ancestor) {
|
||
// Build a context that negates all scroll frames in the ancestor chain. This keeps the background
|
||
// fixed relative to the viewport.
|
||
auto fixed_background_context = own_state;
|
||
for (auto index = own_state; index.value(); index = visual_context_tree.node_at(index).parent_index) {
|
||
auto const& node = visual_context_tree.node_at(index);
|
||
if (auto const* scroll = node.data.get_pointer<ScrollData>()) {
|
||
fixed_background_context = append_node(fixed_background_context, ScrollCompensation { scroll->scroll_frame_index });
|
||
}
|
||
}
|
||
paintable_box.set_fixed_background_visual_context(fixed_background_context);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Build state for descendants: own state + perspective + clip + scroll.
|
||
VisualContextIndex state_for_descendants = own_state;
|
||
|
||
if (auto perspective_matrix = compute_perspective_matrix(paintable_box, computed_values); perspective_matrix.has_value()) {
|
||
auto scaled_matrix = scale_matrix_for_device_pixels(*perspective_matrix, scale);
|
||
state_for_descendants = append_node(state_for_descendants, PerspectiveData { scaled_matrix });
|
||
}
|
||
|
||
if (auto clip_data = compute_clip_data(paintable_box, computed_values, converter); clip_data.has_value())
|
||
state_for_descendants = append_node(state_for_descendants, clip_data.value());
|
||
|
||
if (paintable_box.own_scroll_frame_index().value()) {
|
||
auto is_sticky_without_scrollable_overflow = paintable_box.is_sticky_position() && paintable_box.enclosing_scroll_frame_index() == paintable_box.own_scroll_frame_index();
|
||
if (!is_sticky_without_scrollable_overflow)
|
||
state_for_descendants = append_node(state_for_descendants, ScrollData { paintable_box.own_scroll_frame_index(), false });
|
||
}
|
||
|
||
paintable_box.set_accumulated_visual_context_for_descendants(state_for_descendants);
|
||
if (paintable_box.layout_node().establishes_an_absolute_positioning_containing_block())
|
||
state_for_absolute_position_descendants = state_for_descendants;
|
||
if (paintable_box.layout_node().establishes_a_fixed_positioning_containing_block())
|
||
state_for_fixed_position_descendants = state_for_descendants;
|
||
|
||
DescendantVisualContexts child_contexts {
|
||
state_for_descendants,
|
||
state_for_absolute_position_descendants,
|
||
state_for_fixed_position_descendants,
|
||
};
|
||
paintable_box.for_each_child_of_type<PaintableBox>([&](PaintableBox& child) {
|
||
self(self, child, child_contexts);
|
||
return IterationDecision::Continue;
|
||
});
|
||
};
|
||
|
||
DescendantVisualContexts viewport_contexts {
|
||
viewport_state_for_descendants,
|
||
viewport_state_for_descendants,
|
||
visual_viewport_context_index,
|
||
};
|
||
viewport_paintable.for_each_child_of_type<PaintableBox>([&](PaintableBox& child) {
|
||
build_paintable_box(build_paintable_box, child, viewport_contexts);
|
||
return IterationDecision::Continue;
|
||
});
|
||
|
||
return visual_context_tree;
|
||
}
|
||
|
||
void update_visual_viewport_accumulated_visual_context(ViewportPaintable& viewport_paintable)
|
||
{
|
||
viewport_paintable.visual_context_tree().set_visual_viewport_transform(visual_viewport_transform_data(viewport_paintable.document()));
|
||
}
|
||
|
||
VisualContextIndex AccumulatedVisualContextTree::append(VisualContextData data, VisualContextIndex parent_index)
|
||
{
|
||
VERIFY(parent_index.value() < m_nodes.size());
|
||
size_t depth = m_nodes[parent_index.value()].depth + 1;
|
||
|
||
bool empty_clip = false;
|
||
if (m_nodes[parent_index.value()].has_empty_effective_clip) {
|
||
empty_clip = true;
|
||
} else if (data.has<ClipData>()) {
|
||
empty_clip = data.get<ClipData>().rect.is_empty();
|
||
} else if (data.has<ClipPathData>()) {
|
||
empty_clip = data.get<ClipPathData>().path.bounding_box().is_empty();
|
||
}
|
||
|
||
auto index = VisualContextIndex(m_nodes.size());
|
||
m_nodes.append({ move(data), parent_index, depth, empty_clip });
|
||
return index;
|
||
}
|
||
|
||
void AccumulatedVisualContextTree::set_visual_viewport_transform(TransformData transform)
|
||
{
|
||
VERIFY(!m_nodes.is_empty());
|
||
VERIFY(m_nodes[VISUAL_VIEWPORT_NODE_INDEX.value()].data.has<TransformData>());
|
||
m_nodes[VISUAL_VIEWPORT_NODE_INDEX.value()].data = move(transform);
|
||
}
|
||
|
||
VisualContextIndex AccumulatedVisualContextTree::find_common_ancestor(VisualContextIndex a, VisualContextIndex b) const
|
||
{
|
||
VERIFY(a.value() < m_nodes.size());
|
||
VERIFY(b.value() < m_nodes.size());
|
||
size_t a_index = a.value();
|
||
size_t b_index = b.value();
|
||
while (m_nodes[a_index].depth > m_nodes[b_index].depth)
|
||
a_index = m_nodes[a_index].parent_index.value();
|
||
while (m_nodes[b_index].depth > m_nodes[a_index].depth)
|
||
b_index = m_nodes[b_index].parent_index.value();
|
||
while (a_index != b_index) {
|
||
a_index = m_nodes[a_index].parent_index.value();
|
||
b_index = m_nodes[b_index].parent_index.value();
|
||
}
|
||
return a_index;
|
||
}
|
||
|
||
Vector<size_t, 8> AccumulatedVisualContextTree::build_ancestor_chain(VisualContextIndex index) const
|
||
{
|
||
VERIFY(index.value() < m_nodes.size());
|
||
auto const& node = m_nodes[index.value()];
|
||
Vector<size_t, 8> chain;
|
||
chain.ensure_capacity(node.depth + 1);
|
||
for (size_t i = index.value();; i = m_nodes[i].parent_index.value()) {
|
||
chain.append(i);
|
||
if (i == VISUAL_VIEWPORT_NODE_INDEX.value())
|
||
break;
|
||
}
|
||
return chain;
|
||
}
|
||
|
||
Optional<Gfx::FloatPoint> AccumulatedVisualContextTree::transform_point_for_hit_test(VisualContextIndex index, Gfx::FloatPoint screen_point, ScrollStateSnapshot const& scroll_state) const
|
||
{
|
||
auto chain = build_ancestor_chain(index);
|
||
|
||
auto point = screen_point;
|
||
for (size_t i = chain.size(); i > 0; --i) {
|
||
auto const& node = m_nodes[chain[i - 1]];
|
||
|
||
auto result = node.data.visit(
|
||
[&](PerspectiveData const& perspective) -> Optional<Gfx::FloatPoint> {
|
||
auto affine = Gfx::extract_2d_affine_transform(perspective.matrix);
|
||
auto inverse = affine.inverse();
|
||
if (!inverse.has_value())
|
||
return {};
|
||
point = inverse->map(point);
|
||
return point;
|
||
},
|
||
[&](ScrollData const& scroll) -> Optional<Gfx::FloatPoint> {
|
||
point.translate_by(-scroll_state.device_offset_for_index(scroll.scroll_frame_index));
|
||
return point;
|
||
},
|
||
[&](TransformData const& transform) -> Optional<Gfx::FloatPoint> {
|
||
auto affine = Gfx::extract_2d_affine_transform(transform.matrix);
|
||
auto inverse = affine.inverse();
|
||
if (!inverse.has_value())
|
||
return {};
|
||
|
||
auto offset_point = point - transform.origin;
|
||
auto transformed = inverse->map(offset_point);
|
||
point = transformed + transform.origin;
|
||
return point;
|
||
},
|
||
[&](ClipData const& clip) -> Optional<Gfx::FloatPoint> {
|
||
// NOTE: The clip rect is in absolute device-pixel coordinates. After inverse-transforming, `point`
|
||
// is also in device-pixel coordinates, so we compare them directly.
|
||
if (!clip.contains(point.to_type<int>().to_type<DevicePixels>()))
|
||
return {};
|
||
return point;
|
||
},
|
||
[&](ClipPathData const& clip_path) -> Optional<Gfx::FloatPoint> {
|
||
// NOTE: The clip path is in absolute device-pixel coordinates. After inverse-transforming, `point`
|
||
// is also in device-pixel coordinates, so we compare them directly.
|
||
if (!clip_path.bounding_rect.contains(point.to_type<int>().to_type<DevicePixels>()))
|
||
return {};
|
||
if (!clip_path.path.contains(point, clip_path.fill_rule))
|
||
return {};
|
||
return point;
|
||
},
|
||
[&](EffectsData const&) -> Optional<Gfx::FloatPoint> {
|
||
// Effects don't affect coordinate transforms
|
||
return point;
|
||
},
|
||
[&](ScrollCompensation const& compensation) -> Optional<Gfx::FloatPoint> {
|
||
point.translate_by(scroll_state.device_offset_for_index(compensation.scroll_frame_index));
|
||
return point;
|
||
});
|
||
|
||
if (!result.has_value())
|
||
return {};
|
||
}
|
||
|
||
return point;
|
||
}
|
||
|
||
Gfx::FloatPoint AccumulatedVisualContextTree::inverse_transform_point(VisualContextIndex index, Gfx::FloatPoint screen_point) const
|
||
{
|
||
auto chain = build_ancestor_chain(index);
|
||
|
||
auto point = screen_point;
|
||
for (size_t i = chain.size(); i > 0; --i) {
|
||
auto const& node = m_nodes[chain[i - 1]];
|
||
|
||
node.data.visit(
|
||
[&](PerspectiveData const& perspective) {
|
||
auto affine = Gfx::extract_2d_affine_transform(perspective.matrix);
|
||
auto inverse = affine.inverse();
|
||
if (inverse.has_value())
|
||
point = inverse->map(point);
|
||
},
|
||
[&](TransformData const& transform) {
|
||
auto affine = Gfx::extract_2d_affine_transform(transform.matrix);
|
||
auto inverse = affine.inverse();
|
||
if (inverse.has_value()) {
|
||
auto offset_point = point - transform.origin;
|
||
auto transformed = inverse->map(offset_point);
|
||
point = transformed + transform.origin;
|
||
}
|
||
},
|
||
[&](auto const&) {});
|
||
}
|
||
|
||
return point;
|
||
}
|
||
|
||
Gfx::FloatRect AccumulatedVisualContextTree::transform_rect_to_viewport(VisualContextIndex index, Gfx::FloatRect const& source_rect, ScrollStateSnapshot const& scroll_state) const
|
||
{
|
||
auto rect = source_rect;
|
||
for (size_t i = index.value();; i = m_nodes[i].parent_index.value()) {
|
||
auto const& node = m_nodes[i];
|
||
node.data.visit(
|
||
[&](TransformData const& transform) {
|
||
auto affine = Gfx::extract_2d_affine_transform(transform.matrix);
|
||
rect.translate_by(-transform.origin);
|
||
rect = affine.map(rect);
|
||
rect.translate_by(transform.origin);
|
||
},
|
||
[&](PerspectiveData const& perspective) {
|
||
auto affine = Gfx::extract_2d_affine_transform(perspective.matrix);
|
||
rect = affine.map(rect);
|
||
},
|
||
[&](ScrollData const& scroll) {
|
||
rect.translate_by(scroll_state.device_offset_for_index(scroll.scroll_frame_index));
|
||
},
|
||
[&](ScrollCompensation const& compensation) {
|
||
auto offset = scroll_state.device_offset_for_index(compensation.scroll_frame_index);
|
||
rect.translate_by(-offset);
|
||
},
|
||
[&](ClipData const&) { /* clips don't affect rect coordinates */ },
|
||
[&](ClipPathData const&) { /* clip paths don't affect rect coordinates */ },
|
||
[&](EffectsData const&) { /* effects don't affect rect coordinates */ });
|
||
if (i == VISUAL_VIEWPORT_NODE_INDEX.value())
|
||
break;
|
||
}
|
||
|
||
return rect;
|
||
}
|
||
|
||
void AccumulatedVisualContextTree::dump(VisualContextIndex index, StringBuilder& builder) const
|
||
{
|
||
auto const& node = m_nodes[index.value()];
|
||
node.data.visit(
|
||
[&](PerspectiveData const&) {
|
||
builder.append("perspective"sv);
|
||
},
|
||
[&](ScrollData const& scroll) {
|
||
builder.appendff("scroll_frame_id={}", scroll.scroll_frame_index);
|
||
if (scroll.is_sticky)
|
||
builder.append(" (sticky)"sv);
|
||
},
|
||
[&](TransformData const& transform) {
|
||
auto const& matrix = transform.matrix.elements();
|
||
auto const& origin = transform.origin;
|
||
builder.appendff("transform=[{},{},{},{},{},{}] origin=({},{})", matrix[0][0], matrix[0][1], matrix[1][0], matrix[1][1], matrix[0][3], matrix[1][3], origin.x(), origin.y());
|
||
},
|
||
[&](ClipData const& clip) {
|
||
auto const& rect = clip.rect;
|
||
builder.appendff("clip=[{},{} {}x{}]", rect.x(), rect.y(), rect.width(), rect.height());
|
||
|
||
if (clip.corner_radii.has_any_radius()) {
|
||
auto const& corner_radii = clip.corner_radii;
|
||
builder.appendff(" radii=({},{},{},{})", corner_radii.top_left.horizontal_radius, corner_radii.top_right.horizontal_radius, corner_radii.bottom_right.horizontal_radius, corner_radii.bottom_left.horizontal_radius);
|
||
}
|
||
},
|
||
[&](ClipPathData const& clip_path) {
|
||
auto const& rect = clip_path.bounding_rect;
|
||
builder.appendff("clip_path=[bounds: {},{} {}x{}, path: {}]", rect.x(), rect.y(), rect.width(), rect.height(), clip_path.path.to_svg_string());
|
||
},
|
||
[&](EffectsData const& effects) {
|
||
builder.append("effects=["sv);
|
||
bool has_content = false;
|
||
if (effects.opacity < 1.0f) {
|
||
builder.appendff("opacity={}", effects.opacity);
|
||
has_content = true;
|
||
}
|
||
if (effects.blend_mode != Gfx::CompositingAndBlendingOperator::Normal) {
|
||
if (has_content)
|
||
builder.append(' ');
|
||
builder.appendff("blend_mode={}", static_cast<int>(effects.blend_mode));
|
||
has_content = true;
|
||
}
|
||
if (effects.gfx_filter.has_value()) {
|
||
if (has_content)
|
||
builder.append(' ');
|
||
builder.append("filter"sv);
|
||
has_content = true;
|
||
}
|
||
builder.append("]"sv);
|
||
},
|
||
[&](ScrollCompensation const& compensation) {
|
||
builder.appendff("scroll_compensation(frame_id={})", compensation.scroll_frame_index.value());
|
||
});
|
||
}
|
||
|
||
}
|
||
|
||
namespace IPC {
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::ScrollData const& data)
|
||
{
|
||
TRY(encoder.encode(data.scroll_frame_index));
|
||
TRY(encoder.encode(data.is_sticky));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::ScrollData> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::ScrollData {
|
||
.scroll_frame_index = TRY(decoder.decode<Web::Painting::ScrollFrameIndex>()),
|
||
.is_sticky = TRY(decoder.decode<bool>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::ClipData const& data)
|
||
{
|
||
TRY(encoder.encode(data.rect));
|
||
TRY(encoder.encode(data.corner_radii));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::ClipData> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::ClipData {
|
||
TRY(decoder.decode<Web::DevicePixelRect>()),
|
||
TRY(decoder.decode<Gfx::CornerRadii>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::TransformData const& data)
|
||
{
|
||
TRY(encoder.encode(data.matrix));
|
||
TRY(encoder.encode(data.origin));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::TransformData> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::TransformData {
|
||
.matrix = TRY(decoder.decode<Gfx::FloatMatrix4x4>()),
|
||
.origin = TRY(decoder.decode<Gfx::FloatPoint>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::PerspectiveData const& data)
|
||
{
|
||
TRY(encoder.encode(data.matrix));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::PerspectiveData> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::PerspectiveData {
|
||
.matrix = TRY(decoder.decode<Gfx::FloatMatrix4x4>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::ClipPathData const& data)
|
||
{
|
||
TRY(encoder.encode(data.path));
|
||
TRY(encoder.encode(data.bounding_rect));
|
||
TRY(encoder.encode(data.fill_rule));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::ClipPathData> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::ClipPathData {
|
||
.path = TRY(decoder.decode<Gfx::Path>()),
|
||
.bounding_rect = TRY(decoder.decode<Web::DevicePixelRect>()),
|
||
.fill_rule = TRY(decoder.decode<Gfx::WindingRule>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::EffectsData const& data)
|
||
{
|
||
TRY(encoder.encode(data.opacity));
|
||
TRY(encoder.encode(data.blend_mode));
|
||
TRY(encoder.encode(data.gfx_filter));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::EffectsData> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::EffectsData {
|
||
.opacity = TRY(decoder.decode<float>()),
|
||
.blend_mode = TRY(decoder.decode<Gfx::CompositingAndBlendingOperator>()),
|
||
.gfx_filter = TRY(decoder.decode<Optional<Gfx::Filter>>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::ScrollCompensation const& data)
|
||
{
|
||
TRY(encoder.encode(data.scroll_frame_index));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::ScrollCompensation> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::ScrollCompensation {
|
||
.scroll_frame_index = TRY(decoder.decode<Web::Painting::ScrollFrameIndex>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::AccumulatedVisualContextNode const& node)
|
||
{
|
||
TRY(encoder.encode(node.data));
|
||
TRY(encoder.encode(node.parent_index));
|
||
TRY(encoder.encode(node.depth));
|
||
TRY(encoder.encode(node.has_empty_effective_clip));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::AccumulatedVisualContextNode> decode(Decoder& decoder)
|
||
{
|
||
return Web::Painting::AccumulatedVisualContextNode {
|
||
.data = TRY(decoder.decode<Web::Painting::VisualContextData>()),
|
||
.parent_index = TRY(decoder.decode<Web::Painting::VisualContextIndex>()),
|
||
.depth = TRY(decoder.decode<size_t>()),
|
||
.has_empty_effective_clip = TRY(decoder.decode<bool>()),
|
||
};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<void> encode(Encoder& encoder, Web::Painting::AccumulatedVisualContextTree const& tree)
|
||
{
|
||
TRY(encoder.encode(tree.m_version));
|
||
TRY(encoder.encode(tree.m_nodes));
|
||
return {};
|
||
}
|
||
|
||
template<>
|
||
ErrorOr<Web::Painting::AccumulatedVisualContextTree> decode(Decoder& decoder)
|
||
{
|
||
auto version = TRY(decoder.decode<u64>());
|
||
auto nodes = TRY(decoder.decode<Vector<Web::Painting::AccumulatedVisualContextNode>>());
|
||
if (nodes.is_empty())
|
||
return Error::from_string_literal("IPC decode: AccumulatedVisualContextTree missing visual viewport node");
|
||
if (!nodes[Web::Painting::VISUAL_VIEWPORT_NODE_INDEX.value()].data.has<Web::Painting::TransformData>())
|
||
return Error::from_string_literal("IPC decode: AccumulatedVisualContextTree visual viewport node is not a transform");
|
||
return Web::Painting::AccumulatedVisualContextTree { version, move(nodes) };
|
||
}
|
||
|
||
}
|