mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-06-01 16:02:53 +00:00
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:
parent
8ba18dfd40
commit
fcf293a8df
Notes:
sideshowbarker
2024-07-17 20:22:04 +09:00
Author: https://github.com/kalenikaliaksandr
Commit: fcf293a8df
Pull-request: https://github.com/SerenityOS/serenity/pull/23260
Issue: https://github.com/SerenityOS/serenity/issues/23197
8 changed files with 287 additions and 1 deletions
|
@ -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]
|
2
Tests/LibWeb/Text/expected/ResizeObserver/observe.txt
Normal file
2
Tests/LibWeb/Text/expected/ResizeObserver/observe.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Size changed: 200px x 200px
|
||||
Size changed: 400px x 400px
|
|
@ -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>
|
52
Tests/LibWeb/Text/input/ResizeObserver/observe.html
Normal file
52
Tests/LibWeb/Text/input/ResizeObserver/observe.html
Normal 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>
|
|
@ -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 element’s 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 observer’s [[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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue