Browse Source

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.
Aliaksandr Kalenik 1 year ago
parent
commit
fcf293a8df

+ 2 - 0
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]

+ 2 - 0
Tests/LibWeb/Text/expected/ResizeObserver/observe.txt

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

+ 57 - 0
Tests/LibWeb/Text/input/ResizeObserver/observe-border-box.html

@@ -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 - 0
Tests/LibWeb/Text/input/ResizeObserver/observe.html

@@ -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>

+ 130 - 0
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<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;
+}
+
 }

+ 5 - 0
Userland/Libraries/LibWeb/DOM/Document.h

@@ -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;

+ 39 - 0
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);

+ 0 - 1
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<int>() };
 
-    document->update_layout();
     auto background_color = document->background_color();
 
     recording_painter.fill_rect(bitmap_rect, background_color);