1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 11:18:11 +00:00

LibWeb: Make responsive images react to changes in size viewport

This patch implements "react to changes in the environment" from the
HTML spec and hooks HTMLImageElement up with viewport rect change
notifications (from the browsing context).

This fixes the issue where we'd load a low-resolution image and not
switch to a high-resolution image after resizing the window.
This commit is contained in:
Andreas Kling 2023-07-29 10:20:53 +02:00
parent 95097e47a7
commit df249f269b
2 changed files with 192 additions and 66 deletions

View file

@ -38,10 +38,20 @@ HTMLImageElement::HTMLImageElement(DOM::Document& document, DOM::QualifiedName q
{
m_animation_timer = Core::Timer::try_create().release_value_but_fixme_should_propagate_errors();
m_animation_timer->on_timeout = [this] { animate(); };
if (auto* browsing_context = document.browsing_context())
browsing_context->register_viewport_client(*this);
}
HTMLImageElement::~HTMLImageElement() = default;
void HTMLImageElement::finalize()
{
Base::finalize();
if (auto* browsing_context = document().browsing_context())
browsing_context->unregister_viewport_client(*this);
}
JS::ThrowCompletionOr<void> HTMLImageElement::initialize(JS::Realm& realm)
{
MUST_OR_THROW_OOM(Base::initialize(realm));
@ -505,6 +515,49 @@ after_step_7:
if (delay_load_event)
m_load_event_delayer.emplace(document());
add_callbacks_to_image_request(image_request, maybe_omit_events, url_string, previous_url);
// AD-HOC: If the image request is already available or fetching, no need to start another fetch.
if (image_request->is_available() || image_request->is_fetching())
return;
// 18. Let request be the result of creating a potential-CORS request given urlString, "image",
// and the current state of the element's crossorigin content attribute.
auto request = create_potential_CORS_request(vm(), url_string, Fetch::Infrastructure::Request::Destination::Image, m_cors_setting);
// 19. Set request's client to the element's node document's relevant settings object.
request->set_client(&document().relevant_settings_object());
// 20. If the element uses srcset or picture, set request's initiator to "imageset".
if (uses_srcset_or_picture())
request->set_initiator(Fetch::Infrastructure::Request::Initiator::ImageSet);
// 21. Set request's referrer policy to the current state of the element's referrerpolicy attribute.
request->set_referrer_policy(ReferrerPolicy::from_string(attribute(HTML::AttributeNames::referrerpolicy)));
// FIXME: 22. Set request's priority to the current state of the element's fetchpriority attribute.
// 24. If the will lazy load element steps given the img return true, then:
if (will_lazy_load()) {
// 1. Set the img's lazy load resumption steps to the rest of this algorithm starting with the step labeled fetch the image.
m_lazy_load_resumption_steps = [this, request, image_request]() {
image_request->fetch_image(realm(), request);
};
// 2. Start intersection-observing a lazy loading element for the img element.
document().start_intersection_observing_a_lazy_loading_element(*this);
// 3. Return.
return;
}
image_request->fetch_image(realm(), request);
});
return {};
}
void HTMLImageElement::add_callbacks_to_image_request(NonnullRefPtr<ImageRequest> image_request, bool maybe_omit_events, AK::URL const& url_string, AK::URL const& previous_url)
{
image_request->add_callbacks(
[this, image_request, maybe_omit_events, url_string, previous_url]() {
batching_dispatcher().enqueue([this, image_request, maybe_omit_events, url_string, previous_url] {
@ -548,7 +601,7 @@ after_step_7:
m_load_event_delayer.clear();
});
},
[this, image_request, maybe_omit_events, url_string, previous_url, selected_source]() {
[this, image_request, maybe_omit_events, url_string, previous_url]() {
// The image data is not in a supported file format;
// the user agent must set image request's state to broken,
@ -570,44 +623,106 @@ after_step_7:
m_load_event_delayer.clear();
});
// AD-HOC: If the image request is already available or fetching, no need to start another fetch.
if (image_request->is_available() || image_request->is_fetching())
return;
// 18. Let request be the result of creating a potential-CORS request given urlString, "image",
// and the current state of the element's crossorigin content attribute.
auto request = create_potential_CORS_request(vm(), url_string, Fetch::Infrastructure::Request::Destination::Image, m_cors_setting);
// 19. Set request's client to the element's node document's relevant settings object.
request->set_client(&document().relevant_settings_object());
// 20. If the element uses srcset or picture, set request's initiator to "imageset".
if (uses_srcset_or_picture())
request->set_initiator(Fetch::Infrastructure::Request::Initiator::ImageSet);
// 21. Set request's referrer policy to the current state of the element's referrerpolicy attribute.
request->set_referrer_policy(ReferrerPolicy::from_string(attribute(HTML::AttributeNames::referrerpolicy)));
// FIXME: 22. Set request's priority to the current state of the element's fetchpriority attribute.
// 24. If the will lazy load element steps given the img return true, then:
if (will_lazy_load()) {
// 1. Set the img's lazy load resumption steps to the rest of this algorithm starting with the step labeled fetch the image.
m_lazy_load_resumption_steps = [this, request, image_request]() {
image_request->fetch_image(realm(), request);
};
// 2. Start intersection-observing a lazy loading element for the img element.
document().start_intersection_observing_a_lazy_loading_element(*this);
// 3. Return.
return;
}
image_request->fetch_image(realm(), request);
void HTMLImageElement::browsing_context_did_set_viewport_rect(CSSPixelRect const& viewport_rect)
{
if (viewport_rect.size() == m_last_seen_viewport_size)
return;
m_last_seen_viewport_size = viewport_rect.size();
batching_dispatcher().enqueue([this] {
react_to_changes_in_the_environment();
});
return {};
}
// https://html.spec.whatwg.org/multipage/images.html#img-environment-changes
void HTMLImageElement::react_to_changes_in_the_environment()
{
// FIXME: 1. Await a stable state.
// The synchronous section consists of all the remaining steps of this algorithm
// until the algorithm says the synchronous section has ended.
// (Steps in synchronous sections are marked with ⌛.)
// 2. ⌛ If the img element does not use srcset or picture,
// its node document is not fully active,
// FIXME: has image data whose resource type is multipart/x-mixed-replace,
// or the pending request is not null,
// then return.
if (!uses_srcset_or_picture() || !document().is_fully_active() || m_pending_request)
return;
// 3. ⌛ Let selected source and selected pixel density be the URL and pixel density
// that results from selecting an image source, respectively.
Optional<String> selected_source;
Optional<float> pixel_density;
if (auto result = select_an_image_source(); result.has_value()) {
selected_source = result.value().source.url;
pixel_density = result.value().pixel_density;
}
// 4. ⌛ If selected source is null, then return.
if (!selected_source.has_value())
return;
// 5. ⌛ If selected source and selected pixel density are the same
// as the element's last selected source and current pixel density, then return.
if (selected_source == m_last_selected_source && pixel_density == m_current_request->current_pixel_density())
return;
// 6. ⌛ Parse selected source, relative to the element's node document,
// and let urlString be the resulting URL string. If that is not successful, then return.
auto url_string = document().parse_url(selected_source.value());
if (!url_string.is_valid())
return;
// 7. ⌛ Let corsAttributeState be the state of the element's crossorigin content attribute.
auto cors_attribute_state = m_cors_setting;
// 8. ⌛ Let origin be the img element's node document's origin.
auto origin = document().origin();
// 9. ⌛ Let client be the img element's node document's relevant settings object.
auto& client = document().relevant_settings_object();
// 10. ⌛ Let key be a tuple consisting of urlString, corsAttributeState, and, if corsAttributeState is not No CORS, origin.
ListOfAvailableImages::Key key;
key.url = url_string;
key.mode = m_cors_setting;
if (cors_attribute_state != CORSSettingAttribute::NoCORS)
key.origin = document().origin();
// 11. ⌛ Let image request be a new image request whose current URL is urlString
auto image_request = ImageRequest::create(*document().page()).release_value_but_fixme_should_propagate_errors();
image_request->set_current_url(url_string);
// 12. ⌛ Let the element's pending request be image request.
m_pending_request = image_request;
// FIXME: 13. End the synchronous section, continuing the remaining steps in parallel.
// 14. If the list of available images contains an entry for key, then set image request's image data to that of the entry.
// Continue to the next step.
if (auto entry = document().list_of_available_images().get(key)) {
image_request->set_image_data(entry->image_data);
}
// Otherwise:
else {
// 1. Let request be the result of creating a potential-CORS request given urlString, "image", and corsAttributeState.
auto request = create_potential_CORS_request(vm(), url_string, Fetch::Infrastructure::Request::Destination::Image, m_cors_setting);
// 2. Set request's client to client, initiator to "imageset", and set request's synchronous flag.
request->set_client(&client);
request->set_initiator(Fetch::Infrastructure::Request::Initiator::ImageSet);
// 3. Set request's referrer policy to the current state of the element's referrerpolicy attribute.
request->set_referrer_policy(ReferrerPolicy::from_string(attribute(HTML::AttributeNames::referrerpolicy)));
// FIXME: 4. Set request's priority to the current state of the element's fetchpriority attribute.
// 5. Let response be the result of fetching request.
add_callbacks_to_image_request(image_request, false, url_string, AK::URL());
image_request->fetch_image(realm(), request);
}
}
// https://html.spec.whatwg.org/multipage/images.html#upgrade-the-pending-request-to-the-current-request

View file

@ -10,6 +10,7 @@
#include <AK/OwnPtr.h>
#include <LibGfx/Forward.h>
#include <LibWeb/DOM/DocumentLoadEventDelayer.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/CORSSettingAttribute.h>
#include <LibWeb/HTML/FormAssociatedElement.h>
#include <LibWeb/HTML/HTMLElement.h>
@ -21,7 +22,8 @@ namespace Web::HTML {
class HTMLImageElement final
: public HTMLElement
, public FormAssociatedElement
, public Layout::ImageProvider {
, public Layout::ImageProvider
, public BrowsingContext::ViewportClient {
WEB_PLATFORM_OBJECT(HTMLImageElement, HTMLElement);
FORM_ASSOCIATED_ELEMENT(HTMLElement, HTMLImageElement)
@ -49,6 +51,9 @@ public:
virtual Optional<ARIA::Role> default_role() const override;
// https://html.spec.whatwg.org/multipage/images.html#img-environment-changes
void react_to_changes_in_the_environment();
// https://html.spec.whatwg.org/multipage/images.html#update-the-image-data
ErrorOr<void> update_the_image_data(bool restart_the_animations = false, bool maybe_omit_events = false);
@ -90,13 +95,17 @@ private:
HTMLImageElement(DOM::Document&, DOM::QualifiedName);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
virtual void finalize() override;
virtual void apply_presentational_hints(CSS::StyleProperties&) const override;
virtual JS::GCPtr<Layout::Node> create_layout_node(NonnullRefPtr<CSS::StyleProperties>) override;
virtual void browsing_context_did_set_viewport_rect(CSSPixelRect const&) override;
void handle_successful_fetch(AK::URL const&, StringView mime_type, ImageRequest&, ByteBuffer, bool maybe_omit_events, AK::URL const& previous_url);
void handle_failed_fetch();
void add_callbacks_to_image_request(NonnullRefPtr<ImageRequest>, bool maybe_omit_events, AK::URL const& url_string, AK::URL const& previous_url);
void animate();
@ -123,6 +132,8 @@ private:
// https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-load-resumption-steps
// Each img and iframe element has associated lazy load resumption steps, initially null.
JS::SafeFunction<void()> m_lazy_load_resumption_steps;
CSSPixelSize m_last_seen_viewport_size;
};
}