From fcf293a8df741c983c52c94e0a1762b76c4f58d8 Mon Sep 17 00:00:00 2001 From: Aliaksandr Kalenik Date: Mon, 19 Feb 2024 05:10:05 +0100 Subject: [PATCH] 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. --- .../ResizeObserver/observe-border-box.txt | 2 + .../Text/expected/ResizeObserver/observe.txt | 2 + .../ResizeObserver/observe-border-box.html | 57 ++++++++ .../Text/input/ResizeObserver/observe.html | 52 +++++++ Userland/Libraries/LibWeb/DOM/Document.cpp | 130 ++++++++++++++++++ Userland/Libraries/LibWeb/DOM/Document.h | 5 + .../LibWeb/HTML/EventLoop/EventLoop.cpp | 39 ++++++ Userland/Libraries/LibWeb/HTML/Navigable.cpp | 1 - 8 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 Tests/LibWeb/Text/expected/ResizeObserver/observe-border-box.txt create mode 100644 Tests/LibWeb/Text/expected/ResizeObserver/observe.txt create mode 100644 Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html create mode 100644 Tests/LibWeb/Text/input/ResizeObserver/observe.html diff --git a/Tests/LibWeb/Text/expected/ResizeObserver/observe-border-box.txt b/Tests/LibWeb/Text/expected/ResizeObserver/observe-border-box.txt new file mode 100644 index 0000000000..9a3f941bad --- /dev/null +++ b/Tests/LibWeb/Text/expected/ResizeObserver/observe-border-box.txt @@ -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] diff --git a/Tests/LibWeb/Text/expected/ResizeObserver/observe.txt b/Tests/LibWeb/Text/expected/ResizeObserver/observe.txt new file mode 100644 index 0000000000..c9398473cd --- /dev/null +++ b/Tests/LibWeb/Text/expected/ResizeObserver/observe.txt @@ -0,0 +1,2 @@ + Size changed: 200px x 200px +Size changed: 400px x 400px diff --git a/Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html b/Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html new file mode 100644 index 0000000000..6d5d8716e1 --- /dev/null +++ b/Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html @@ -0,0 +1,57 @@ + + + + + + + +
+ + + diff --git a/Tests/LibWeb/Text/input/ResizeObserver/observe.html b/Tests/LibWeb/Text/input/ResizeObserver/observe.html new file mode 100644 index 0000000000..aab45650c4 --- /dev/null +++ b/Tests/LibWeb/Text/input/ResizeObserver/observe.html @@ -0,0 +1,52 @@ + + + + + +
+ + + diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 9eb02dbddf..940b677875 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -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::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> 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; +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index 55a040d5a8..a994c65502 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -590,6 +590,11 @@ public: virtual Vector supported_property_names() const override; Vector> 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; diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index 923be22bca..a4ae628dee 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -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); diff --git a/Userland/Libraries/LibWeb/HTML/Navigable.cpp b/Userland/Libraries/LibWeb/HTML/Navigable.cpp index 1860354d61..e9f73bed96 100644 --- a/Userland/Libraries/LibWeb/HTML/Navigable.cpp +++ b/Userland/Libraries/LibWeb/HTML/Navigable.cpp @@ -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() }; - document->update_layout(); auto background_color = document->background_color(); recording_painter.fill_rect(bitmap_rect, background_color);