LibWeb+WebContent: Instantly update DPI for painting and media queries

If our UI informed the page of a DPI change, we would store the new
device pixel ratio and leave it at that. It would take a layout/style
update (e.g. by clicking the page) to actually render the page using the
new DPI. This is very visible on macOS when moving the Ladybird window
from a 1x resolution monitor to a HiDPI 2x monitor.

We now instantly update the backing stores and mark media queries for
reevaluation. Moving the Ladybird window on macOS now immediately
updates the page when dragging it to a HiDPI monitor.
This commit is contained in:
Jelle Raaijmakers 2026-02-05 18:02:28 +01:00 committed by Jelle Raaijmakers
parent 12ba454a5b
commit c56c933014
9 changed files with 76 additions and 1 deletions

View file

@ -172,6 +172,8 @@ public:
void set_viewport_size(CSSPixelSize);
void perform_scroll_of_viewport_scrolling_box(CSSPixelPoint position);
Painting::BackingStoreManager& backing_store_manager() { return *m_backing_store_manager; }
// https://html.spec.whatwg.org/multipage/webappapis.html#rendering-opportunity
[[nodiscard]] bool has_a_rendering_opportunity() const;

View file

@ -437,6 +437,11 @@ void Internals::set_browser_zoom(double factor)
page().client().page_did_set_browser_zoom(factor);
}
void Internals::set_device_pixel_ratio(double ratio)
{
page().client().page_did_set_device_pixel_ratio_for_testing(ratio);
}
bool Internals::headless()
{
return page().client().is_headless();

View file

@ -69,6 +69,7 @@ public:
static void set_echo_server_port(u16 port);
void set_browser_zoom(double factor);
void set_device_pixel_ratio(double ratio);
bool headless();

View file

@ -58,6 +58,7 @@ interface Internals {
unsigned short getEchoServerPort();
undefined setBrowserZoom(double factor);
undefined setDevicePixelRatio(double ratio);
readonly attribute boolean headless;

View file

@ -417,6 +417,7 @@ public:
virtual void page_did_receive_test_variant_metadata(JsonValue) { }
virtual void page_did_set_browser_zoom([[maybe_unused]] double factor) { }
virtual void page_did_set_device_pixel_ratio_for_testing([[maybe_unused]] double ratio) { }
virtual void page_did_change_theme_color(Gfx::Color) { }

View file

@ -24,6 +24,7 @@
#include <LibWeb/HTML/HTMLLinkElement.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/InvalidateDisplayList.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWebView/SiteIsolation.h>
@ -207,6 +208,23 @@ void PageClient::set_viewport_size(Web::DevicePixelSize const& size)
page().top_level_traversable()->set_viewport_size(page().device_to_css_size(size));
}
void PageClient::set_device_pixel_ratio(double device_pixel_ratio)
{
if (m_device_pixel_ratio == device_pixel_ratio)
return;
m_device_pixel_ratio = device_pixel_ratio;
auto traversable = page().top_level_traversable();
traversable->backing_store_manager()
.resize_backing_stores_if_needed(Web::Painting::BackingStoreManager::WindowResizingInProgress::No);
if (auto document = traversable->active_document()) {
document->set_needs_media_query_evaluation();
document->set_needs_display(Web::InvalidateDisplayList::Yes);
}
}
void PageClient::set_maximum_frames_per_second(u64 maximum_frames_per_second)
{
m_maximum_frames_per_second = maximum_frames_per_second;
@ -372,6 +390,11 @@ void PageClient::page_did_set_browser_zoom(double factor)
}));
}
void PageClient::page_did_set_device_pixel_ratio_for_testing(double ratio)
{
set_device_pixel_ratio(ratio);
}
void PageClient::page_did_request_context_menu(Web::CSSPixelPoint content_position)
{
client().async_did_request_context_menu(m_id, page().css_to_device_point(content_position).to_type<int>());

View file

@ -58,7 +58,7 @@ public:
m_all_screen_rects = rects;
m_main_screen_index = main_screen_index;
}
void set_device_pixel_ratio(double device_pixel_ratio) { m_device_pixel_ratio = device_pixel_ratio; }
void set_device_pixel_ratio(double device_pixel_ratio);
void set_zoom_level(double zoom_level) { m_zoom_level = zoom_level; }
void set_maximum_frames_per_second(u64 maximum_frames_per_second);
void set_preferred_color_scheme(Web::CSS::PreferredColorScheme);
@ -182,6 +182,7 @@ private:
virtual void page_did_receive_reference_test_metadata(JsonValue) override;
virtual void page_did_receive_test_variant_metadata(JsonValue) override;
virtual void page_did_set_browser_zoom(double factor) override;
virtual void page_did_set_device_pixel_ratio_for_testing(double ratio) override;
virtual void page_did_change_theme_color(Gfx::Color color) override;
virtual void page_did_insert_clipboard_entry(Web::Clipboard::SystemClipboardRepresentation const&, StringView presentation_style) override;
virtual void page_did_request_clipboard_entries(u64 request_id) override;

View file

@ -0,0 +1,5 @@
Initial matches (at 1x): false
Change event 1: matches = true
PASS: Media query matched after DPI changed to 2x
Change event 2: matches = false
PASS: Media query unmatched after DPI changed back to 1x

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<script src="include.js"></script>
<script>
asyncTest(done => {
const mql = window.matchMedia("(resolution: 2dppx)");
println(`Initial matches (at 1x): ${mql.matches}`);
let changeCount = 0;
mql.addEventListener("change", (event) => {
changeCount++;
println(`Change event ${changeCount}: matches = ${event.matches}`);
if (changeCount === 1) {
// First change: 1x -> 2x, should now match.
if (event.matches) {
println("PASS: Media query matched after DPI changed to 2x");
} else {
println("FAIL: Media query should match at 2x DPI");
}
internals.setDevicePixelRatio(1);
} else if (changeCount === 2) {
// Second change: 2x -> 1x, should no longer match.
if (!event.matches) {
println("PASS: Media query unmatched after DPI changed back to 1x");
} else {
println("FAIL: Media query should not match at 1x DPI");
}
done();
}
});
// Change to 2x DPI - this should trigger the first change event.
internals.setDevicePixelRatio(2);
});
</script>