mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-06-28 12:10:28 +00:00
Visual viewport scroll and pinch zoom used to invalidate the full accumulated visual context tree and display list, even though those changes only modify the root visual viewport transform. That forced a full display list rerecord before sending updated compositor state. Patch the reserved visual viewport AVC node in place instead. VisualViewport marks the tree for compositor update without invalidating the display list, and Navigable sends the replacement tree through the new IPC path before updating scroll state.
215 lines
7.2 KiB
C++
215 lines
7.2 KiB
C++
/*
|
||
* Copyright (c) 2023, Andreas Kling <andreas@ladybird.org>
|
||
* Copyright (c) 2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
|
||
*
|
||
* SPDX-License-Identifier: BSD-2-Clause
|
||
*/
|
||
|
||
#include <LibWeb/Bindings/Intrinsics.h>
|
||
#include <LibWeb/Bindings/VisualViewport.h>
|
||
#include <LibWeb/CSS/VisualViewport.h>
|
||
#include <LibWeb/DOM/Document.h>
|
||
#include <LibWeb/DOM/EventDispatcher.h>
|
||
#include <LibWeb/HTML/EventNames.h>
|
||
#include <LibWeb/HTML/Navigable.h>
|
||
#include <LibWeb/Painting/ViewportPaintable.h>
|
||
|
||
namespace Web::CSS {
|
||
|
||
GC_DEFINE_ALLOCATOR(VisualViewport);
|
||
|
||
GC::Ref<VisualViewport> VisualViewport::create(DOM::Document& document)
|
||
{
|
||
return document.realm().create<VisualViewport>(document);
|
||
}
|
||
|
||
VisualViewport::VisualViewport(DOM::Document& document)
|
||
: DOM::EventTarget(document.realm())
|
||
, m_document(document)
|
||
{
|
||
}
|
||
|
||
void VisualViewport::initialize(JS::Realm& realm)
|
||
{
|
||
WEB_SET_PROTOTYPE_FOR_INTERFACE(VisualViewport);
|
||
Base::initialize(realm);
|
||
}
|
||
|
||
void VisualViewport::visit_edges(Cell::Visitor& visitor)
|
||
{
|
||
Base::visit_edges(visitor);
|
||
visitor.visit(m_document);
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-visualviewport-offsetleft
|
||
double VisualViewport::offset_left() const
|
||
{
|
||
// 1. If the visual viewport’s associated document is not fully active, return 0.
|
||
if (!m_document->is_fully_active())
|
||
return 0;
|
||
|
||
// 2. Otherwise, return the offset of the left edge of the visual viewport from the left edge of the layout viewport.
|
||
return m_offset.x().to_double();
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-visualviewport-offsettop
|
||
double VisualViewport::offset_top() const
|
||
{
|
||
// 1. If the visual viewport’s associated document is not fully active, return 0.
|
||
if (!m_document->is_fully_active())
|
||
return 0;
|
||
|
||
// 2. Otherwise, return the offset of the top edge of the visual viewport from the top edge of the layout viewport.
|
||
return m_offset.y().to_double();
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-visualviewport-pageleft
|
||
double VisualViewport::page_left() const
|
||
{
|
||
// 1. If the visual viewport’s associated document is not fully active, return 0.
|
||
if (!m_document->is_fully_active())
|
||
return 0;
|
||
|
||
// 2. Otherwise, return the offset of the left edge of the visual viewport from the
|
||
// left edge of the initial containing block of the layout viewport’s document.
|
||
return m_document->viewport_rect().x().to_double() + offset_left();
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-visualviewport-pagetop
|
||
double VisualViewport::page_top() const
|
||
{
|
||
// 1. If the visual viewport’s associated document is not fully active, return 0.
|
||
if (!m_document->is_fully_active())
|
||
return 0;
|
||
|
||
// 2. Otherwise, return the offset of the top edge of the visual viewport from the
|
||
// top edge of the initial containing block of the layout viewport’s document.
|
||
return m_document->viewport_rect().y().to_double() + offset_top();
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-visualviewport-width
|
||
double VisualViewport::width() const
|
||
{
|
||
// 1. If the visual viewport’s associated document is not fully active, return 0.
|
||
if (!m_document->is_fully_active())
|
||
return 0;
|
||
|
||
// 2. Otherwise, return the width of the visual viewport
|
||
// FIXME: excluding the width of any rendered vertical classic scrollbar that is fixed to the visual viewport.
|
||
return m_document->viewport_rect().size().width() / m_scale;
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-visualviewport-height
|
||
double VisualViewport::height() const
|
||
{
|
||
// 1. If the visual viewport’s associated document is not fully active, return 0.
|
||
if (!m_document->is_fully_active())
|
||
return 0;
|
||
|
||
// 2. Otherwise, return the height of the visual viewport
|
||
// FIXME: excluding the height of any rendered vertical classic scrollbar that is fixed to the visual viewport.
|
||
return m_document->viewport_rect().size().height() / m_scale;
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom-view/#dom-visualviewport-scale
|
||
double VisualViewport::scale() const
|
||
{
|
||
return m_scale;
|
||
}
|
||
|
||
void VisualViewport::set_onresize(WebIDL::CallbackType* event_handler)
|
||
{
|
||
set_event_handler_attribute(HTML::EventNames::resize, event_handler);
|
||
}
|
||
|
||
WebIDL::CallbackType* VisualViewport::onresize()
|
||
{
|
||
return event_handler_attribute(HTML::EventNames::resize);
|
||
}
|
||
|
||
void VisualViewport::set_onscroll(WebIDL::CallbackType* event_handler)
|
||
{
|
||
set_event_handler_attribute(HTML::EventNames::scroll, event_handler);
|
||
}
|
||
|
||
WebIDL::CallbackType* VisualViewport::onscroll()
|
||
{
|
||
return event_handler_attribute(HTML::EventNames::scroll);
|
||
}
|
||
|
||
void VisualViewport::set_onscrollend(WebIDL::CallbackType* event_handler)
|
||
{
|
||
set_event_handler_attribute(HTML::EventNames::scrollend, event_handler);
|
||
}
|
||
|
||
WebIDL::CallbackType* VisualViewport::onscrollend()
|
||
{
|
||
return event_handler_attribute(HTML::EventNames::scrollend);
|
||
}
|
||
|
||
void VisualViewport::scroll_by(CSSPixelPoint delta)
|
||
{
|
||
if (delta.is_zero())
|
||
return;
|
||
m_offset += delta;
|
||
update_accumulated_visual_context();
|
||
m_document->set_needs_repaint(Badge<CSS::VisualViewport> {}, InvalidateDisplayList::No);
|
||
}
|
||
|
||
Gfx::AffineTransform VisualViewport::transform() const
|
||
{
|
||
Gfx::AffineTransform transform;
|
||
auto offset = m_offset.to_type<double>() * m_scale;
|
||
transform.translate(-offset.x(), -offset.y());
|
||
transform.scale({ m_scale, m_scale });
|
||
return transform;
|
||
}
|
||
|
||
void VisualViewport::zoom(CSSPixelPoint position, double scale_delta)
|
||
{
|
||
static constexpr double MIN_ALLOWED_SCALE = 1.0;
|
||
static constexpr double MAX_ALLOWED_SCALE = 5.0;
|
||
double new_scale = clamp(m_scale * (1 + scale_delta), MIN_ALLOWED_SCALE, MAX_ALLOWED_SCALE);
|
||
double applied_delta = new_scale / m_scale;
|
||
|
||
// For pinch zoom we want focal_point to stay put on screen:
|
||
// scale_new * (focal_point - offset_new) = scale_old * (focal_point - offset_old)
|
||
auto new_offset = m_offset.to_type<double>() * m_scale * applied_delta;
|
||
new_offset += position.to_type<int>().to_type<double>() * (applied_delta - 1.0f);
|
||
|
||
auto viewport_float_size = m_document->navigable()->viewport_rect().size().to_type<double>();
|
||
auto max_x_offset = max(0.0, viewport_float_size.width() * (new_scale - 1.0f));
|
||
auto max_y_offset = max(0.0, viewport_float_size.height() * (new_scale - 1.0f));
|
||
new_offset = { clamp(new_offset.x(), 0.0f, max_x_offset), clamp(new_offset.y(), 0.0f, max_y_offset) };
|
||
|
||
m_scale = new_scale;
|
||
m_offset = (new_offset / m_scale).to_type<CSSPixels>();
|
||
update_accumulated_visual_context();
|
||
m_document->set_needs_repaint(Badge<CSS::VisualViewport> {}, InvalidateDisplayList::No);
|
||
}
|
||
|
||
CSSPixelPoint VisualViewport::map_to_layout_viewport(CSSPixelPoint position) const
|
||
{
|
||
auto inverse = transform().inverse().value_or({});
|
||
return inverse.map(position.to_type<int>()).to_type<CSSPixels>();
|
||
}
|
||
|
||
void VisualViewport::reset()
|
||
{
|
||
m_scale = 1.0;
|
||
m_offset = { 0, 0 };
|
||
update_accumulated_visual_context();
|
||
m_document->set_needs_repaint(Badge<CSS::VisualViewport> {}, InvalidateDisplayList::No);
|
||
}
|
||
|
||
void VisualViewport::update_accumulated_visual_context()
|
||
{
|
||
if (auto paintable = m_document->unsafe_paintable(); paintable && paintable->has_visual_context_tree()) {
|
||
paintable->update_visual_viewport_accumulated_visual_context();
|
||
return;
|
||
}
|
||
|
||
m_document->set_needs_accumulated_visual_contexts_update(true);
|
||
}
|
||
|
||
}
|