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.
This commit is contained in:
Tim Ledbetter 2024-05-29 20:09:33 +01:00 committed by Andreas Kling
commit 7aea87c9df
Notes: sideshowbarker 2024-07-16 18:06:41 +09:00
9 changed files with 197 additions and 0 deletions

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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<>

View file

@ -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 {

View file

@ -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());

View file

@ -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();

View file

@ -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())

View file

@ -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);

View file

@ -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) =|