LibWeb+LibWebView+WebContent: Inform the UI about DOM mutations

This will allow our DevTools server to inform the Firefox DevTools
client about DOM mutations.
This commit is contained in:
Timothy Flynn 2025-03-06 17:32:43 -05:00 committed by Andreas Kling
parent bf723aad98
commit 2c4b420acc
Notes: github-actions[bot] 2025-03-08 00:27:41 +00:00
17 changed files with 253 additions and 12 deletions

View file

@ -57,6 +57,7 @@
#include <LibWeb/Layout/TextNode.h>
#include <LibWeb/MathML/MathMLElement.h>
#include <LibWeb/Namespace.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/SVG/SVGElement.h>
@ -2232,8 +2233,11 @@ Painting::PaintableBox* Node::paintable_box()
}
// https://dom.spec.whatwg.org/#queue-a-mutation-record
void Node::queue_mutation_record(FlyString const& type, Optional<FlyString> const& attribute_name, Optional<FlyString> const& attribute_namespace, Optional<String> const& old_value, Vector<GC::Root<Node>> added_nodes, Vector<GC::Root<Node>> removed_nodes, Node* previous_sibling, Node* next_sibling) const
void Node::queue_mutation_record(FlyString const& type, Optional<FlyString> const& attribute_name, Optional<FlyString> const& attribute_namespace, Optional<String> const& old_value, Vector<GC::Root<Node>> added_nodes, Vector<GC::Root<Node>> removed_nodes, Node* previous_sibling, Node* next_sibling)
{
auto& document = this->document();
auto& page = document.page();
// NOTE: We defer garbage collection until the end of the scope, since we can't safely use MutationObserver* as a hashmap key otherwise.
// FIXME: This is a total hack.
GC::DeferGC defer_gc(heap());
@ -2278,22 +2282,22 @@ void Node::queue_mutation_record(FlyString const& type, Optional<FlyString> cons
}
// OPTIMIZATION: If there are no interested observers, bail without doing any more work.
if (interested_observers.is_empty())
if (interested_observers.is_empty() && !page.listen_for_dom_mutations())
return;
// FIXME: The MutationRecord constructor shuld take an Optional<FlyString> attribute name and namespace
Optional<String> string_attribute_name;
if (attribute_name.has_value())
string_attribute_name = attribute_name->to_string();
Optional<String> string_attribute_namespace;
if (attribute_namespace.has_value())
string_attribute_namespace = attribute_namespace->to_string();
auto added_nodes_list = StaticNodeList::create(realm(), move(added_nodes));
auto removed_nodes_list = StaticNodeList::create(realm(), move(removed_nodes));
// 4. For each observer → mappedOldValue of interestedObservers:
for (auto& interested_observer : interested_observers) {
// FIXME: The MutationRecord constructor shuld take an Optional<FlyString> attribute name and namespace
Optional<String> string_attribute_name;
if (attribute_name.has_value())
string_attribute_name = attribute_name->to_string();
Optional<String> string_attribute_namespace;
if (attribute_namespace.has_value())
string_attribute_namespace = attribute_namespace->to_string();
// 1. Let record be a new MutationRecord object with its type set to type, target set to target, attributeName set to name, attributeNamespace set to namespace, oldValue set to mappedOldValue,
// addedNodes set to addedNodes, removedNodes set to removedNodes, previousSibling set to previousSibling, and nextSibling set to nextSibling.
auto record = MutationRecord::create(realm(), type, *this, added_nodes_list, removed_nodes_list, previous_sibling, next_sibling, string_attribute_name, string_attribute_namespace, /* mappedOldValue */ interested_observer.value);
@ -2303,7 +2307,11 @@ void Node::queue_mutation_record(FlyString const& type, Optional<FlyString> cons
}
// 5. Queue a mutation observer microtask.
Bindings::queue_mutation_observer_microtask(document());
Bindings::queue_mutation_observer_microtask(document);
// AD-HOC: Notify the UI if it is interested in DOM mutations (i.e. for DevTools).
if (page.listen_for_dom_mutations())
page.client().page_did_mutate_dom(type, *this, added_nodes_list, removed_nodes_list, previous_sibling, next_sibling, string_attribute_name);
}
// https://dom.spec.whatwg.org/#queue-a-tree-mutation-record

View file

@ -355,7 +355,7 @@ public:
void add_registered_observer(RegisteredObserver&);
void queue_mutation_record(FlyString const& type, Optional<FlyString> const& attribute_name, Optional<FlyString> const& attribute_namespace, Optional<String> const& old_value, Vector<GC::Root<Node>> added_nodes, Vector<GC::Root<Node>> removed_nodes, Node* previous_sibling, Node* next_sibling) const;
void queue_mutation_record(FlyString const& type, Optional<FlyString> const& attribute_name, Optional<FlyString> const& attribute_namespace, Optional<String> const& old_value, Vector<GC::Root<Node>> added_nodes, Vector<GC::Root<Node>> removed_nodes, Node* previous_sibling, Node* next_sibling);
// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-descendant
template<typename Callback>

View file

@ -222,6 +222,9 @@ public:
FindInPageResult find_in_page_previous_match();
Optional<FindInPageQuery> last_find_in_page_query() const { return m_last_find_in_page_query; }
bool listen_for_dom_mutations() const { return m_listen_for_dom_mutations; }
void set_listen_for_dom_mutations(bool listen_for_dom_mutations) { m_listen_for_dom_mutations = listen_for_dom_mutations; }
private:
explicit Page(GC::Ref<PageClient>);
virtual void visit_edges(Visitor&) override;
@ -287,9 +290,12 @@ private:
// Spec Note: This value also impacts the navigation processing model.
// FIXME: Actually support pdf viewing
bool m_pdf_viewer_supported { false };
size_t m_find_in_page_match_index { 0 };
Optional<FindInPageQuery> m_last_find_in_page_query;
URL::URL m_last_find_in_page_url;
bool m_listen_for_dom_mutations { false };
};
struct PaintOptions {
@ -397,6 +403,8 @@ public:
virtual IPC::File request_worker_agent() { return IPC::File {}; }
virtual void page_did_mutate_dom([[maybe_unused]] FlyString const& type, [[maybe_unused]] DOM::Node const& target, [[maybe_unused]] DOM::NodeList& added_nodes, [[maybe_unused]] DOM::NodeList& removed_nodes, [[maybe_unused]] GC::Ptr<DOM::Node> previous_sibling, [[maybe_unused]] GC::Ptr<DOM::Node> next_sibling, [[maybe_unused]] Optional<String> const& attribute_name) { }
virtual void inspector_did_load() { }
virtual void inspector_did_select_dom_node([[maybe_unused]] UniqueNodeID node_id, [[maybe_unused]] Optional<CSS::Selector::PseudoElement::Type> const& pseudo_element) { }
virtual void inspector_did_set_dom_node_text([[maybe_unused]] UniqueNodeID node_id, [[maybe_unused]] String const& text) { }

View file

@ -9,6 +9,7 @@ set(SOURCES
Database.cpp
HelperProcess.cpp
InspectorClient.cpp
Mutation.cpp
Plugins/FontPlugin.cpp
Plugins/ImageCodecPlugin.cpp
Process.cpp

View file

@ -21,6 +21,7 @@ class WebContentClient;
struct Attribute;
struct ConsoleOutput;
struct CookieStorageKey;
struct Mutation;
struct ProcessHandle;
struct SearchEngine;

View file

@ -0,0 +1,81 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibIPC/Decoder.h>
#include <LibIPC/Encoder.h>
#include <LibWebView/Mutation.h>
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, WebView::AttributeMutation const& mutation)
{
TRY(encoder.encode(mutation.attribute_name));
TRY(encoder.encode(mutation.new_value));
return {};
}
template<>
ErrorOr<WebView::AttributeMutation> IPC::decode(Decoder& decoder)
{
auto attribute_name = TRY(decoder.decode<String>());
auto new_value = TRY(decoder.decode<Optional<String>>());
return WebView::AttributeMutation { move(attribute_name), move(new_value) };
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, WebView::CharacterDataMutation const& mutation)
{
TRY(encoder.encode(mutation.new_value));
return {};
}
template<>
ErrorOr<WebView::CharacterDataMutation> IPC::decode(Decoder& decoder)
{
auto new_value = TRY(decoder.decode<String>());
return WebView::CharacterDataMutation { move(new_value) };
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, WebView::ChildListMutation const& mutation)
{
TRY(encoder.encode(mutation.added));
TRY(encoder.encode(mutation.removed));
TRY(encoder.encode(mutation.target_child_count));
return {};
}
template<>
ErrorOr<WebView::ChildListMutation> IPC::decode(Decoder& decoder)
{
auto added = TRY(decoder.decode<Vector<Web::UniqueNodeID>>());
auto removed = TRY(decoder.decode<Vector<Web::UniqueNodeID>>());
auto target_child_count = TRY(decoder.decode<size_t>());
return WebView::ChildListMutation { move(added), move(removed), target_child_count };
}
template<>
ErrorOr<void> IPC::encode(Encoder& encoder, WebView::Mutation const& mutation)
{
TRY(encoder.encode(mutation.type));
TRY(encoder.encode(mutation.target));
TRY(encoder.encode(mutation.serialized_target));
TRY(encoder.encode(mutation.mutation));
return {};
}
template<>
ErrorOr<WebView::Mutation> IPC::decode(Decoder& decoder)
{
auto type = TRY(decoder.decode<String>());
auto target = TRY(decoder.decode<Web::UniqueNodeID>());
auto serialized_target = TRY(decoder.decode<String>());
auto mutation = TRY(decoder.decode<WebView::Mutation::Type>());
return WebView::Mutation { move(type), target, move(serialized_target), move(mutation) };
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibIPC/Forward.h>
#include <LibWeb/Forward.h>
namespace WebView {
struct AttributeMutation {
String attribute_name;
Optional<String> new_value;
};
struct CharacterDataMutation {
String new_value;
};
struct ChildListMutation {
Vector<Web::UniqueNodeID> added;
Vector<Web::UniqueNodeID> removed;
size_t target_child_count { 0 };
};
struct Mutation {
using Type = Variant<AttributeMutation, CharacterDataMutation, ChildListMutation>;
String type;
Web::UniqueNodeID target { 0 };
String serialized_target;
Type mutation;
};
}
namespace IPC {
template<>
ErrorOr<void> encode(Encoder&, WebView::AttributeMutation const&);
template<>
ErrorOr<WebView::AttributeMutation> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, WebView::CharacterDataMutation const&);
template<>
ErrorOr<WebView::CharacterDataMutation> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, WebView::ChildListMutation const&);
template<>
ErrorOr<WebView::ChildListMutation> decode(Decoder&);
template<>
ErrorOr<void> encode(Encoder&, WebView::Mutation const&);
template<>
ErrorOr<WebView::Mutation> decode(Decoder&);
}

View file

@ -333,6 +333,11 @@ void ViewImplementation::clear_highlighted_dom_node()
highlight_dom_node(0, {});
}
void ViewImplementation::set_listen_for_dom_mutations(bool listen_for_dom_mutations)
{
client().async_set_listen_for_dom_mutations(page_id(), listen_for_dom_mutations);
}
void ViewImplementation::set_dom_node_text(Web::UniqueNodeID node_id, String text)
{
client().async_set_dom_node_text(page_id(), node_id, move(text));

View file

@ -112,6 +112,7 @@ public:
void highlight_dom_node(Web::UniqueNodeID node_id, Optional<Web::CSS::Selector::PseudoElement::Type> pseudo_element);
void clear_highlighted_dom_node();
void set_listen_for_dom_mutations(bool);
void set_dom_node_text(Web::UniqueNodeID node_id, String text);
void set_dom_node_tag(Web::UniqueNodeID node_id, String name);
void add_dom_node_attributes(Web::UniqueNodeID node_id, Vector<Attribute> attributes);
@ -213,6 +214,7 @@ public:
Function<void(Web::CSS::StyleSheetIdentifier const&)> on_inspector_requested_style_sheet_source;
Function<void(Web::CSS::StyleSheetIdentifier const&, URL::URL const&, String const&)> on_received_style_sheet_source;
Function<void(Web::UniqueNodeID)> on_received_hovered_node_id;
Function<void(Mutation)> on_dom_mutation_received;
Function<void(Optional<Web::UniqueNodeID> const& node_id)> on_finshed_editing_dom_node;
Function<void(String const&)> on_received_dom_node_html;
Function<void(JsonValue)> on_received_js_console_result;

View file

@ -341,6 +341,14 @@ void WebContentClient::did_finish_editing_dom_node(u64 page_id, Optional<Web::Un
}
}
void WebContentClient::did_mutate_dom(u64 page_id, Mutation const& mutation)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_dom_mutation_received)
view->on_dom_mutation_received(move(const_cast<Mutation&>(mutation)));
}
}
void WebContentClient::did_get_dom_node_html(u64 page_id, String const& html)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {

View file

@ -76,6 +76,7 @@ private:
virtual void did_inspect_accessibility_tree(u64 page_id, String const&) override;
virtual void did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID const& node_id) override;
virtual void did_finish_editing_dom_node(u64 page_id, Optional<Web::UniqueNodeID> const& node_id) override;
virtual void did_mutate_dom(u64 page_id, Mutation const&) override;
virtual void did_get_dom_node_html(u64 page_id, String const& html) override;
virtual void did_take_screenshot(u64 page_id, Gfx::ShareableBitmap const& screenshot) override;
virtual void did_get_internal_page_info(u64 page_id, PageInfoType, String const&) override;

View file

@ -663,6 +663,15 @@ void ConnectionFromClient::request_style_sheet_source(u64 page_id, Web::CSS::Sty
}
}
void ConnectionFromClient::set_listen_for_dom_mutations(u64 page_id, bool listen_for_dom_mutations)
{
auto page = this->page(page_id);
if (!page.has_value())
return;
page->page().set_listen_for_dom_mutations(listen_for_dom_mutations);
}
void ConnectionFromClient::set_dom_node_text(u64 page_id, Web::UniqueNodeID const& node_id, String const& text)
{
auto* dom_node = Web::DOM::Node::from_unique_id(node_id);

View file

@ -83,6 +83,7 @@ private:
virtual void list_style_sheets(u64 page_id) override;
virtual void request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier const& identifier) override;
virtual void set_listen_for_dom_mutations(u64 page_id, bool) override;
virtual void set_dom_node_text(u64 page_id, Web::UniqueNodeID const& node_id, String const& text) override;
virtual void set_dom_node_tag(u64 page_id, Web::UniqueNodeID const& node_id, String const& name) override;
virtual void add_dom_node_attributes(u64 page_id, Web::UniqueNodeID const& node_id, Vector<WebView::Attribute> const& attributes) override;

View file

@ -7,6 +7,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonObjectSerializer.h>
#include <AK/JsonValue.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibJS/Console.h>
@ -15,7 +16,11 @@
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/Cookie/ParsedCookie.h>
#include <LibWeb/DOM/Attr.h>
#include <LibWeb/DOM/CharacterData.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/MutationType.h>
#include <LibWeb/DOM/NamedNodeMap.h>
#include <LibWeb/DOM/NodeList.h>
#include <LibWeb/HTML/HTMLLinkElement.h>
#include <LibWeb/HTML/HTMLStyleElement.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
@ -656,6 +661,44 @@ IPC::File PageClient::request_worker_agent()
return response->take_socket();
}
void PageClient::page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr<Web::DOM::Node>, GC::Ptr<Web::DOM::Node>, Optional<String> const& attribute_name)
{
Optional<WebView::Mutation::Type> mutation;
if (type == Web::DOM::MutationType::attributes) {
VERIFY(attribute_name.has_value());
auto const& element = as<Web::DOM::Element>(target);
mutation = WebView::AttributeMutation { *attribute_name, element.attribute(*attribute_name) };
} else if (type == Web::DOM::MutationType::characterData) {
auto const& character_data = as<Web::DOM::CharacterData>(target);
mutation = WebView::CharacterDataMutation { character_data.data() };
} else if (type == Web::DOM::MutationType::childList) {
Vector<Web::UniqueNodeID> added;
added.ensure_capacity(added_nodes.length());
Vector<Web::UniqueNodeID> removed;
removed.ensure_capacity(removed_nodes.length());
for (auto i = 0u; i < added_nodes.length(); ++i)
added.unchecked_append(added_nodes.item(i)->unique_id());
for (auto i = 0u; i < removed_nodes.length(); ++i)
removed.unchecked_append(removed_nodes.item(i)->unique_id());
mutation = WebView::ChildListMutation { move(added), move(removed), target.child_count() };
} else {
VERIFY_NOT_REACHED();
}
StringBuilder builder;
auto serializer = MUST(JsonObjectSerializer<>::try_create(builder));
target.serialize_tree_as_json(serializer);
MUST(serializer.finish());
auto serialized_target = MUST(builder.to_string());
client().async_did_mutate_dom(m_id, { type.to_string(), target.unique_id(), move(serialized_target), mutation.release_value() });
}
void PageClient::inspector_did_load()
{
client().async_inspector_did_load(m_id);

View file

@ -173,6 +173,7 @@ private:
virtual void page_did_change_audio_play_state(Web::HTML::AudioPlayState) override;
virtual void page_did_allocate_backing_stores(i32 front_bitmap_id, Gfx::ShareableBitmap front_bitmap, i32 back_bitmap_id, Gfx::ShareableBitmap back_bitmap) override;
virtual IPC::File request_worker_agent() override;
virtual void page_did_mutate_dom(FlyString const& type, Web::DOM::Node const& target, Web::DOM::NodeList& added_nodes, Web::DOM::NodeList& removed_nodes, GC::Ptr<Web::DOM::Node> previous_sibling, GC::Ptr<Web::DOM::Node> next_sibling, Optional<String> const& attribute_name) override;
virtual void inspector_did_load() override;
virtual void inspector_did_select_dom_node(Web::UniqueNodeID, Optional<Web::CSS::Selector::PseudoElement::Type> const& pseudo_element) override;
virtual void inspector_did_set_dom_node_text(Web::UniqueNodeID, String const& text) override;

View file

@ -17,6 +17,7 @@
#include <LibWeb/Page/Page.h>
#include <LibWebView/Attribute.h>
#include <LibWebView/ConsoleOutput.h>
#include <LibWebView/Mutation.h>
#include <LibWebView/PageInfo.h>
#include <LibWebView/ProcessHandle.h>
@ -54,6 +55,7 @@ endpoint WebContentClient
did_inspect_accessibility_tree(u64 page_id, String accessibility_tree) =|
did_get_hovered_node_id(u64 page_id, Web::UniqueNodeID node_id) =|
did_finish_editing_dom_node(u64 page_id, Optional<Web::UniqueNodeID> node_id) =|
did_mutate_dom(u64 page_id, WebView::Mutation mutation) =|
did_get_dom_node_html(u64 page_id, String html) =|
inspector_did_list_style_sheets(u64 page_id, Vector<Web::CSS::StyleSheetIdentifier> style_sheets) =|

View file

@ -56,6 +56,7 @@ endpoint WebContentServer
list_style_sheets(u64 page_id) =|
request_style_sheet_source(u64 page_id, Web::CSS::StyleSheetIdentifier identifier) =|
set_listen_for_dom_mutations(u64 page_id, bool listen_for_dom_mutations) =|
set_dom_node_text(u64 page_id, Web::UniqueNodeID node_id, String text) =|
set_dom_node_tag(u64 page_id, Web::UniqueNodeID node_id, String name) =|
add_dom_node_attributes(u64 page_id, Web::UniqueNodeID node_id, Vector<WebView::Attribute> attributes) =|