Files
ladybird/Libraries/LibWeb/Page/AutoScrollHandler.cpp
Aliaksandr Kalenik 568b7ce7ea LibWeb: Make Paintable tree ref-counted
The Paintable tree and its supplemental painting data structures were
GC allocated because that was the easiest way to manage it and avoid
leaks introduced by ref cycles. This included the Paintable subclasses
themselves plus StackingContext, ChromeWidget, Scrollbar, ResizeHandle,
and scroll-frame state.

We are now trying to reduce GC allocation churn on layout and painting
updates, so keeping this short-lived rendering tree outside the JS heap
is a better fit. Move Paintable to RefCountedTreeNode, make painting
helpers ref-counted or weakly reference Paintables, and update the
layout and event-handler call sites to use RefPtr/WeakPtr ownership.
2026-05-07 15:03:44 +02:00

216 lines
7.7 KiB
C++

/*
* Copyright (c) 2026, Jelle Raaijmakers <jelle@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentFragment.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/Page/AutoScrollHandler.h>
#include <LibWeb/Page/EventHandler.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/ViewportPaintable.h>
namespace Web {
static constexpr int auto_scroll_interval_ms = 16;
static constexpr CSSPixels auto_scroll_edge_threshold { 7 };
static constexpr CSSPixels viewport_edge_inset { 25 };
// Returns the scrollport shrunk by per-side effective auto scroll edge thresholds. Sides close to a viewport edge get
// a larger inset so the distance-based speed ramp works predictably, even if the user's mouse is limited in reach (e.g.
// by the window/screen boundary).
static CSSPixelRect compute_effective_auto_scroll_edge(CSSPixelRect const& scrollport, CSSPixelRect const& viewport_rect)
{
auto effective = [](CSSPixels distance_to_viewport_edge) {
return auto_scroll_edge_threshold + max(CSSPixels(0), viewport_edge_inset - distance_to_viewport_edge);
};
return scrollport.shrunken(
effective(scrollport.top() - viewport_rect.top()),
effective(viewport_rect.right() - scrollport.right()),
effective(viewport_rect.bottom() - scrollport.bottom()),
effective(scrollport.left() - viewport_rect.left()));
}
static Optional<CSSPixelRect> scrollport_rect_in_viewport(Painting::PaintableBox const& paintable_box)
{
auto scrollport = paintable_box.absolute_padding_box_rect();
// The viewport's scrollport is already in viewport coordinates.
if (paintable_box.is_viewport_paintable())
return scrollport;
if (!paintable_box.accumulated_visual_context_index().value())
return {};
return paintable_box.transform_rect_to_viewport(scrollport);
}
// Returns scroll speed in CSS pixels per second for each axis, based on how far the mouse is past the auto scroll edge.
static CSSPixelPoint compute_auto_scroll_speed(CSSPixelPoint mouse, CSSPixelRect const& edge)
{
static CSSPixels constexpr distance_to_speed_factor { 100 };
static CSSPixels constexpr max_speed_per_second { 5000 };
auto compute_axis_speed = [&](CSSPixels mouse_pos, CSSPixels edge_start, CSSPixels edge_end) -> CSSPixels {
if (mouse_pos < edge_start) {
auto distance = edge_start - mouse_pos;
return -min(distance * distance_to_speed_factor, max_speed_per_second);
}
if (mouse_pos > edge_end) {
auto distance = mouse_pos - edge_end;
return min(distance * distance_to_speed_factor, max_speed_per_second);
}
return 0;
};
return {
compute_axis_speed(mouse.x(), edge.x(), edge.x() + edge.width()),
compute_axis_speed(mouse.y(), edge.y(), edge.y() + edge.height()),
};
}
static bool is_in_form_associated_text_control(DOM::Element const& element)
{
auto const& host = element.containing_shadow_root() ? *element.containing_shadow_root()->host() : element;
return is<HTML::FormAssociatedTextControlElement>(host);
}
AutoScrollHandler::AutoScrollHandler(HTML::Navigable& navigable, DOM::Element& container)
: m_navigable(navigable)
, m_container_element(container)
{
}
void AutoScrollHandler::visit_edges(JS::Cell::Visitor& visitor) const
{
visitor.visit(m_navigable);
visitor.visit(m_container_element);
}
CSSPixelPoint AutoScrollHandler::process(CSSPixelPoint mouse_position)
{
m_mouse_position = mouse_position;
m_container_element->document().update_layout(DOM::UpdateLayoutReason::AutoScrollSelection);
auto paintable_box = auto_scroll_paintable(m_container_element);
if (!paintable_box)
return mouse_position;
auto scrollport = scrollport_rect_in_viewport(*paintable_box);
if (!scrollport.has_value())
return mouse_position;
CSSPixelRect viewport_rect { {}, m_navigable->viewport_size() };
auto effective_edge = compute_effective_auto_scroll_edge(*scrollport, viewport_rect);
if (effective_edge.contains(mouse_position)) {
deactivate();
return mouse_position;
}
activate();
if (is_in_form_associated_text_control(m_container_element))
return constrained(mouse_position, *scrollport);
return mouse_position;
}
GC::Ptr<DOM::Element> AutoScrollHandler::find_scrollable_ancestor(Painting::Paintable const& paintable)
{
auto paintable_box = paintable.containing_block();
while (paintable_box) {
if (paintable_box->could_be_scrolled_by_wheel_event()) {
if (auto* element = as_if<DOM::Element>(paintable_box->dom_node().ptr()))
return element;
}
// The viewport is always a potential scroll container, but may not report has_scrollable_overflow() and its DOM
// node is Document (not Element).
if (paintable_box->is_viewport_paintable() && paintable_box->could_be_scrolled_by_wheel_event()) {
if (auto scrolling_element = paintable_box->document().scrolling_element())
return const_cast<DOM::Element*>(scrolling_element.ptr());
}
paintable_box = paintable_box->containing_block();
}
return {};
}
// Returns the paintable box that manages the scrollport for an auto-scroll container element. When the element is the
// document's scrolling element, the viewport paintable is the scroll container.
RefPtr<Painting::PaintableBox> AutoScrollHandler::auto_scroll_paintable(DOM::Element& element)
{
if (element.document().scrolling_element().ptr() == &element)
return element.document().paintable();
return element.paintable_box();
}
void AutoScrollHandler::activate()
{
m_active = true;
}
void AutoScrollHandler::deactivate()
{
m_active = false;
m_fractional_delta = {};
}
void AutoScrollHandler::perform_tick()
{
if (!m_active)
return;
if (!m_navigable->event_handler().is_handling_mouse_selection()) {
deactivate();
return;
}
auto& document = *m_navigable->active_document();
document.update_layout(DOM::UpdateLayoutReason::AutoScrollSelection);
auto paintable_box = auto_scroll_paintable(m_container_element);
if (!paintable_box || !document.paintable()) {
deactivate();
return;
}
auto scrollport = scrollport_rect_in_viewport(*paintable_box);
if (!scrollport.has_value()) {
deactivate();
return;
}
CSSPixelRect viewport_rect { {}, m_navigable->viewport_size() };
auto effective_edge = compute_effective_auto_scroll_edge(*scrollport, viewport_rect);
if (effective_edge.contains(m_mouse_position)) {
deactivate();
return;
}
auto elapsed_seconds = static_cast<double>(auto_scroll_interval_ms) / 1000.0;
auto speed = compute_auto_scroll_speed(m_mouse_position, effective_edge);
// Accumulate sub-pixel deltas across ticks, since scroll_by() only accepts whole pixels.
m_fractional_delta += CSSPixelPoint {
speed.x() * CSSPixels(elapsed_seconds),
speed.y() * CSSPixels(elapsed_seconds),
};
int scroll_x = m_fractional_delta.x().to_int();
int scroll_y = m_fractional_delta.y().to_int();
m_fractional_delta -= CSSPixelPoint { scroll_x, scroll_y };
if (paintable_box->scroll_by(scroll_x, scroll_y) == Painting::PaintableBox::ScrollHandled::No)
return;
auto selection_position = is_in_form_associated_text_control(m_container_element)
? constrained(m_mouse_position, *scrollport)
: m_mouse_position;
m_navigable->event_handler().apply_mouse_selection(selection_position);
}
}