When image data becomes available, intrinsic dimensions can change, but that does not require relayout for image elements whose computed width, height, and min/max constraints are independent of intrinsic sizing. In that case, invalidate paint and intrinsic-size caches instead of marking layout dirty. Keep the conservative relayout path for auto, percentage, calc-percentage, and intrinsic sizing constraints, since decoded image dimensions can affect used size in indefinite contexts. Query image availability while painting so an existing paintable can switch from alt text to image content without relayout. Add text coverage that waits for image load events and checks auto, percentage, calc-percentage, intrinsic keyword, min/max percentage, and fixed-size cases without relying on timeouts.
91 lines
4.1 KiB
C++
91 lines
4.1 KiB
C++
/*
|
|
* Copyright (c) 2018-2023, Andreas Kling <andreas@ladybird.org>
|
|
* Copyright (c) 2026, Jelle Raaijmakers <jelle@ladybird.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibWeb/CSS/StyleValues/PositionStyleValue.h>
|
|
#include <LibWeb/HTML/DecodedImageData.h>
|
|
#include <LibWeb/HTML/HTMLImageElement.h>
|
|
#include <LibWeb/Painting/BorderRadiusCornerClipper.h>
|
|
#include <LibWeb/Painting/DisplayListRecorder.h>
|
|
#include <LibWeb/Painting/ImagePaintable.h>
|
|
#include <LibWeb/Painting/ReplacedElementCommon.h>
|
|
#include <LibWeb/Platform/FontPlugin.h>
|
|
|
|
namespace Web::Painting {
|
|
|
|
NonnullRefPtr<ImagePaintable> ImagePaintable::create(Layout::SVGImageBox const& layout_box)
|
|
{
|
|
return adopt_ref(*new ImagePaintable(layout_box, layout_box.dom_node(), false, String {}, true));
|
|
}
|
|
|
|
NonnullRefPtr<ImagePaintable> ImagePaintable::create(Layout::ImageBox const& layout_box)
|
|
{
|
|
String alt;
|
|
if (auto element = layout_box.dom_node())
|
|
alt = element->get_attribute_value(HTML::AttributeNames::alt);
|
|
return adopt_ref(*new ImagePaintable(layout_box, layout_box.image_provider(), layout_box.renders_as_alt_text(), move(alt), false));
|
|
}
|
|
|
|
ImagePaintable::ImagePaintable(Layout::Box const& layout_box, Layout::ImageProvider const& image_provider, bool renders_as_alt_text, String alt_text, bool is_svg_image)
|
|
: PaintableBox(layout_box)
|
|
, m_renders_as_alt_text(renders_as_alt_text)
|
|
, m_alt_text(move(alt_text))
|
|
, m_image_provider(image_provider)
|
|
, m_is_svg_image(is_svg_image)
|
|
{
|
|
}
|
|
|
|
void ImagePaintable::reset_for_relayout()
|
|
{
|
|
PaintableBox::reset_for_relayout();
|
|
|
|
if (!m_is_svg_image) {
|
|
m_renders_as_alt_text = !m_image_provider.is_image_available();
|
|
if (auto const* image_box = as_if<Layout::ImageBox>(layout_node())) {
|
|
if (auto element = image_box->dom_node())
|
|
m_alt_text = element->get_attribute_value(HTML::AttributeNames::alt);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ImagePaintable::paint(DisplayListRecordingContext& context, PaintPhase phase) const
|
|
{
|
|
if (!is_visible())
|
|
return;
|
|
|
|
PaintableBox::paint(context, phase);
|
|
|
|
if (phase == PaintPhase::Foreground) {
|
|
auto image_rect = absolute_rect();
|
|
auto image_rect_device_pixels = context.rounded_device_rect(image_rect);
|
|
auto renders_as_alt_text = m_is_svg_image ? m_renders_as_alt_text : !m_image_provider.is_image_available();
|
|
if (renders_as_alt_text) {
|
|
if (!m_alt_text.is_empty()) {
|
|
auto enclosing_rect = context.enclosing_device_rect(image_rect).to_type<int>();
|
|
context.display_list_recorder().draw_text(enclosing_rect, Utf16String::from_utf8(m_alt_text), layout_node().font(context), Gfx::TextAlignment::Center, computed_values().color());
|
|
}
|
|
} else if (auto decoded_image_data = m_image_provider.decoded_image_data()) {
|
|
ScopedCornerRadiusClip corner_clip { context, image_rect_device_pixels, normalized_border_radii_data(ShrinkRadiiForBorders::Yes) };
|
|
auto image_int_rect_device_pixels = image_rect_device_pixels.to_type<int>();
|
|
auto bitmap_rect = decoded_image_data->frame_rect(m_image_provider.current_frame_index()).value_or(image_int_rect_device_pixels);
|
|
auto scaling_mode = to_gfx_scaling_mode(computed_values().image_rendering(), bitmap_rect.size(), image_int_rect_device_pixels.size());
|
|
|
|
// https://drafts.csswg.org/css-images/#the-object-fit
|
|
auto object_fit = m_is_svg_image ? CSS::ObjectFit::Contain : computed_values().object_fit();
|
|
auto draw_rect = get_replaced_box_painting_area(*this, context, object_fit, bitmap_rect.size());
|
|
if (!draw_rect.is_empty())
|
|
decoded_image_data->paint(context, m_image_provider.current_frame_index(), draw_rect, image_int_rect_device_pixels, scaling_mode);
|
|
}
|
|
|
|
if (selection_state() != SelectionState::None) {
|
|
auto selection_background_color = selection_style().background_color;
|
|
if (selection_background_color.alpha() > 0)
|
|
context.display_list_recorder().fill_rect(image_rect_device_pixels.to_type<int>(), selection_background_color);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|