From 7aea87c9df6b821cea669e7998c787ecfb0b9aa4 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Wed, 29 May 2024 20:09:33 +0100 Subject: [PATCH] LibWeb+LibWebView+WebContent: Add basic find in page functionality This allows the browser to send a query to the WebContent process, which will search the page for the given string and highlight any occurrences of that string. --- Userland/Libraries/LibWeb/DOM/Document.cpp | 29 +++++ Userland/Libraries/LibWeb/DOM/Document.h | 2 + Userland/Libraries/LibWeb/Page/Page.cpp | 103 ++++++++++++++++++ Userland/Libraries/LibWeb/Page/Page.h | 10 ++ .../LibWebView/ViewImplementation.cpp | 15 +++ .../Libraries/LibWebView/ViewImplementation.h | 3 + .../WebContent/ConnectionFromClient.cpp | 27 +++++ .../WebContent/ConnectionFromClient.h | 4 + .../Services/WebContent/WebContentServer.ipc | 4 + 9 files changed, 197 insertions(+) diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 0387854622b..5706f2b513e 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -5068,4 +5068,33 @@ void Document::set_needs_to_refresh_scroll_state(bool b) paintable->set_needs_to_refresh_scroll_state(b); } +Vector> Document::find_matching_text(String const& query) +{ + if (!document_element() || !document_element()->layout_node()) + return {}; + + Vector> matches; + document_element()->layout_node()->for_each_in_inclusive_subtree_of_type([&](auto const& text_node) { + auto const& text = text_node.text_for_rendering(); + size_t offset = 0; + while (true) { + auto match_index = text.find_byte_offset(query, offset); + if (!match_index.has_value()) + break; + + auto range = create_range(); + auto& dom_node = const_cast(text_node.dom_node()); + (void)range->set_start(dom_node, match_index.value()); + (void)range->set_end(dom_node, match_index.value() + query.code_points().length()); + + matches.append(range); + offset = match_index.value() + 1; + } + + return TraversalDecision::Continue; + }); + + return matches; +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 491ee6ea90d..ef588725f93 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -667,6 +667,8 @@ public: // Does document represent an embedded svg img [[nodiscard]] bool is_decoded_svg() const; + Vector> find_matching_text(String const&); + protected: virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; diff --git a/Userland/Libraries/LibWeb/Page/Page.cpp b/Userland/Libraries/LibWeb/Page/Page.cpp index f45960b7c67..7b8eaf1fa8c 100644 --- a/Userland/Libraries/LibWeb/Page/Page.cpp +++ b/Userland/Libraries/LibWeb/Page/Page.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2020, Andreas Kling * Copyright (c) 2022, Sam Atkins + * Copyright (c) 2024, Tim Ledbetter * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -20,8 +22,10 @@ #include #include #include +#include #include #include +#include namespace Web { @@ -44,6 +48,7 @@ void Page::visit_edges(JS::Cell::Visitor& visitor) Base::visit_edges(visitor); visitor.visit(m_top_level_traversable); visitor.visit(m_client); + visitor.visit(m_find_in_page_matches); } HTML::Navigable& Page::focused_navigable() @@ -519,6 +524,104 @@ void Page::set_user_style(String source) } } +void Page::clear_selection() +{ + auto documents = HTML::main_thread_event_loop().documents_in_this_event_loop(); + for (auto const& document : documents) { + if (&document->page() != this) + continue; + + auto selection = document->get_selection(); + if (!selection) + continue; + + selection->remove_all_ranges(); + } +} + +void Page::find_in_page(String const& query) +{ + m_find_in_page_match_index = 0; + + if (query.is_empty()) { + m_find_in_page_matches = {}; + update_find_in_page_selection(); + return; + } + + auto documents = HTML::main_thread_event_loop().documents_in_this_event_loop(); + Vector> all_matches; + for (auto const& document : documents) { + if (&document->page() != this) + continue; + + auto matches = document->find_matching_text(query); + all_matches.extend(move(matches)); + } + + m_find_in_page_matches.clear_with_capacity(); + for (auto& match : all_matches) + m_find_in_page_matches.append(*match); + + update_find_in_page_selection(); +} + +void Page::find_in_page_next_match() +{ + if (m_find_in_page_matches.is_empty()) + return; + + if (m_find_in_page_match_index == m_find_in_page_matches.size() - 1) { + m_find_in_page_match_index = 0; + } else { + m_find_in_page_match_index++; + } + + update_find_in_page_selection(); +} + +void Page::find_in_page_previous_match() +{ + if (m_find_in_page_matches.is_empty()) + return; + + if (m_find_in_page_match_index == 0) { + m_find_in_page_match_index = m_find_in_page_matches.size() - 1; + } else { + m_find_in_page_match_index--; + } + + update_find_in_page_selection(); +} + +void Page::update_find_in_page_selection() +{ + clear_selection(); + + if (m_find_in_page_matches.is_empty()) + return; + + auto current_range = m_find_in_page_matches[m_find_in_page_match_index]; + auto common_ancestor_container = current_range->common_ancestor_container(); + auto& document = common_ancestor_container->document(); + if (!document.window()) + return; + + auto selection = document.get_selection(); + if (!selection) + return; + + selection->add_range(*current_range); + + if (auto* element = common_ancestor_container->parent_element()) { + DOM::ScrollIntoViewOptions scroll_options; + scroll_options.block = Bindings::ScrollLogicalPosition::Nearest; + scroll_options.inline_ = Bindings::ScrollLogicalPosition::Nearest; + scroll_options.behavior = Bindings::ScrollBehavior::Instant; + (void)element->scroll_into_view(scroll_options); + } +} + } template<> diff --git a/Userland/Libraries/LibWeb/Page/Page.h b/Userland/Libraries/LibWeb/Page/Page.h index e2f347c909d..2911089cd6a 100644 --- a/Userland/Libraries/LibWeb/Page/Page.h +++ b/Userland/Libraries/LibWeb/Page/Page.h @@ -178,12 +178,20 @@ public: bool pdf_viewer_supported() const { return m_pdf_viewer_supported; } + void clear_selection(); + + void find_in_page(String const& query); + void find_in_page_next_match(); + void find_in_page_previous_match(); + private: explicit Page(JS::NonnullGCPtr); virtual void visit_edges(Visitor&) override; JS::GCPtr media_context_menu_element(); + void update_find_in_page_selection(); + JS::NonnullGCPtr m_client; WeakPtr m_focused_navigable; @@ -225,6 +233,8 @@ 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 }; + Vector> m_find_in_page_matches; }; struct PaintOptions { diff --git a/Userland/Libraries/LibWebView/ViewImplementation.cpp b/Userland/Libraries/LibWebView/ViewImplementation.cpp index bba1b31e6ae..1fc13499ce2 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.cpp +++ b/Userland/Libraries/LibWebView/ViewImplementation.cpp @@ -180,6 +180,21 @@ void ViewImplementation::paste(String const& text) client().async_paste(page_id(), text); } +void ViewImplementation::find_in_page(String const& query) +{ + client().async_find_in_page(page_id(), query); +} + +void ViewImplementation::find_in_page_next_match() +{ + client().async_find_in_page_next_match(page_id()); +} + +void ViewImplementation::find_in_page_previous_match() +{ + client().async_find_in_page_previous_match(page_id()); +} + void ViewImplementation::get_source() { client().async_get_source(page_id()); diff --git a/Userland/Libraries/LibWebView/ViewImplementation.h b/Userland/Libraries/LibWebView/ViewImplementation.h index 7eddf695468..dcc242e7870 100644 --- a/Userland/Libraries/LibWebView/ViewImplementation.h +++ b/Userland/Libraries/LibWebView/ViewImplementation.h @@ -66,6 +66,9 @@ public: ByteString selected_text(); Optional selected_text_with_whitespace_collapsed(); void select_all(); + void find_in_page(String const& query); + void find_in_page_next_match(); + void find_in_page_previous_match(); void paste(String const&); void get_source(); diff --git a/Userland/Services/WebContent/ConnectionFromClient.cpp b/Userland/Services/WebContent/ConnectionFromClient.cpp index 303aa6d3a85..3fe422f0399 100644 --- a/Userland/Services/WebContent/ConnectionFromClient.cpp +++ b/Userland/Services/WebContent/ConnectionFromClient.cpp @@ -827,6 +827,33 @@ void ConnectionFromClient::select_all(u64 page_id) page->page().focused_navigable().select_all(); } +void ConnectionFromClient::find_in_page(u64 page_id, String const& query) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + page->page().find_in_page(query); +} + +void ConnectionFromClient::find_in_page_next_match(u64 page_id) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + page->page().find_in_page_next_match(); +} + +void ConnectionFromClient::find_in_page_previous_match(u64 page_id) +{ + auto page = this->page(page_id); + if (!page.has_value()) + return; + + page->page().find_in_page_previous_match(); +} + void ConnectionFromClient::paste(u64 page_id, String const& text) { if (auto page = this->page(page_id); page.has_value()) diff --git a/Userland/Services/WebContent/ConnectionFromClient.h b/Userland/Services/WebContent/ConnectionFromClient.h index 6759c961638..e487facd25f 100644 --- a/Userland/Services/WebContent/ConnectionFromClient.h +++ b/Userland/Services/WebContent/ConnectionFromClient.h @@ -130,6 +130,10 @@ private: virtual Messages::WebContentServer::GetSelectedTextResponse get_selected_text(u64 page_id) override; virtual void select_all(u64 page_id) override; + virtual void find_in_page(u64 page_id, String const& query) override; + virtual void find_in_page_next_match(u64 page_id) override; + virtual void find_in_page_previous_match(u64 page_id) override; + virtual void paste(u64 page_id, String const& text) override; void report_finished_handling_input_event(u64 page_id, bool event_was_handled); diff --git a/Userland/Services/WebContent/WebContentServer.ipc b/Userland/Services/WebContent/WebContentServer.ipc index 90634584ca5..b58f06ef7bd 100644 --- a/Userland/Services/WebContent/WebContentServer.ipc +++ b/Userland/Services/WebContent/WebContentServer.ipc @@ -69,6 +69,10 @@ endpoint WebContentServer select_all(u64 page_id) =| paste(u64 page_id, String text) =| + find_in_page(u64 page_id, String query) =| + find_in_page_next_match(u64 page_id) =| + find_in_page_previous_match(u64 page_id) =| + set_content_filters(u64 page_id, Vector filters) =| set_autoplay_allowed_on_all_websites(u64 page_id) =| set_autoplay_allowlist(u64 page_id, Vector allowlist) =|