mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 12:32:43 +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
					
				
					 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
	
	 Aliaksandr Kalenik
						Aliaksandr Kalenik