LibWeb: Paint inspector overlays as a separate pass

The overlay shown for the node hovered in the inspector is painted as
part of the normal tree traversal of all paintables. This works well in
most cases, but falls short in specific scenarios:
* If the hovered node or one of its ancestors establishes a stacking
  context and there is another element that establishes a stacking
  context close by or overlapping it, the overlay and especially the
  tooltip can become partially hidden behind the second element. Ditto
  for elements that act as if they established a stacking context.
* If the hovered node or one of its ancestors involves clipping, the
  clip is applied to the overlay and espicially the tooltip. This can
  cause them to be partially invisible.
* Similarly, if the hovered node or one of its ancestors has a defined
  mask, the mask is applied to the overlay, often making it mostly
  invisible.
* No overlays are shown for SVG nodes because they are painted
  differently from HTML documents.

Some of these problems may be fixable with the current system. But some
seem like they fundamentally cannot work fully when the overlays are
painted as part of the regular tree traversal.

Instead we pull out painting the overlay as a separate pass executed
after the tree traversal. This way we ensure that the overlays are
always painted last and therefore on top of everything else. This also
makes sure that the overlays are unaffected by clips and masks. And
since overlay painting is independent from painting the actual elements,
it just works as well.

However we need to be careful, because we still need to apply some of
the steps of the tree traversal to get the correct result. Namely we
need to apply scroll offsets and transforms. To do so, we collect all
ancestors of the hovered node and apply those as if we were in the
normal tree traversal.
This commit is contained in:
InvalidUsernameException 2025-09-02 22:57:41 +02:00 committed by Jelle Raaijmakers
commit fbf47e57d0
Notes: github-actions[bot] 2025-09-19 08:18:59 +00:00
7 changed files with 104 additions and 41 deletions

View file

@ -6541,6 +6541,10 @@ RefPtr<Painting::DisplayList> Document::record_display_list(HTML::PaintConfig co
viewport_paintable.paint_all_phases(context);
if (highlighted_node() && highlighted_node()->paintable()) {
highlighted_node()->paintable()->paint_inspector_overlay(context);
}
m_cached_display_list = display_list;
m_cached_display_list_paint_config = config;

View file

@ -6,6 +6,8 @@
*/
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/DisplayListRecordingContext.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/StackingContext.h>
@ -156,6 +158,44 @@ StackingContext* Paintable::enclosing_stacking_context()
VERIFY_NOT_REACHED();
}
void Paintable::paint_inspector_overlay(DisplayListRecordingContext& context) const
{
Vector<Painting::Paintable const*> self_and_ancestors {};
for (Paintable const* paintable = this; paintable; paintable = paintable->parent()) {
self_and_ancestors.append(paintable);
}
for (auto const* paintable : self_and_ancestors.in_reverse()) {
if (auto const* box = as_if<PaintableBox>(paintable)) {
box->apply_scroll_offset(context);
if (box->stacking_context()) {
auto to_device_pixels_scale = float(context.device_pixels_per_css_pixel());
auto transform_matrix = box->transform();
auto transform_origin = box->transform_origin().to_type<float>();
// We only want the transform here, everything else undesirable for the inspector overlay
DisplayListRecorder::PushStackingContextParams push_stacking_context_params {
.opacity = 1.0,
.compositing_and_blending_operator = Gfx::CompositingAndBlendingOperator::Normal,
.isolate = false,
.transform = StackingContextTransform(transform_origin, transform_matrix, to_device_pixels_scale),
};
context.display_list_recorder().push_stacking_context(push_stacking_context_params);
}
}
}
paint_inspector_overlay_internal(context);
for (auto const* paintable : self_and_ancestors) {
if (auto const* box = as_if<PaintableBox>(paintable)) {
if (box->stacking_context()) {
context.display_list_recorder().pop_stacking_context();
}
box->reset_scroll_offset(context);
}
}
}
void Paintable::set_needs_display(InvalidateDisplayList should_invalidate_display_list)
{
auto& document = const_cast<DOM::Document&>(this->document());

View file

@ -75,6 +75,7 @@ public:
virtual void after_paint(DisplayListRecordingContext&, PaintPhase) const { }
virtual void paint(DisplayListRecordingContext&, PaintPhase) const { }
void paint_inspector_overlay(DisplayListRecordingContext&) const;
[[nodiscard]] virtual TraversalDecision hit_test(CSSPixelPoint, HitTestType, Function<TraversalDecision(HitTestResult)> const& callback) const;
@ -154,6 +155,7 @@ public:
protected:
explicit Paintable(Layout::Node const&);
virtual void paint_inspector_overlay_internal(DisplayListRecordingContext&) const { }
virtual void visit_edges(Cell::Visitor&) override;
private:

View file

@ -502,8 +502,10 @@ void PaintableBox::paint(DisplayListRecordingContext& context, PaintPhase phase)
context.display_list_recorder().paint_scrollbar(own_scroll_frame_id().value(), gutter_rect, thumb_rect, scrollbar_data->scroll_length, scrollbar_colors.thumb_color, scrollbar_colors.track_color, false);
}
}
}
if (phase == PaintPhase::Overlay && layout_node().document().highlighted_layout_node() == &layout_node_with_style_and_box_metrics()) {
void PaintableBox::paint_inspector_overlay_internal(DisplayListRecordingContext& context) const
{
auto content_rect = absolute_united_content_rect();
auto margin_rect = united_rect_for_continuation_chain(*this, [](PaintableBox const& box) {
auto margin_box = box.box_model().margin_box();
@ -547,7 +549,6 @@ void PaintableBox::paint(DisplayListRecordingContext& context, PaintPhase phase)
context.display_list_recorder().draw_rect(size_text_device_rect, context.palette().threed_shadow1());
context.display_list_recorder().draw_text(size_text_device_rect, size_text, font->with_size(font->point_size() * context.device_pixels_per_css_pixel()), Gfx::TextAlignment::Center, context.palette().color(Gfx::ColorRole::TooltipText));
}
}
void PaintableBox::set_stacking_context(NonnullOwnPtr<StackingContext> stacking_context)
{
@ -976,7 +977,7 @@ void paint_text_fragment(DisplayListRecordingContext& context, TextPaintable con
auto fragment_absolute_rect = fragment.absolute_rect();
auto fragment_enclosing_device_rect = context.enclosing_device_rect(fragment_absolute_rect).to_type<int>();
if (paintable.document().highlighted_layout_node() == &paintable.layout_node() || context.should_show_line_box_borders())
if (context.should_show_line_box_borders())
paint_text_fragment_debug_highlight(context, fragment);
auto glyph_run = fragment.glyph_run();

View file

@ -274,6 +274,8 @@ protected:
virtual void paint_background(DisplayListRecordingContext&) const;
virtual void paint_box_shadow(DisplayListRecordingContext&) const;
virtual void paint_inspector_overlay_internal(DisplayListRecordingContext&) const override;
virtual CSSPixelRect compute_absolute_rect() const;
virtual CSSPixelRect compute_absolute_paint_rect() const;

View file

@ -59,4 +59,15 @@ TextPaintable::DispatchEventOfSameName TextPaintable::handle_mousemove(Badge<Eve
return DispatchEventOfSameName::Yes;
}
void TextPaintable::paint_inspector_overlay_internal(DisplayListRecordingContext& context) const
{
if (auto const* parent_paintable = as_if<PaintableWithLines>(parent())) {
for (auto const& fragment : parent_paintable->fragments()) {
if (&fragment.paintable() == this) {
paint_text_fragment_debug_highlight(context, fragment);
}
}
}
}
}

View file

@ -25,6 +25,9 @@ public:
virtual DispatchEventOfSameName handle_mouseup(Badge<EventHandler>, CSSPixelPoint, unsigned button, unsigned modifiers) override;
virtual DispatchEventOfSameName handle_mousemove(Badge<EventHandler>, CSSPixelPoint, unsigned button, unsigned modifiers) override;
protected:
virtual void paint_inspector_overlay_internal(DisplayListRecordingContext&) const override;
private:
virtual bool is_text_paintable() const override { return true; }