Move the inline dom_node() method to Viewport.cpp so the header no longer needs the full Document definition. Add explicit includes to files that relied on the transitive dependency.
553 lines
26 KiB
C++
553 lines
26 KiB
C++
/*
|
|
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2024-2026, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/CSS/PropertyID.h>
|
|
#include <LibWeb/CSS/VisualViewport.h>
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/DOM/Range.h>
|
|
#include <LibWeb/Layout/TextNode.h>
|
|
#include <LibWeb/Layout/Viewport.h>
|
|
#include <LibWeb/Painting/AccumulatedVisualContext.h>
|
|
#include <LibWeb/Painting/Blending.h>
|
|
#include <LibWeb/Painting/DisplayListRecorder.h>
|
|
#include <LibWeb/Painting/ScrollFrame.h>
|
|
#include <LibWeb/Painting/StackingContext.h>
|
|
#include <LibWeb/Painting/ViewportPaintable.h>
|
|
#include <LibWeb/Selection/Selection.h>
|
|
|
|
namespace Web::Painting {
|
|
|
|
GC_DEFINE_ALLOCATOR(ViewportPaintable);
|
|
|
|
GC::Ref<ViewportPaintable> ViewportPaintable::create(Layout::Viewport const& layout_viewport)
|
|
{
|
|
return layout_viewport.heap().allocate<ViewportPaintable>(layout_viewport);
|
|
}
|
|
|
|
ViewportPaintable::ViewportPaintable(Layout::Viewport const& layout_viewport)
|
|
: PaintableWithLines(layout_viewport)
|
|
{
|
|
}
|
|
|
|
ViewportPaintable::~ViewportPaintable() = default;
|
|
|
|
void ViewportPaintable::reset_for_relayout()
|
|
{
|
|
PaintableWithLines::reset_for_relayout();
|
|
m_scroll_state.clear();
|
|
m_scroll_state_snapshot = {};
|
|
m_needs_to_refresh_scroll_state = true;
|
|
m_paintable_boxes_with_auto_content_visibility.clear();
|
|
m_next_accumulated_visual_context_id = 1;
|
|
}
|
|
|
|
void ViewportPaintable::build_stacking_context_tree_if_needed()
|
|
{
|
|
if (stacking_context())
|
|
return;
|
|
build_stacking_context_tree();
|
|
}
|
|
|
|
void ViewportPaintable::build_stacking_context_tree()
|
|
{
|
|
set_stacking_context(heap().allocate<StackingContext>(*this, nullptr, 0));
|
|
|
|
size_t index_in_tree_order = 1;
|
|
for_each_in_subtree_of_type<PaintableBox>([&](auto& paintable_box) {
|
|
paintable_box.invalidate_stacking_context();
|
|
auto* parent_context = paintable_box.enclosing_stacking_context();
|
|
auto establishes_stacking_context = paintable_box.layout_node().establishes_stacking_context();
|
|
if ((paintable_box.is_positioned() || establishes_stacking_context) && paintable_box.computed_values().z_index().value_or(0) == 0)
|
|
parent_context->m_positioned_descendants_and_stacking_contexts_with_stack_level_0.append(paintable_box);
|
|
if (!paintable_box.is_positioned() && paintable_box.is_floating())
|
|
parent_context->m_non_positioned_floating_descendants.append(paintable_box);
|
|
if (!establishes_stacking_context) {
|
|
VERIFY(!paintable_box.stacking_context());
|
|
return TraversalDecision::Continue;
|
|
}
|
|
VERIFY(parent_context);
|
|
paintable_box.set_stacking_context(heap().allocate<StackingContext>(paintable_box, parent_context, index_in_tree_order++));
|
|
return TraversalDecision::Continue;
|
|
});
|
|
|
|
stacking_context()->sort();
|
|
}
|
|
|
|
void ViewportPaintable::paint_all_phases(DisplayListRecordingContext& context)
|
|
{
|
|
build_stacking_context_tree_if_needed();
|
|
context.display_list_recorder().save_layer();
|
|
stacking_context()->paint(context);
|
|
context.display_list_recorder().restore();
|
|
}
|
|
|
|
void ViewportPaintable::assign_scroll_frames()
|
|
{
|
|
auto precompute_sticky_constraints = [](ScrollFrame& sticky_frame, PaintableBox const& paintable_box) {
|
|
auto nearest_scrolling_ancestor_frame = sticky_frame.nearest_scrolling_ancestor();
|
|
if (!nearest_scrolling_ancestor_frame)
|
|
return;
|
|
|
|
auto const& scroll_ancestor_paintable = nearest_scrolling_ancestor_frame->paintable_box();
|
|
auto sticky_border_box_rect = paintable_box.absolute_border_box_rect();
|
|
auto const* containing_block_of_sticky = paintable_box.containing_block();
|
|
|
|
CSSPixelRect containing_block_region;
|
|
bool needs_parent_offset_adjustment = false;
|
|
if (containing_block_of_sticky == &scroll_ancestor_paintable) {
|
|
containing_block_region = { {}, containing_block_of_sticky->scrollable_overflow_rect()->size() };
|
|
} else {
|
|
containing_block_region = containing_block_of_sticky->absolute_border_box_rect()
|
|
.translated(-scroll_ancestor_paintable.absolute_rect().top_left());
|
|
needs_parent_offset_adjustment = true;
|
|
}
|
|
|
|
sticky_frame.set_sticky_constraints({
|
|
.position_relative_to_scroll_ancestor = sticky_border_box_rect.top_left() - scroll_ancestor_paintable.absolute_rect().top_left(),
|
|
.border_box_size = sticky_border_box_rect.size(),
|
|
.scrollport_size = scroll_ancestor_paintable.absolute_rect().size(),
|
|
.containing_block_region = containing_block_region,
|
|
.needs_parent_offset_adjustment = needs_parent_offset_adjustment,
|
|
.insets = paintable_box.sticky_insets(),
|
|
});
|
|
};
|
|
|
|
for_each_in_inclusive_subtree_of_type<PaintableBox>([&](auto& paintable_box) {
|
|
RefPtr<ScrollFrame> sticky_scroll_frame;
|
|
if (paintable_box.is_sticky_position()) {
|
|
auto parent_scroll_frame = paintable_box.nearest_scroll_frame();
|
|
sticky_scroll_frame = m_scroll_state.create_sticky_frame_for(paintable_box, parent_scroll_frame);
|
|
precompute_sticky_constraints(*sticky_scroll_frame, paintable_box);
|
|
paintable_box.set_enclosing_scroll_frame(sticky_scroll_frame);
|
|
paintable_box.set_own_scroll_frame(sticky_scroll_frame);
|
|
}
|
|
|
|
if (paintable_box.has_scrollable_overflow() || is<ViewportPaintable>(paintable_box)) {
|
|
RefPtr<ScrollFrame const> parent_scroll_frame;
|
|
if (sticky_scroll_frame) {
|
|
parent_scroll_frame = sticky_scroll_frame;
|
|
} else {
|
|
parent_scroll_frame = paintable_box.nearest_scroll_frame();
|
|
}
|
|
auto scroll_frame = m_scroll_state.create_scroll_frame_for(paintable_box, parent_scroll_frame);
|
|
paintable_box.set_own_scroll_frame(scroll_frame);
|
|
}
|
|
|
|
return TraversalDecision::Continue;
|
|
});
|
|
|
|
for_each_in_subtree([&](auto& paintable) {
|
|
if (paintable.is_fixed_position() || paintable.is_sticky_position())
|
|
return TraversalDecision::Continue;
|
|
|
|
for (auto block = paintable.containing_block(); block; block = block->containing_block()) {
|
|
if (auto scroll_frame = block->own_scroll_frame(); scroll_frame) {
|
|
if (auto* paintable_box = as_if<PaintableBox>(paintable))
|
|
paintable_box->set_enclosing_scroll_frame(*scroll_frame);
|
|
|
|
return TraversalDecision::Continue;
|
|
}
|
|
if (block->is_fixed_position()) {
|
|
return TraversalDecision::Continue;
|
|
}
|
|
}
|
|
VERIFY_NOT_REACHED();
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
static Optional<TransformData> compute_transform(PaintableBox const& paintable_box, CSS::ComputedValues const& computed_values)
|
|
{
|
|
if (!paintable_box.has_css_transform())
|
|
return {};
|
|
|
|
auto matrix = Gfx::FloatMatrix4x4::identity();
|
|
if (auto const& translate = computed_values.translate())
|
|
matrix = matrix * translate->to_matrix(paintable_box).release_value();
|
|
if (auto const& rotate = computed_values.rotate())
|
|
matrix = matrix * rotate->to_matrix(paintable_box).release_value();
|
|
if (auto const& scale = computed_values.scale())
|
|
matrix = matrix * scale->to_matrix(paintable_box).release_value();
|
|
for (auto const& transform : computed_values.transformations())
|
|
matrix = matrix * transform->to_matrix(paintable_box).release_value();
|
|
|
|
// https://drafts.csswg.org/css-transforms-2/#accumulated-3d-transformation-matrix-computation
|
|
// AD-HOC: Flatten an element's transform only if it's not in a 3D rendering context.
|
|
// The spec defines a whole algorithm for how we should be doing positioning and rendering inside of a
|
|
// 3D rendering context, but just not flattening the transform is a reasonable approximation of that
|
|
// for now.
|
|
if (paintable_box.transform_style_used_value() == CSS::TransformStyle::Flat) {
|
|
matrix = matrix * CSS::TransformationStyleValue::create(CSS::PropertyID::Transform, CSS::TransformFunction::ScaleZ, CSS::StyleValueVector { CSS::LengthStyleValue::create(CSS::Length::make_px(0)) })->to_matrix({}).release_value();
|
|
}
|
|
|
|
auto const& css_transform_origin = computed_values.transform_origin();
|
|
auto reference_box = paintable_box.transform_reference_box();
|
|
auto origin_x = reference_box.left() + css_transform_origin.x.to_px(paintable_box.layout_node(), reference_box.width());
|
|
auto origin_y = reference_box.top() + css_transform_origin.y.to_px(paintable_box.layout_node(), reference_box.height());
|
|
return TransformData { matrix, { origin_x, origin_y } };
|
|
}
|
|
|
|
// 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
|
|
// NB: Length values less than 1px being clamped to 1px is handled by the perspective() function already.
|
|
// FIXME: Create the matrix directly.
|
|
perspective_matrix = perspective_matrix * CSS::TransformationStyleValue::create(CSS::PropertyID::Transform, CSS::TransformFunction::Perspective, CSS::StyleValueVector { CSS::LengthStyleValue::create(CSS::Length::make_px(perspective.value())) })->to_matrix({}).release_value();
|
|
|
|
// 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;
|
|
}
|
|
|
|
void ViewportPaintable::assign_accumulated_visual_contexts()
|
|
{
|
|
m_next_accumulated_visual_context_id = 1;
|
|
|
|
auto append_node = [&](RefPtr<AccumulatedVisualContext const> parent, VisualContextData data) {
|
|
return AccumulatedVisualContext::create(allocate_accumulated_visual_context_id(), move(data), parent);
|
|
};
|
|
|
|
auto make_effects_data = [](PaintableBox const& box) -> Optional<EffectsData> {
|
|
auto const& computed_values = box.computed_values();
|
|
EffectsData effects {
|
|
computed_values.opacity(),
|
|
mix_blend_mode_to_compositing_and_blending_operator(computed_values.mix_blend_mode()),
|
|
box.filter(),
|
|
computed_values.isolation() == CSS::Isolation::Isolate
|
|
};
|
|
if (!effects.needs_layer())
|
|
return {};
|
|
return effects;
|
|
};
|
|
|
|
// Create visual viewport transform as root (if not identity)
|
|
m_visual_viewport_context = nullptr;
|
|
auto transform = document().visual_viewport()->transform();
|
|
if (!transform.is_identity()) {
|
|
m_visual_viewport_context = append_node(nullptr, TransformData { transform.to_matrix(), CSSPixelPoint { 0, 0 } });
|
|
}
|
|
|
|
RefPtr<AccumulatedVisualContext const> viewport_state_for_descendants = m_visual_viewport_context;
|
|
if (own_scroll_frame())
|
|
viewport_state_for_descendants = append_node(m_visual_viewport_context, ScrollData { own_scroll_frame()->id(), false });
|
|
set_accumulated_visual_context(nullptr);
|
|
set_accumulated_visual_context_for_descendants(viewport_state_for_descendants);
|
|
|
|
for_each_in_subtree_of_type<PaintableBox>([&](auto& paintable_box) {
|
|
auto* visual_parent = as_if<PaintableBox>(paintable_box.parent());
|
|
if (!visual_parent)
|
|
return TraversalDecision::Continue;
|
|
|
|
RefPtr<AccumulatedVisualContext const> inherited_state;
|
|
|
|
if (paintable_box.is_fixed_position()) {
|
|
inherited_state = m_visual_viewport_context;
|
|
} else if (paintable_box.is_absolutely_positioned()) {
|
|
// For position: absolute, use containing block's state to correctly escape scroll containers.
|
|
auto* containing = paintable_box.containing_block();
|
|
inherited_state = containing->accumulated_visual_context_for_descendants();
|
|
|
|
// Abspos elements escape scroll containers and overflow clips of non-positioned
|
|
// ancestors, but cannot escape stacking contexts created by intermediate effects
|
|
// (opacity, mix-blend-mode, isolation). Walk from visual parent to containing
|
|
// block and collect these intermediate effects.
|
|
// NOTE: transforms/perspectives/filters establish containing blocks for abspos,
|
|
// so they cannot appear as intermediates.
|
|
Vector<VisualContextData, 4> intermediate_effects;
|
|
for (Paintable* paintable = visual_parent; paintable && paintable != containing; paintable = paintable->parent()) {
|
|
auto* ancestor_box = as_if<PaintableBox>(paintable);
|
|
if (!ancestor_box)
|
|
continue;
|
|
if (auto effects = make_effects_data(*ancestor_box); effects.has_value())
|
|
intermediate_effects.append(effects.release_value());
|
|
}
|
|
for (auto& effects : intermediate_effects.in_reverse())
|
|
inherited_state = append_node(inherited_state, move(effects));
|
|
} else {
|
|
// For position: relative/static, use visual parent's state directly.
|
|
// This avoids duplicate transform/perspective allocations that would occur with
|
|
// the containing block + intermediate walk approach.
|
|
inherited_state = visual_parent->accumulated_visual_context_for_descendants();
|
|
}
|
|
|
|
// Build this element's own state from inherited state.
|
|
RefPtr<AccumulatedVisualContext const> own_state = inherited_state;
|
|
|
|
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_frame = paintable_box.enclosing_scroll_frame(); sticky_frame && sticky_frame->is_sticky())
|
|
own_state = append_node(own_state, ScrollData { sticky_frame->id(), true });
|
|
}
|
|
|
|
auto const& computed_values = paintable_box.computed_values();
|
|
|
|
if (auto effects = make_effects_data(paintable_box); effects.has_value())
|
|
own_state = append_node(own_state, effects.release_value());
|
|
|
|
if (auto transform_data = compute_transform(paintable_box, computed_values); transform_data.has_value())
|
|
own_state = append_node(own_state, *transform_data);
|
|
|
|
if (auto css_clip = paintable_box.get_clip_rect(); css_clip.has_value())
|
|
own_state = append_node(own_state, ClipData { effective_css_clip_rect(*css_clip), {} });
|
|
|
|
// FIXME: Support other geometry boxes. See: https://drafts.fxtf.org/css-masking/#typedef-geometry-box
|
|
if (auto const& clip_path = computed_values.clip_path(); clip_path.has_value() && clip_path->is_basic_shape()) {
|
|
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; });
|
|
own_state = append_node(own_state, ClipPathData { move(path), masking_area, fill_rule });
|
|
}
|
|
|
|
paintable_box.set_accumulated_visual_context(own_state);
|
|
|
|
// Build state for descendants: own state + perspective + clip + scroll.
|
|
RefPtr<AccumulatedVisualContext const> state_for_descendants = own_state;
|
|
|
|
if (auto perspective_matrix = compute_perspective_matrix(paintable_box, computed_values); perspective_matrix.has_value())
|
|
state_for_descendants = append_node(state_for_descendants, PerspectiveData { *perspective_matrix });
|
|
|
|
auto overflow_x = computed_values.overflow_x();
|
|
auto overflow_y = computed_values.overflow_y();
|
|
auto has_hidden_overflow = overflow_x != CSS::Overflow::Visible || overflow_y != CSS::Overflow::Visible;
|
|
|
|
auto should_clip_overflow = has_hidden_overflow && paintable_box.overflow_property_applies();
|
|
if (should_clip_overflow || paintable_box.layout_node().has_paint_containment()) {
|
|
bool clip_x = overflow_x != CSS::Overflow::Visible;
|
|
bool clip_y = overflow_y != CSS::Overflow::Visible;
|
|
|
|
if (paintable_box.layout_node().has_paint_containment()) {
|
|
clip_x = true;
|
|
clip_y = true;
|
|
}
|
|
|
|
if (clip_x || clip_y) {
|
|
auto clip_rect = paintable_box.overflow_clip_edge_rect();
|
|
if (!clip_x) {
|
|
clip_rect.set_left(0);
|
|
clip_rect.set_right(CSSPixels::max_integer_value);
|
|
}
|
|
if (!clip_y) {
|
|
clip_rect.set_top(0);
|
|
clip_rect.set_bottom(CSSPixels::max_integer_value);
|
|
}
|
|
auto radii = (clip_x && clip_y) ? paintable_box.normalized_border_radii_data(ShrinkRadiiForBorders::Yes) : BorderRadiiData {};
|
|
state_for_descendants = append_node(state_for_descendants, ClipData { clip_rect, radii });
|
|
}
|
|
}
|
|
|
|
if (paintable_box.own_scroll_frame()) {
|
|
auto is_sticky_without_scrollable_overflow = paintable_box.is_sticky_position() && paintable_box.enclosing_scroll_frame() == paintable_box.own_scroll_frame();
|
|
if (!is_sticky_without_scrollable_overflow)
|
|
state_for_descendants = append_node(state_for_descendants, ScrollData { paintable_box.own_scroll_frame()->id(), false });
|
|
}
|
|
|
|
paintable_box.set_accumulated_visual_context_for_descendants(state_for_descendants);
|
|
|
|
return TraversalDecision::Continue;
|
|
});
|
|
}
|
|
|
|
void ViewportPaintable::refresh_scroll_state()
|
|
{
|
|
if (!m_needs_to_refresh_scroll_state)
|
|
return;
|
|
m_needs_to_refresh_scroll_state = false;
|
|
|
|
m_scroll_state.for_each_sticky_frame([&](auto& scroll_frame) {
|
|
auto nearest_scrolling_ancestor_frame = scroll_frame->nearest_scrolling_ancestor();
|
|
if (!nearest_scrolling_ancestor_frame || !scroll_frame->has_sticky_constraints())
|
|
return;
|
|
|
|
auto const& sticky_data = scroll_frame->sticky_constraints();
|
|
auto const& sticky_insets = sticky_data.insets;
|
|
auto const& scroll_ancestor_paintable = nearest_scrolling_ancestor_frame->paintable_box();
|
|
|
|
// For nested sticky elements, the parent sticky's offset is applied via cumulative_offset.
|
|
// We need to adjust all position calculations to account for this, so we work in the
|
|
// coordinate space where the parent sticky is at its current (offset) position.
|
|
CSSPixelPoint parent_sticky_offset;
|
|
if (auto parent = scroll_frame->parent(); parent && parent->is_sticky())
|
|
parent_sticky_offset = parent->cumulative_offset();
|
|
|
|
auto sticky_position_in_ancestor = sticky_data.position_relative_to_scroll_ancestor + parent_sticky_offset;
|
|
|
|
auto containing_block_region = sticky_data.containing_block_region;
|
|
if (sticky_data.needs_parent_offset_adjustment)
|
|
containing_block_region.translate_by(parent_sticky_offset);
|
|
CSSPixelPoint min_offset_within_containing_block = containing_block_region.top_left();
|
|
CSSPixelPoint max_offset_within_containing_block = {
|
|
containing_block_region.right() - sticky_data.border_box_size.width(),
|
|
containing_block_region.bottom() - sticky_data.border_box_size.height()
|
|
};
|
|
|
|
CSSPixelRect scrollport_rect { scroll_ancestor_paintable.scroll_offset(), sticky_data.scrollport_size };
|
|
CSSPixelPoint sticky_offset;
|
|
|
|
if (sticky_insets.top.has_value()) {
|
|
if (scrollport_rect.top() > sticky_position_in_ancestor.y() - *sticky_insets.top)
|
|
sticky_offset.set_y(min(scrollport_rect.top() + *sticky_insets.top, max_offset_within_containing_block.y()) - sticky_position_in_ancestor.y());
|
|
}
|
|
if (sticky_insets.left.has_value()) {
|
|
if (scrollport_rect.left() > sticky_position_in_ancestor.x() - *sticky_insets.left)
|
|
sticky_offset.set_x(min(scrollport_rect.left() + *sticky_insets.left, max_offset_within_containing_block.x()) - sticky_position_in_ancestor.x());
|
|
}
|
|
if (sticky_insets.bottom.has_value()) {
|
|
if (scrollport_rect.bottom() < sticky_position_in_ancestor.y() + sticky_data.border_box_size.height() + *sticky_insets.bottom)
|
|
sticky_offset.set_y(max(scrollport_rect.bottom() - sticky_data.border_box_size.height() - *sticky_insets.bottom, min_offset_within_containing_block.y()) - sticky_position_in_ancestor.y());
|
|
}
|
|
if (sticky_insets.right.has_value()) {
|
|
if (scrollport_rect.right() < sticky_position_in_ancestor.x() + sticky_data.border_box_size.width() + *sticky_insets.right)
|
|
sticky_offset.set_x(max(scrollport_rect.right() - sticky_data.border_box_size.width() - *sticky_insets.right, min_offset_within_containing_block.x()) - sticky_position_in_ancestor.x());
|
|
}
|
|
|
|
scroll_frame->set_own_offset(sticky_offset);
|
|
});
|
|
|
|
m_scroll_state.for_each_scroll_frame([&](auto& scroll_frame) {
|
|
scroll_frame->set_own_offset(-scroll_frame->paintable_box().scroll_offset());
|
|
});
|
|
|
|
m_scroll_state_snapshot = m_scroll_state.snapshot();
|
|
}
|
|
|
|
static void resolve_paint_only_properties_in_subtree(Paintable& root)
|
|
{
|
|
root.for_each_in_inclusive_subtree([&](auto& paintable) {
|
|
paintable.resolve_paint_properties();
|
|
paintable.set_needs_paint_only_properties_update(false);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
}
|
|
|
|
void ViewportPaintable::resolve_paint_only_properties()
|
|
{
|
|
// Resolves layout-dependent properties not handled during layout and stores them in the paint tree.
|
|
// Properties resolved include:
|
|
// - Border radii
|
|
// - Box shadows
|
|
// - Text shadows
|
|
// - Transforms
|
|
// - Transform origins
|
|
// - Outlines
|
|
for_each_in_inclusive_subtree([&](Paintable& paintable) {
|
|
if (paintable.needs_paint_only_properties_update()) {
|
|
resolve_paint_only_properties_in_subtree(paintable);
|
|
return TraversalDecision::SkipChildrenAndContinue;
|
|
}
|
|
return TraversalDecision::Continue;
|
|
});
|
|
}
|
|
|
|
GC::Ptr<Selection::Selection> ViewportPaintable::selection() const
|
|
{
|
|
return document().get_selection();
|
|
}
|
|
|
|
void ViewportPaintable::recompute_selection_states(DOM::Range& range)
|
|
{
|
|
// 1. Start by resetting the selection state of all layout nodes to None.
|
|
for_each_in_inclusive_subtree([&](auto& layout_node) {
|
|
layout_node.set_selection_state(SelectionState::None);
|
|
return TraversalDecision::Continue;
|
|
});
|
|
|
|
auto start_container = range.start_container();
|
|
auto end_container = range.end_container();
|
|
|
|
// 2. If the selection starts and ends in the same node:
|
|
if (start_container == end_container) {
|
|
// 1. If the selection starts and ends at the same offset, return.
|
|
if (range.start_offset() == range.end_offset()) {
|
|
// NOTE: A zero-length selection should not be visible.
|
|
return;
|
|
}
|
|
|
|
// 2. If it's a text node, mark it as StartAndEnd and return.
|
|
if (is<DOM::Text>(*start_container) && !range.start().node->is_inert()) {
|
|
if (auto* paintable = start_container->paintable())
|
|
paintable->set_selection_state(SelectionState::StartAndEnd);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 3. Mark the selection start node as Start (if text) or Full (if anything else).
|
|
if (auto* paintable = start_container->paintable(); paintable && !range.start().node->is_inert()) {
|
|
if (is<DOM::Text>(*start_container))
|
|
paintable->set_selection_state(SelectionState::Start);
|
|
else
|
|
paintable->set_selection_state(SelectionState::Full);
|
|
}
|
|
|
|
// 4. Mark the nodes between the start and end of the selection as Full.
|
|
auto* start_at = start_container->child_at_index(range.start_offset());
|
|
// If the start container has no child at that index, we need to start on the node right after the start container.
|
|
if (!start_at) {
|
|
if (auto* last_child = start_container->last_child()) {
|
|
start_at = last_child->next_in_pre_order();
|
|
} else {
|
|
start_at = start_container->next_in_pre_order();
|
|
}
|
|
}
|
|
|
|
DOM::Node* stop_at = end_container->child_at_index(range.end_offset());
|
|
// Only stop at the end container if it has no children that may need to be included.
|
|
for (auto* node = start_at; node && (node != stop_at && !(node == end_container && !end_container->has_children())); node = node->next_in_pre_order(end_container)) {
|
|
if (node->is_inert())
|
|
continue;
|
|
if (auto* paintable = node->paintable())
|
|
paintable->set_selection_state(SelectionState::Full);
|
|
}
|
|
|
|
// 5. Mark the selection end node as End if it is a text node.
|
|
if (auto* paintable = end_container->paintable(); paintable && !range.end().node->is_inert() && is<DOM::Text>(*end_container)) {
|
|
paintable->set_selection_state(SelectionState::End);
|
|
}
|
|
}
|
|
|
|
bool ViewportPaintable::handle_mousewheel(Badge<EventHandler>, CSSPixelPoint, unsigned, unsigned, int, int)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void ViewportPaintable::visit_edges(Visitor& visitor)
|
|
{
|
|
Base::visit_edges(visitor);
|
|
visitor.visit(m_paintable_boxes_with_auto_content_visibility);
|
|
}
|
|
|
|
}
|