LibWeb: Add a caret hit-test debug overlay

Add a debug-menu toggle for caret hit testing at the mouse position.
Paint the insertion rect and log the result so selection bugs can be
inspected without temporary probes.

Request frames and repaint invalidation when the overlay state changes.
Also repaint when the caret rect moves within the same text node.
This commit is contained in:
Andreas Kling 2026-05-30 10:04:28 +02:00 committed by Andreas Kling
parent 5c26e86354
commit acc86e9eb1
10 changed files with 89 additions and 2 deletions

View file

@ -8436,10 +8436,32 @@ RefPtr<Painting::DisplayList> Document::record_display_list(HTML::PaintConfig co
paintable_box->paint_grid_inspector_overlay(context, grid_highlight.options);
}
if (config.should_show_caret_hit_test_debug_overlay && m_caret_hit_test_debug_rect.has_value()) {
auto caret_rect = context.enclosing_device_rect(*m_caret_hit_test_debug_rect).to_type<int>();
auto caret_x = caret_rect.x();
auto caret_top = caret_rect.y();
auto caret_bottom = caret_rect.bottom();
auto marker_color = Color::Magenta;
display_list_recorder.draw_line({ caret_x, caret_top }, { caret_x, caret_bottom }, marker_color, 2);
display_list_recorder.draw_line({ caret_x - 4, caret_top }, { caret_x + 4, caret_top }, marker_color, 2);
display_list_recorder.draw_line({ caret_x - 4, caret_bottom }, { caret_x + 4, caret_bottom }, marker_color, 2);
}
m_hit_test_display_list = move(hit_test_display_list);
return display_list;
}
void Document::set_caret_hit_test_debug_rect(Optional<CSSPixelRect> rect)
{
if (m_caret_hit_test_debug_rect == rect)
return;
m_caret_hit_test_debug_rect = rect;
set_needs_repaint(InvalidateDisplayList::Yes);
page().client().request_frame();
}
Painting::HitTestDisplayList const* Document::ensure_hit_test_display_list()
{
auto viewport_paintable = paintable();

View file

@ -985,6 +985,8 @@ public:
GC::Ptr<CaretPosition> caret_position_from_point(double x, double y, Bindings::CaretPositionFromPointOptions const&);
TraversalDecision hit_test_all(CSSPixelPoint, Function<TraversalDecision(Painting::HitTestResult)> const&);
void set_caret_hit_test_debug_rect(Optional<CSSPixelRect>);
void set_needs_to_record_display_list();
Unicode::Segmenter& grapheme_segmenter() const;
@ -1480,6 +1482,7 @@ private:
bool m_needs_accumulated_visual_contexts_update { false };
bool m_needs_invalidation_of_elements_affected_by_has { false };
RefPtr<Painting::HitTestDisplayList> m_hit_test_display_list;
Optional<CSSPixelRect> m_caret_hit_test_debug_rect;
mutable StyleInvalidationCounters m_style_invalidation_counters;
mutable u64 m_style_invalidations_since_last_counter_dump { 0 };

View file

@ -434,8 +434,10 @@ void Navigable::initialize_navigable(NonnullRefPtr<DocumentState> document_state
// 5. Set navigable's parent to parent.
m_parent = parent;
if (parent)
if (parent) {
m_should_show_line_box_borders = parent->m_should_show_line_box_borders;
m_should_show_caret_hit_test_debug_overlay = parent->m_should_show_caret_hit_test_debug_overlay;
}
if (parent && !m_is_svg_page && has_compositor_context() && parent->has_compositor_context()) {
m_compositor_surface_id = Painting::allocate_compositor_surface_id();
compositor_context().set_presentation_mode(Compositor::PublishToCompositorSurface {
@ -3444,6 +3446,21 @@ void Navigable::set_should_show_line_box_borders(bool value)
child_navigable->set_should_show_line_box_borders(value);
}
void Navigable::set_should_show_caret_hit_test_debug_overlay(bool value)
{
m_should_show_caret_hit_test_debug_overlay = value;
if (auto document = active_document()) {
if (value)
document->set_needs_repaint(Badge<HTML::Navigable> {}, InvalidateDisplayList::Yes);
else
document->set_caret_hit_test_debug_rect({});
}
for (auto const& child_navigable : child_navigables())
child_navigable->set_should_show_caret_hit_test_debug_overlay(value);
}
bool Navigable::record_display_list_and_scroll_state(PaintConfig paint_config)
{
if (!has_compositor_context())
@ -3504,7 +3521,7 @@ void Navigable::paint_next_frame()
}
auto viewport_rect = page().css_to_device_rect(this->viewport_rect()).to_type<int>();
PaintConfig paint_config { .paint_overlay = true, .should_show_line_box_borders = m_should_show_line_box_borders };
PaintConfig paint_config { .paint_overlay = true, .should_show_line_box_borders = m_should_show_line_box_borders, .should_show_caret_hit_test_debug_overlay = m_should_show_caret_hit_test_debug_overlay };
if (is_top_level_traversable()) {
paint_config.canvas_fill_rect = Gfx::IntRect { {}, viewport_rect.size() };
} else {

View file

@ -257,6 +257,8 @@ public:
bool pending_set_browser_zoom_request() const { return m_pending_set_browser_zoom_request; }
void set_should_show_line_box_borders(bool);
void set_should_show_caret_hit_test_debug_overlay(bool);
bool should_show_caret_hit_test_debug_overlay() const { return m_should_show_caret_hit_test_debug_overlay; }
bool is_svg_page() const { return m_is_svg_page; }
@ -344,6 +346,7 @@ private:
bool m_needs_to_record_display_list { true };
bool m_pending_set_browser_zoom_request { false };
bool m_should_show_line_box_borders { false };
bool m_should_show_caret_hit_test_debug_overlay { false };
Optional<PaintConfig> m_compositor_display_list_paint_config;
Painting::DisplayListResourceStorage m_display_list_resource_storage;
Painting::DisplayListResourceSet m_compositor_display_list_resources;

View file

@ -14,6 +14,7 @@ namespace Web::HTML {
struct PaintConfig {
bool paint_overlay { false };
bool should_show_line_box_borders { false };
bool should_show_caret_hit_test_debug_overlay { false };
Optional<Gfx::IntRect> canvas_fill_rect {};
bool operator==(PaintConfig const& other) const = default;

View file

@ -271,6 +271,34 @@ EventResult EventHandler::handle_mousemove(CSSPixelPoint visual_viewport_positio
if (!paint_root())
return EventResult::Dropped;
ArmedScopeGuard update_caret_hit_test_debug_overlay = [&] {
if (!m_navigable->should_show_caret_hit_test_debug_overlay())
return;
if (m_navigable->active_document() != document)
return;
if (!document->is_fully_active())
return;
document->update_layout(DOM::UpdateLayoutReason::EventHandlerHandleMouseMove);
if (!paint_root())
return;
auto caret_position = document->caret_position_from_point(visual_viewport_position);
if (caret_position.has_value()) {
document->set_caret_hit_test_debug_rect(caret_position->debug_rect);
dbgln("Caret hit test: point=({}, {}) boundary=({}, {}) paintable={} debug_rect={}",
visual_viewport_position.x(),
visual_viewport_position.y(),
caret_position->boundary.node->debug_description(),
caret_position->boundary.offset,
caret_position->paintable->debug_description(),
caret_position->debug_rect);
} else {
document->set_caret_hit_test_debug_rect({});
dbgln("Caret hit test: point=({}, {}) no caret position", visual_viewport_position.x(), visual_viewport_position.y());
}
};
if (m_mousedown_target_is_drag_candidate) {
static constexpr CSSPixels DRAG_THRESHOLD = 5;
auto delta = visual_viewport_position - *m_mousedown_visual_viewport_position;

View file

@ -1519,6 +1519,9 @@ void Application::initialize_actions()
m_show_line_box_borders_action = Action::create_checkable("Show Line Box Borders"sv, ActionID::ShowLineBoxBorders, check(m_show_line_box_borders_action, "set-line-box-borders"sv));
m_debug_menu->add_action(*m_show_line_box_borders_action);
m_show_caret_hit_test_debug_overlay_action = Action::create_checkable("Show Caret Hit Test Debug Overlay"sv, ActionID::ShowCaretHitTestDebugOverlay, check(m_show_caret_hit_test_debug_overlay_action, "set-caret-hit-test-debug-overlay"sv));
m_debug_menu->add_action(*m_show_caret_hit_test_debug_overlay_action);
m_debug_menu->add_separator();
m_debug_menu->add_action(Action::create("Collect Garbage"sv, ActionID::CollectGarbage, debug_request("collect-garbage"sv)));
@ -1565,6 +1568,7 @@ void Application::apply_view_options(Badge<ViewImplementation>, ViewImplementati
view.set_preferred_motion(m_motion);
view.debug_request("set-line-box-borders"sv, m_show_line_box_borders_action->checked() ? "on"sv : "off"sv);
view.debug_request("set-caret-hit-test-debug-overlay"sv, m_show_caret_hit_test_debug_overlay_action->checked() ? "on"sv : "off"sv);
view.debug_request("scripting"sv, m_enable_scripting_action->checked() ? "on"sv : "off"sv);
view.debug_request("content-blocking"sv, m_enable_content_blocking_action->checked() ? "on"sv : "off"sv);
if (m_content_blocker_list_buffer.has_value())

View file

@ -375,6 +375,7 @@ private:
RefPtr<Menu> m_debug_menu;
RefPtr<Action> m_show_line_box_borders_action;
RefPtr<Action> m_show_caret_hit_test_debug_overlay_action;
RefPtr<Action> m_enable_scripting_action;
RefPtr<Action> m_enable_content_blocking_action;
RefPtr<Action> m_block_pop_ups_action;

View file

@ -100,6 +100,7 @@ enum class ActionID {
DumpGCGraph,
DumpWasmStats,
ShowLineBoxBorders,
ShowCaretHitTestDebugOverlay,
CollectGarbage,
CrashCurrentPage,
SpoofUserAgent,

View file

@ -463,6 +463,13 @@ void ConnectionFromClient::debug_request(u64 page_id, ByteString request, ByteSt
return;
}
if (request == "set-caret-hit-test-debug-overlay") {
bool state = argument == "on";
auto traversable = page->page().top_level_traversable();
traversable->set_should_show_caret_hit_test_debug_overlay(state);
return;
}
if (request == "clear-cache") {
Web::Fetch::Fetching::clear_http_memory_cache();
return;