mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2026-04-18 18:00:31 +00:00
LibWeb: Integrate clip-path into AccumulatedVisualContext
Previously, clip-path was applied only during painting in StackingContext::paint(), which meant hit testing did not respect clip-path boundaries. Clicks outside the visible clipped region but inside the element's bounding box would incorrectly register as hits. By moving clip-path into AccumulatedVisualContext, it becomes part of the same system that handles transforms, clips, and scroll offsets for both painting and hit testing, ensuring consistent behavior.
This commit is contained in:
parent
795639fd2b
commit
98afd82491
Notes:
github-actions[bot]
2026-01-16 12:40:14 +00:00
Author: https://github.com/kalenikaliaksandr
Commit: 98afd82491
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/7484
Reviewed-by: https://github.com/gmta ✅
13 changed files with 52 additions and 54 deletions
|
|
@ -61,6 +61,14 @@ Optional<CSSPixelPoint> AccumulatedVisualContext::transform_point_for_hit_test(C
|
|||
if (!clip.rect.contains(point_in_document))
|
||||
return {};
|
||||
return point;
|
||||
},
|
||||
[&](ClipPathData const& clip_path) -> Optional<CSSPixelPoint> {
|
||||
auto point_in_document = current_to_document.map(point.to_type<float>()).to_type<CSSPixels>();
|
||||
if (!clip_path.bounding_rect.contains(point_in_document))
|
||||
return {};
|
||||
if (!clip_path.path.contains(point_in_document.to_type<float>(), clip_path.fill_rule))
|
||||
return {};
|
||||
return point;
|
||||
});
|
||||
|
||||
if (!result.has_value())
|
||||
|
|
@ -94,6 +102,10 @@ void AccumulatedVisualContext::dump(StringBuilder& builder) const
|
|||
auto const& corner_radii = clip.corner_radii;
|
||||
builder.appendff(" radii=({},{},{},{})", corner_radii.top_left.horizontal_radius, corner_radii.top_right.horizontal_radius, corner_radii.bottom_right.horizontal_radius, corner_radii.bottom_left.horizontal_radius);
|
||||
}
|
||||
},
|
||||
[&](ClipPathData const& clip_path) {
|
||||
auto const& rect = clip_path.bounding_rect;
|
||||
builder.appendff("clip_path=[bounds: {},{} {}x{}]", rect.x().to_float(), rect.y().to_float(), rect.width().to_float(), rect.height().to_float());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
#include <AK/AtomicRefCounted.h>
|
||||
#include <AK/Variant.h>
|
||||
#include <LibGfx/Matrix4x4.h>
|
||||
#include <LibGfx/Path.h>
|
||||
#include <LibGfx/WindingRule.h>
|
||||
#include <LibWeb/Painting/BorderRadiiData.h>
|
||||
#include <LibWeb/Painting/ScrollState.h>
|
||||
|
||||
|
|
@ -50,7 +52,13 @@ struct PerspectiveData {
|
|||
Gfx::FloatMatrix4x4 matrix;
|
||||
};
|
||||
|
||||
using VisualContextData = Variant<ScrollData, ClipData, TransformData, PerspectiveData>;
|
||||
struct ClipPathData {
|
||||
Gfx::Path path;
|
||||
CSSPixelRect bounding_rect;
|
||||
Gfx::WindingRule fill_rule;
|
||||
};
|
||||
|
||||
using VisualContextData = Variant<ScrollData, ClipData, TransformData, PerspectiveData, ClipPathData>;
|
||||
|
||||
class AccumulatedVisualContext : public AtomicRefCounted<AccumulatedVisualContext> {
|
||||
public:
|
||||
|
|
@ -63,6 +71,7 @@ public:
|
|||
bool is_clip() const { return m_data.has<ClipData>(); }
|
||||
bool is_transform() const { return m_data.has<TransformData>(); }
|
||||
bool is_perspective() const { return m_data.has<PerspectiveData>(); }
|
||||
bool is_clip_path() const { return m_data.has<ClipPathData>(); }
|
||||
|
||||
size_t depth() const { return m_depth; }
|
||||
size_t id() const { return m_id; }
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
|
|||
add_rounded_rect_clip({ .corner_radii = corner_radii, .border_rect = device_rect, .corner_clip = CornerClip::Outside });
|
||||
else
|
||||
add_clip_rect({ .rect = device_rect });
|
||||
},
|
||||
[&](ClipPathData const& clip_path) {
|
||||
auto transformed_path = clip_path.path.copy_transformed(Gfx::AffineTransform {}.set_scale(static_cast<float>(device_pixels_per_css_pixel), static_cast<float>(device_pixels_per_css_pixel)));
|
||||
add_clip_path(transformed_path);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -194,7 +198,6 @@ void DisplayListPlayer::execute_impl(DisplayList& display_list, ScrollStateSnaps
|
|||
else HANDLE_COMMAND(DrawScaledImmutableBitmap, draw_scaled_immutable_bitmap)
|
||||
else HANDLE_COMMAND(DrawRepeatedImmutableBitmap, draw_repeated_immutable_bitmap)
|
||||
else HANDLE_COMMAND(AddClipRect, add_clip_rect)
|
||||
else HANDLE_COMMAND(AddClipPath, add_clip_path)
|
||||
else HANDLE_COMMAND(Save, save)
|
||||
else HANDLE_COMMAND(SaveLayer, save_layer)
|
||||
else HANDLE_COMMAND(Restore, restore)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ private:
|
|||
virtual void restore(Restore const&) = 0;
|
||||
virtual void translate(Translate const&) = 0;
|
||||
virtual void add_clip_rect(AddClipRect const&) = 0;
|
||||
virtual void add_clip_path(AddClipPath const&) = 0;
|
||||
virtual void paint_linear_gradient(PaintLinearGradient const&) = 0;
|
||||
virtual void paint_radial_gradient(PaintRadialGradient const&) = 0;
|
||||
virtual void paint_conic_gradient(PaintConicGradient const&) = 0;
|
||||
|
|
@ -69,6 +68,8 @@ private:
|
|||
virtual void apply_mask_bitmap(ApplyMaskBitmap const&) = 0;
|
||||
virtual bool would_be_fully_clipped_by_painter(Gfx::IntRect) const = 0;
|
||||
|
||||
virtual void add_clip_path(Gfx::Path const&) = 0;
|
||||
|
||||
Vector<NonnullRefPtr<Gfx::PaintingSurface>, 1> m_surfaces;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -72,11 +72,6 @@ void AddClipRect::dump(StringBuilder& builder) const
|
|||
builder.appendff(" rect={}", rect);
|
||||
}
|
||||
|
||||
void AddClipPath::dump(StringBuilder& builder) const
|
||||
{
|
||||
builder.appendff(" bounding_rect={}", bounding_rectangle);
|
||||
}
|
||||
|
||||
void PaintLinearGradient::dump(StringBuilder& builder) const
|
||||
{
|
||||
builder.appendff(" rect={}", gradient_rect);
|
||||
|
|
|
|||
|
|
@ -141,17 +141,6 @@ struct AddClipRect {
|
|||
void dump(StringBuilder&) const;
|
||||
};
|
||||
|
||||
struct AddClipPath {
|
||||
static constexpr StringView command_name = "AddClipPath"sv;
|
||||
|
||||
Gfx::Path path;
|
||||
Gfx::IntRect bounding_rectangle;
|
||||
|
||||
[[nodiscard]] Gfx::IntRect bounding_rect() const { return bounding_rectangle; }
|
||||
bool is_clip_or_mask() const { return true; }
|
||||
void dump(StringBuilder&) const;
|
||||
};
|
||||
|
||||
struct PaintLinearGradient {
|
||||
static constexpr StringView command_name = "PaintLinearGradient"sv;
|
||||
|
||||
|
|
@ -418,7 +407,6 @@ using DisplayListCommand = Variant<
|
|||
Restore,
|
||||
Translate,
|
||||
AddClipRect,
|
||||
AddClipPath,
|
||||
PaintLinearGradient,
|
||||
PaintRadialGradient,
|
||||
PaintConicGradient,
|
||||
|
|
|
|||
|
|
@ -201,12 +201,6 @@ void DisplayListPlayerSkia::add_clip_rect(AddClipRect const& command)
|
|||
canvas.clipRect(to_skia_rect(rect), true);
|
||||
}
|
||||
|
||||
void DisplayListPlayerSkia::add_clip_path(AddClipPath const& command)
|
||||
{
|
||||
auto& canvas = surface().canvas();
|
||||
canvas.clipPath(to_skia_path(command.path), true);
|
||||
}
|
||||
|
||||
void DisplayListPlayerSkia::save(Save const&)
|
||||
{
|
||||
auto& canvas = surface().canvas();
|
||||
|
|
@ -1016,6 +1010,12 @@ void DisplayListPlayerSkia::apply_mask_bitmap(ApplyMaskBitmap const& command)
|
|||
canvas.clipShader(builder.makeShader());
|
||||
}
|
||||
|
||||
void DisplayListPlayerSkia::add_clip_path(Gfx::Path const& path)
|
||||
{
|
||||
auto& canvas = surface().canvas();
|
||||
canvas.clipPath(to_skia_path(path), true);
|
||||
}
|
||||
|
||||
bool DisplayListPlayerSkia::would_be_fully_clipped_by_painter(Gfx::IntRect rect) const
|
||||
{
|
||||
return surface().canvas().quickReject(to_skia_rect(rect));
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ private:
|
|||
void draw_scaled_immutable_bitmap(DrawScaledImmutableBitmap const&) override;
|
||||
void draw_repeated_immutable_bitmap(DrawRepeatedImmutableBitmap const&) override;
|
||||
void add_clip_rect(AddClipRect const&) override;
|
||||
void add_clip_path(AddClipPath const&) override;
|
||||
void save(Save const&) override;
|
||||
void save_layer(SaveLayer const&) override;
|
||||
void restore(Restore const&) override;
|
||||
|
|
@ -56,6 +55,8 @@ private:
|
|||
void apply_transform(ApplyTransform const&) override;
|
||||
void apply_mask_bitmap(ApplyMaskBitmap const&) override;
|
||||
|
||||
void add_clip_path(Gfx::Path const&) override;
|
||||
|
||||
bool would_be_fully_clipped_by_painter(Gfx::IntRect) const override;
|
||||
|
||||
RefPtr<Gfx::SkiaBackendContext> m_context;
|
||||
|
|
|
|||
|
|
@ -256,11 +256,6 @@ void DisplayListRecorder::add_clip_rect(Gfx::IntRect const& rect)
|
|||
APPEND(AddClipRect { rect });
|
||||
}
|
||||
|
||||
void DisplayListRecorder::add_clip_path(Gfx::Path const& path, Gfx::IntRect bounding_rect)
|
||||
{
|
||||
APPEND(AddClipPath { .path = path, .bounding_rectangle = bounding_rect });
|
||||
}
|
||||
|
||||
void DisplayListRecorder::translate(Gfx::IntPoint delta)
|
||||
{
|
||||
APPEND(Translate { delta });
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ public:
|
|||
void draw_glyph_run(Gfx::FloatPoint baseline_start, Gfx::GlyphRun const& glyph_run, Color color, Gfx::IntRect const& rect, double scale, Gfx::Orientation);
|
||||
|
||||
void add_clip_rect(Gfx::IntRect const& rect);
|
||||
void add_clip_path(Gfx::Path const& path, Gfx::IntRect bounding_rect);
|
||||
|
||||
void translate(Gfx::IntPoint delta);
|
||||
|
||||
|
|
|
|||
|
|
@ -311,24 +311,11 @@ void StackingContext::paint(DisplayListRecordingContext& context) const
|
|||
auto compositing_and_blending_operator = mix_blend_mode_to_compositing_and_blending_operator(computed_values.mix_blend_mode());
|
||||
bool isolate = computed_values.isolation() == CSS::Isolation::Isolate;
|
||||
|
||||
Optional<Gfx::Path> clip_path;
|
||||
Gfx::IntRect clip_path_bounding_rect;
|
||||
if (auto cp = computed_values.clip_path(); cp.has_value() && cp->is_basic_shape()) {
|
||||
auto const& masking_area = paintable_box().get_masking_area();
|
||||
auto const& basic_shape = cp->basic_shape();
|
||||
auto path = basic_shape.to_path(*masking_area, paintable_box().layout_node());
|
||||
auto device_pixel_scale = context.device_pixels_per_css_pixel();
|
||||
auto source_paintable_rect = context.enclosing_device_rect(paintable_box().absolute_paint_rect()).to_type<int>();
|
||||
clip_path = path.copy_transformed(Gfx::AffineTransform {}.set_scale(device_pixel_scale, device_pixel_scale).set_translation(source_paintable_rect.location().to_type<float>()));
|
||||
clip_path_bounding_rect = source_paintable_rect;
|
||||
}
|
||||
|
||||
auto mask_image = computed_values.mask_image();
|
||||
Optional<Gfx::Filter> resolved_filter;
|
||||
if (computed_values.filter().has_filters())
|
||||
resolved_filter = paintable_box().resolve_filter(context, computed_values.filter());
|
||||
|
||||
bool needs_clip_path = clip_path.has_value();
|
||||
bool needs_opacity_layer = opacity != 1.0f || isolate;
|
||||
bool needs_blend_layer = compositing_and_blending_operator != Gfx::CompositingAndBlendingOperator::Normal;
|
||||
bool needs_stacking_layer = needs_opacity_layer || needs_blend_layer;
|
||||
|
|
@ -341,18 +328,12 @@ void StackingContext::paint(DisplayListRecordingContext& context) const
|
|||
|
||||
context.display_list_recorder().set_accumulated_visual_context(stacking_state);
|
||||
|
||||
if (needs_clip_path) {
|
||||
context.display_list_recorder().save();
|
||||
context.display_list_recorder().add_clip_path(*clip_path, clip_path_bounding_rect);
|
||||
restore_count++;
|
||||
}
|
||||
|
||||
if (needs_stacking_layer || resolved_filter.has_value()) {
|
||||
context.display_list_recorder().apply_effects(opacity, compositing_and_blending_operator, resolved_filter);
|
||||
restore_count++;
|
||||
}
|
||||
|
||||
if (needs_to_save_state && !needs_stacking_layer && !needs_clip_path && !resolved_filter.has_value()) {
|
||||
if (needs_to_save_state && !needs_stacking_layer && !resolved_filter.has_value()) {
|
||||
context.display_list_recorder().save();
|
||||
restore_count++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,6 +182,20 @@ void ViewportPaintable::assign_accumulated_visual_contexts()
|
|||
if (auto css_clip = paintable_box.get_clip_rect(); css_clip.has_value())
|
||||
own_state = append_node(own_state, ClipData { effective_css_clip_rect(*css_clip), {} });
|
||||
|
||||
if (auto const& clip_path = paintable_box.computed_values().clip_path(); clip_path.has_value() && clip_path->is_basic_shape()) {
|
||||
if (auto masking_area = paintable_box.get_masking_area(); masking_area.has_value()) {
|
||||
auto reference_box = CSSPixelRect { {}, masking_area->size() };
|
||||
auto const& basic_shape = clip_path->basic_shape();
|
||||
auto path = basic_shape.to_path(reference_box, paintable_box.layout_node());
|
||||
path.offset(masking_area->top_left().template to_type<float>());
|
||||
auto fill_rule = basic_shape.basic_shape().visit(
|
||||
[](CSS::Polygon const& polygon) { return polygon.fill_rule; },
|
||||
[](CSS::Path const& path) { return path.fill_rule; },
|
||||
[](auto const&) { return Gfx::WindingRule::Nonzero; });
|
||||
own_state = append_node(own_state, ClipPathData { move(path), *masking_area, fill_rule });
|
||||
}
|
||||
}
|
||||
|
||||
paintable_box.set_accumulated_visual_context(own_state);
|
||||
|
||||
// Build state for descendants: own state + perspective + clip + scroll.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
Inside circle: PASS
|
||||
Outside circle (in bbox): FAIL
|
||||
Outside circle (in bbox): PASS
|
||||
Inside circle edge: PASS
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue