layout: Store scroll offsets in the ScrollTree (#37428)

There are currently five places that scroll offsets are stored:

 - DOM: A set of scroll offsets used for script.
 - Layout: An array of scroll offsets that is used for tracking
   layout-side scroll offsets.
 - Layout: The scroll offsets stored in the `ScrollTree`. These are
   currently unset and unused.
 - Compositor: The scroll offsets stored in the `ScrollTree` mirrored
   from layout.
 - WebRender: The scrolled offsets stored in the WebRender spatial tree.

This change is the first step in combining the first three into the
layout `ScrollTree`. It eliminates the extra array of scroll offsets
stored in layout in favor of the storing them in the `ScrollTree`. A
followup change will eliminate the ones stored in the DOM.

- In addition the `ScrollState` data structure is eliminated as these
are
now stored in a `HashMap` everywhere when passing them via IPC.
- The offsests stored in layout can now never scroll past the boundaries
of the scrolled content.

Testing: This should not change behavior and is thus covered by existing
WPT tests.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: stevennovaryo <steven.novaryo@gmail.com>
This commit is contained in:
Martin Robinson 2025-06-13 14:01:27 +02:00 committed by GitHub
parent 6cac782fb1
commit f451dccd0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 182 additions and 122 deletions

View file

@ -30,7 +30,6 @@ use embedder_traits::{
TouchEventType, UntrustedNodeAddress, ViewportDetails, WheelDelta, WheelEvent, WheelMode, TouchEventType, UntrustedNodeAddress, ViewportDetails, WheelDelta, WheelEvent, WheelMode,
}; };
use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D}; use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D};
use fnv::FnvHashMap;
use ipc_channel::ipc::{self, IpcSharedMemory}; use ipc_channel::ipc::{self, IpcSharedMemory};
use libc::c_void; use libc::c_void;
use log::{debug, info, trace, warn}; use log::{debug, info, trace, warn};
@ -48,10 +47,10 @@ use webrender_api::units::{
}; };
use webrender_api::{ use webrender_api::{
self, BuiltDisplayList, DirtyRect, DisplayListPayload, DocumentId, Epoch as WebRenderEpoch, self, BuiltDisplayList, DirtyRect, DisplayListPayload, DocumentId, Epoch as WebRenderEpoch,
ExternalScrollId, FontInstanceFlags, FontInstanceKey, FontInstanceOptions, FontKey, FontInstanceFlags, FontInstanceKey, FontInstanceOptions, FontKey, HitTestFlags,
HitTestFlags, PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind, PipelineId as WebRenderPipelineId, PropertyBinding, ReferenceFrameKind, RenderReasons,
RenderReasons, SampledScrollOffset, ScrollLocation, SpaceAndClipInfo, SpatialId, SampledScrollOffset, ScrollLocation, SpaceAndClipInfo, SpatialId, SpatialTreeItemKey,
SpatialTreeItemKey, TransformStyle, TransformStyle,
}; };
use crate::InitialCompositorState; use crate::InitialCompositorState;
@ -260,26 +259,9 @@ impl PipelineDetails {
} }
fn install_new_scroll_tree(&mut self, new_scroll_tree: ScrollTree) { fn install_new_scroll_tree(&mut self, new_scroll_tree: ScrollTree) {
let old_scroll_offsets: FnvHashMap<ExternalScrollId, LayoutVector2D> = self let old_scroll_offsets = self.scroll_tree.scroll_offsets();
.scroll_tree
.nodes
.drain(..)
.filter_map(|node| match (node.external_id(), node.offset()) {
(Some(external_id), Some(offset)) => Some((external_id, offset)),
_ => None,
})
.collect();
self.scroll_tree = new_scroll_tree; self.scroll_tree = new_scroll_tree;
for node in self.scroll_tree.nodes.iter_mut() { self.scroll_tree.set_all_scroll_offsets(&old_scroll_offsets);
match node.external_id() {
Some(external_id) => match old_scroll_offsets.get(&external_id) {
Some(new_offset) => node.set_offset(*new_offset),
None => continue,
},
_ => continue,
};
}
} }
} }
@ -728,7 +710,7 @@ impl IOCompositor {
self.global.borrow_mut().send_transaction(txn); self.global.borrow_mut().send_transaction(txn);
}, },
CompositorMsg::SendScrollNode(webview_id, pipeline_id, point, external_scroll_id) => { CompositorMsg::SendScrollNode(webview_id, pipeline_id, offset, external_scroll_id) => {
let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else {
return; return;
}; };
@ -739,15 +721,17 @@ impl IOCompositor {
return; return;
}; };
let offset = LayoutVector2D::new(point.x, point.y);
let Some(offset) = pipeline_details let Some(offset) = pipeline_details
.scroll_tree .scroll_tree
.set_scroll_offsets_for_node_with_external_scroll_id( .set_scroll_offset_for_node_with_external_scroll_id(
external_scroll_id, external_scroll_id,
-offset, offset,
ScrollType::Script, ScrollType::Script,
) )
else { else {
// The renderer should be fully up-to-date with script at this point and script
// should never try to scroll to an invalid location.
warn!("Could not scroll node with id: {external_scroll_id:?}");
return; return;
}; };

View file

@ -13,7 +13,7 @@ use compositing_traits::viewport_description::{
DEFAULT_ZOOM, MAX_ZOOM, MIN_ZOOM, ViewportDescription, DEFAULT_ZOOM, MAX_ZOOM, MIN_ZOOM, ViewportDescription,
}; };
use compositing_traits::{SendableFrameTree, WebViewTrait}; use compositing_traits::{SendableFrameTree, WebViewTrait};
use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType}; use constellation_traits::{EmbedderToConstellationMessage, WindowSizeType};
use embedder_traits::{ use embedder_traits::{
AnimationState, CompositorHitTestResult, InputEvent, MouseButton, MouseButtonAction, AnimationState, CompositorHitTestResult, InputEvent, MouseButton, MouseButtonAction,
MouseButtonEvent, MouseMoveEvent, ShutdownState, TouchEvent, TouchEventResult, TouchEventType, MouseButtonEvent, MouseMoveEvent, ShutdownState, TouchEvent, TouchEventResult, TouchEventType,
@ -204,18 +204,18 @@ impl WebViewRenderer {
return; return;
}; };
let mut scroll_states = Vec::new(); let scroll_offsets = details.scroll_tree.scroll_offsets();
details.scroll_tree.nodes.iter().for_each(|node| {
if let (Some(scroll_id), Some(scroll_offset)) = (node.external_id(), node.offset()) { // This might be true if we have not received a display list from the layout
scroll_states.push(ScrollState { // associated with this pipeline yet. In that case, the layout is not ready to
scroll_id, // receive scroll offsets anyway, so just save time and prevent other issues by
scroll_offset, // not sending them.
}); if scroll_offsets.is_empty() {
} return;
}); }
let _ = self.global.borrow().constellation_sender.send( let _ = self.global.borrow().constellation_sender.send(
EmbedderToConstellationMessage::SetScrollStates(pipeline_id, scroll_states), EmbedderToConstellationMessage::SetScrollStates(pipeline_id, scroll_offsets),
); );
} }

View file

@ -116,9 +116,8 @@ use constellation_traits::{
EmbedderToConstellationMessage, IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, EmbedderToConstellationMessage, IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState,
IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry, MessagePortMsg, NavigationHistoryBehavior, IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry, MessagePortMsg, NavigationHistoryBehavior,
PaintMetricEvent, PortMessageTask, PortTransferInfo, SWManagerMsg, SWManagerSenders, PaintMetricEvent, PortMessageTask, PortTransferInfo, SWManagerMsg, SWManagerSenders,
ScriptToConstellationChan, ScriptToConstellationMessage, ScrollState, ScriptToConstellationChan, ScriptToConstellationMessage, ServiceWorkerManagerFactory,
ServiceWorkerManagerFactory, ServiceWorkerMsg, StructuredSerializedData, TraversalDirection, ServiceWorkerMsg, StructuredSerializedData, TraversalDirection, WindowSizeType,
WindowSizeType,
}; };
use crossbeam_channel::{Receiver, Select, Sender, unbounded}; use crossbeam_channel::{Receiver, Select, Sender, unbounded};
use devtools_traits::{ use devtools_traits::{
@ -167,7 +166,8 @@ use webgpu_traits::{WebGPU, WebGPURequest};
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
use webrender::RenderApi; use webrender::RenderApi;
use webrender::RenderApiSender; use webrender::RenderApiSender;
use webrender_api::{DocumentId, ImageKey}; use webrender_api::units::LayoutVector2D;
use webrender_api::{DocumentId, ExternalScrollId, ImageKey};
use crate::browsingcontext::{ use crate::browsingcontext::{
AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator, AllBrowsingContextsIterator, BrowsingContext, FullyActiveBrowsingContextsIterator,
@ -6051,7 +6051,11 @@ where
feature = "tracing", feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
)] )]
fn handle_set_scroll_states(&self, pipeline_id: PipelineId, scroll_states: Vec<ScrollState>) { fn handle_set_scroll_states(
&self,
pipeline_id: PipelineId,
scroll_states: HashMap<ExternalScrollId, LayoutVector2D>,
) {
let Some(pipeline) = self.pipelines.get(&pipeline_id) else { let Some(pipeline) = self.pipelines.get(&pipeline_id) else {
warn!("Discarding scroll offset update for unknown pipeline"); warn!("Discarding scroll offset update for unknown pipeline");
return; return;

View file

@ -15,16 +15,16 @@ use app_units::Au;
use base::Epoch; use base::Epoch;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::ScrollState; use compositing_traits::display_list::ScrollType;
use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails}; use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect}; use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect};
use euclid::{Point2D, Scale, Size2D, Vector2D}; use euclid::{Point2D, Scale, Size2D};
use fnv::FnvHashMap; use fnv::FnvHashMap;
use fonts::{FontContext, FontContextWebFontMethods}; use fonts::{FontContext, FontContextWebFontMethods};
use fonts_traits::StylesheetWebFontLoadFinishedCallback; use fonts_traits::StylesheetWebFontLoadFinishedCallback;
use fxhash::FxHashMap; use fxhash::FxHashMap;
use ipc_channel::ipc::IpcSender; use ipc_channel::ipc::IpcSender;
use log::{debug, error}; use log::{debug, error, warn};
use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps}; use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps};
use net_traits::image_cache::{ImageCache, UsePlaceholder}; use net_traits::image_cache::{ImageCache, UsePlaceholder};
use parking_lot::{Mutex, RwLock}; use parking_lot::{Mutex, RwLock};
@ -74,7 +74,7 @@ use style::{Zero, driver};
use style_traits::{CSSPixel, SpeculativePainter}; use style_traits::{CSSPixel, SpeculativePainter};
use stylo_atoms::Atom; use stylo_atoms::Atom;
use url::Url; use url::Url;
use webrender_api::units::{DevicePixel, DevicePoint, LayoutPixel, LayoutPoint, LayoutSize}; use webrender_api::units::{DevicePixel, DevicePoint, LayoutSize, LayoutVector2D};
use webrender_api::{ExternalScrollId, HitTestFlags}; use webrender_api::{ExternalScrollId, HitTestFlags};
use crate::context::{CachedImageOrError, LayoutContext}; use crate::context::{CachedImageOrError, LayoutContext};
@ -149,6 +149,12 @@ pub struct LayoutThread {
/// layout trees remain the same. /// layout trees remain the same.
need_new_display_list: Cell<bool>, need_new_display_list: Cell<bool>,
/// Whether or not the existing stacking context tree is dirty and needs to be
/// rebuilt. This happens after a relayout or overflow update. The reason that we
/// don't simply clear the stacking context tree when it becomes dirty is that we need
/// to preserve scroll offsets from the old tree to the new one.
need_new_stacking_context_tree: Cell<bool>,
/// The box tree. /// The box tree.
box_tree: RefCell<Option<Arc<BoxTree>>>, box_tree: RefCell<Option<Arc<BoxTree>>>,
@ -161,9 +167,6 @@ pub struct LayoutThread {
/// A counter for epoch messages /// A counter for epoch messages
epoch: Cell<Epoch>, epoch: Cell<Epoch>,
/// Scroll offsets of nodes that scroll.
scroll_offsets: RefCell<HashMap<ExternalScrollId, Vector2D<f32, LayoutPixel>>>,
// A cache that maps image resources specified in CSS (e.g as the `url()` value // A cache that maps image resources specified in CSS (e.g as the `url()` value
// for `background-image` or `content` properties) to either the final resolved // for `background-image` or `content` properties) to either the final resolved
// image data, or an error if the image cache failed to load/decode the image. // image data, or an error if the image cache failed to load/decode the image.
@ -485,11 +488,20 @@ impl Layout for LayoutThread {
) { ) {
} }
fn set_scroll_offsets(&mut self, scroll_states: &[ScrollState]) { fn set_scroll_offsets_from_renderer(
*self.scroll_offsets.borrow_mut() = scroll_states &mut self,
.iter() scroll_states: &HashMap<ExternalScrollId, LayoutVector2D>,
.map(|scroll_state| (scroll_state.scroll_id, scroll_state.scroll_offset)) ) {
.collect(); let mut stacking_context_tree = self.stacking_context_tree.borrow_mut();
let Some(stacking_context_tree) = stacking_context_tree.as_mut() else {
warn!("Received scroll offsets before finishing layout.");
return;
};
stacking_context_tree
.compositor_info
.scroll_tree
.set_all_scroll_offsets(scroll_states);
} }
} }
@ -533,13 +545,13 @@ impl LayoutThread {
have_added_user_agent_stylesheets: false, have_added_user_agent_stylesheets: false,
have_ever_generated_display_list: Cell::new(false), have_ever_generated_display_list: Cell::new(false),
need_new_display_list: Cell::new(false), need_new_display_list: Cell::new(false),
need_new_stacking_context_tree: Cell::new(false),
box_tree: Default::default(), box_tree: Default::default(),
fragment_tree: Default::default(), fragment_tree: Default::default(),
stacking_context_tree: Default::default(), stacking_context_tree: Default::default(),
// Epoch starts at 1 because of the initial display list for epoch 0 that we send to WR // Epoch starts at 1 because of the initial display list for epoch 0 that we send to WR
epoch: Cell::new(Epoch(1)), epoch: Cell::new(Epoch(1)),
compositor_api: config.compositor_api, compositor_api: config.compositor_api,
scroll_offsets: Default::default(),
stylist: Stylist::new(device, QuirksMode::NoQuirks), stylist: Stylist::new(device, QuirksMode::NoQuirks),
resolved_images_cache: Default::default(), resolved_images_cache: Default::default(),
debug: opts::get().debug.clone(), debug: opts::get().debug.clone(),
@ -674,8 +686,9 @@ impl LayoutThread {
let built_display_list = let built_display_list =
self.build_display_list(&reflow_request, damage, &mut layout_context); self.build_display_list(&reflow_request, damage, &mut layout_context);
if let ReflowGoal::UpdateScrollNode(scroll_state) = reflow_request.reflow_goal { if let ReflowGoal::UpdateScrollNode(external_scroll_id, offset) = reflow_request.reflow_goal
self.update_scroll_node_state(&scroll_state); {
self.set_scroll_offset_from_script(external_scroll_id, offset);
} }
let pending_images = std::mem::take(&mut *layout_context.pending_images.lock()); let pending_images = std::mem::take(&mut *layout_context.pending_images.lock());
@ -828,12 +841,10 @@ impl LayoutThread {
*self.fragment_tree.borrow_mut() = Some(fragment_tree); *self.fragment_tree.borrow_mut() = Some(fragment_tree);
// The FragmentTree has been updated, so any existing StackingContext tree that layout // Changes to layout require us to generate a new stacking context tree and display
// had is now out of date and should be rebuilt. // list the next time one is requested.
*self.stacking_context_tree.borrow_mut() = None;
// Force display list generation as layout has changed.
self.need_new_display_list.set(true); self.need_new_display_list.set(true);
self.need_new_stacking_context_tree.set(true);
if self.debug.dump_style_tree { if self.debug.dump_style_tree {
println!( println!(
@ -866,6 +877,11 @@ impl LayoutThread {
fragment_tree.print(); fragment_tree.print();
} }
} }
// Changes to overflow require us to generate a new stacking context tree and
// display list the next time one is requested.
self.need_new_display_list.set(true);
self.need_new_stacking_context_tree.set(true);
} }
fn build_stacking_context_tree(&self, reflow_request: &ReflowRequest, damage: RestyleDamage) { fn build_stacking_context_tree(&self, reflow_request: &ReflowRequest, damage: RestyleDamage) {
@ -878,7 +894,7 @@ impl LayoutThread {
return; return;
}; };
if !damage.contains(RestyleDamage::REBUILD_STACKING_CONTEXT) && if !damage.contains(RestyleDamage::REBUILD_STACKING_CONTEXT) &&
self.stacking_context_tree.borrow().is_some() !self.need_new_stacking_context_tree.get()
{ {
return; return;
} }
@ -889,19 +905,39 @@ impl LayoutThread {
viewport_size.height.to_f32_px(), viewport_size.height.to_f32_px(),
); );
let mut stacking_context_tree = self.stacking_context_tree.borrow_mut();
let old_scroll_offsets = stacking_context_tree
.as_ref()
.map(|tree| tree.compositor_info.scroll_tree.scroll_offsets());
// Build the StackingContextTree. This turns the `FragmentTree` into a // Build the StackingContextTree. This turns the `FragmentTree` into a
// tree of fragments in CSS painting order and also creates all // tree of fragments in CSS painting order and also creates all
// applicable spatial and clip nodes. // applicable spatial and clip nodes.
*self.stacking_context_tree.borrow_mut() = Some(StackingContextTree::new( let mut new_stacking_context_tree = StackingContextTree::new(
fragment_tree, fragment_tree,
viewport_size, viewport_size,
self.id.into(), self.id.into(),
!self.have_ever_generated_display_list.get(), !self.have_ever_generated_display_list.get(),
&self.debug, &self.debug,
)); );
// When a new StackingContextTree is built, it contains a freshly built
// ScrollTree. We want to preserve any existing scroll offsets in that tree,
// adjusted by any new scroll constraints.
if let Some(old_scroll_offsets) = old_scroll_offsets {
new_stacking_context_tree
.compositor_info
.scroll_tree
.set_all_scroll_offsets(&old_scroll_offsets);
}
*stacking_context_tree = Some(new_stacking_context_tree);
// Force display list generation as layout has changed. // Force display list generation as layout has changed.
self.need_new_display_list.set(true); self.need_new_display_list.set(true);
// The stacking context tree is up-to-date again.
self.need_new_stacking_context_tree.set(false);
} }
/// Build the display list for the current layout and send it to the renderer. If no display /// Build the display list for the current layout and send it to the renderer. If no display
@ -959,17 +995,32 @@ impl LayoutThread {
true true
} }
fn update_scroll_node_state(&self, state: &ScrollState) { fn set_scroll_offset_from_script(
self.scroll_offsets &self,
.borrow_mut() external_scroll_id: ExternalScrollId,
.insert(state.scroll_id, state.scroll_offset); offset: LayoutVector2D,
let point = Point2D::new(-state.scroll_offset.x, -state.scroll_offset.y); ) {
self.compositor_api.send_scroll_node( let mut stacking_context_tree = self.stacking_context_tree.borrow_mut();
self.webview_id, let Some(stacking_context_tree) = stacking_context_tree.as_mut() else {
self.id.into(), return;
LayoutPoint::from_untyped(point), };
state.scroll_id,
); if let Some(offset) = stacking_context_tree
.compositor_info
.scroll_tree
.set_scroll_offset_for_node_with_external_scroll_id(
external_scroll_id,
offset,
ScrollType::Script,
)
{
self.compositor_api.send_scroll_node(
self.webview_id,
self.id.into(),
offset,
external_scroll_id,
);
}
} }
/// Returns profiling information which is passed to the time profiler. /// Returns profiling information which is passed to the time profiler.

View file

@ -24,7 +24,7 @@ use canvas_traits::webgl::WebGLChan;
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{ use constellation_traits::{
DocumentState, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan, DocumentState, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan,
ScriptToConstellationMessage, ScrollState, StructuredSerializedData, WindowSizeType, ScriptToConstellationMessage, StructuredSerializedData, WindowSizeType,
}; };
use crossbeam_channel::{Sender, unbounded}; use crossbeam_channel::{Sender, unbounded};
use cssparser::SourceLocation; use cssparser::SourceLocation;
@ -2097,10 +2097,7 @@ impl Window {
// TODO(mrobinson, #18709): Add smooth scrolling support to WebRender so that we can // TODO(mrobinson, #18709): Add smooth scrolling support to WebRender so that we can
// properly process ScrollBehavior here. // properly process ScrollBehavior here.
self.reflow( self.reflow(
ReflowGoal::UpdateScrollNode(ScrollState { ReflowGoal::UpdateScrollNode(scroll_id, Vector2D::new(-x, -y)),
scroll_id,
scroll_offset: Vector2D::new(-x, -y),
}),
can_gc, can_gc,
); );
} }
@ -3234,7 +3231,7 @@ fn should_move_clip_rect(clip_rect: UntypedRect<Au>, new_viewport: UntypedRect<f
fn debug_reflow_events(id: PipelineId, reflow_goal: &ReflowGoal) { fn debug_reflow_events(id: PipelineId, reflow_goal: &ReflowGoal) {
let goal_string = match *reflow_goal { let goal_string = match *reflow_goal {
ReflowGoal::UpdateTheRendering => "\tFull", ReflowGoal::UpdateTheRendering => "\tFull",
ReflowGoal::UpdateScrollNode(_) => "\tUpdateScrollNode", ReflowGoal::UpdateScrollNode(..) => "\tUpdateScrollNode",
ReflowGoal::LayoutQuery(ref query_msg) => match *query_msg { ReflowGoal::LayoutQuery(ref query_msg) => match *query_msg {
QueryMsg::ContentBox => "\tContentBoxQuery", QueryMsg::ContentBox => "\tContentBoxQuery",
QueryMsg::ContentBoxes => "\tContentBoxesQuery", QueryMsg::ContentBoxes => "\tContentBoxesQuery",

View file

@ -39,7 +39,7 @@ use chrono::{DateTime, Local};
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{ use constellation_traits::{
JsEvalResult, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan, JsEvalResult, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan,
ScriptToConstellationMessage, ScrollState, StructuredSerializedData, WindowSizeType, ScriptToConstellationMessage, StructuredSerializedData, WindowSizeType,
}; };
use content_security_policy::{self as csp}; use content_security_policy::{self as csp};
use crossbeam_channel::unbounded; use crossbeam_channel::unbounded;
@ -99,7 +99,8 @@ use timers::{TimerEventRequest, TimerId, TimerScheduler};
use url::Position; use url::Position;
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
use webgpu_traits::{WebGPUDevice, WebGPUMsg}; use webgpu_traits::{WebGPUDevice, WebGPUMsg};
use webrender_api::units::DevicePixel; use webrender_api::ExternalScrollId;
use webrender_api::units::{DevicePixel, LayoutVector2D};
use crate::document_collection::DocumentCollection; use crate::document_collection::DocumentCollection;
use crate::document_loader::DocumentLoader; use crate::document_loader::DocumentLoader;
@ -2010,7 +2011,11 @@ impl ScriptThread {
} }
} }
fn handle_set_scroll_states(&self, pipeline_id: PipelineId, scroll_states: Vec<ScrollState>) { fn handle_set_scroll_states(
&self,
pipeline_id: PipelineId,
scroll_states: HashMap<ExternalScrollId, LayoutVector2D>,
) {
let Some(window) = self.documents.borrow().find_window(pipeline_id) else { let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
warn!("Received scroll states for closed pipeline {pipeline_id}"); warn!("Received scroll states for closed pipeline {pipeline_id}");
return; return;
@ -2020,16 +2025,15 @@ impl ScriptThread {
ScriptThreadEventCategory::SetScrollState, ScriptThreadEventCategory::SetScrollState,
Some(pipeline_id), Some(pipeline_id),
|| { || {
window.layout_mut().set_scroll_offsets(&scroll_states); window
.layout_mut()
.set_scroll_offsets_from_renderer(&scroll_states);
let mut scroll_offsets = HashMap::new(); let mut scroll_offsets = HashMap::new();
for scroll_state in scroll_states.into_iter() { for (scroll_id, scroll_offset) in scroll_states.into_iter() {
let scroll_offset = scroll_state.scroll_offset; if scroll_id.is_root() {
if scroll_state.scroll_id.is_root() {
window.update_viewport_for_scroll(-scroll_offset.x, -scroll_offset.y); window.update_viewport_for_scroll(-scroll_offset.x, -scroll_offset.y);
} else if let Some(node_id) = } else if let Some(node_id) = node_id_from_scroll_id(scroll_id.0 as usize) {
node_id_from_scroll_id(scroll_state.scroll_id.0 as usize)
{
scroll_offsets.insert(OpaqueNode(node_id), -scroll_offset); scroll_offsets.insert(OpaqueNode(node_id), -scroll_offset);
} }
} }

View file

@ -4,6 +4,8 @@
//! Defines data structures which are consumed by the Compositor. //! Defines data structures which are consumed by the Compositor.
use std::collections::HashMap;
use base::id::ScrollTreeNodeId; use base::id::ScrollTreeNodeId;
use bitflags::bitflags; use bitflags::bitflags;
use embedder_traits::Cursor; use embedder_traits::Cursor;
@ -338,7 +340,7 @@ impl ScrollTree {
/// Given an [`ExternalScrollId`] and an offset, update the scroll offset of the scroll node /// Given an [`ExternalScrollId`] and an offset, update the scroll offset of the scroll node
/// with the given id. /// with the given id.
pub fn set_scroll_offsets_for_node_with_external_scroll_id( pub fn set_scroll_offset_for_node_with_external_scroll_id(
&mut self, &mut self,
external_scroll_id: ExternalScrollId, external_scroll_id: ExternalScrollId,
offset: LayoutVector2D, offset: LayoutVector2D,
@ -353,6 +355,29 @@ impl ScrollTree {
_ => None, _ => None,
}) })
} }
/// Given a set of all scroll offsets coming from the Servo renderer, update all of the offsets
/// for nodes that actually exist in this tree.
pub fn set_all_scroll_offsets(&mut self, offsets: &HashMap<ExternalScrollId, LayoutVector2D>) {
for node in self.nodes.iter_mut() {
if let SpatialTreeNodeInfo::Scroll(ref mut scroll_info) = node.info {
if let Some(offset) = offsets.get(&scroll_info.external_id) {
scroll_info.offset = *offset;
}
}
}
}
/// Collect all of the scroll offsets of the scrolling nodes of this tree into a
/// [`HashMap`] which can be applied to another tree.
pub fn scroll_offsets(&self) -> HashMap<ExternalScrollId, LayoutVector2D> {
HashMap::from_iter(self.nodes.iter().filter_map(|node| match node.info {
SpatialTreeNodeInfo::Scroll(ref scroll_info) => {
Some((scroll_info.external_id, scroll_info.offset))
},
_ => None,
}))
}
} }
/// A data structure which stores compositor-side information about /// A data structure which stores compositor-side information about

View file

@ -35,7 +35,7 @@ use ipc_channel::ipc::{self, IpcSharedMemory};
use profile_traits::mem::{OpaqueSender, ReportsChan}; use profile_traits::mem::{OpaqueSender, ReportsChan};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize}; use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize};
use webrender_api::units::{DevicePoint, LayoutPoint, TexelRect}; use webrender_api::units::{DevicePoint, LayoutVector2D, TexelRect};
use webrender_api::{ use webrender_api::{
BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData, BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData,
ExternalImageHandler, ExternalImageId, ExternalImageSource, ExternalScrollId, ExternalImageHandler, ExternalImageId, ExternalImageSource, ExternalScrollId,
@ -124,7 +124,7 @@ pub enum CompositorMsg {
SendScrollNode( SendScrollNode(
WebViewId, WebViewId,
WebRenderPipelineId, WebRenderPipelineId,
LayoutPoint, LayoutVector2D,
ExternalScrollId, ExternalScrollId,
), ),
/// Inform WebRender of a new display list for the given pipeline. /// Inform WebRender of a new display list for the given pipeline.
@ -232,7 +232,7 @@ impl CrossProcessCompositorApi {
&self, &self,
webview_id: WebViewId, webview_id: WebViewId,
pipeline_id: WebRenderPipelineId, pipeline_id: WebRenderPipelineId,
point: LayoutPoint, point: LayoutVector2D,
scroll_id: ExternalScrollId, scroll_id: ExternalScrollId,
) { ) {
if let Err(e) = self.0.send(CompositorMsg::SendScrollNode( if let Err(e) = self.0.send(CompositorMsg::SendScrollNode(

View file

@ -22,7 +22,6 @@ use embedder_traits::{
CompositorHitTestResult, Cursor, InputEvent, JavaScriptEvaluationId, MediaSessionActionType, CompositorHitTestResult, Cursor, InputEvent, JavaScriptEvaluationId, MediaSessionActionType,
Theme, ViewportDetails, WebDriverCommandMsg, Theme, ViewportDetails, WebDriverCommandMsg,
}; };
use euclid::Vector2D;
pub use from_script_message::*; pub use from_script_message::*;
use ipc_channel::ipc::IpcSender; use ipc_channel::ipc::IpcSender;
use malloc_size_of_derive::MallocSizeOf; use malloc_size_of_derive::MallocSizeOf;
@ -32,7 +31,7 @@ use servo_url::{ImmutableOrigin, ServoUrl};
pub use structured_data::*; pub use structured_data::*;
use strum_macros::IntoStaticStr; use strum_macros::IntoStaticStr;
use webrender_api::ExternalScrollId; use webrender_api::ExternalScrollId;
use webrender_api::units::LayoutPixel; use webrender_api::units::LayoutVector2D;
/// Messages to the Constellation from the embedding layer, whether from `ServoRenderer` or /// Messages to the Constellation from the embedding layer, whether from `ServoRenderer` or
/// from `libservo` itself. /// from `libservo` itself.
@ -90,7 +89,7 @@ pub enum EmbedderToConstellationMessage {
SetWebViewThrottled(WebViewId, bool), SetWebViewThrottled(WebViewId, bool),
/// The Servo renderer scrolled and is updating the scroll states of the nodes in the /// The Servo renderer scrolled and is updating the scroll states of the nodes in the
/// given pipeline via the constellation. /// given pipeline via the constellation.
SetScrollStates(PipelineId, Vec<ScrollState>), SetScrollStates(PipelineId, HashMap<ExternalScrollId, LayoutVector2D>),
/// Notify the constellation that a particular paint metric event has happened for the given pipeline. /// Notify the constellation that a particular paint metric event has happened for the given pipeline.
PaintMetric(PipelineId, PaintMetricEvent), PaintMetric(PipelineId, PaintMetricEvent),
/// Evaluate a JavaScript string in the context of a `WebView`. When execution is complete or an /// Evaluate a JavaScript string in the context of a `WebView`. When execution is complete or an
@ -136,15 +135,6 @@ pub enum WindowSizeType {
Resize, Resize,
} }
/// The scroll state of a stacking context.
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub struct ScrollState {
/// The ID of the scroll root.
pub scroll_id: ExternalScrollId,
/// The scrolling offset of this stacking context.
pub scroll_offset: Vector2D<f32, LayoutPixel>,
}
/// The direction of a history traversal /// The direction of a history traversal
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum TraversalDirection { pub enum TraversalDirection {

View file

@ -9,6 +9,7 @@
#![deny(missing_docs)] #![deny(missing_docs)]
#![deny(unsafe_code)] #![deny(unsafe_code)]
use std::collections::HashMap;
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
@ -20,8 +21,8 @@ use bluetooth_traits::BluetoothRequest;
use canvas_traits::webgl::WebGLPipeline; use canvas_traits::webgl::WebGLPipeline;
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{ use constellation_traits::{
LoadData, NavigationHistoryBehavior, ScriptToConstellationChan, ScrollState, LoadData, NavigationHistoryBehavior, ScriptToConstellationChan, StructuredSerializedData,
StructuredSerializedData, WindowSizeType, WindowSizeType,
}; };
use crossbeam_channel::{RecvTimeoutError, Sender}; use crossbeam_channel::{RecvTimeoutError, Sender};
use devtools_traits::ScriptToDevtoolsControlMsg; use devtools_traits::ScriptToDevtoolsControlMsg;
@ -47,8 +48,8 @@ use style_traits::{CSSPixel, SpeculativePainter};
use stylo_atoms::Atom; use stylo_atoms::Atom;
#[cfg(feature = "webgpu")] #[cfg(feature = "webgpu")]
use webgpu_traits::WebGPUMsg; use webgpu_traits::WebGPUMsg;
use webrender_api::ImageKey; use webrender_api::units::{DevicePixel, LayoutVector2D};
use webrender_api::units::DevicePixel; use webrender_api::{ExternalScrollId, ImageKey};
/// The initial data required to create a new layout attached to an existing script thread. /// The initial data required to create a new layout attached to an existing script thread.
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -246,7 +247,7 @@ pub enum ScriptThreadMessage {
SetWebGPUPort(IpcReceiver<WebGPUMsg>), SetWebGPUPort(IpcReceiver<WebGPUMsg>),
/// The compositor scrolled and is updating the scroll states of the nodes in the given /// The compositor scrolled and is updating the scroll states of the nodes in the given
/// pipeline via the Constellation. /// pipeline via the Constellation.
SetScrollStates(PipelineId, Vec<ScrollState>), SetScrollStates(PipelineId, HashMap<ExternalScrollId, LayoutVector2D>),
/// Evaluate the given JavaScript and return a result via a corresponding message /// Evaluate the given JavaScript and return a result via a corresponding message
/// to the Constellation. /// to the Constellation.
EvaluateJavaScript(PipelineId, JavaScriptEvaluationId, String), EvaluateJavaScript(PipelineId, JavaScriptEvaluationId, String),

View file

@ -11,6 +11,7 @@
pub mod wrapper_traits; pub mod wrapper_traits;
use std::any::Any; use std::any::Any;
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicIsize, AtomicU64, Ordering}; use std::sync::atomic::{AtomicIsize, AtomicU64, Ordering};
@ -19,7 +20,7 @@ use atomic_refcell::AtomicRefCell;
use base::Epoch; use base::Epoch;
use base::id::{BrowsingContextId, PipelineId, WebViewId}; use base::id::{BrowsingContextId, PipelineId, WebViewId};
use compositing_traits::CrossProcessCompositorApi; use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{LoadData, ScrollState}; use constellation_traits::LoadData;
use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails}; use embedder_traits::{Theme, UntrustedNodeAddress, ViewportDetails};
use euclid::default::{Point2D, Rect}; use euclid::default::{Point2D, Rect};
use fnv::FnvHashMap; use fnv::FnvHashMap;
@ -48,8 +49,8 @@ use style::properties::PropertyId;
use style::properties::style_structs::Font; use style::properties::style_structs::Font;
use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot}; use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot};
use style::stylesheets::Stylesheet; use style::stylesheets::Stylesheet;
use webrender_api::ImageKey; use webrender_api::units::{DeviceIntSize, LayoutVector2D};
use webrender_api::units::DeviceIntSize; use webrender_api::{ExternalScrollId, ImageKey};
pub trait GenericLayoutDataTrait: Any + MallocSizeOfTrait { pub trait GenericLayoutDataTrait: Any + MallocSizeOfTrait {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
@ -245,7 +246,10 @@ pub trait Layout {
); );
/// Set the scroll states of this layout after a compositor scroll. /// Set the scroll states of this layout after a compositor scroll.
fn set_scroll_offsets(&mut self, scroll_states: &[ScrollState]); fn set_scroll_offsets_from_renderer(
&mut self,
scroll_states: &HashMap<ExternalScrollId, LayoutVector2D>,
);
fn query_content_box(&self, node: TrustedNodeAddress) -> Option<Rect<Au>>; fn query_content_box(&self, node: TrustedNodeAddress) -> Option<Rect<Au>>;
fn query_content_boxes(&self, node: TrustedNodeAddress) -> Vec<Rect<Au>>; fn query_content_boxes(&self, node: TrustedNodeAddress) -> Vec<Rect<Au>>;
@ -333,7 +337,7 @@ pub enum ReflowGoal {
/// Tells layout about a single new scrolling offset from the script. The rest will /// Tells layout about a single new scrolling offset from the script. The rest will
/// remain untouched and layout won't forward this back to script. /// remain untouched and layout won't forward this back to script.
UpdateScrollNode(ScrollState), UpdateScrollNode(ExternalScrollId, LayoutVector2D),
} }
impl ReflowGoal { impl ReflowGoal {
@ -341,7 +345,7 @@ impl ReflowGoal {
/// be present or false if it only needs stacking-relative positions. /// be present or false if it only needs stacking-relative positions.
pub fn needs_display_list(&self) -> bool { pub fn needs_display_list(&self) -> bool {
match *self { match *self {
ReflowGoal::UpdateTheRendering | ReflowGoal::UpdateScrollNode(_) => true, ReflowGoal::UpdateTheRendering | ReflowGoal::UpdateScrollNode(..) => true,
ReflowGoal::LayoutQuery(ref querymsg) => match *querymsg { ReflowGoal::LayoutQuery(ref querymsg) => match *querymsg {
QueryMsg::ElementInnerOuterTextQuery | QueryMsg::ElementInnerOuterTextQuery |
QueryMsg::InnerWindowDimensionsQuery | QueryMsg::InnerWindowDimensionsQuery |
@ -363,7 +367,7 @@ impl ReflowGoal {
/// false if a layout_thread display list is sufficient. /// false if a layout_thread display list is sufficient.
pub fn needs_display(&self) -> bool { pub fn needs_display(&self) -> bool {
match *self { match *self {
ReflowGoal::UpdateTheRendering | ReflowGoal::UpdateScrollNode(_) => true, ReflowGoal::UpdateTheRendering | ReflowGoal::UpdateScrollNode(..) => true,
ReflowGoal::LayoutQuery(ref querymsg) => match *querymsg { ReflowGoal::LayoutQuery(ref querymsg) => match *querymsg {
QueryMsg::NodesFromPointQuery | QueryMsg::NodesFromPointQuery |
QueryMsg::TextIndexQuery | QueryMsg::TextIndexQuery |