LibWeb: Paint CSS mask image layers with sizing

Store CSS mask image layers in computed values so painting can honor the
coordinated mask-position, mask-size, mask-repeat, mask-origin, and
mask-clip longhands. Paint the CSS mask display list through the same
layer resolution path used by backgrounds, but keep the nested mask
commands local to the mask rect so existing mask-image placement remains
unchanged.

Add ref coverage for a no-repeat mask image sized and positioned at the
bottom of an element, which previously painted as a full-element mask.
This commit is contained in:
Andreas Kling 2026-05-30 16:49:20 +02:00 committed by Andreas Kling
parent 6b8bd2699a
commit 114c8a7939
7 changed files with 139 additions and 9 deletions

View file

@ -662,6 +662,87 @@ Vector<BackgroundLayerData> ComputedProperties::background_layers() const
return layers;
}
Vector<BackgroundLayerData> ComputedProperties::mask_layers() const
{
auto property_values = [&](PropertyID property_id) {
auto const& value = property(property_id);
if (value.is_value_list())
return value.as_value_list().values();
return StyleValueVector { value };
};
auto const mask_image_values = property_values(PropertyID::MaskImage);
if (all_of(mask_image_values, [](auto const& value) { return value->to_keyword() == Keyword::None; }))
return {};
auto mask_clip_values = property_values(PropertyID::MaskClip);
auto mask_origin_values = property_values(PropertyID::MaskOrigin);
auto mask_position_values = property_values(PropertyID::MaskPosition);
auto mask_repeat_values = property_values(PropertyID::MaskRepeat);
auto mask_size_values = property_values(PropertyID::MaskSize);
Vector<BackgroundLayerData> layers;
layers.ensure_capacity(mask_image_values.size());
for (size_t i = 0; i < mask_image_values.size(); i++) {
auto const& mask_image_value = mask_image_values[i];
if (mask_image_value->to_keyword() == Keyword::None || !mask_image_value->is_abstract_image())
continue;
auto const& mask_clip_value = mask_clip_values[i % mask_clip_values.size()];
auto const& mask_origin_value = mask_origin_values[i % mask_origin_values.size()];
auto const& mask_position_value = mask_position_values[i % mask_position_values.size()];
auto const& mask_repeat_value = mask_repeat_values[i % mask_repeat_values.size()];
auto const& mask_size_value = mask_size_values[i % mask_size_values.size()];
BackgroundLayerData layer {
.background_image = mask_image_value->as_abstract_image(),
.origin = BackgroundBox::BorderBox,
.clip = BackgroundBox::BorderBox,
};
if (mask_clip_value->to_keyword() != Keyword::NoClip) {
if (auto clip = keyword_to_background_box(mask_clip_value->to_keyword()); clip.has_value())
layer.clip = clip.release_value();
}
if (auto origin = keyword_to_background_box(mask_origin_value->to_keyword()); origin.has_value())
layer.origin = origin.release_value();
auto const& position = mask_position_value->as_position();
layer.position_x = LengthPercentage::from_style_value(position.edge_x()->as_edge().offset());
layer.position_y = LengthPercentage::from_style_value(position.edge_y()->as_edge().offset());
layer.repeat_x = mask_repeat_value->as_repeat_style().repeat_x();
layer.repeat_y = mask_repeat_value->as_repeat_style().repeat_y();
if (mask_size_value->is_background_size()) {
layer.size_type = CSS::BackgroundSize::LengthPercentage;
layer.size_x = CSS::LengthPercentageOrAuto::from_style_value(mask_size_value->as_background_size().size_x());
layer.size_y = CSS::LengthPercentageOrAuto::from_style_value(mask_size_value->as_background_size().size_y());
} else if (mask_size_value->is_keyword()) {
switch (mask_size_value->to_keyword()) {
case CSS::Keyword::Contain:
layer.size_type = CSS::BackgroundSize::Contain;
break;
case CSS::Keyword::Cover:
layer.size_type = CSS::BackgroundSize::Cover;
break;
default:
VERIFY_NOT_REACHED();
}
} else {
VERIFY_NOT_REACHED();
}
layers.unchecked_append(layer);
}
return layers;
}
BackgroundBox ComputedProperties::background_color_clip() const
{
// The background color is clipped according to the final layer's background-clip value. We propagate this

View file

@ -109,6 +109,7 @@ public:
CSSPixels text_underline_offset() const;
TextUnderlinePosition text_underline_position() const;
Vector<BackgroundLayerData> background_layers() const;
Vector<BackgroundLayerData> mask_layers() const;
BackgroundBox background_color_clip() const;
Length border_spacing_horizontal() const;
Length border_spacing_vertical() const;

View file

@ -704,6 +704,7 @@ public:
Color background_color() const { return m_noninherited.background_color; }
BackgroundBox background_color_clip() const { return m_noninherited.background_color_clip; }
Vector<BackgroundLayerData> const& background_layers() const { return m_noninherited.background_layers; }
Vector<BackgroundLayerData> const& mask_layers() const { return m_noninherited.mask_layers; }
Color webkit_text_fill_color() const { return m_inherited.webkit_text_fill_color; }
@ -893,6 +894,7 @@ protected:
Color background_color { InitialValues::background_color() };
int order { InitialValues::order() };
Vector<BackgroundLayerData> background_layers;
Vector<BackgroundLayerData> mask_layers;
FlexDirection flex_direction { InitialValues::flex_direction() };
ColumnSpan column_span { InitialValues::column_span() };
BackgroundBox background_color_clip { InitialValues::background_color_clip() };
@ -980,6 +982,8 @@ protected:
{
for (auto& layer : background_layers)
layer.background_image->visit_edges(visitor);
for (auto& layer : mask_layers)
layer.background_image->visit_edges(visitor);
if (mask_image)
mask_image->visit_edges(visitor);
for (auto const& transform : transformations)
@ -1031,6 +1035,7 @@ public:
void set_background_color(Color color) { m_noninherited.background_color = color; }
void set_background_color_clip(BackgroundBox box) { m_noninherited.background_color_clip = box; }
void set_background_layers(Vector<BackgroundLayerData>&& layers) { m_noninherited.background_layers = move(layers); }
void set_mask_layers(Vector<BackgroundLayerData>&& layers) { m_noninherited.mask_layers = move(layers); }
void set_float(Float value) { m_noninherited.float_ = value; }
void set_clear(Clear value) { m_noninherited.clear = value; }
void set_z_index(Optional<int> value) { m_noninherited.z_index = move(value); }

View file

@ -732,6 +732,13 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_background_layers(move(background_layers));
auto mask_layers = computed_style.mask_layers();
for (auto const& layer : mask_layers)
const_cast<CSS::AbstractImageStyleValue&>(*layer.background_image).load_any_resources(*this);
computed_values.set_mask_layers(move(mask_layers));
computed_values.set_background_color(computed_style.color(CSS::PropertyID::BackgroundColor, color_resolution_context));
computed_values.set_background_color_clip(computed_style.background_color_clip());
@ -956,7 +963,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_shape_rendering(computed_style.shape_rendering());
computed_values.set_paint_order(computed_style.paint_order());
// FIXME: We should actually support more than one mask image rather than just using the first
// FIXME: We should support SVG mask references in every mask layer rather than just using the first.
auto const& mask_image = [&] -> CSS::StyleValue const& {
auto const& value = computed_style.property(CSS::PropertyID::MaskImage);

View file

@ -14,6 +14,7 @@
#include <LibWeb/Layout/ReplacedBox.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/BackgroundPainting.h>
#include <LibWeb/Painting/DisplayList.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/PaintableBox.h>
@ -349,11 +350,7 @@ void StackingContext::paint(DisplayListRecordingContext& context) const
});
auto const& computed_values = paintable_box().computed_values();
auto mask_image = computed_values.mask_image();
if (mask_image) {
mask_image->resolve_for_size(paintable_box().layout_node_with_style_and_box_metrics(), paintable_box().absolute_padding_box_rect().size());
}
auto const& mask_layers = computed_values.mask_layers();
auto effective_context_index = paintable_box().accumulated_visual_context_index();
context.display_list_recorder().set_accumulated_visual_context(effective_context_index);
@ -368,13 +365,16 @@ void StackingContext::paint(DisplayListRecordingContext& context) const
// Collect all masks (CSS mask-image, SVG <mask>, SVG <clipPath>).
Vector<DisplayListRecorder::MaskInfo> masks;
if (mask_image) {
if (!mask_layers.is_empty()) {
auto visual_context_tree = AccumulatedVisualContextTree::create();
auto mask_display_list = DisplayList::create(visual_context_tree);
DisplayListRecorder display_list_recorder(*mask_display_list, visual_context_tree, context.display_list_recorder().resource_storage());
auto mask_painting_context = context.clone(display_list_recorder);
auto mask_rect_in_device_pixels = context.enclosing_device_rect(paintable_box().absolute_padding_box_rect());
mask_image->paint(mask_painting_context, { {}, mask_rect_in_device_pixels.size() }, CSS::ImageRendering::Auto);
auto absolute_mask_rect = paintable_box().absolute_border_box_rect();
auto mask_rect_in_device_pixels = context.enclosing_device_rect(absolute_mask_rect);
auto mask_rect = CSSPixelRect { {}, absolute_mask_rect.size() };
auto resolved_mask = resolve_background_layers(mask_layers, paintable_box(), Color::Transparent, CSS::BackgroundBox::BorderBox, mask_rect, {});
paint_background(mask_painting_context, paintable_box(), CSS::ImageRendering::Auto, resolved_mask, {});
masks.append({ { *mask_display_list, move(visual_context_tree) }, mask_rect_in_device_pixels.to_type<int>(), Gfx::MaskKind::Alpha });
}

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<style>
body {
margin: 0;
background: red;
}
.spacer {
width: 100px;
height: 80px;
}
.green {
width: 100px;
height: 20px;
background: green;
}
</style>
<div class="spacer"></div>
<div class="green"></div>

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<link rel="match" href="../expected/css-mask-position-size-no-repeat-ref.html" />
<style>
body {
margin: 0;
background: red;
}
.masked {
width: 100px;
height: 100px;
background: green;
mask: linear-gradient(#000, #000) bottom / 100% 20px no-repeat;
}
</style>
<div class="masked"></div>