diff --git a/Libraries/LibWeb/DOM/Node.cpp b/Libraries/LibWeb/DOM/Node.cpp index f3c655e0eca..9e240fc5934 100644 --- a/Libraries/LibWeb/DOM/Node.cpp +++ b/Libraries/LibWeb/DOM/Node.cpp @@ -57,6 +57,7 @@ #include #include #include +#include #include #include #include @@ -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 const& attribute_name, Optional const& attribute_namespace, Optional const& old_value, Vector> added_nodes, Vector> removed_nodes, Node* previous_sibling, Node* next_sibling) const +void Node::queue_mutation_record(FlyString const& type, Optional const& attribute_name, Optional const& attribute_namespace, Optional const& old_value, Vector> added_nodes, Vector> 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 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 attribute name and namespace + Optional string_attribute_name; + if (attribute_name.has_value()) + string_attribute_name = attribute_name->to_string(); + Optional 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 attribute name and namespace - Optional string_attribute_name; - if (attribute_name.has_value()) - string_attribute_name = attribute_name->to_string(); - Optional 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 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 diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index c5d6c48d879..29e21297597 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -355,7 +355,7 @@ public: void add_registered_observer(RegisteredObserver&); - void queue_mutation_record(FlyString const& type, Optional const& attribute_name, Optional const& attribute_namespace, Optional const& old_value, Vector> added_nodes, Vector> removed_nodes, Node* previous_sibling, Node* next_sibling) const; + void queue_mutation_record(FlyString const& type, Optional const& attribute_name, Optional const& attribute_namespace, Optional const& old_value, Vector> added_nodes, Vector> removed_nodes, Node* previous_sibling, Node* next_sibling); // https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-descendant template diff --git a/Libraries/LibWeb/Page/Page.h b/Libraries/LibWeb/Page/Page.h index 112b0a4225f..3d0fadae5c6 100644 --- a/Libraries/LibWeb/Page/Page.h +++ b/Libraries/LibWeb/Page/Page.h @@ -222,6 +222,9 @@ public: FindInPageResult find_in_page_previous_match(); Optional 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); 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 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 previous_sibling, [[maybe_unused]] GC::Ptr next_sibling, [[maybe_unused]] Optional const& attribute_name) { } + virtual void inspector_did_load() { } virtual void inspector_did_select_dom_node([[maybe_unused]] UniqueNodeID node_id, [[maybe_unused]] Optional const& pseudo_element) { } virtual void inspector_did_set_dom_node_text([[maybe_unused]] UniqueNodeID node_id, [[maybe_unused]] String const& text) { } diff --git a/Libraries/LibWebView/CMakeLists.txt b/Libraries/LibWebView/CMakeLists.txt index 4e7773c08a6..bbab6874b47 100644 --- a/Libraries/LibWebView/CMakeLists.txt +++ b/Libraries/LibWebView/CMakeLists.txt @@ -9,6 +9,7 @@ set(SOURCES Database.cpp HelperProcess.cpp InspectorClient.cpp + Mutation.cpp Plugins/FontPlugin.cpp Plugins/ImageCodecPlugin.cpp Process.cpp diff --git a/Libraries/LibWebView/Forward.h b/Libraries/LibWebView/Forward.h index 4da025bd409..b1a15ab19fc 100644 --- a/Libraries/LibWebView/Forward.h +++ b/Libraries/LibWebView/Forward.h @@ -21,6 +21,7 @@ class WebContentClient; struct Attribute; struct ConsoleOutput; struct CookieStorageKey; +struct Mutation; struct ProcessHandle; struct SearchEngine; diff --git a/Libraries/LibWebView/Mutation.cpp b/Libraries/LibWebView/Mutation.cpp new file mode 100644 index 00000000000..467ec05eec0 --- /dev/null +++ b/Libraries/LibWebView/Mutation.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +template<> +ErrorOr IPC::encode(Encoder& encoder, WebView::AttributeMutation const& mutation) +{ + TRY(encoder.encode(mutation.attribute_name)); + TRY(encoder.encode(mutation.new_value)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto attribute_name = TRY(decoder.decode()); + auto new_value = TRY(decoder.decode>()); + + return WebView::AttributeMutation { move(attribute_name), move(new_value) }; +} + +template<> +ErrorOr IPC::encode(Encoder& encoder, WebView::CharacterDataMutation const& mutation) +{ + TRY(encoder.encode(mutation.new_value)); + return {}; +} + +template<> +ErrorOr IPC::decode(Decoder& decoder) +{ + auto new_value = TRY(decoder.decode()); + + return WebView::CharacterDataMutation { move(new_value) }; +} + +template<> +ErrorOr 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 IPC::decode(Decoder& decoder) +{ + auto added = TRY(decoder.decode>()); + auto removed = TRY(decoder.decode>()); + auto target_child_count = TRY(decoder.decode()); + + return WebView::ChildListMutation { move(added), move(removed), target_child_count }; +} + +template<> +ErrorOr 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 IPC::decode(Decoder& decoder) +{ + auto type = TRY(decoder.decode()); + auto target = TRY(decoder.decode()); + auto serialized_target = TRY(decoder.decode()); + auto mutation = TRY(decoder.decode()); + + return WebView::Mutation { move(type), target, move(serialized_target), move(mutation) }; +} diff --git a/Libraries/LibWebView/Mutation.h b/Libraries/LibWebView/Mutation.h new file mode 100644 index 00000000000..94187bc8602 --- /dev/null +++ b/Libraries/LibWebView/Mutation.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace WebView { + +struct AttributeMutation { + String attribute_name; + Optional new_value; +}; + +struct CharacterDataMutation { + String new_value; +}; + +struct ChildListMutation { + Vector added; + Vector removed; + size_t target_child_count { 0 }; +}; + +struct Mutation { + using Type = Variant; + + String type; + Web::UniqueNodeID target { 0 }; + String serialized_target; + Type mutation; +}; + +} + +namespace IPC { + +template<> +ErrorOr encode(Encoder&, WebView::AttributeMutation const&); + +template<> +ErrorOr decode(Decoder&); + +template<> +ErrorOr encode(Encoder&, WebView::CharacterDataMutation const&); + +template<> +ErrorOr decode(Decoder&); + +template<> +ErrorOr encode(Encoder&, WebView::ChildListMutation const&); + +template<> +ErrorOr decode(Decoder&); + +template<> +ErrorOr encode(Encoder&, WebView::Mutation const&); + +template<> +ErrorOr decode(Decoder&); + +} diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index 1e35e675cef..b2b4fcca775 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -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)); diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 2c3b6c1d32b..d46d582d462 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -112,6 +112,7 @@ public: void highlight_dom_node(Web::UniqueNodeID node_id, Optional 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 attributes); @@ -213,6 +214,7 @@ public: Function on_inspector_requested_style_sheet_source; Function on_received_style_sheet_source; Function on_received_hovered_node_id; + Function on_dom_mutation_received; Function const& node_id)> on_finshed_editing_dom_node; Function on_received_dom_node_html; Function on_received_js_console_result; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index d94842b5f0b..0698f49557b 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -341,6 +341,14 @@ void WebContentClient::did_finish_editing_dom_node(u64 page_id, Optionalon_dom_mutation_received) + view->on_dom_mutation_received(move(const_cast(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()) { diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index 3dfb0d2dc9a..ef8d6e1c5a8 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -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 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; diff --git a/Services/WebContent/ConnectionFromClient.cpp b/Services/WebContent/ConnectionFromClient.cpp index 9c8db9a5b93..aa624802dbb 100644 --- a/Services/WebContent/ConnectionFromClient.cpp +++ b/Services/WebContent/ConnectionFromClient.cpp @@ -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); diff --git a/Services/WebContent/ConnectionFromClient.h b/Services/WebContent/ConnectionFromClient.h index c7be6e9c733..ca1785b505d 100644 --- a/Services/WebContent/ConnectionFromClient.h +++ b/Services/WebContent/ConnectionFromClient.h @@ -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 const& attributes) override; diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index e3006f80e3f..7740f099bf9 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -7,6 +7,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -15,7 +16,11 @@ #include #include #include +#include +#include +#include #include +#include #include #include #include @@ -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, GC::Ptr, Optional const& attribute_name) +{ + Optional mutation; + + if (type == Web::DOM::MutationType::attributes) { + VERIFY(attribute_name.has_value()); + + auto const& element = as(target); + mutation = WebView::AttributeMutation { *attribute_name, element.attribute(*attribute_name) }; + } else if (type == Web::DOM::MutationType::characterData) { + auto const& character_data = as(target); + mutation = WebView::CharacterDataMutation { character_data.data() }; + } else if (type == Web::DOM::MutationType::childList) { + Vector added; + added.ensure_capacity(added_nodes.length()); + + Vector 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); diff --git a/Services/WebContent/PageClient.h b/Services/WebContent/PageClient.h index 689db3bedcb..5313ce3bc73 100644 --- a/Services/WebContent/PageClient.h +++ b/Services/WebContent/PageClient.h @@ -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 previous_sibling, GC::Ptr next_sibling, Optional const& attribute_name) override; virtual void inspector_did_load() override; virtual void inspector_did_select_dom_node(Web::UniqueNodeID, Optional const& pseudo_element) override; virtual void inspector_did_set_dom_node_text(Web::UniqueNodeID, String const& text) override; diff --git a/Services/WebContent/WebContentClient.ipc b/Services/WebContent/WebContentClient.ipc index 3e315bd2fee..766a8dbb1e6 100644 --- a/Services/WebContent/WebContentClient.ipc +++ b/Services/WebContent/WebContentClient.ipc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -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 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 style_sheets) =| diff --git a/Services/WebContent/WebContentServer.ipc b/Services/WebContent/WebContentServer.ipc index 845a1423e4d..18b0b036fcc 100644 --- a/Services/WebContent/WebContentServer.ipc +++ b/Services/WebContent/WebContentServer.ipc @@ -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 attributes) =|