浏览代码

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.
Tim Ledbetter 1 年之前
父节点
当前提交
7aea87c9df

+ 29 - 0
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<JS::Handle<DOM::Range>> Document::find_matching_text(String const& query)
+{
+    if (!document_element() || !document_element()->layout_node())
+        return {};
+
+    Vector<JS::Handle<DOM::Range>> matches;
+    document_element()->layout_node()->for_each_in_inclusive_subtree_of_type<Layout::TextNode>([&](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<DOM::Text&>(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;
+}
+
 }

+ 2 - 0
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<JS::Handle<DOM::Range>> find_matching_text(String const&);
+
 protected:
     virtual void initialize(JS::Realm&) override;
     virtual void visit_edges(Cell::Visitor&) override;

+ 103 - 0
Userland/Libraries/LibWeb/Page/Page.cpp

@@ -1,6 +1,7 @@
 /*
  * Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
+ * Copyright (c) 2024, Tim Ledbetter <timledbetter@gmail.com>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -11,6 +12,7 @@
 #include <LibIPC/Encoder.h>
 #include <LibWeb/CSS/StyleComputer.h>
 #include <LibWeb/DOM/Document.h>
+#include <LibWeb/DOM/Range.h>
 #include <LibWeb/HTML/BrowsingContext.h>
 #include <LibWeb/HTML/EventLoop/EventLoop.h>
 #include <LibWeb/HTML/HTMLInputElement.h>
@@ -20,8 +22,10 @@
 #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
 #include <LibWeb/HTML/SelectedFile.h>
 #include <LibWeb/HTML/TraversableNavigable.h>
+#include <LibWeb/HTML/Window.h>
 #include <LibWeb/Page/Page.h>
 #include <LibWeb/Platform/EventLoopPlugin.h>
+#include <LibWeb/Selection/Selection.h>
 
 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<JS::Handle<DOM::Range>> 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<>

+ 10 - 0
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<PageClient>);
     virtual void visit_edges(Visitor&) override;
 
     JS::GCPtr<HTML::HTMLMediaElement> media_context_menu_element();
 
+    void update_find_in_page_selection();
+
     JS::NonnullGCPtr<PageClient> m_client;
 
     WeakPtr<HTML::Navigable> 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<JS::NonnullGCPtr<DOM::Range>> m_find_in_page_matches;
 };
 
 struct PaintOptions {

+ 15 - 0
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());

+ 3 - 0
Userland/Libraries/LibWebView/ViewImplementation.h

@@ -66,6 +66,9 @@ public:
     ByteString selected_text();
     Optional<String> 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();

+ 27 - 0
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())

+ 4 - 0
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);

+ 4 - 0
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<String> filters) =|
     set_autoplay_allowed_on_all_websites(u64 page_id) =|
     set_autoplay_allowlist(u64 page_id, Vector<String> allowlist) =|