mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-05-02 20:42:11 +02:00
ImageSetStyleValue::set_style_sheet() previously stored the style sheet on itself but didn't propagate to its candidate images. As a result, candidates were never registered as pending image resources, so their fetches didn't start until layout time and didn't delay the document's load event. This caused css-image-set-background-type.html to be flaky: under load, the screenshot could be captured before the selected SVG candidate had finished decoding, producing an empty box instead of the expected color. Propagate set_style_sheet() to each candidate image whose type() filter does not exclude it, mirroring StyleValueList and ShorthandStyleValue. The candidates now register themselves as pending so SharedResourceRequest's load event delayer correctly delays the load event until decoding completes.
260 lines
9.2 KiB
C++
260 lines
9.2 KiB
C++
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/CSS/CSSStyleSheet.h>
|
|
#include <LibWeb/CSS/Resolution.h>
|
|
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/ImageSetStyleValue.h>
|
|
#include <LibWeb/CSS/StyleValues/ResolutionStyleValue.h>
|
|
#include <LibWeb/DOM/AbstractElement.h>
|
|
#include <LibWeb/DOM/Document.h>
|
|
#include <LibWeb/DOM/Element.h>
|
|
#include <LibWeb/HTML/SupportedImageTypes.h>
|
|
#include <LibWeb/Layout/Node.h>
|
|
#include <LibWeb/Page/Page.h>
|
|
#include <LibWeb/Painting/DisplayListRecordingContext.h>
|
|
|
|
namespace Web::CSS {
|
|
|
|
ValueComparingNonnullRefPtr<ImageSetStyleValue const> ImageSetStyleValue::create(Vector<Option> options)
|
|
{
|
|
return adopt_ref(*new (nothrow) ImageSetStyleValue(move(options)));
|
|
}
|
|
|
|
ImageSetStyleValue::ImageSetStyleValue(Vector<Option> options)
|
|
: AbstractImageStyleValue(Type::ImageSet)
|
|
, m_options(move(options))
|
|
{
|
|
}
|
|
|
|
static Optional<double> option_resolution_in_dppx(ImageSetStyleValue::Option const& option, Optional<CalculationResolutionContext> const& calculation_resolution_context)
|
|
{
|
|
if (option.resolution->is_resolution())
|
|
return option.resolution->as_resolution().resolution().to_dots_per_pixel();
|
|
if (option.resolution->is_calculated()) {
|
|
auto resolution = option.resolution->as_calculated().resolve_resolution(calculation_resolution_context.value_or({}));
|
|
if (resolution.has_value())
|
|
return resolution->to_dots_per_pixel();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
AbstractImageStyleValue const* ImageSetStyleValue::select_image(double device_pixels_per_css_pixel, Optional<CalculationResolutionContext> const& calculation_resolution_context) const
|
|
{
|
|
ImageSetStyleValue::Option const* best_below_or_equal = nullptr;
|
|
Optional<double> best_below_or_equal_resolution;
|
|
ImageSetStyleValue::Option const* best_above = nullptr;
|
|
Optional<double> best_above_resolution;
|
|
|
|
for (auto const& option : m_options) {
|
|
if (option.type.has_value() && !HTML::is_supported_image_type(*option.type))
|
|
continue;
|
|
|
|
auto resolution = option_resolution_in_dppx(option, calculation_resolution_context);
|
|
if (!resolution.has_value())
|
|
continue;
|
|
|
|
if (*resolution >= device_pixels_per_css_pixel) {
|
|
if (!best_above_resolution.has_value() || *resolution < *best_above_resolution) {
|
|
best_above = &option;
|
|
best_above_resolution = *resolution;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!best_below_or_equal_resolution.has_value() || *resolution > *best_below_or_equal_resolution) {
|
|
best_below_or_equal = &option;
|
|
best_below_or_equal_resolution = *resolution;
|
|
}
|
|
}
|
|
|
|
if (best_above)
|
|
return best_above->image.ptr();
|
|
if (best_below_or_equal)
|
|
return best_below_or_equal->image.ptr();
|
|
return nullptr;
|
|
}
|
|
|
|
void ImageSetStyleValue::visit_edges(JS::Cell::Visitor& visitor) const
|
|
{
|
|
Base::visit_edges(visitor);
|
|
visitor.visit(m_style_sheet);
|
|
for (auto const& option : m_options)
|
|
option.image->visit_edges(visitor);
|
|
}
|
|
|
|
void ImageSetStyleValue::serialize(StringBuilder& builder, SerializationMode mode) const
|
|
{
|
|
builder.append("image-set("sv);
|
|
for (size_t i = 0; i < m_options.size(); ++i) {
|
|
if (i > 0)
|
|
builder.append(", "sv);
|
|
auto const& option = m_options[i];
|
|
option.image->serialize(builder, mode);
|
|
builder.append(' ');
|
|
option.resolution->serialize(builder, mode);
|
|
if (option.type.has_value()) {
|
|
builder.append(" type(\""sv);
|
|
builder.append_escaped_for_json(*option.type);
|
|
builder.append("\")"sv);
|
|
}
|
|
}
|
|
builder.append(')');
|
|
}
|
|
|
|
bool ImageSetStyleValue::equals(StyleValue const& other) const
|
|
{
|
|
if (type() != other.type())
|
|
return false;
|
|
auto const& other_image_set = other.as_image_set();
|
|
if (m_options.size() != other_image_set.m_options.size())
|
|
return false;
|
|
|
|
for (size_t i = 0; i < m_options.size(); ++i) {
|
|
auto const& option = m_options[i];
|
|
auto const& other_option = other_image_set.m_options[i];
|
|
if (!option.image->equals(*other_option.image))
|
|
return false;
|
|
if (!option.resolution->equals(*other_option.resolution))
|
|
return false;
|
|
if (option.type != other_option.type)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ImageSetStyleValue::is_computationally_independent() const
|
|
{
|
|
for (auto const& option : m_options) {
|
|
if (!option.image->is_computationally_independent())
|
|
return false;
|
|
if (!option.resolution->is_computationally_independent())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ImageSetStyleValue::load_any_resources(DOM::Document& document)
|
|
{
|
|
auto dpr = document.page().client().device_pixels_per_css_pixel();
|
|
if (auto const* image = select_image(dpr, {}); image && image != m_selected_image) {
|
|
const_cast<AbstractImageStyleValue&>(*image).set_style_sheet(m_style_sheet);
|
|
m_selected_image = image;
|
|
}
|
|
if (m_selected_image)
|
|
const_cast<AbstractImageStyleValue&>(*m_selected_image).load_any_resources(document);
|
|
}
|
|
|
|
void ImageSetStyleValue::load_any_resources(Layout::NodeWithStyle const& layout_node)
|
|
{
|
|
update_selected_image_for_layout_node(layout_node);
|
|
if (m_selected_image)
|
|
const_cast<AbstractImageStyleValue&>(*m_selected_image).load_any_resources(const_cast<DOM::Document&>(layout_node.document()));
|
|
}
|
|
|
|
void ImageSetStyleValue::update_selected_image_for_layout_node(Layout::NodeWithStyle const& layout_node) const
|
|
{
|
|
Optional<DOM::AbstractElement> abstract_element;
|
|
if (layout_node.is_generated_for_pseudo_element()) {
|
|
if (auto const* pseudo_element_generator = layout_node.pseudo_element_generator())
|
|
abstract_element = DOM::AbstractElement { *pseudo_element_generator, layout_node.generated_for_pseudo_element() };
|
|
} else if (auto const* dom_node = layout_node.dom_node(); dom_node && dom_node->is_element()) {
|
|
abstract_element = DOM::AbstractElement { static_cast<DOM::Element const&>(*dom_node) };
|
|
}
|
|
|
|
auto context = CalculationResolutionContext {
|
|
.length_resolution_context = Length::ResolutionContext::for_layout_node(layout_node),
|
|
.abstract_element = abstract_element,
|
|
};
|
|
auto dpr = layout_node.document().page().client().device_pixels_per_css_pixel();
|
|
|
|
if (auto const* image = select_image(dpr, context); image && image != m_selected_image) {
|
|
const_cast<AbstractImageStyleValue&>(*image).set_style_sheet(m_style_sheet);
|
|
m_selected_image = image;
|
|
}
|
|
}
|
|
|
|
Optional<CSSPixels> ImageSetStyleValue::natural_width() const
|
|
{
|
|
if (m_selected_image)
|
|
return m_selected_image->natural_width();
|
|
return {};
|
|
}
|
|
|
|
Optional<CSSPixels> ImageSetStyleValue::natural_height() const
|
|
{
|
|
if (m_selected_image)
|
|
return m_selected_image->natural_height();
|
|
return {};
|
|
}
|
|
|
|
Optional<CSSPixelFraction> ImageSetStyleValue::natural_aspect_ratio() const
|
|
{
|
|
if (m_selected_image)
|
|
return m_selected_image->natural_aspect_ratio();
|
|
return {};
|
|
}
|
|
|
|
void ImageSetStyleValue::resolve_for_size(Layout::NodeWithStyle const& layout_node, CSSPixelSize size) const
|
|
{
|
|
update_selected_image_for_layout_node(layout_node);
|
|
if (m_selected_image)
|
|
m_selected_image->resolve_for_size(layout_node, size);
|
|
}
|
|
|
|
bool ImageSetStyleValue::is_paintable() const
|
|
{
|
|
if (m_selected_image)
|
|
return m_selected_image->is_paintable();
|
|
return false;
|
|
}
|
|
|
|
void ImageSetStyleValue::paint(DisplayListRecordingContext& context, DevicePixelRect const& dest_rect, ImageRendering image_rendering) const
|
|
{
|
|
if (m_selected_image)
|
|
m_selected_image->paint(context, dest_rect, image_rendering);
|
|
}
|
|
|
|
Optional<Gfx::Color> ImageSetStyleValue::color_if_single_pixel_bitmap() const
|
|
{
|
|
if (m_selected_image)
|
|
return m_selected_image->color_if_single_pixel_bitmap();
|
|
return {};
|
|
}
|
|
|
|
void ImageSetStyleValue::set_style_sheet(GC::Ptr<CSSStyleSheet> style_sheet)
|
|
{
|
|
Base::set_style_sheet(style_sheet);
|
|
m_style_sheet = style_sheet;
|
|
|
|
// Propagate the style sheet to candidate images whose type() filter does not exclude them. This ensures the
|
|
// candidate images register themselves as pending image resources on the style sheet, so their fetches start when
|
|
// the style sheet is associated with the document, properly delaying the document's load event.
|
|
for (auto const& option : m_options) {
|
|
if (option.type.has_value() && !HTML::is_supported_image_type(*option.type))
|
|
continue;
|
|
const_cast<AbstractImageStyleValue&>(*option.image).set_style_sheet(style_sheet);
|
|
}
|
|
}
|
|
|
|
ValueComparingNonnullRefPtr<StyleValue const> ImageSetStyleValue::absolutized(ComputationContext const& context) const
|
|
{
|
|
Vector<Option> options;
|
|
options.ensure_capacity(m_options.size());
|
|
for (auto const& option : m_options) {
|
|
auto image = option.image->absolutized(context);
|
|
VERIFY(image->is_abstract_image());
|
|
options.unchecked_append({
|
|
.image = image->as_abstract_image(),
|
|
.resolution = option.resolution->absolutized(context),
|
|
.type = option.type,
|
|
});
|
|
}
|
|
return ImageSetStyleValue::create(move(options));
|
|
}
|
|
|
|
}
|