mirror of
https://github.com/RGBCube/serenity
synced 2025-05-18 22:45:08 +00:00

This patch adds the "has a rendering opportunity" concept from the spec to BrowsingContext and uses it to filter out contexts that are unable to render right now when doing the event loop's rendering updates. Note that we actually consider all contexts to have a rendering opportunity at all times right now. Coming up with reasons to avoid rendering is left as a FIXME. :^)
361 lines
11 KiB
C++
361 lines
11 KiB
C++
/*
|
|
* Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/DOM/HTMLCollection.h>
|
|
#include <LibWeb/DOM/Window.h>
|
|
#include <LibWeb/HTML/EventLoop/EventLoop.h>
|
|
#include <LibWeb/HTML/HTMLAnchorElement.h>
|
|
#include <LibWeb/Layout/BreakNode.h>
|
|
#include <LibWeb/Layout/InitialContainingBlock.h>
|
|
#include <LibWeb/Layout/TextNode.h>
|
|
#include <LibWeb/Page/BrowsingContext.h>
|
|
#include <LibWeb/Page/Page.h>
|
|
|
|
namespace Web {
|
|
|
|
BrowsingContext::BrowsingContext(Page& page, HTML::BrowsingContextContainer* container)
|
|
: m_page(page)
|
|
, m_loader(*this)
|
|
, m_event_handler({}, *this)
|
|
, m_container(container)
|
|
{
|
|
m_cursor_blink_timer = Core::Timer::construct(500, [this] {
|
|
if (!is_focused_context())
|
|
return;
|
|
if (m_cursor_position.node() && m_cursor_position.node()->layout_node()) {
|
|
m_cursor_blink_state = !m_cursor_blink_state;
|
|
m_cursor_position.node()->layout_node()->set_needs_display();
|
|
}
|
|
});
|
|
}
|
|
|
|
BrowsingContext::~BrowsingContext()
|
|
{
|
|
}
|
|
|
|
void BrowsingContext::did_edit(Badge<EditEventHandler>)
|
|
{
|
|
reset_cursor_blink_cycle();
|
|
}
|
|
|
|
void BrowsingContext::reset_cursor_blink_cycle()
|
|
{
|
|
m_cursor_blink_state = true;
|
|
m_cursor_blink_timer->restart();
|
|
m_cursor_position.node()->layout_node()->set_needs_display();
|
|
}
|
|
|
|
bool BrowsingContext::is_focused_context() const
|
|
{
|
|
return m_page && &m_page->focused_context() == this;
|
|
}
|
|
|
|
void BrowsingContext::set_active_document(DOM::Document* document)
|
|
{
|
|
if (m_active_document == document)
|
|
return;
|
|
|
|
m_cursor_position = {};
|
|
|
|
if (m_active_document)
|
|
m_active_document->detach_from_browsing_context({}, *this);
|
|
|
|
m_active_document = document;
|
|
|
|
if (m_active_document) {
|
|
m_active_document->attach_to_browsing_context({}, *this);
|
|
if (m_page && is_top_level())
|
|
m_page->client().page_did_change_title(m_active_document->title());
|
|
}
|
|
|
|
if (m_page)
|
|
m_page->client().page_did_set_document_in_top_level_browsing_context(m_active_document);
|
|
}
|
|
|
|
void BrowsingContext::set_viewport_rect(Gfx::IntRect const& rect)
|
|
{
|
|
bool did_change = false;
|
|
|
|
if (m_size != rect.size()) {
|
|
m_size = rect.size();
|
|
did_change = true;
|
|
}
|
|
|
|
if (m_viewport_scroll_offset != rect.location()) {
|
|
m_viewport_scroll_offset = rect.location();
|
|
did_change = true;
|
|
}
|
|
|
|
if (did_change) {
|
|
for (auto* client : m_viewport_clients)
|
|
client->browsing_context_did_set_viewport_rect(rect);
|
|
}
|
|
|
|
// Schedule the HTML event loop to ensure that a `resize` event gets fired.
|
|
HTML::main_thread_event_loop().schedule();
|
|
}
|
|
|
|
void BrowsingContext::set_size(Gfx::IntSize const& size)
|
|
{
|
|
if (m_size == size)
|
|
return;
|
|
m_size = size;
|
|
|
|
for (auto* client : m_viewport_clients)
|
|
client->browsing_context_did_set_viewport_rect(viewport_rect());
|
|
|
|
// Schedule the HTML event loop to ensure that a `resize` event gets fired.
|
|
HTML::main_thread_event_loop().schedule();
|
|
}
|
|
|
|
void BrowsingContext::set_viewport_scroll_offset(Gfx::IntPoint const& offset)
|
|
{
|
|
if (m_viewport_scroll_offset == offset)
|
|
return;
|
|
m_viewport_scroll_offset = offset;
|
|
|
|
for (auto* client : m_viewport_clients)
|
|
client->browsing_context_did_set_viewport_rect(viewport_rect());
|
|
}
|
|
|
|
void BrowsingContext::set_needs_display(Gfx::IntRect const& rect)
|
|
{
|
|
if (!viewport_rect().intersects(rect))
|
|
return;
|
|
|
|
if (is_top_level()) {
|
|
if (m_page)
|
|
m_page->client().page_did_invalidate(to_top_level_rect(rect));
|
|
return;
|
|
}
|
|
|
|
if (container() && container()->layout_node())
|
|
container()->layout_node()->set_needs_display();
|
|
}
|
|
|
|
void BrowsingContext::scroll_to_anchor(String const& fragment)
|
|
{
|
|
if (!active_document())
|
|
return;
|
|
|
|
auto element = active_document()->get_element_by_id(fragment);
|
|
if (!element) {
|
|
auto candidates = active_document()->get_elements_by_name(fragment);
|
|
for (auto& candidate : candidates->collect_matching_elements()) {
|
|
if (is<HTML::HTMLAnchorElement>(*candidate)) {
|
|
element = verify_cast<HTML::HTMLAnchorElement>(*candidate);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// FIXME: This is overly aggressive and should be something more like a "update_layout_if_needed()"
|
|
active_document()->force_layout();
|
|
|
|
if (!element || !element->layout_node())
|
|
return;
|
|
|
|
auto& layout_node = *element->layout_node();
|
|
|
|
Gfx::FloatRect float_rect { layout_node.box_type_agnostic_position(), { (float)viewport_rect().width(), (float)viewport_rect().height() } };
|
|
if (is<Layout::Box>(layout_node)) {
|
|
auto& layout_box = verify_cast<Layout::Box>(layout_node);
|
|
auto padding_box = layout_box.box_model().padding_box();
|
|
float_rect.translate_by(-padding_box.left, -padding_box.top);
|
|
}
|
|
|
|
if (m_page)
|
|
m_page->client().page_did_request_scroll_into_view(enclosing_int_rect(float_rect));
|
|
}
|
|
|
|
Gfx::IntRect BrowsingContext::to_top_level_rect(Gfx::IntRect const& a_rect)
|
|
{
|
|
auto rect = a_rect;
|
|
rect.set_location(to_top_level_position(a_rect.location()));
|
|
return rect;
|
|
}
|
|
|
|
Gfx::IntPoint BrowsingContext::to_top_level_position(Gfx::IntPoint const& a_position)
|
|
{
|
|
auto position = a_position;
|
|
for (auto* ancestor = parent(); ancestor; ancestor = ancestor->parent()) {
|
|
if (ancestor->is_top_level())
|
|
break;
|
|
if (!ancestor->container())
|
|
return {};
|
|
if (!ancestor->container()->layout_node())
|
|
return {};
|
|
position.translate_by(ancestor->container()->layout_node()->box_type_agnostic_position().to_type<int>());
|
|
}
|
|
return position;
|
|
}
|
|
|
|
void BrowsingContext::set_cursor_position(DOM::Position position)
|
|
{
|
|
if (m_cursor_position == position)
|
|
return;
|
|
|
|
if (m_cursor_position.node() && m_cursor_position.node()->layout_node())
|
|
m_cursor_position.node()->layout_node()->set_needs_display();
|
|
|
|
m_cursor_position = move(position);
|
|
|
|
if (m_cursor_position.node() && m_cursor_position.node()->layout_node())
|
|
m_cursor_position.node()->layout_node()->set_needs_display();
|
|
|
|
reset_cursor_blink_cycle();
|
|
}
|
|
|
|
String BrowsingContext::selected_text() const
|
|
{
|
|
StringBuilder builder;
|
|
if (!active_document())
|
|
return {};
|
|
auto* layout_root = active_document()->layout_node();
|
|
if (!layout_root)
|
|
return {};
|
|
if (!layout_root->selection().is_valid())
|
|
return {};
|
|
|
|
auto selection = layout_root->selection().normalized();
|
|
|
|
if (selection.start().layout_node == selection.end().layout_node) {
|
|
if (!is<Layout::TextNode>(*selection.start().layout_node))
|
|
return "";
|
|
return verify_cast<Layout::TextNode>(*selection.start().layout_node).text_for_rendering().substring(selection.start().index_in_node, selection.end().index_in_node - selection.start().index_in_node);
|
|
}
|
|
|
|
// Start node
|
|
auto layout_node = selection.start().layout_node;
|
|
if (is<Layout::TextNode>(*layout_node)) {
|
|
auto& text = verify_cast<Layout::TextNode>(*layout_node).text_for_rendering();
|
|
builder.append(text.substring(selection.start().index_in_node, text.length() - selection.start().index_in_node));
|
|
}
|
|
|
|
// Middle nodes
|
|
layout_node = layout_node->next_in_pre_order();
|
|
while (layout_node && layout_node != selection.end().layout_node) {
|
|
if (is<Layout::TextNode>(*layout_node))
|
|
builder.append(verify_cast<Layout::TextNode>(*layout_node).text_for_rendering());
|
|
else if (is<Layout::BreakNode>(*layout_node) || is<Layout::BlockBox>(*layout_node))
|
|
builder.append('\n');
|
|
|
|
layout_node = layout_node->next_in_pre_order();
|
|
}
|
|
|
|
// End node
|
|
VERIFY(layout_node == selection.end().layout_node);
|
|
if (is<Layout::TextNode>(*layout_node)) {
|
|
auto& text = verify_cast<Layout::TextNode>(*layout_node).text_for_rendering();
|
|
builder.append(text.substring(0, selection.end().index_in_node));
|
|
}
|
|
|
|
return builder.to_string();
|
|
}
|
|
|
|
void BrowsingContext::select_all()
|
|
{
|
|
if (!active_document())
|
|
return;
|
|
auto* layout_root = active_document()->layout_node();
|
|
if (!layout_root)
|
|
return;
|
|
|
|
Layout::Node const* first_layout_node = layout_root;
|
|
|
|
for (;;) {
|
|
auto* next = first_layout_node->next_in_pre_order();
|
|
if (!next)
|
|
break;
|
|
first_layout_node = next;
|
|
if (is<Layout::TextNode>(*first_layout_node))
|
|
break;
|
|
}
|
|
|
|
Layout::Node const* last_layout_node = first_layout_node;
|
|
|
|
for (Layout::Node const* layout_node = first_layout_node; layout_node; layout_node = layout_node->next_in_pre_order()) {
|
|
if (is<Layout::TextNode>(*layout_node))
|
|
last_layout_node = layout_node;
|
|
}
|
|
|
|
VERIFY(first_layout_node);
|
|
VERIFY(last_layout_node);
|
|
|
|
int last_layout_node_index_in_node = 0;
|
|
if (is<Layout::TextNode>(*last_layout_node)) {
|
|
auto const& text_for_rendering = verify_cast<Layout::TextNode>(*last_layout_node).text_for_rendering();
|
|
if (!text_for_rendering.is_empty())
|
|
last_layout_node_index_in_node = text_for_rendering.length() - 1;
|
|
}
|
|
|
|
layout_root->set_selection({ { first_layout_node, 0 }, { last_layout_node, last_layout_node_index_in_node } });
|
|
}
|
|
|
|
void BrowsingContext::register_viewport_client(ViewportClient& client)
|
|
{
|
|
auto result = m_viewport_clients.set(&client);
|
|
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
|
|
}
|
|
|
|
void BrowsingContext::unregister_viewport_client(ViewportClient& client)
|
|
{
|
|
bool was_removed = m_viewport_clients.remove(&client);
|
|
VERIFY(was_removed);
|
|
}
|
|
|
|
void BrowsingContext::register_frame_nesting(AK::URL const& url)
|
|
{
|
|
m_frame_nesting_levels.ensure(url)++;
|
|
}
|
|
|
|
bool BrowsingContext::is_frame_nesting_allowed(AK::URL const& url) const
|
|
{
|
|
return m_frame_nesting_levels.get(url).value_or(0) < 3;
|
|
}
|
|
|
|
bool BrowsingContext::increment_cursor_position_offset()
|
|
{
|
|
if (!m_cursor_position.increment_offset())
|
|
return false;
|
|
reset_cursor_blink_cycle();
|
|
return true;
|
|
}
|
|
|
|
bool BrowsingContext::decrement_cursor_position_offset()
|
|
{
|
|
if (!m_cursor_position.decrement_offset())
|
|
return false;
|
|
reset_cursor_blink_cycle();
|
|
return true;
|
|
}
|
|
|
|
DOM::Document* BrowsingContext::container_document()
|
|
{
|
|
if (auto* container = this->container())
|
|
return &container->document();
|
|
return nullptr;
|
|
}
|
|
|
|
DOM::Document const* BrowsingContext::container_document() const
|
|
{
|
|
if (auto* container = this->container())
|
|
return &container->document();
|
|
return nullptr;
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/#rendering-opportunity
|
|
bool BrowsingContext::has_a_rendering_opportunity() const
|
|
{
|
|
// A browsing context has a rendering opportunity if the user agent is currently able to present the contents of the browsing context to the user,
|
|
// accounting for hardware refresh rate constraints and user agent throttling for performance reasons, but considering content presentable even if it's outside the viewport.
|
|
|
|
// FIXME: We should at the very least say `false` here if we're an inactive browser tab.
|
|
return true;
|
|
}
|
|
|
|
}
|