LibWeb: Implement gathering and broadcasting of resize observations

Extends event loop processing steps to include gathering and
broadcasting resize observations.

Moves layout updates from Navigable::paint() to event loop processing
steps. This ensures resize observation processing occurs between layout
updates and painting.
This commit is contained in:
Aliaksandr Kalenik 2024-02-19 05:10:05 +01:00 committed by Andreas Kling
parent 8ba18dfd40
commit fcf293a8df
Notes: sideshowbarker 2024-07-17 20:22:04 +09:00
8 changed files with 287 additions and 1 deletions

View file

@ -0,0 +1,2 @@
contentSize: 100px x 200px; borderBoxSize [inline=140px, block=240px]; contentBoxSize [inline=100px, block=200px]; deviceBoxSize [inline=140px, block=240px]
contentSize: 100px x 200px; borderBoxSize [inline=140px, block=280px]; contentBoxSize [inline=100px, block=200px]; deviceBoxSize [inline=140px, block=280px]

View file

@ -0,0 +1,2 @@
Size changed: 200px x 200px
Size changed: 400px x 400px

View file

@ -0,0 +1,57 @@
<!DOCTYPE html>
<head>
<style>
#box {
width: 100px;
height: 200px;
background-color: lightblue;
border: 10px solid pink;
padding: 10px;
}
</style>
</head>
<body>
<div id="box"></div>
</body>
<script src="../include.js"></script>
<script>
asyncTest(async done => {
const box = document.getElementById("box");
let resolve = null;
function createResizeObserverPromise() {
return new Promise(r => {
resolve = r;
});
}
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
const borderBoxSize = entry.borderBoxSize[0];
const contentBoxSize = entry.contentBoxSize[0];
const deviceBoxSize = entry.devicePixelContentBoxSize[0];
let string = `contentSize: ${width}px x ${height}px`;
string += `; borderBoxSize [inline=${borderBoxSize.inlineSize}px, block=${borderBoxSize.blockSize}px]`;
string += `; contentBoxSize [inline=${contentBoxSize.inlineSize}px, block=${contentBoxSize.blockSize}px]`;
string += `; deviceBoxSize [inline=${deviceBoxSize.inlineSize}px, block=${deviceBoxSize.blockSize}px]`;
println(string);
}
if (resolve) resolve();
});
let observerCallbackInvocation = createResizeObserverPromise();
resizeObserver.observe(box, { box: "border-box" });
await observerCallbackInvocation;
box.style.borderTopWidth = "50px";
observerCallbackInvocation = createResizeObserverPromise();
await observerCallbackInvocation;
done();
});
</script>

View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<head>
<style>
#box {
width: 200px;
height: 200px;
background-color: lightblue;
}
</style>
</head>
<body>
<div id="box"></div>
</body>
<script src="../include.js"></script>
<script>
asyncTest(async done => {
const box = document.getElementById("box");
let resolve = null;
function createResizeObserverPromise() {
return new Promise(r => {
resolve = r;
});
}
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
println(`Size changed: ${width}px x ${height}px`);
}
if (resolve) resolve();
});
let observerCallbackInvocation = createResizeObserverPromise();
resizeObserver.observe(box);
await observerCallbackInvocation;
// Change size of box multiple times.
// Observer callback is expected to be invoked only once.
box.style.width = "300px";
box.style.height = "300px";
box.style.width = "400px";
box.style.height = "400px";
observerCallbackInvocation = createResizeObserverPromise();
await observerCallbackInvocation;
done();
});
</script>

View file

@ -4138,4 +4138,134 @@ String Document::query_command_value(String)
return String {};
}
// https://drafts.csswg.org/resize-observer-1/#calculate-depth-for-node
static size_t calculate_depth_for_node(Node const& node)
{
// 1. Let p be the parent-traversal path from node to a root Element of this elements flattened DOM tree.
// 2. Return number of nodes in p.
size_t depth = 0;
for (auto const* current = &node; current; current = current->parent())
++depth;
return depth;
}
// https://drafts.csswg.org/resize-observer-1/#gather-active-observations-h
void Document::gather_active_observations_at_depth(size_t depth)
{
// 1. Let depth be the depth passed in.
// 2. For each observer in [[resizeObservers]] run these steps:
for (auto const& observer : m_resize_observers) {
// 1. Clear observers [[activeTargets]], and [[skippedTargets]].
observer->active_targets().clear();
observer->skipped_targets().clear();
// 2. For each observation in observer.[[observationTargets]] run this step:
for (auto const& observation : observer->observation_targets()) {
// 1. If observation.isActive() is true
if (observation->is_active()) {
// 1. Let targetDepth be result of calculate depth for node for observation.target.
auto target_depth = calculate_depth_for_node(*observation->target());
// 2. If targetDepth is greater than depth then add observation to [[activeTargets]].
if (target_depth > depth) {
observer->active_targets().append(observation);
} else {
// 3. Else add observation to [[skippedTargets]].
observer->skipped_targets().append(observation);
}
}
}
}
}
// https://drafts.csswg.org/resize-observer-1/#broadcast-active-resize-observations
size_t Document::broadcast_active_resize_observations()
{
// 1. Let shallowestTargetDepth be ∞
auto shallowest_target_depth = NumericLimits<size_t>::max();
// 2. For each observer in document.[[resizeObservers]] run these steps:
for (auto const& observer : m_resize_observers) {
// 1. If observer.[[activeTargets]] slot is empty, continue.
if (observer->active_targets().is_empty()) {
continue;
}
// 2. Let entries be an empty list of ResizeObserverEntryies.
Vector<JS::NonnullGCPtr<ResizeObserver::ResizeObserverEntry>> entries;
// 3. For each observation in [[activeTargets]] perform these steps:
for (auto const& observation : observer->active_targets()) {
// 1. Let entry be the result of running create and populate a ResizeObserverEntry given observation.target.
auto entry = ResizeObserver::ResizeObserverEntry::create_and_populate(realm(), *observation->target()).release_value_but_fixme_should_propagate_errors();
// 2. Add entry to entries.
entries.append(entry);
// 3. Set observation.lastReportedSizes to matching entry sizes.
switch (observation->observed_box()) {
case Bindings::ResizeObserverBoxOptions::BorderBox:
// Matching sizes are entry.borderBoxSize if observation.observedBox is "border-box"
observation->last_reported_sizes() = entry->border_box_size();
break;
case Bindings::ResizeObserverBoxOptions::ContentBox:
// Matching sizes are entry.contentBoxSize if observation.observedBox is "content-box"
observation->last_reported_sizes() = entry->content_box_size();
break;
case Bindings::ResizeObserverBoxOptions::DevicePixelContentBox:
// Matching sizes are entry.devicePixelContentBoxSize if observation.observedBox is "device-pixel-content-box"
observation->last_reported_sizes() = entry->device_pixel_content_box_size();
break;
default:
VERIFY_NOT_REACHED();
}
// 4. Set targetDepth to the result of calculate depth for node for observation.target.
auto target_depth = calculate_depth_for_node(*observation->target());
// 5. Set shallowestTargetDepth to targetDepth if targetDepth < shallowestTargetDepth
if (target_depth < shallowest_target_depth)
shallowest_target_depth = target_depth;
}
// 4. Invoke observer.[[callback]] with entries.
observer->invoke_callback(entries);
// 5. Clear observer.[[activeTargets]].
observer->active_targets().clear();
}
return shallowest_target_depth;
}
// https://drafts.csswg.org/resize-observer-1/#has-active-observations-h
bool Document::has_active_resize_observations()
{
// 1. For each observer in [[resizeObservers]] run this step:
for (auto const& observer : m_resize_observers) {
// 1. If observer.[[activeTargets]] is not empty, return true.
if (!observer->active_targets().is_empty())
return true;
}
// 2. Return false.
return false;
}
// https://drafts.csswg.org/resize-observer-1/#has-skipped-observations-h
bool Document::has_skipped_resize_observations()
{
// 1. For each observer in [[resizeObservers]] run this step:
for (auto const& observer : m_resize_observers) {
// 1. If observer.[[skippedTargets]] is not empty, return true.
if (!observer->skipped_targets().is_empty())
return true;
}
// 2. Return false.
return false;
}
}

View file

@ -590,6 +590,11 @@ public:
virtual Vector<FlyString> supported_property_names() const override;
Vector<JS::NonnullGCPtr<DOM::Element>> const& potentially_named_elements() const { return m_potentially_named_elements; }
void gather_active_observations_at_depth(size_t depth);
[[nodiscard]] size_t broadcast_active_resize_observations();
[[nodiscard]] bool has_active_resize_observations();
[[nodiscard]] bool has_skipped_resize_observations();
protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;

View file

@ -207,6 +207,45 @@ void EventLoop::process()
run_animation_frame_callbacks(document, now);
});
// FIXME: This step is implemented following the latest specification, while the rest of this method uses an outdated spec.
// NOTE: Gathering and broadcasting of resize observations need to happen after evaluating media queries but before
// updating intersection observations steps.
for_each_fully_active_document_in_docs([&](DOM::Document& document) {
// 1. Let resizeObserverDepth be 0.
size_t resize_observer_depth = 0;
// 2. While true:
while (true) {
// 1. Recalculate styles and update layout for doc.
// NOTE: Recalculation of styles is handled by update_layout()
document.update_layout();
// FIXME: 2. Let hadInitialVisibleContentVisibilityDetermination be false.
// FIXME: 3. For each element element with 'auto' used value of 'content-visibility':
// FIXME: 4. If hadInitialVisibleContentVisibilityDetermination is true, then continue.
// 5. Gather active resize observations at depth resizeObserverDepth for doc.
document.gather_active_observations_at_depth(resize_observer_depth);
// 6. If doc has active resize observations:
if (document.has_active_resize_observations()) {
// 1. Set resizeObserverDepth to the result of broadcasting active resize observations given doc.
resize_observer_depth = document.broadcast_active_resize_observations();
// 2. Continue.
continue;
}
// 7. Otherwise, break.
break;
}
// 3. If doc has skipped resize observations, then deliver resize loop error given doc.
if (document.has_skipped_resize_observations()) {
// FIXME: Deliver resize loop error.
}
});
// 14. For each fully active Document in docs, run the update intersection observations steps for that Document, passing in now as the timestamp. [INTERSECTIONOBSERVER]
for_each_fully_active_document_in_docs([&](DOM::Document& document) {
document.run_the_update_intersection_observations_steps(now);

View file

@ -2093,7 +2093,6 @@ void Navigable::paint(Painting::RecordingPainter& recording_painter, PaintConfig
auto viewport_rect = page.css_to_device_rect(this->viewport_rect());
Gfx::IntRect bitmap_rect { {}, viewport_rect.size().to_type<int>() };
document->update_layout();
auto background_color = document->background_color();
recording_painter.fill_rect(bitmap_rect, background_color);