diff options
Diffstat (limited to 'components')
97 files changed, 2870 insertions, 1671 deletions
diff --git a/components/canvas/Cargo.toml b/components/canvas/Cargo.toml index a9c06784d18..7e7b00efe11 100644 --- a/components/canvas/Cargo.toml +++ b/components/canvas/Cargo.toml @@ -38,6 +38,7 @@ pixels = { path = "../pixels" } range = { path = "../range" } raqote = "0.8.5" servo_arc = { workspace = true } +snapshot = { workspace = true } stylo = { workspace = true } surfman = { workspace = true } unicode-script = { workspace = true } diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index d6e35b2cbaf..99d6273813e 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -19,6 +19,7 @@ use log::warn; use num_traits::ToPrimitive; use range::Range; use servo_arc::Arc as ServoArc; +use snapshot::Snapshot; use style::color::AbsoluteColor; use style::properties::style_structs::Font as FontStyleStruct; use unicode_script::Script; @@ -521,8 +522,7 @@ pub trait GenericDrawTarget { stroke_options: &StrokeOptions, draw_options: &DrawOptions, ); - fn snapshot_data(&self, f: &dyn Fn(&[u8]) -> Vec<u8>) -> Vec<u8>; - fn snapshot_data_owned(&self) -> Vec<u8>; + fn snapshot_data(&self) -> &[u8]; } pub enum GradientStop { @@ -605,9 +605,8 @@ impl<'a> CanvasData<'a> { offset: 0, flags: ImageDescriptorFlags::empty(), }; - let data = SerializableImageData::Raw(IpcSharedMemory::from_bytes( - &draw_target.snapshot_data_owned(), - )); + let data = + SerializableImageData::Raw(IpcSharedMemory::from_bytes(draw_target.snapshot_data())); compositor_api.update_images(vec![ImageUpdate::AddImage(image_key, descriptor, data)]); CanvasData { backend, @@ -628,7 +627,7 @@ impl<'a> CanvasData<'a> { pub fn draw_image( &mut self, image_data: &[u8], - image_size: Size2D<f64>, + image_size: Size2D<u64>, dest_rect: Rect<f64>, source_rect: Rect<f64>, smoothing_enabled: bool, @@ -637,8 +636,8 @@ impl<'a> CanvasData<'a> { // We round up the floating pixel values to draw the pixels let source_rect = source_rect.ceil(); // It discards the extra pixels (if any) that won't be painted - let image_data = if Rect::from_size(image_size).contains_rect(&source_rect) { - pixels::rgba8_get_rect(image_data, image_size.to_u64(), source_rect.to_u64()).into() + let image_data = if Rect::from_size(image_size.to_f64()).contains_rect(&source_rect) { + pixels::rgba8_get_rect(image_data, image_size, source_rect.to_u64()).into() } else { image_data.into() }; @@ -1412,12 +1411,8 @@ impl<'a> CanvasData<'a> { self.update_image_rendering(); } - pub fn send_pixels(&mut self, chan: IpcSender<IpcSharedMemory>) { - self.drawtarget.snapshot_data(&|bytes| { - let data = IpcSharedMemory::from_bytes(bytes); - chan.send(data).unwrap(); - vec![] - }); + pub fn snapshot(&self) { + self.drawtarget.snapshot_data(); } /// Update image in WebRender @@ -1430,7 +1425,7 @@ impl<'a> CanvasData<'a> { flags: ImageDescriptorFlags::empty(), }; let data = SerializableImageData::Raw(IpcSharedMemory::from_bytes( - &self.drawtarget.snapshot_data_owned(), + self.drawtarget.snapshot_data(), )); self.compositor_api @@ -1529,18 +1524,36 @@ impl<'a> CanvasData<'a> { /// canvas_size: The size of the canvas we're reading from /// read_rect: The area of the canvas we want to read from #[allow(unsafe_code)] - pub fn read_pixels(&self, read_rect: Rect<u64>, canvas_size: Size2D<u64>) -> Vec<u8> { - let canvas_rect = Rect::from_size(canvas_size); - if canvas_rect - .intersection(&read_rect) - .is_none_or(|rect| rect.is_empty()) - { - return vec![]; - } + pub fn read_pixels( + &self, + read_rect: Option<Rect<u64>>, + canvas_size: Option<Size2D<u64>>, + ) -> Snapshot { + let canvas_size = canvas_size.unwrap_or(self.drawtarget.get_size().cast()); + + let data = if let Some(read_rect) = read_rect { + let canvas_rect = Rect::from_size(canvas_size); + if canvas_rect + .intersection(&read_rect) + .is_none_or(|rect| rect.is_empty()) + { + vec![] + } else { + let bytes = self.drawtarget.snapshot_data(); + pixels::rgba8_get_rect(bytes, canvas_size, read_rect).to_vec() + } + } else { + self.drawtarget.snapshot_data().to_vec() + }; - self.drawtarget.snapshot_data(&|bytes| { - pixels::rgba8_get_rect(bytes, canvas_size, read_rect).into_owned() - }) + Snapshot::from_vec( + canvas_size, + snapshot::PixelFormat::BGRA, + snapshot::AlphaMode::Transparent { + premultiplied: true, + }, + data, + ) } } diff --git a/components/canvas/canvas_paint_thread.rs b/components/canvas/canvas_paint_thread.rs index 8b1b5038334..bb940d7ef81 100644 --- a/components/canvas/canvas_paint_thread.rs +++ b/components/canvas/canvas_paint_thread.rs @@ -76,7 +76,11 @@ impl<'a> CanvasPaintThread<'a> { }, Ok(CanvasMsg::FromScript(message, canvas_id)) => match message { FromScriptMsg::SendPixels(chan) => { - canvas_paint_thread.canvas(canvas_id).send_pixels(chan); + chan.send(canvas_paint_thread + .canvas(canvas_id) + .read_pixels(None, None) + .as_ipc() + ).unwrap(); }, }, Err(e) => { @@ -159,24 +163,21 @@ impl<'a> CanvasPaintThread<'a> { Canvas2dMsg::IsPointInPath(path, x, y, fill_rule, chan) => self .canvas(canvas_id) .is_point_in_path_(&path[..], x, y, fill_rule, chan), - Canvas2dMsg::DrawImage( - ref image_data, - image_size, - dest_rect, - source_rect, - smoothing_enabled, - ) => self.canvas(canvas_id).draw_image( - image_data, - image_size, - dest_rect, - source_rect, - smoothing_enabled, - true, - ), + Canvas2dMsg::DrawImage(snapshot, dest_rect, source_rect, smoothing_enabled) => { + let snapshot = snapshot.to_owned(); + self.canvas(canvas_id).draw_image( + snapshot.data(), + snapshot.size(), + dest_rect, + source_rect, + smoothing_enabled, + !snapshot.alpha_mode().is_premultiplied(), + ) + }, Canvas2dMsg::DrawEmptyImage(image_size, dest_rect, source_rect) => { self.canvas(canvas_id).draw_image( &vec![0; image_size.area() as usize * 4], - image_size, + image_size.to_u64(), dest_rect, source_rect, false, @@ -192,10 +193,10 @@ impl<'a> CanvasPaintThread<'a> { ) => { let image_data = self .canvas(canvas_id) - .read_pixels(source_rect.to_u64(), image_size.to_u64()); + .read_pixels(Some(source_rect.to_u64()), Some(image_size.to_u64())); self.canvas(other_canvas_id).draw_image( - &image_data, - source_rect.size, + image_data.data(), + source_rect.size.to_u64(), dest_rect, source_rect, smoothing, @@ -244,8 +245,10 @@ impl<'a> CanvasPaintThread<'a> { self.canvas(canvas_id).set_global_composition(op) }, Canvas2dMsg::GetImageData(dest_rect, canvas_size, sender) => { - let pixels = self.canvas(canvas_id).read_pixels(dest_rect, canvas_size); - sender.send(&pixels).unwrap(); + let snapshot = self + .canvas(canvas_id) + .read_pixels(Some(dest_rect), Some(canvas_size)); + sender.send(snapshot.as_ipc()).unwrap(); }, Canvas2dMsg::PutImageData(rect, receiver) => { self.canvas(canvas_id) diff --git a/components/canvas/raqote_backend.rs b/components/canvas/raqote_backend.rs index e40367a4ee8..12137e41f41 100644 --- a/components/canvas/raqote_backend.rs +++ b/components/canvas/raqote_backend.rs @@ -630,7 +630,7 @@ impl GenericDrawTarget for raqote::DrawTarget { self.set_transform(matrix); } fn snapshot(&self) -> SourceSurface { - SourceSurface::Raqote(self.snapshot_data_owned()) + SourceSurface::Raqote(self.snapshot_data().to_vec()) } fn stroke( &mut self, @@ -694,20 +694,9 @@ impl GenericDrawTarget for raqote::DrawTarget { ); } #[allow(unsafe_code)] - fn snapshot_data(&self, f: &dyn Fn(&[u8]) -> Vec<u8>) -> Vec<u8> { + fn snapshot_data(&self) -> &[u8] { let v = self.get_data(); - f( - unsafe { - std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) - }, - ) - } - #[allow(unsafe_code)] - fn snapshot_data_owned(&self) -> Vec<u8> { - let v = self.get_data(); - unsafe { - std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)).into() - } + unsafe { std::slice::from_raw_parts(v.as_ptr() as *const u8, std::mem::size_of_val(v)) } } } diff --git a/components/canvas/webgl_thread.rs b/components/canvas/webgl_thread.rs index e3f0c77b4b3..b1ac2b2d3c4 100644 --- a/components/canvas/webgl_thread.rs +++ b/components/canvas/webgl_thread.rs @@ -31,6 +31,7 @@ use glow::{ bytes_per_type, components_per_format, }; use half::f16; +use ipc_channel::ipc::IpcSharedMemory; use log::{debug, error, trace, warn}; use pixels::{self, PixelFormat, unmultiply_inplace}; use surfman::chains::{PreserveBuffer, SwapChains, SwapChainsAPI}; @@ -1212,7 +1213,13 @@ impl WebGLImpl { glow::PixelPackData::Slice(Some(&mut pixels)), ) }; - sender.send(&pixels).unwrap(); + let alpha_mode = match (attributes.alpha, attributes.premultiplied_alpha) { + (true, premultiplied) => snapshot::AlphaMode::Transparent { premultiplied }, + (false, _) => snapshot::AlphaMode::Opaque, + }; + sender + .send((IpcSharedMemory::from_bytes(&pixels), alpha_mode)) + .unwrap(); }, WebGLCommand::ReadPixelsPP(rect, format, pixel_type, offset) => unsafe { gl.read_pixels( diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 4550188a7fa..41286a2760a 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -21,12 +21,12 @@ use compositing_traits::rendering_context::RenderingContext; use compositing_traits::{ CompositionPipeline, CompositorMsg, ImageUpdate, SendableFrameTree, WebViewTrait, }; -use constellation_traits::{AnimationTickType, EmbedderToConstellationMessage, PaintMetricEvent}; +use constellation_traits::{EmbedderToConstellationMessage, PaintMetricEvent}; use crossbeam_channel::{Receiver, Sender}; use dpi::PhysicalSize; use embedder_traits::{ - AnimationState, CompositorHitTestResult, Cursor, InputEvent, MouseButtonEvent, MouseMoveEvent, - ShutdownState, TouchEventType, UntrustedNodeAddress, ViewportDetails, + CompositorHitTestResult, Cursor, InputEvent, MouseButtonEvent, MouseMoveEvent, ShutdownState, + TouchEventType, UntrustedNodeAddress, ViewportDetails, }; use euclid::{Point2D, Rect, Scale, Size2D, Transform3D}; use fnv::FnvHashMap; @@ -197,9 +197,6 @@ pub(crate) struct PipelineDetails { /// The pipeline associated with this PipelineDetails object. pub pipeline: Option<CompositionPipeline>, - /// The [`PipelineId`] of this pipeline. - pub id: PipelineId, - /// The id of the parent pipeline, if any. pub parent_pipeline_id: Option<PipelineId>, @@ -243,32 +240,12 @@ impl PipelineDetails { pub(crate) fn animating(&self) -> bool { !self.throttled && (self.animation_callbacks_running || self.animations_running) } - - pub(crate) fn tick_animations(&self, compositor: &IOCompositor) { - if !self.animating() { - return; - } - - let mut tick_type = AnimationTickType::empty(); - if self.animations_running { - tick_type.insert(AnimationTickType::CSS_ANIMATIONS_AND_TRANSITIONS); - } - if self.animation_callbacks_running { - tick_type.insert(AnimationTickType::REQUEST_ANIMATION_FRAME); - } - - let msg = EmbedderToConstellationMessage::TickAnimation(self.id, tick_type); - if let Err(e) = compositor.global.borrow().constellation_sender.send(msg) { - warn!("Sending tick to constellation failed ({:?}).", e); - } - } } impl PipelineDetails { - pub(crate) fn new(id: PipelineId) -> PipelineDetails { + pub(crate) fn new() -> PipelineDetails { PipelineDetails { pipeline: None, - id, parent_pipeline_id: None, most_recent_display_list_epoch: None, animations_running: false, @@ -543,22 +520,14 @@ impl IOCompositor { pipeline_id, animation_state, ) => { - let mut throttled = true; if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { - throttled = webview_renderer - .change_running_animations_state(pipeline_id, animation_state); - } - - // These operations should eventually happen per-WebView, but they are global now as rendering - // is still global to all WebViews. - if !throttled && animation_state == AnimationState::AnimationsPresent { - self.set_needs_repaint(RepaintReason::ChangedAnimationState); - } - - if !throttled && animation_state == AnimationState::AnimationCallbacksPresent { - // We need to fetch the WebView again in order to avoid a double borrow. - if let Some(webview_renderer) = self.webview_renderers.get(webview_id) { - webview_renderer.tick_animations_for_pipeline(pipeline_id, self); + if webview_renderer + .change_pipeline_running_animations_state(pipeline_id, animation_state) && + webview_renderer.animating() + { + // These operations should eventually happen per-WebView, but they are + // global now as rendering is still global to all WebViews. + self.process_animations(true); } } }, @@ -605,8 +574,13 @@ impl IOCompositor { CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => { if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { - webview_renderer.set_throttled(pipeline_id, throttled); - self.process_animations(true); + if webview_renderer.set_throttled(pipeline_id, throttled) && + webview_renderer.animating() + { + // These operations should eventually happen per-WebView, but they are + // global now as rendering is still global to all WebViews. + self.process_animations(true); + } } }, @@ -1283,8 +1257,23 @@ impl IOCompositor { } self.last_animation_tick = Instant::now(); - for webview_renderer in self.webview_renderers.iter() { - webview_renderer.tick_all_animations(self); + let animating_webviews: Vec<_> = self + .webview_renderers + .iter() + .filter_map(|webview_renderer| { + if webview_renderer.animating() { + Some(webview_renderer.id) + } else { + None + } + }) + .collect(); + if !animating_webviews.is_empty() { + if let Err(error) = self.global.borrow().constellation_sender.send( + EmbedderToConstellationMessage::TickAnimation(animating_webviews), + ) { + warn!("Sending tick to constellation failed ({error:?})."); + } } } diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index 6ad77d46043..614ef0ff4c3 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -86,6 +86,9 @@ pub(crate) struct WebViewRenderer { /// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled /// by the embedding layer. hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>, + /// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with + /// active animations or animation frame callbacks. + animating: bool, } impl Drop for WebViewRenderer { @@ -119,6 +122,7 @@ impl WebViewRenderer { min_viewport_zoom: Some(PinchZoomFactor::new(1.0)), max_viewport_zoom: None, hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), + animating: false, } } @@ -138,6 +142,10 @@ impl WebViewRenderer { self.pipelines.keys() } + pub(crate) fn animating(&self) -> bool { + self.animating + } + /// Returns the [`PipelineDetails`] for the given [`PipelineId`], creating it if needed. pub(crate) fn ensure_pipeline_details( &mut self, @@ -148,14 +156,10 @@ impl WebViewRenderer { .borrow_mut() .pipeline_to_webview_map .insert(pipeline_id, self.id); - PipelineDetails::new(pipeline_id) + PipelineDetails::new() }) } - pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) { - self.ensure_pipeline_details(pipeline_id).throttled = throttled; - } - pub(crate) fn remove_pipeline(&mut self, pipeline_id: PipelineId) { self.global .borrow_mut() @@ -245,51 +249,45 @@ impl WebViewRenderer { }) } - /// Sets or unsets the animations-running flag for the given pipeline. Returns true if - /// the pipeline is throttled. - pub(crate) fn change_running_animations_state( + /// Sets or unsets the animations-running flag for the given pipeline. Returns + /// true if the [`WebViewRenderer`]'s overall animating state changed. + pub(crate) fn change_pipeline_running_animations_state( &mut self, pipeline_id: PipelineId, animation_state: AnimationState, ) -> bool { - let throttled = { - let pipeline_details = self.ensure_pipeline_details(pipeline_id); - match animation_state { - AnimationState::AnimationsPresent => { - pipeline_details.animations_running = true; - }, - AnimationState::AnimationCallbacksPresent => { - pipeline_details.animation_callbacks_running = true; - }, - AnimationState::NoAnimationsPresent => { - pipeline_details.animations_running = false; - }, - AnimationState::NoAnimationCallbacksPresent => { - pipeline_details.animation_callbacks_running = false; - }, - } - pipeline_details.throttled - }; - - let animating = self.pipelines.values().any(PipelineDetails::animating); - self.webview.set_animating(animating); - throttled + let pipeline_details = self.ensure_pipeline_details(pipeline_id); + match animation_state { + AnimationState::AnimationsPresent => { + pipeline_details.animations_running = true; + }, + AnimationState::AnimationCallbacksPresent => { + pipeline_details.animation_callbacks_running = true; + }, + AnimationState::NoAnimationsPresent => { + pipeline_details.animations_running = false; + }, + AnimationState::NoAnimationCallbacksPresent => { + pipeline_details.animation_callbacks_running = false; + }, + } + self.update_animation_state() } - pub(crate) fn tick_all_animations(&self, compositor: &IOCompositor) { - for pipeline_details in self.pipelines.values() { - pipeline_details.tick_animations(compositor) - } + /// Sets or unsets the throttled flag for the given pipeline. Returns + /// true if the [`WebViewRenderer`]'s overall animating state changed. + pub(crate) fn set_throttled(&mut self, pipeline_id: PipelineId, throttled: bool) -> bool { + self.ensure_pipeline_details(pipeline_id).throttled = throttled; + + // Throttling a pipeline can cause it to be taken into the "not-animating" state. + self.update_animation_state() } - pub(crate) fn tick_animations_for_pipeline( - &self, - pipeline_id: PipelineId, - compositor: &IOCompositor, - ) { - if let Some(pipeline_details) = self.pipelines.get(&pipeline_id) { - pipeline_details.tick_animations(compositor); - } + pub(crate) fn update_animation_state(&mut self) -> bool { + let animating = self.pipelines.values().any(PipelineDetails::animating); + let old_animating = std::mem::replace(&mut self.animating, animating); + self.webview.set_animating(self.animating); + old_animating != self.animating } /// On a Window refresh tick (e.g. vsync) diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 3f70b0abb89..05081fe0ba7 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -112,13 +112,12 @@ use compositing_traits::{ CompositorMsg, CompositorProxy, SendableFrameTree, WebrenderExternalImageRegistry, }; use constellation_traits::{ - AnimationTickType, AuxiliaryWebViewCreationRequest, AuxiliaryWebViewCreationResponse, - BroadcastMsg, DocumentState, EmbedderToConstellationMessage, IFrameLoadInfo, - IFrameLoadInfoWithData, IFrameSandboxState, IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry, - MessagePortMsg, NavigationHistoryBehavior, PaintMetricEvent, PortMessageTask, SWManagerMsg, - SWManagerSenders, ScriptToConstellationChan, ScriptToConstellationMessage, ScrollState, - ServiceWorkerManagerFactory, ServiceWorkerMsg, StructuredSerializedData, TraversalDirection, - WindowSizeType, + AuxiliaryWebViewCreationRequest, AuxiliaryWebViewCreationResponse, BroadcastMsg, DocumentState, + EmbedderToConstellationMessage, IFrameLoadInfo, IFrameLoadInfoWithData, IFrameSandboxState, + IFrameSizeMsg, Job, LoadData, LoadOrigin, LogEntry, MessagePortMsg, NavigationHistoryBehavior, + PaintMetricEvent, PortMessageTask, SWManagerMsg, SWManagerSenders, ScriptToConstellationChan, + ScriptToConstellationMessage, ScrollState, ServiceWorkerManagerFactory, ServiceWorkerMsg, + StructuredSerializedData, TraversalDirection, WindowSizeType, }; use crossbeam_channel::{Receiver, Select, Sender, unbounded}; use devtools_traits::{ @@ -1398,8 +1397,8 @@ where EmbedderToConstellationMessage::ThemeChange(theme) => { self.handle_theme_change(theme); }, - EmbedderToConstellationMessage::TickAnimation(pipeline_id, tick_type) => { - self.handle_tick_animation(pipeline_id, tick_type) + EmbedderToConstellationMessage::TickAnimation(webview_ids) => { + self.handle_tick_animation(webview_ids) }, EmbedderToConstellationMessage::WebDriverCommand(command) => { self.handle_webdriver_msg(command); @@ -3528,15 +3527,24 @@ where feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn handle_tick_animation(&mut self, pipeline_id: PipelineId, tick_type: AnimationTickType) { - let pipeline = match self.pipelines.get(&pipeline_id) { - Some(pipeline) => pipeline, - None => return warn!("{}: Got script tick after closure", pipeline_id), - }; + fn handle_tick_animation(&mut self, webview_ids: Vec<WebViewId>) { + let mut animating_event_loops = HashSet::new(); - let message = ScriptThreadMessage::TickAllAnimations(pipeline_id, tick_type); - if let Err(e) = pipeline.event_loop.send(message) { - self.handle_send_error(pipeline_id, e); + for webview_id in webview_ids.iter() { + for browsing_context in self.fully_active_browsing_contexts_iter(*webview_id) { + let Some(pipeline) = self.pipelines.get(&browsing_context.pipeline_id) else { + continue; + }; + animating_event_loops.insert(pipeline.event_loop.clone()); + } + } + + for event_loop in animating_event_loops { + // No error handling here. It's unclear what to do when this fails as the error isn't associated + // with a particular pipeline. In addition, the danger of not progressing animations is pretty + // low, so it's probably safe to ignore this error and handle the crashed ScriptThread on + // some other message. + let _ = event_loop.send(ScriptThreadMessage::TickAllAnimations(webview_ids.clone())); } } diff --git a/components/constellation/event_loop.rs b/components/constellation/event_loop.rs index 362960eba64..46542e7212f 100644 --- a/components/constellation/event_loop.rs +++ b/components/constellation/event_loop.rs @@ -6,17 +6,36 @@ //! view of a script thread. When an `EventLoop` is dropped, an `ExitScriptThread` //! message is sent to the script thread, asking it to shut down. +use std::hash::Hash; use std::marker::PhantomData; use std::rc::Rc; +use std::sync::atomic::{AtomicUsize, Ordering}; use ipc_channel::Error; use ipc_channel::ipc::IpcSender; use script_traits::ScriptThreadMessage; +static CURRENT_EVENT_LOOP_ID: AtomicUsize = AtomicUsize::new(0); + /// <https://html.spec.whatwg.org/multipage/#event-loop> pub struct EventLoop { script_chan: IpcSender<ScriptThreadMessage>, dont_send_or_sync: PhantomData<Rc<()>>, + id: usize, +} + +impl PartialEq for EventLoop { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for EventLoop {} + +impl Hash for EventLoop { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.id.hash(state); + } } impl Drop for EventLoop { @@ -28,9 +47,11 @@ impl Drop for EventLoop { impl EventLoop { /// Create a new event loop from the channel to its script thread. pub fn new(script_chan: IpcSender<ScriptThreadMessage>) -> Rc<EventLoop> { + let id = CURRENT_EVENT_LOOP_ID.fetch_add(1, Ordering::Relaxed); Rc::new(EventLoop { script_chan, dont_send_or_sync: PhantomData, + id, }) } diff --git a/components/devtools/actors/browsing_context.rs b/components/devtools/actors/browsing_context.rs index c4ead7272bd..5de0855df4a 100644 --- a/components/devtools/actors/browsing_context.rs +++ b/components/devtools/actors/browsing_context.rs @@ -31,6 +31,7 @@ use crate::actors::thread::ThreadActor; use crate::actors::watcher::{SessionContext, SessionContextType, WatcherActor}; use crate::id::{DevtoolsBrowserId, DevtoolsBrowsingContextId, DevtoolsOuterWindowId, IdMap}; use crate::protocol::JsonPacketStream; +use crate::resource::ResourceAvailable; use crate::{EmptyReplyMsg, StreamId}; #[derive(Serialize)] @@ -57,14 +58,6 @@ struct FrameUpdateMsg { } #[derive(Serialize)] -struct ResourceAvailableReply<T: Serialize> { - from: String, - #[serde(rename = "type")] - type_: String, - array: Vec<(String, Vec<T>)>, -} - -#[derive(Serialize)] struct TabNavigated { from: String, #[serde(rename = "type")] @@ -152,6 +145,16 @@ pub(crate) struct BrowsingContextActor { pub watcher: String, } +impl ResourceAvailable for BrowsingContextActor { + fn actor_name(&self) -> String { + self.name.clone() + } + + fn get_streams(&self) -> &RefCell<HashMap<StreamId, TcpStream>> { + &self.streams + } +} + impl Actor for BrowsingContextActor { fn name(&self) -> String { self.name.clone() @@ -358,26 +361,6 @@ impl BrowsingContextActor { }); } - pub(crate) fn resource_available<T: Serialize>(&self, resource: T, resource_type: String) { - self.resources_available(vec![resource], resource_type); - } - - pub(crate) fn resources_available<T: Serialize>( - &self, - resources: Vec<T>, - resource_type: String, - ) { - let msg = ResourceAvailableReply::<T> { - from: self.name(), - type_: "resources-available-array".into(), - array: vec![(resource_type, resources)], - }; - - for stream in self.streams.borrow_mut().values_mut() { - let _ = stream.write_json_packet(&msg); - } - } - pub fn simulate_color_scheme(&self, theme: Theme) -> Result<(), ()> { self.script_chan .send(SimulateColorScheme(self.active_pipeline_id.get(), theme)) diff --git a/components/devtools/actors/console.rs b/components/devtools/actors/console.rs index ecd718e47d4..3897ffa0fce 100644 --- a/components/devtools/actors/console.rs +++ b/components/devtools/actors/console.rs @@ -30,6 +30,7 @@ use crate::actors::browsing_context::BrowsingContextActor; use crate::actors::object::ObjectActor; use crate::actors::worker::WorkerActor; use crate::protocol::JsonPacketStream; +use crate::resource::ResourceAvailable; use crate::{StreamId, UniqueId}; trait EncodableConsoleMessage { diff --git a/components/devtools/actors/source.rs b/components/devtools/actors/source.rs new file mode 100644 index 00000000000..9d29bc1d3ef --- /dev/null +++ b/components/devtools/actors/source.rs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::{Ref, RefCell}; +use std::collections::BTreeSet; + +use serde::Serialize; +use servo_url::ServoUrl; + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SourceData { + pub actor: String, + /// URL of the script, or URL of the page for inline scripts. + pub url: String, + pub is_black_boxed: bool, +} + +#[derive(Serialize)] +pub(crate) struct SourcesReply { + pub from: String, + pub sources: Vec<SourceData>, +} + +pub(crate) struct Source { + actor_name: String, + source_urls: RefCell<BTreeSet<SourceData>>, +} + +impl Source { + pub fn new(actor_name: String) -> Self { + Self { + actor_name, + source_urls: RefCell::new(BTreeSet::default()), + } + } + + pub fn add_source(&self, url: ServoUrl) { + self.source_urls.borrow_mut().insert(SourceData { + actor: self.actor_name.clone(), + url: url.to_string(), + is_black_boxed: false, + }); + } + + pub fn sources(&self) -> Ref<BTreeSet<SourceData>> { + self.source_urls.borrow() + } +} diff --git a/components/devtools/actors/thread.rs b/components/devtools/actors/thread.rs index 85ff2b732eb..7ff11dff675 100644 --- a/components/devtools/actors/thread.rs +++ b/components/devtools/actors/thread.rs @@ -2,14 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::{Ref, RefCell}; -use std::collections::BTreeSet; use std::net::TcpStream; use serde::Serialize; use serde_json::{Map, Value}; -use servo_url::ServoUrl; +use super::source::{Source, SourcesReply}; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::protocol::JsonPacketStream; use crate::{EmptyReplyMsg, StreamId}; @@ -52,45 +50,18 @@ struct ThreadInterruptedReply { type_: String, } -#[derive(Serialize)] -struct SourcesReply { - from: String, - sources: Vec<Source>, -} - -#[derive(Eq, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Source { - pub actor: String, - /// URL of the script, or URL of the page for inline scripts. - pub url: String, - pub is_black_boxed: bool, -} - pub struct ThreadActor { - name: String, - source_urls: RefCell<BTreeSet<Source>>, + pub name: String, + pub source_manager: Source, } impl ThreadActor { pub fn new(name: String) -> ThreadActor { ThreadActor { - name, - source_urls: RefCell::new(BTreeSet::default()), + name: name.clone(), + source_manager: Source::new(name), } } - - pub fn add_source(&self, url: ServoUrl) { - self.source_urls.borrow_mut().insert(Source { - actor: self.name.clone(), - url: url.to_string(), - is_black_boxed: false, - }); - } - - pub fn sources(&self) -> Ref<BTreeSet<Source>> { - self.source_urls.borrow() - } } impl Actor for ThreadActor { diff --git a/components/devtools/actors/watcher.rs b/components/devtools/actors/watcher.rs index 77f82c1023a..6a84499b6dd 100644 --- a/components/devtools/actors/watcher.rs +++ b/components/devtools/actors/watcher.rs @@ -29,6 +29,7 @@ use crate::actors::watcher::thread_configuration::{ ThreadConfigurationActor, ThreadConfigurationActorMsg, }; use crate::protocol::JsonPacketStream; +use crate::resource::ResourceAvailable; use crate::{EmptyReplyMsg, StreamId}; pub mod network_parent; @@ -264,7 +265,7 @@ impl Actor for WatcherActor { }, "source" => { let thread_actor = registry.find::<ThreadActor>(&target.thread); - let sources = thread_actor.sources(); + let sources = thread_actor.source_manager.sources(); target.resources_available(sources.iter().collect(), "source".into()); }, "console-message" | "error-message" => {}, diff --git a/components/devtools/actors/worker.rs b/components/devtools/actors/worker.rs index 046befe9dc9..42c9d9a9c28 100644 --- a/components/devtools/actors/worker.rs +++ b/components/devtools/actors/worker.rs @@ -17,6 +17,7 @@ use servo_url::ServoUrl; use crate::StreamId; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::protocol::JsonPacketStream; +use crate::resource::ResourceAvailable; #[derive(Clone, Copy)] #[allow(dead_code)] @@ -53,6 +54,16 @@ impl WorkerActor { } } +impl ResourceAvailable for WorkerActor { + fn actor_name(&self) -> String { + self.name.clone() + } + + fn get_streams(&self) -> &RefCell<HashMap<StreamId, TcpStream>> { + &self.streams + } +} + impl Actor for WorkerActor { fn name(&self) -> String { self.name.clone() diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index 74b028d5655..4d1e0222177 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -19,7 +19,7 @@ use std::net::{Shutdown, TcpListener, TcpStream}; use std::sync::{Arc, Mutex}; use std::thread; -use actors::thread::Source; +use actors::source::SourceData; use base::id::{BrowsingContextId, PipelineId, WebViewId}; use crossbeam_channel::{Receiver, Sender, unbounded}; use devtools_traits::{ @@ -30,6 +30,7 @@ use devtools_traits::{ use embedder_traits::{AllowOrDeny, EmbedderMsg, EmbedderProxy}; use ipc_channel::ipc::{self, IpcSender}; use log::trace; +use resource::ResourceAvailable; use serde::Serialize; use servo_rand::RngCore; @@ -65,6 +66,7 @@ mod actors { pub mod process; pub mod reflow; pub mod root; + pub mod source; pub mod stylesheets; pub mod tab; pub mod thread; @@ -75,6 +77,7 @@ mod actors { mod id; mod network_handler; mod protocol; +mod resource; #[derive(Clone, Debug, Eq, Hash, PartialEq)] enum UniqueId { @@ -523,9 +526,11 @@ impl DevtoolsInstance { .clone(); let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name); - thread_actor.add_source(source_info.url.clone()); + thread_actor + .source_manager + .add_source(source_info.url.clone()); - let source = Source { + let source = SourceData { actor: thread_actor_name.clone(), url: source_info.url.to_string(), is_black_boxed: false, diff --git a/components/devtools/resource.rs b/components/devtools/resource.rs new file mode 100644 index 00000000000..7cef8188cc8 --- /dev/null +++ b/components/devtools/resource.rs @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::RefCell; +use std::collections::HashMap; +use std::net::TcpStream; + +use serde::Serialize; + +use crate::StreamId; +use crate::protocol::JsonPacketStream; + +#[derive(Serialize)] +pub(crate) struct ResourceAvailableReply<T: Serialize> { + pub from: String, + #[serde(rename = "type")] + pub type_: String, + pub array: Vec<(String, Vec<T>)>, +} + +pub(crate) trait ResourceAvailable { + fn actor_name(&self) -> String; + + fn get_streams(&self) -> &RefCell<HashMap<StreamId, TcpStream>>; + + fn resource_available<T: Serialize>(&self, resource: T, resource_type: String) { + self.resources_available(vec![resource], resource_type); + } + + fn resources_available<T: Serialize>(&self, resources: Vec<T>, resource_type: String) { + let msg = ResourceAvailableReply::<T> { + from: self.actor_name(), + type_: "resources-available-array".into(), + array: vec![(resource_type, resources)], + }; + + for stream in self.get_streams().borrow_mut().values_mut() { + let _ = stream.write_json_packet(&msg); + } + } +} diff --git a/components/layout/dom.rs b/components/layout/dom.rs index 6db4dbccd41..add4b3ac2d5 100644 --- a/components/layout/dom.rs +++ b/components/layout/dom.rs @@ -2,18 +2,21 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::any::Any; use std::marker::PhantomData; use std::sync::Arc; use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; use base::id::{BrowsingContextId, PipelineId}; use html5ever::{local_name, ns}; +use malloc_size_of_derive::MallocSizeOf; use pixels::Image; use script_layout_interface::wrapper_traits::{ LayoutDataTrait, LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, }; use script_layout_interface::{ - HTMLCanvasDataSource, LayoutElementType, LayoutNodeType as ScriptLayoutNodeType, + GenericLayoutDataTrait, HTMLCanvasDataSource, LayoutElementType, + LayoutNodeType as ScriptLayoutNodeType, }; use servo_arc::Arc as ServoArc; use style::properties::ComputedValues; @@ -31,7 +34,7 @@ use crate::table::TableLevelBox; use crate::taffy::TaffyItemBox; /// The data that is stored in each DOM node that is used by layout. -#[derive(Default)] +#[derive(Default, MallocSizeOf)] pub struct InnerDOMLayoutData { pub(super) self_box: ArcRefCell<Option<LayoutBox>>, pub(super) pseudo_before_box: ArcRefCell<Option<LayoutBox>>, @@ -54,10 +57,11 @@ impl InnerDOMLayoutData { } /// A box that is stored in one of the `DOMLayoutData` slots. +#[derive(MallocSizeOf)] pub(super) enum LayoutBox { DisplayContents, BlockLevel(ArcRefCell<BlockLevelBox>), - InlineLevel(ArcRefCell<InlineItem>), + InlineLevel(Vec<ArcRefCell<InlineItem>>), FlexLevel(ArcRefCell<FlexLevelBox>), TableLevelBox(TableLevelBox), TaffyItemBox(ArcRefCell<TaffyItemBox>), @@ -70,8 +74,10 @@ impl LayoutBox { LayoutBox::BlockLevel(block_level_box) => { block_level_box.borrow().invalidate_cached_fragment() }, - LayoutBox::InlineLevel(inline_item) => { - inline_item.borrow().invalidate_cached_fragment() + LayoutBox::InlineLevel(inline_items) => { + for inline_item in inline_items.iter() { + inline_item.borrow().invalidate_cached_fragment() + } }, LayoutBox::FlexLevel(flex_level_box) => { flex_level_box.borrow().invalidate_cached_fragment() @@ -87,7 +93,10 @@ impl LayoutBox { match self { LayoutBox::DisplayContents => vec![], LayoutBox::BlockLevel(block_level_box) => block_level_box.borrow().fragments(), - LayoutBox::InlineLevel(inline_item) => inline_item.borrow().fragments(), + LayoutBox::InlineLevel(inline_items) => inline_items + .iter() + .flat_map(|inline_item| inline_item.borrow().fragments()) + .collect(), LayoutBox::FlexLevel(flex_level_box) => flex_level_box.borrow().fragments(), LayoutBox::TaffyItemBox(taffy_item_box) => taffy_item_box.borrow().fragments(), LayoutBox::TableLevelBox(table_box) => table_box.fragments(), @@ -98,11 +107,16 @@ impl LayoutBox { /// A wrapper for [`InnerDOMLayoutData`]. This is necessary to give the entire data /// structure interior mutability, as we will need to mutate the layout data of /// non-mutable DOM nodes. -#[derive(Default)] +#[derive(Default, MallocSizeOf)] pub struct DOMLayoutData(AtomicRefCell<InnerDOMLayoutData>); // The implementation of this trait allows the data to be stored in the DOM. impl LayoutDataTrait for DOMLayoutData {} +impl GenericLayoutDataTrait for DOMLayoutData { + fn as_any(&self) -> &dyn Any { + self + } +} pub struct BoxSlot<'dom> { pub(crate) slot: Option<ArcRefCell<Option<LayoutBox>>>, @@ -255,6 +269,7 @@ where } LayoutNode::layout_data(&self) .unwrap() + .as_any() .downcast_ref::<DOMLayoutData>() .unwrap() .0 @@ -262,8 +277,13 @@ where } fn layout_data(self) -> Option<AtomicRef<'dom, InnerDOMLayoutData>> { - LayoutNode::layout_data(&self) - .map(|data| data.downcast_ref::<DOMLayoutData>().unwrap().0.borrow()) + LayoutNode::layout_data(&self).map(|data| { + data.as_any() + .downcast_ref::<DOMLayoutData>() + .unwrap() + .0 + .borrow() + }) } fn element_box_slot(&self) -> BoxSlot<'dom> { diff --git a/components/layout/flow/construct.rs b/components/layout/flow/construct.rs index a6471756db8..5ed567f513b 100644 --- a/components/layout/flow/construct.rs +++ b/components/layout/flow/construct.rs @@ -458,15 +458,14 @@ where self.propagated_data.without_text_decorations(), ), ); - box_slot.set(LayoutBox::InlineLevel(atomic)); + box_slot.set(LayoutBox::InlineLevel(vec![atomic])); return; }; // Otherwise, this is just a normal inline box. Whatever happened before, all we need to do // before recurring is to remember this ongoing inline level box. - let inline_item = self - .inline_formatting_context_builder - .start_inline_box(InlineBox::new(info)); + self.inline_formatting_context_builder + .start_inline_box(InlineBox::new(info), None); if is_list_item { if let Some((marker_info, marker_contents)) = @@ -486,8 +485,14 @@ where self.finish_anonymous_table_if_needed(); - self.inline_formatting_context_builder.end_inline_box(); - box_slot.set(LayoutBox::InlineLevel(inline_item)); + // As we are ending this inline box, during the course of the `traverse()` above, the ongoing + // inline formatting context may have been split around block-level elements. In that case, + // more than a single inline box tree item may have been produced for this inline-level box. + // `InlineFormattingContextBuilder::end_inline_box()` is returning all of those box tree + // items. + box_slot.set(LayoutBox::InlineLevel( + self.inline_formatting_context_builder.end_inline_box(), + )); } fn handle_block_level_element( @@ -574,7 +579,7 @@ where display_inside, contents, )); - box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + box_slot.set(LayoutBox::InlineLevel(vec![inline_level_box])); return; } @@ -607,7 +612,7 @@ where contents, self.propagated_data, )); - box_slot.set(LayoutBox::InlineLevel(inline_level_box)); + box_slot.set(LayoutBox::InlineLevel(vec![inline_level_box])); return; } diff --git a/components/layout/flow/inline/construct.rs b/components/layout/flow/inline/construct.rs index 7c668751ef6..61292701a9f 100644 --- a/components/layout/flow/inline/construct.rs +++ b/components/layout/flow/inline/construct.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use std::char::{ToLowercase, ToUppercase}; use icu_segmenter::WordSegmenter; +use itertools::izip; use servo_arc::Arc; use style::computed_values::white_space_collapse::T as WhiteSpaceCollapse; use style::values::specified::text::TextTransformCase; @@ -67,6 +68,16 @@ pub(crate) struct InlineFormattingContextBuilder { /// When an inline box ends, it's removed from this stack. inline_box_stack: Vec<InlineBoxIdentifier>, + /// Normally, an inline box produces a single box tree [`InlineItem`]. When a block + /// element causes an inline box [to be split], it can produce multiple + /// [`InlineItem`]s, all inserted into different [`InlineFormattingContext`]s. + /// [`Self::block_in_inline_splits`] is responsible for tracking all of these split + /// inline box results, so that they can be inserted into the [`crate::dom::BoxSlot`] + /// for the DOM element once it has been processed for BoxTree construction. + /// + /// [to be split]: https://www.w3.org/TR/CSS2/visuren.html#anonymous-block-level + block_in_inline_splits: Vec<Vec<ArcRefCell<InlineItem>>>, + /// Whether or not the inline formatting context under construction has any /// uncollapsible text content. pub has_uncollapsible_text_content: bool, @@ -162,29 +173,42 @@ impl InlineFormattingContextBuilder { inline_level_box } - pub(crate) fn start_inline_box(&mut self, inline_box: InlineBox) -> ArcRefCell<InlineItem> { + pub(crate) fn start_inline_box( + &mut self, + inline_box: InlineBox, + block_in_inline_splits: Option<Vec<ArcRefCell<InlineItem>>>, + ) { self.push_control_character_string(inline_box.base.style.bidi_control_chars().0); let (identifier, inline_box) = self.inline_boxes.start_inline_box(inline_box); let inline_level_box = ArcRefCell::new(InlineItem::StartInlineBox(inline_box)); self.inline_items.push(inline_level_box.clone()); self.inline_box_stack.push(identifier); - inline_level_box + + let mut block_in_inline_splits = block_in_inline_splits.unwrap_or_default(); + block_in_inline_splits.push(inline_level_box); + self.block_in_inline_splits.push(block_in_inline_splits); } - pub(crate) fn end_inline_box(&mut self) -> ArcRefCell<InlineBox> { - let identifier = self.end_inline_box_internal(); + /// End the ongoing inline box in this [`InlineFormattingContextBuilder`], returning + /// shared references to all of the box tree items that were created for it. More than + /// a single box tree items may be produced for a single inline box when that inline + /// box is split around a block-level element. + pub(crate) fn end_inline_box(&mut self) -> Vec<ArcRefCell<InlineItem>> { + let (identifier, block_in_inline_splits) = self.end_inline_box_internal(); let inline_level_box = self.inline_boxes.get(&identifier); - inline_level_box.borrow_mut().is_last_fragment = true; - - self.push_control_character_string( - inline_level_box.borrow().base.style.bidi_control_chars().1, - ); + { + let mut inline_level_box = inline_level_box.borrow_mut(); + inline_level_box.is_last_split = true; + self.push_control_character_string(inline_level_box.base.style.bidi_control_chars().1); + } - inline_level_box + block_in_inline_splits.unwrap_or_default() } - fn end_inline_box_internal(&mut self) -> InlineBoxIdentifier { + fn end_inline_box_internal( + &mut self, + ) -> (InlineBoxIdentifier, Option<Vec<ArcRefCell<InlineItem>>>) { let identifier = self .inline_box_stack .pop() @@ -193,7 +217,12 @@ impl InlineFormattingContextBuilder { .push(ArcRefCell::new(InlineItem::EndInlineBox)); self.inline_boxes.end_inline_box(identifier); - identifier + + // This might be `None` if this builder has already drained its block-in-inline-splits + // into the new builder on the other side of a new block-in-inline split. + let block_in_inline_splits = self.block_in_inline_splits.pop(); + + (identifier, block_in_inline_splits) } pub(crate) fn push_text<'dom, Node: NodeExt<'dom>>( @@ -295,12 +324,21 @@ impl InlineFormattingContextBuilder { // marked as not being the first fragment. No inline content is carried over to this new // builder. let mut new_builder = InlineFormattingContextBuilder::new(); - for identifier in self.inline_box_stack.iter() { + let block_in_inline_splits = std::mem::take(&mut self.block_in_inline_splits); + for (identifier, historical_inline_boxes) in + izip!(self.inline_box_stack.iter(), block_in_inline_splits) + { + // Start a new inline box for every ongoing inline box in this + // InlineFormattingContext once we are done processing this block element, + // being sure to give the block-in-inline-split to the new + // InlineFormattingContext. These will finally be inserted into the DOM's + // BoxSlot once the inline box has been fully processed. new_builder.start_inline_box( self.inline_boxes .get(identifier) .borrow() .split_around_block(), + Some(historical_inline_boxes), ); } let mut inline_builder_from_before_split = std::mem::replace(self, new_builder); diff --git a/components/layout/flow/inline/inline_box.rs b/components/layout/flow/inline/inline_box.rs index 97398d6e708..de79f876340 100644 --- a/components/layout/flow/inline/inline_box.rs +++ b/components/layout/flow/inline/inline_box.rs @@ -23,8 +23,12 @@ pub(crate) struct InlineBox { pub base: LayoutBoxBase, /// The identifier of this inline box in the containing [`super::InlineFormattingContext`]. pub(super) identifier: InlineBoxIdentifier, - pub is_first_fragment: bool, - pub is_last_fragment: bool, + /// Whether or not this is the first instance of an [`InlineBox`] before a possible + /// block-in-inline split. When no split occurs, this is always true. + pub is_first_split: bool, + /// Whether or not this is the last instance of an [`InlineBox`] before a possible + /// block-in-inline split. When no split occurs, this is always true. + pub is_last_split: bool, /// The index of the default font in the [`super::InlineFormattingContext`]'s font metrics store. /// This is initialized during IFC shaping. pub default_font_index: Option<usize>, @@ -36,8 +40,8 @@ impl InlineBox { base: LayoutBoxBase::new(info.into(), info.style.clone()), // This will be assigned later, when the box is actually added to the IFC. identifier: InlineBoxIdentifier::default(), - is_first_fragment: true, - is_last_fragment: false, + is_first_split: true, + is_last_split: false, default_font_index: None, } } @@ -45,8 +49,8 @@ impl InlineBox { pub(crate) fn split_around_block(&self) -> Self { Self { base: LayoutBoxBase::new(self.base.base_fragment_info, self.base.style.clone()), - is_first_fragment: false, - is_last_fragment: false, + is_first_split: false, + is_last_split: false, ..*self } } diff --git a/components/layout/flow/inline/mod.rs b/components/layout/flow/inline/mod.rs index 490917d95a3..dabb9773410 100644 --- a/components/layout/flow/inline/mod.rs +++ b/components/layout/flow/inline/mod.rs @@ -744,7 +744,7 @@ impl InlineFormattingContextLayout<'_> { self.containing_block, self.layout_context, self.current_inline_container_state(), - inline_box.is_last_fragment, + inline_box.is_last_split, inline_box .default_font_index .map(|index| &self.ifc.font_metrics[index].metrics), @@ -773,7 +773,7 @@ impl InlineFormattingContextLayout<'_> { ); } - if inline_box.is_first_fragment { + if inline_box.is_first_split { self.current_line_segment.inline_size += inline_box_state.pbm.padding.inline_start + inline_box_state.pbm.border.inline_start + inline_box_state.pbm.margin.inline_start.auto_is(Au::zero); @@ -2349,10 +2349,10 @@ impl<'layout_data> ContentSizesComputation<'layout_data> { .auto_is(Au::zero); let pbm = margin + padding + border; - if inline_box.is_first_fragment { + if inline_box.is_first_split { self.add_inline_size(pbm.inline_start); } - if inline_box.is_last_fragment { + if inline_box.is_last_split { self.ending_inline_pbm_stack.push(pbm.inline_end); } else { self.ending_inline_pbm_stack.push(Au::zero()); diff --git a/components/layout/flow/mod.rs b/components/layout/flow/mod.rs index f92650ef340..d983e8592c4 100644 --- a/components/layout/flow/mod.rs +++ b/components/layout/flow/mod.rs @@ -896,6 +896,7 @@ fn layout_in_flow_non_replaced_block_level_same_formatting_context( block_sizes, depends_on_block_constraints, available_block_size, + justify_self, } = solve_containing_block_padding_and_border_for_in_flow_box( containing_block, &layout_style, @@ -909,6 +910,7 @@ fn layout_in_flow_non_replaced_block_level_same_formatting_context( containing_block, &pbm, containing_block_for_children.size.inline, + justify_self, ); let computed_block_size = style.content_block_size(); @@ -1154,6 +1156,7 @@ impl IndependentNonReplacedContents { block_sizes, depends_on_block_constraints, available_block_size, + justify_self, } = solve_containing_block_padding_and_border_for_in_flow_box( containing_block, &layout_style, @@ -1185,7 +1188,7 @@ impl IndependentNonReplacedContents { let ResolvedMargins { margin, effective_margin_inline_start, - } = solve_margins(containing_block, &pbm, inline_size); + } = solve_margins(containing_block, &pbm, inline_size, justify_self); let content_rect = LogicalRect { start_corner: LogicalVec2 { @@ -1300,17 +1303,12 @@ impl IndependentNonReplacedContents { .sizes }; - // TODO: the automatic inline size should take `justify-self` into account. + let justify_self = resolve_justify_self(style, containing_block.style); let is_table = self.is_table(); - let automatic_inline_size = if is_table { - Size::FitContent - } else { - Size::Stretch - }; let compute_inline_size = |stretch_size| { content_box_sizes.inline.resolve( Direction::Inline, - automatic_inline_size, + automatic_inline_size(justify_self, is_table), Au::zero, Some(stretch_size), get_inline_content_sizes, @@ -1472,6 +1470,7 @@ impl IndependentNonReplacedContents { &pbm, content_size.inline + pbm.padding_border_sums.inline, placement_rect, + justify_self, ); let margin = LogicalSides { @@ -1558,6 +1557,7 @@ impl ReplacedContents { let effective_margin_inline_start; let (margin_block_start, margin_block_end) = solve_block_margins_for_in_flow_block_level(pbm); + let justify_self = resolve_justify_self(&base.style, containing_block.style); let containing_block_writing_mode = containing_block.style.writing_mode; let physical_content_size = content_size.to_physical_size(containing_block_writing_mode); @@ -1597,6 +1597,7 @@ impl ReplacedContents { pbm, size.inline, placement_rect, + justify_self, ); // Clearance prevents margin collapse between this block and previous ones, @@ -1620,6 +1621,7 @@ impl ReplacedContents { containing_block, pbm, content_size.inline, + justify_self, ); }; @@ -1671,6 +1673,7 @@ struct ContainingBlockPaddingAndBorder<'a> { block_sizes: Sizes, depends_on_block_constraints: bool, available_block_size: Option<Au>, + justify_self: AlignFlags, } struct ResolvedMargins { @@ -1719,6 +1722,9 @@ fn solve_containing_block_padding_and_border_for_in_flow_box<'a>( // The available block size may actually be definite, but it should be irrelevant // since the sizing properties are set to their initial value. available_block_size: None, + // The initial `justify-self` is `auto`, but use `normal` (behaving as `stretch`). + // This is being discussed in <https://github.com/w3c/csswg-drafts/issues/11461>. + justify_self: AlignFlags::NORMAL, }; } @@ -1755,16 +1761,11 @@ fn solve_containing_block_padding_and_border_for_in_flow_box<'a>( None, /* TODO: support preferred aspect ratios on non-replaced boxes */ )) }; - // TODO: the automatic inline size should take `justify-self` into account. + let justify_self = resolve_justify_self(style, containing_block.style); let is_table = layout_style.is_table(); - let automatic_inline_size = if is_table { - Size::FitContent - } else { - Size::Stretch - }; let inline_size = content_box_sizes.inline.resolve( Direction::Inline, - automatic_inline_size, + automatic_inline_size(justify_self, is_table), Au::zero, Some(available_inline_size), get_inline_content_sizes, @@ -1793,6 +1794,7 @@ fn solve_containing_block_padding_and_border_for_in_flow_box<'a>( block_sizes: content_box_sizes.block, depends_on_block_constraints, available_block_size, + justify_self, } } @@ -1804,9 +1806,15 @@ fn solve_margins( containing_block: &ContainingBlock<'_>, pbm: &PaddingBorderMargin, inline_size: Au, + justify_self: AlignFlags, ) -> ResolvedMargins { let (inline_margins, effective_margin_inline_start) = - solve_inline_margins_for_in_flow_block_level(containing_block, pbm, inline_size); + solve_inline_margins_for_in_flow_block_level( + containing_block, + pbm, + inline_size, + justify_self, + ); let block_margins = solve_block_margins_for_in_flow_block_level(pbm); ResolvedMargins { margin: LogicalSides { @@ -1829,14 +1837,63 @@ fn solve_block_margins_for_in_flow_block_level(pbm: &PaddingBorderMargin) -> (Au ) } -/// This is supposed to handle 'justify-self', but no browser supports it on block boxes. -/// Instead, `<center>` and `<div align>` are implemented via internal 'text-align' values. +/// Resolves the `justify-self` value, preserving flags. +fn resolve_justify_self(style: &ComputedValues, parent_style: &ComputedValues) -> AlignFlags { + let is_ltr = |style: &ComputedValues| style.writing_mode.line_left_is_inline_start(); + let alignment = match style.clone_justify_self().0.0 { + AlignFlags::AUTO => parent_style.clone_justify_items().computed.0, + alignment => alignment, + }; + let alignment_value = match alignment.value() { + AlignFlags::LEFT if is_ltr(parent_style) => AlignFlags::START, + AlignFlags::LEFT => AlignFlags::END, + AlignFlags::RIGHT if is_ltr(parent_style) => AlignFlags::END, + AlignFlags::RIGHT => AlignFlags::START, + AlignFlags::SELF_START if is_ltr(parent_style) == is_ltr(style) => AlignFlags::START, + AlignFlags::SELF_START => AlignFlags::END, + AlignFlags::SELF_END if is_ltr(parent_style) == is_ltr(style) => AlignFlags::END, + AlignFlags::SELF_END => AlignFlags::START, + alignment_value => alignment_value, + }; + alignment.flags() | alignment_value +} + +/// Determines the automatic size for the inline axis of a block-level box. +/// <https://drafts.csswg.org/css-sizing-3/#automatic-size> +#[inline] +fn automatic_inline_size<T>(justify_self: AlignFlags, is_table: bool) -> Size<T> { + match justify_self { + AlignFlags::STRETCH => Size::Stretch, + AlignFlags::NORMAL if !is_table => Size::Stretch, + _ => Size::FitContent, + } +} + +/// Justifies a block-level box, distributing the free space according to `justify-self`. +/// Note `<center>` and `<div align>` are implemented via internal 'text-align' values, +/// which are also handled here. /// The provided free space should already take margins into account. In particular, /// it should be zero if there is an auto margin. /// <https://drafts.csswg.org/css-align/#justify-block> -fn justify_self_alignment(containing_block: &ContainingBlock, free_space: Au) -> Au { +fn justify_self_alignment( + containing_block: &ContainingBlock, + free_space: Au, + justify_self: AlignFlags, +) -> Au { + let mut alignment = justify_self.value(); + let is_safe = justify_self.flags() == AlignFlags::SAFE || alignment == AlignFlags::NORMAL; + if is_safe && free_space <= Au::zero() { + alignment = AlignFlags::START + } + match alignment { + AlignFlags::NORMAL => {}, + AlignFlags::CENTER => return free_space / 2, + AlignFlags::END => return free_space, + _ => return Au::zero(), + } + + // For `justify-self: normal`, fall back to the special 'text-align' values. let style = containing_block.style; - debug_assert!(free_space >= Au::zero()); match style.clone_text_align() { TextAlignKeyword::MozCenter => free_space / 2, TextAlignKeyword::MozLeft if !style.writing_mode.line_left_is_inline_start() => free_space, @@ -1861,6 +1918,7 @@ fn solve_inline_margins_for_in_flow_block_level( containing_block: &ContainingBlock, pbm: &PaddingBorderMargin, inline_size: Au, + justify_self: AlignFlags, ) -> ((Au, Au), Au) { let free_space = containing_block.size.inline - pbm.padding_border_sums.inline - inline_size; let mut justification = Au::zero(); @@ -1878,8 +1936,8 @@ fn solve_inline_margins_for_in_flow_block_level( // But here we may still have some free space to perform 'justify-self' alignment. // This aligns the margin box within the containing block, or in other words, // aligns the border box within the margin-shrunken containing block. - let free_space = Au::zero().max(free_space - start - end); - justification = justify_self_alignment(containing_block, free_space); + justification = + justify_self_alignment(containing_block, free_space - start - end, justify_self); (start, end) }, }; @@ -1902,6 +1960,7 @@ fn solve_inline_margins_avoiding_floats( pbm: &PaddingBorderMargin, inline_size: Au, placement_rect: LogicalRect<Au>, + justify_self: AlignFlags, ) -> ((Au, Au), Au) { let free_space = placement_rect.size.inline - inline_size; debug_assert!(free_space >= Au::zero()); @@ -1922,7 +1981,7 @@ fn solve_inline_margins_avoiding_floats( // and Blink and WebKit are broken anyways. So we match Gecko instead: this aligns // the border box within the instersection of the float-shrunken containing-block // and the margin-shrunken containing-block. - justification = justify_self_alignment(containing_block, free_space); + justification = justify_self_alignment(containing_block, free_space, justify_self); (start, end) }, }; diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs index 390b4664e60..187726595f8 100644 --- a/components/layout/flow/root.rs +++ b/components/layout/flow/root.rs @@ -195,16 +195,17 @@ impl BoxTree { }, _ => return None, }, - LayoutBox::InlineLevel(inline_level_box) => match &*inline_level_box.borrow() { - InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) - if box_style.position.is_absolutely_positioned() => - { - UpdatePoint::AbsolutelyPositionedInlineLevelBox( - inline_level_box.clone(), - *text_offset_index, - ) - }, - _ => return None, + LayoutBox::InlineLevel(inline_level_items) => { + let inline_level_box = inline_level_items.first()?; + let InlineItem::OutOfFlowAbsolutelyPositionedBox(_, text_offset_index) = + &*inline_level_box.borrow() + else { + return None; + }; + UpdatePoint::AbsolutelyPositionedInlineLevelBox( + inline_level_box.clone(), + *text_offset_index, + ) }, LayoutBox::FlexLevel(flex_level_box) => match &*flex_level_box.borrow() { FlexLevelBox::OutOfFlowAbsolutelyPositionedBox(_) @@ -428,13 +429,14 @@ impl BoxTree { acc.union(&child_overflow) }); - FragmentTree { + FragmentTree::new( + layout_context, root_fragments, scrollable_overflow, - initial_containing_block: physical_containing_block, - canvas_background: self.canvas_background.clone(), - viewport_scroll_sensitivity: self.viewport_scroll_sensitivity, - } + physical_containing_block, + self.canvas_background.clone(), + self.viewport_scroll_sensitivity, + ) } } diff --git a/components/layout/fragment_tree/base_fragment.rs b/components/layout/fragment_tree/base_fragment.rs index 48d672a8547..0cf6ee511cb 100644 --- a/components/layout/fragment_tree/base_fragment.rs +++ b/components/layout/fragment_tree/base_fragment.rs @@ -132,11 +132,6 @@ impl Tag { Tag { node, pseudo } } - /// Returns true if this tag is for a pseudo element. - pub(crate) fn is_pseudo(&self) -> bool { - self.pseudo.is_some() - } - pub(crate) fn to_display_list_fragment_id(self) -> u64 { combine_id_with_fragment_type(self.node.id(), self.pseudo.into()) } diff --git a/components/layout/fragment_tree/box_fragment.rs b/components/layout/fragment_tree/box_fragment.rs index 30be154caf1..e87826ec3ca 100644 --- a/components/layout/fragment_tree/box_fragment.rs +++ b/components/layout/fragment_tree/box_fragment.rs @@ -65,6 +65,10 @@ pub(crate) struct BoxFragment { /// does not include padding, border, or margin -- it only includes content. pub content_rect: PhysicalRect<Au>, + /// This [`BoxFragment`]'s containing block rectangle in coordinates relative to + /// the initial containing block, but not taking into account any transforms. + pub cumulative_containing_block_rect: PhysicalRect<Au>, + pub padding: PhysicalSides<Au>, pub border: PhysicalSides<Au>, pub margin: PhysicalSides<Au>, @@ -120,6 +124,7 @@ impl BoxFragment { style, children, content_rect, + cumulative_containing_block_rect: Default::default(), padding, border, margin, @@ -195,6 +200,8 @@ impl BoxFragment { self } + /// Get the scrollable overflow for this [`BoxFragment`] relative to its + /// containing block. pub fn scrollable_overflow(&self) -> PhysicalRect<Au> { let physical_padding_rect = self.padding_rect(); let content_origin = self.content_rect.origin.to_vector(); @@ -205,6 +212,14 @@ impl BoxFragment { ) } + pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) { + self.cumulative_containing_block_rect = *containing_block; + } + + pub fn offset_by_containing_block(&self, rect: &PhysicalRect<Au>) -> PhysicalRect<Au> { + rect.translate(self.cumulative_containing_block_rect.origin.to_vector()) + } + pub(crate) fn padding_rect(&self) -> PhysicalRect<Au> { self.content_rect.outer_rect(self.padding) } @@ -278,10 +293,7 @@ impl BoxFragment { overflow } - pub(crate) fn calculate_resolved_insets_if_positioned( - &self, - containing_block: &PhysicalRect<Au>, - ) -> PhysicalSides<AuOrAuto> { + pub(crate) fn calculate_resolved_insets_if_positioned(&self) -> PhysicalSides<AuOrAuto> { let position = self.style.get_box().position; debug_assert_ne!( position, @@ -309,7 +321,10 @@ impl BoxFragment { // used value. Otherwise the resolved value is the computed value." // https://drafts.csswg.org/cssom/#resolved-values let insets = self.style.physical_box_offsets(); - let (cb_width, cb_height) = (containing_block.width(), containing_block.height()); + let (cb_width, cb_height) = ( + self.cumulative_containing_block_rect.width(), + self.cumulative_containing_block_rect.height(), + ); if position == ComputedPosition::Relative { let get_resolved_axis = |start: &LengthPercentageOrAuto, end: &LengthPercentageOrAuto, diff --git a/components/layout/fragment_tree/fragment.rs b/components/layout/fragment_tree/fragment.rs index d0d1b9b1104..7708b0893ee 100644 --- a/components/layout/fragment_tree/fragment.rs +++ b/components/layout/fragment_tree/fragment.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use app_units::Au; use base::id::PipelineId; use base::print_tree::PrintTree; +use euclid::{Point2D, Rect, Size2D, UnknownUnit}; use fonts::{ByteIndex, FontMetrics, GlyphStore}; use malloc_size_of_derive::MallocSizeOf; use range::Range as ServoRange; @@ -21,7 +22,7 @@ use super::{ Tag, }; use crate::cell::ArcRefCell; -use crate::geom::{LogicalSides, PhysicalRect}; +use crate::geom::{LogicalSides, PhysicalPoint, PhysicalRect}; use crate::style_ext::ComputedValuesExt; #[derive(Clone, MallocSizeOf)] @@ -112,6 +113,7 @@ impl Fragment { Fragment::Float(fragment) => fragment.borrow().base.clone(), }) } + pub(crate) fn mutate_content_rect(&mut self, callback: impl FnOnce(&mut PhysicalRect<Au>)) { match self { Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => { @@ -124,6 +126,28 @@ impl Fragment { } } + pub(crate) fn set_containing_block(&self, containing_block: &PhysicalRect<Au>) { + match self { + Fragment::Box(box_fragment) => box_fragment + .borrow_mut() + .set_containing_block(containing_block), + Fragment::Float(float_fragment) => float_fragment + .borrow_mut() + .set_containing_block(containing_block), + Fragment::Positioning(positioning_fragment) => positioning_fragment + .borrow_mut() + .set_containing_block(containing_block), + Fragment::AbsoluteOrFixedPositioned(hoisted_shared_fragment) => { + if let Some(ref fragment) = hoisted_shared_fragment.borrow().fragment { + fragment.set_containing_block(containing_block); + } + }, + Fragment::Text(_) => {}, + Fragment::Image(_) => {}, + Fragment::IFrame(_) => {}, + } + } + pub fn tag(&self) -> Option<Tag> { self.base().and_then(|base| base.tag) } @@ -146,12 +170,12 @@ impl Fragment { } } - pub fn scrolling_area(&self, containing_block: &PhysicalRect<Au>) -> PhysicalRect<Au> { + pub fn scrolling_area(&self) -> PhysicalRect<Au> { match self { - Fragment::Box(fragment) | Fragment::Float(fragment) => fragment - .borrow() - .scrollable_overflow() - .translate(containing_block.origin.to_vector()), + Fragment::Box(fragment) | Fragment::Float(fragment) => { + let fragment = fragment.borrow(); + fragment.offset_by_containing_block(&fragment.scrollable_overflow()) + }, _ => self.scrollable_overflow(), } } @@ -169,6 +193,59 @@ impl Fragment { } } + pub(crate) fn cumulative_border_box_rect(&self) -> Option<PhysicalRect<Au>> { + match self { + Fragment::Box(fragment) | Fragment::Float(fragment) => { + let fragment = fragment.borrow(); + Some(fragment.offset_by_containing_block(&fragment.border_rect())) + }, + Fragment::Positioning(fragment) => { + let fragment = fragment.borrow(); + Some(fragment.offset_by_containing_block(&fragment.rect)) + }, + Fragment::Text(_) | + Fragment::AbsoluteOrFixedPositioned(_) | + Fragment::Image(_) | + Fragment::IFrame(_) => None, + } + } + + pub(crate) fn client_rect(&self) -> Rect<i32, UnknownUnit> { + let rect = match self { + Fragment::Box(fragment) | Fragment::Float(fragment) => { + // https://drafts.csswg.org/cssom-view/#dom-element-clienttop + // " If the element has no associated CSS layout box or if the + // CSS layout box is inline, return zero." For this check we + // also explicitly ignore the list item portion of the display + // style. + let fragment = fragment.borrow(); + if fragment.is_inline_box() { + return Rect::zero(); + } + + if fragment.is_table_wrapper() { + // For tables the border actually belongs to the table grid box, + // so we need to include it in the dimension of the table wrapper box. + let mut rect = fragment.border_rect(); + rect.origin = PhysicalPoint::zero(); + rect + } else { + let mut rect = fragment.padding_rect(); + rect.origin = PhysicalPoint::new(fragment.border.left, fragment.border.top); + rect + } + }, + _ => return Rect::zero(), + } + .to_untyped(); + + let rect = Rect::new( + Point2D::new(rect.origin.x.to_f32_px(), rect.origin.y.to_f32_px()), + Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px()), + ); + rect.round().to_i32() + } + pub(crate) fn find<T>( &self, manager: &ContainingBlockManager<PhysicalRect<Au>>, diff --git a/components/layout/fragment_tree/fragment_tree.rs b/components/layout/fragment_tree/fragment_tree.rs index bb3c659466c..3a082c99389 100644 --- a/components/layout/fragment_tree/fragment_tree.rs +++ b/components/layout/fragment_tree/fragment_tree.rs @@ -5,17 +5,17 @@ use app_units::Au; use base::print_tree::PrintTree; use compositing_traits::display_list::AxesScrollSensitivity; -use euclid::default::{Point2D, Rect, Size2D}; +use euclid::default::Size2D; use fxhash::FxHashSet; use malloc_size_of_derive::MallocSizeOf; use style::animation::AnimationSetKey; -use style::dom::OpaqueNode; use webrender_api::units; -use super::{ContainingBlockManager, Fragment, Tag}; +use super::{ContainingBlockManager, Fragment}; +use crate::context::LayoutContext; use crate::display_list::StackingContext; use crate::flow::CanvasBackground; -use crate::geom::{PhysicalPoint, PhysicalRect}; +use crate::geom::PhysicalRect; #[derive(MallocSizeOf)] pub struct FragmentTree { @@ -44,6 +44,58 @@ pub struct FragmentTree { } impl FragmentTree { + pub(crate) fn new( + layout_context: &LayoutContext, + root_fragments: Vec<Fragment>, + scrollable_overflow: PhysicalRect<Au>, + initial_containing_block: PhysicalRect<Au>, + canvas_background: CanvasBackground, + viewport_scroll_sensitivity: AxesScrollSensitivity, + ) -> Self { + let fragment_tree = Self { + root_fragments, + scrollable_overflow, + initial_containing_block, + canvas_background, + viewport_scroll_sensitivity, + }; + + // As part of building the fragment tree, we want to stop animating elements and + // pseudo-elements that used to be animating or had animating images attached to + // them. Create a set of all elements that used to be animating. + let mut animations = layout_context.style_context.animations.sets.write(); + let mut invalid_animating_nodes: FxHashSet<_> = animations.keys().cloned().collect(); + let mut image_animations = layout_context.node_image_animation_map.write().to_owned(); + let mut invalid_image_animating_nodes: FxHashSet<_> = image_animations + .keys() + .cloned() + .map(|node| AnimationSetKey::new(node, None)) + .collect(); + + fragment_tree.find(|fragment, _level, containing_block| { + if let Some(tag) = fragment.tag() { + invalid_animating_nodes.remove(&AnimationSetKey::new(tag.node, tag.pseudo)); + invalid_image_animating_nodes.remove(&AnimationSetKey::new(tag.node, tag.pseudo)); + } + + fragment.set_containing_block(containing_block); + None::<()> + }); + + // Cancel animations for any elements and pseudo-elements that are no longer found + // in the fragment tree. + for node in &invalid_animating_nodes { + if let Some(state) = animations.get_mut(node) { + state.cancel_all_animations(); + } + } + for node in &invalid_image_animating_nodes { + image_animations.remove(&node.node); + } + + fragment_tree + } + pub(crate) fn build_display_list( &self, builder: &mut crate::display_list::DisplayListBuilder, @@ -86,109 +138,11 @@ impl FragmentTree { .find_map(|child| child.find(&info, 0, &mut process_func)) } - pub fn remove_nodes_in_fragment_tree_from_set(&self, set: &mut FxHashSet<AnimationSetKey>) { - self.find(|fragment, _, _| { - let tag = fragment.tag()?; - set.remove(&AnimationSetKey::new(tag.node, tag.pseudo)); - None::<()> - }); - } - - /// Get the vector of rectangles that surrounds the fragments of the node with the given address. - /// This function answers the `getClientRects()` query and the union of the rectangles answers - /// the `getBoundingClientRect()` query. - /// - /// TODO: This function is supposed to handle scroll offsets, but that isn't happening at all. - pub fn get_content_boxes_for_node(&self, requested_node: OpaqueNode) -> Vec<Rect<Au>> { - let mut content_boxes = Vec::new(); - let tag_to_find = Tag::new(requested_node); - self.find(|fragment, _, containing_block| { - if fragment.tag() != Some(tag_to_find) { - return None::<()>; - } - - let fragment_relative_rect = match fragment { - Fragment::Box(fragment) | Fragment::Float(fragment) => { - fragment.borrow().border_rect() - }, - Fragment::Positioning(fragment) => fragment.borrow().rect, - Fragment::Text(fragment) => fragment.borrow().rect, - Fragment::AbsoluteOrFixedPositioned(_) | - Fragment::Image(_) | - Fragment::IFrame(_) => return None, - }; - - let rect = fragment_relative_rect.translate(containing_block.origin.to_vector()); - - content_boxes.push(rect.to_untyped()); - None::<()> - }); - content_boxes - } - - pub fn get_border_dimensions_for_node(&self, requested_node: OpaqueNode) -> Rect<i32> { - let tag_to_find = Tag::new(requested_node); - self.find(|fragment, _, _containing_block| { - if fragment.tag() != Some(tag_to_find) { - return None; - } - - let rect = match fragment { - Fragment::Box(fragment) | Fragment::Float(fragment) => { - // https://drafts.csswg.org/cssom-view/#dom-element-clienttop - // " If the element has no associated CSS layout box or if the - // CSS layout box is inline, return zero." For this check we - // also explicitly ignore the list item portion of the display - // style. - let fragment = fragment.borrow(); - if fragment.is_inline_box() { - return Some(Rect::zero()); - } - if fragment.is_table_wrapper() { - // For tables the border actually belongs to the table grid box, - // so we need to include it in the dimension of the table wrapper box. - let mut rect = fragment.border_rect(); - rect.origin = PhysicalPoint::zero(); - rect - } else { - let mut rect = fragment.padding_rect(); - rect.origin = PhysicalPoint::new(fragment.border.left, fragment.border.top); - rect - } - }, - Fragment::Positioning(fragment) => fragment.borrow().rect.cast_unit(), - Fragment::Text(text_fragment) => text_fragment.borrow().rect, - _ => return None, - }; - - let rect = Rect::new( - Point2D::new(rect.origin.x.to_f32_px(), rect.origin.y.to_f32_px()), - Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px()), - ); - Some(rect.round().to_i32().to_untyped()) - }) - .unwrap_or_else(Rect::zero) - } - pub fn get_scrolling_area_for_viewport(&self) -> PhysicalRect<Au> { let mut scroll_area = self.initial_containing_block; for fragment in self.root_fragments.iter() { - scroll_area = fragment - .scrolling_area(&self.initial_containing_block) - .union(&scroll_area); + scroll_area = fragment.scrolling_area().union(&scroll_area); } scroll_area } - - pub fn get_scrolling_area_for_node(&self, requested_node: OpaqueNode) -> PhysicalRect<Au> { - let tag_to_find = Tag::new(requested_node); - let scroll_area = self.find(|fragment, _, containing_block| { - if fragment.tag() == Some(tag_to_find) { - Some(fragment.scrolling_area(containing_block)) - } else { - None - } - }); - scroll_area.unwrap_or_else(PhysicalRect::<Au>::zero) - } } diff --git a/components/layout/fragment_tree/positioning_fragment.rs b/components/layout/fragment_tree/positioning_fragment.rs index 853caed6709..1fe968eb484 100644 --- a/components/layout/fragment_tree/positioning_fragment.rs +++ b/components/layout/fragment_tree/positioning_fragment.rs @@ -20,12 +20,17 @@ pub(crate) struct PositioningFragment { pub base: BaseFragment, pub rect: PhysicalRect<Au>, pub children: Vec<Fragment>, + /// The scrollable overflow of this anonymous fragment's children. pub scrollable_overflow: PhysicalRect<Au>, /// If this fragment was created with a style, the style of the fragment. #[conditional_malloc_size_of] pub style: Option<ServoArc<ComputedValues>>, + + /// This [`PositioningFragment`]'s containing block rectangle in coordinates relative to + /// the initial containing block, but not taking into account any transforms. + pub cumulative_containing_block_rect: PhysicalRect<Au>, } impl PositioningFragment { @@ -61,9 +66,18 @@ impl PositioningFragment { rect, children, scrollable_overflow, + cumulative_containing_block_rect: PhysicalRect::zero(), }) } + pub(crate) fn set_containing_block(&mut self, containing_block: &PhysicalRect<Au>) { + self.cumulative_containing_block_rect = *containing_block; + } + + pub fn offset_by_containing_block(&self, rect: &PhysicalRect<Au>) -> PhysicalRect<Au> { + rect.translate(self.cumulative_containing_block_rect.origin.to_vector()) + } + pub fn print(&self, tree: &mut PrintTree) { tree.new_level(format!( "PositioningFragment\ diff --git a/components/layout/layout_impl.rs b/components/layout/layout_impl.rs index 61550df2723..3110899d76e 100644 --- a/components/layout/layout_impl.rs +++ b/components/layout/layout_impl.rs @@ -23,7 +23,7 @@ use euclid::{Point2D, Scale, Size2D, Vector2D}; use fnv::FnvHashMap; use fonts::{FontContext, FontContextWebFontMethods}; use fonts_traits::StylesheetWebFontLoadFinishedCallback; -use fxhash::{FxHashMap, FxHashSet}; +use fxhash::FxHashMap; use ipc_channel::ipc::IpcSender; use log::{debug, error}; use malloc_size_of::{MallocConditionalSizeOf, MallocSizeOf, MallocSizeOfOps}; @@ -34,21 +34,22 @@ use profile_traits::time::{ self as profile_time, TimerMetadata, TimerMetadataFrameType, TimerMetadataReflowType, }; use profile_traits::{path, time_profile}; -use script::layout_dom::{ServoLayoutElement, ServoLayoutNode}; +use rayon::ThreadPool; +use script::layout_dom::{ServoLayoutDocument, ServoLayoutElement, ServoLayoutNode}; use script_layout_interface::{ - ImageAnimationState, Layout, LayoutConfig, LayoutFactory, NodesFromPointQueryType, - OffsetParentResponse, ReflowGoal, ReflowRequest, ReflowResult, TrustedNodeAddress, + Layout, LayoutConfig, LayoutFactory, NodesFromPointQueryType, OffsetParentResponse, ReflowGoal, + ReflowRequest, ReflowResult, TrustedNodeAddress, }; use script_traits::{DrawAPaintImageResult, PaintWorkletError, Painter, ScriptThreadMessage}; use servo_arc::Arc as ServoArc; use servo_config::opts::{self, DebugOptions}; use servo_config::pref; use servo_url::ServoUrl; -use style::animation::{AnimationSetKey, DocumentAnimationSet}; +use style::animation::DocumentAnimationSet; use style::context::{ QuirksMode, RegisteredSpeculativePainter, RegisteredSpeculativePainters, SharedStyleContext, }; -use style::dom::{OpaqueNode, TElement, TNode}; +use style::dom::{OpaqueNode, ShowSubtreeDataAndPrimaryValues, TElement, TNode}; use style::error_reporting::RustLogReporter; use style::font_metrics::FontMetrics; use style::global_style_data::GLOBAL_STYLE_DATA; @@ -80,8 +81,8 @@ use webrender_api::{ExternalScrollId, HitTestFlags}; use crate::context::LayoutContext; use crate::display_list::{DisplayList, WebRenderImageInfo}; use crate::query::{ - get_the_text_steps, process_content_box_request, process_content_boxes_request, - process_node_geometry_request, process_node_scroll_area_request, process_offset_parent_query, + get_the_text_steps, process_client_rect_request, process_content_box_request, + process_content_boxes_request, process_node_scroll_area_request, process_offset_parent_query, process_resolved_font_style_query, process_resolved_style_request, process_text_index_request, }; use crate::traversal::RecalcStyle; @@ -95,6 +96,10 @@ use crate::{BoxTree, FragmentTree}; static STYLE_THREAD_POOL: Mutex<&style::global_style_data::STYLE_THREAD_POOL> = Mutex::new(&style::global_style_data::STYLE_THREAD_POOL); +thread_local!(static SEEN_POINTERS: LazyCell<RefCell<HashSet<*const c_void>>> = const { + LazyCell::new(|| RefCell::new(HashSet::new())) +}); + /// Information needed by layout. pub struct LayoutThread { /// The ID of the pipeline that we belong to. @@ -230,24 +235,27 @@ impl Layout for LayoutThread { feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn query_content_box(&self, node: OpaqueNode) -> Option<UntypedRect<Au>> { - process_content_box_request(node, self.fragment_tree.borrow().clone()) + fn query_content_box(&self, node: TrustedNodeAddress) -> Option<UntypedRect<Au>> { + let node = unsafe { ServoLayoutNode::new(&node) }; + process_content_box_request(node) } #[cfg_attr( feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn query_content_boxes(&self, node: OpaqueNode) -> Vec<UntypedRect<Au>> { - process_content_boxes_request(node, self.fragment_tree.borrow().clone()) + fn query_content_boxes(&self, node: TrustedNodeAddress) -> Vec<UntypedRect<Au>> { + let node = unsafe { ServoLayoutNode::new(&node) }; + process_content_boxes_request(node) } #[cfg_attr( feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn query_client_rect(&self, node: OpaqueNode) -> UntypedRect<i32> { - process_node_geometry_request(node, self.fragment_tree.borrow().clone()) + fn query_client_rect(&self, node: TrustedNodeAddress) -> UntypedRect<i32> { + let node = unsafe { ServoLayoutNode::new(&node) }; + process_client_rect_request(node) } #[cfg_attr( @@ -292,8 +300,9 @@ impl Layout for LayoutThread { feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn query_offset_parent(&self, node: OpaqueNode) -> OffsetParentResponse { - process_offset_parent_query(node, self.fragment_tree.borrow().clone()) + fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse { + let node = unsafe { ServoLayoutNode::new(&node) }; + process_offset_parent_query(node).unwrap_or_default() } #[cfg_attr( @@ -325,14 +334,7 @@ impl Layout for LayoutThread { TraversalFlags::empty(), ); - let fragment_tree = self.fragment_tree.borrow().clone(); - process_resolved_style_request( - &shared_style_context, - node, - &pseudo, - &property_id, - fragment_tree, - ) + process_resolved_style_request(&shared_style_context, node, &pseudo, &property_id) } #[cfg_attr( @@ -375,7 +377,8 @@ impl Layout for LayoutThread { feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn query_scrolling_area(&self, node: Option<OpaqueNode>) -> UntypedRect<i32> { + fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> UntypedRect<i32> { + let node = node.map(|node| unsafe { ServoLayoutNode::new(&node) }); process_node_scroll_area_request(node, self.fragment_tree.borrow().clone()) } @@ -545,43 +548,6 @@ impl LayoutThread { } } - #[allow(clippy::too_many_arguments)] - // Create a layout context for use in building display lists, hit testing, &c. - #[allow(clippy::too_many_arguments)] - fn build_layout_context<'a>( - &'a self, - guards: StylesheetGuards<'a>, - snapshot_map: &'a SnapshotMap, - reflow_request: &mut ReflowRequest, - use_rayon: bool, - ) -> LayoutContext<'a> { - let traversal_flags = match reflow_request.stylesheets_changed { - true => TraversalFlags::ForCSSRuleChanges, - false => TraversalFlags::empty(), - }; - - LayoutContext { - id: self.id, - origin: reflow_request.origin.clone(), - style_context: self.build_shared_style_context( - guards, - snapshot_map, - reflow_request.animation_timeline_value, - &reflow_request.animations, - traversal_flags, - ), - image_cache: self.image_cache.clone(), - font_context: self.font_context.clone(), - webrender_image_cache: self.webrender_image_cache.clone(), - pending_images: Mutex::default(), - node_image_animation_map: Arc::new(RwLock::new(std::mem::take( - &mut reflow_request.node_to_image_animation_map, - ))), - iframe_sizes: Mutex::default(), - use_rayon, - } - } - fn load_all_web_fonts_from_stylesheet_with_guard( &self, stylesheet: &DocumentStyleSheet, @@ -621,51 +587,126 @@ impl LayoutThread { return None; }; - // Calculate the actual viewport as per DEVICE-ADAPT § 6 - // If the entire flow tree is invalid, then it will be reflowed anyhow. let document_shared_lock = document.style_shared_lock(); let author_guard = document_shared_lock.read(); - let ua_stylesheets = &*UA_STYLESHEETS; let ua_or_user_guard = ua_stylesheets.shared_lock.read(); + let rayon_pool = STYLE_THREAD_POOL.lock(); + let rayon_pool = rayon_pool.pool(); + let rayon_pool = rayon_pool.as_ref(); let guards = StylesheetGuards { author: &author_guard, ua_or_user: &ua_or_user_guard, }; - let had_used_viewport_units = self.stylist.device().used_viewport_units(); - let viewport_size_changed = self.viewport_did_change(reflow_request.viewport_details); - let theme_changed = self.theme_did_change(reflow_request.theme); - - if viewport_size_changed || theme_changed { - self.update_device( - reflow_request.viewport_details, - reflow_request.theme, - &guards, - ); - } - - if viewport_size_changed && had_used_viewport_units { + if self.update_device_if_necessary(&reflow_request, &guards) { if let Some(mut data) = root_element.mutate_data() { data.hint.insert(RestyleHint::recascade_subtree()); } } + let mut snapshot_map = SnapshotMap::new(); + let _snapshot_setter = SnapshotSetter::new(&mut reflow_request, &mut snapshot_map); + self.prepare_stylist_for_reflow( + &reflow_request, + document, + root_element, + &guards, + ua_stylesheets, + &snapshot_map, + ); + + let mut layout_context = LayoutContext { + id: self.id, + origin: reflow_request.origin.clone(), + style_context: self.build_shared_style_context( + guards, + &snapshot_map, + reflow_request.animation_timeline_value, + &reflow_request.animations, + match reflow_request.stylesheets_changed { + true => TraversalFlags::ForCSSRuleChanges, + false => TraversalFlags::empty(), + }, + ), + image_cache: self.image_cache.clone(), + font_context: self.font_context.clone(), + webrender_image_cache: self.webrender_image_cache.clone(), + pending_images: Mutex::default(), + node_image_animation_map: Arc::new(RwLock::new(std::mem::take( + &mut reflow_request.node_to_image_animation_map, + ))), + iframe_sizes: Mutex::default(), + use_rayon: rayon_pool.is_some(), + }; + + self.restyle_and_build_trees( + &reflow_request, + root_element, + rayon_pool, + &mut layout_context, + ); + self.build_display_list(&reflow_request, &mut layout_context); + self.first_reflow.set(false); + + if let ReflowGoal::UpdateScrollNode(scroll_state) = reflow_request.reflow_goal { + self.update_scroll_node_state(&scroll_state); + } + + let pending_images = std::mem::take(&mut *layout_context.pending_images.lock()); + let iframe_sizes = std::mem::take(&mut *layout_context.iframe_sizes.lock()); + let node_to_image_animation_map = + std::mem::take(&mut *layout_context.node_image_animation_map.write()); + Some(ReflowResult { + pending_images, + iframe_sizes, + node_to_image_animation_map, + }) + } + + fn update_device_if_necessary( + &mut self, + reflow_request: &ReflowRequest, + guards: &StylesheetGuards, + ) -> bool { + let had_used_viewport_units = self.stylist.device().used_viewport_units(); + let viewport_size_changed = self.viewport_did_change(reflow_request.viewport_details); + let theme_changed = self.theme_did_change(reflow_request.theme); + if !viewport_size_changed && !theme_changed { + return false; + } + self.update_device( + reflow_request.viewport_details, + reflow_request.theme, + guards, + ); + (viewport_size_changed && had_used_viewport_units) || theme_changed + } + + fn prepare_stylist_for_reflow<'dom>( + &mut self, + reflow_request: &ReflowRequest, + document: ServoLayoutDocument<'dom>, + root_element: ServoLayoutElement<'dom>, + guards: &StylesheetGuards, + ua_stylesheets: &UserAgentStylesheets, + snapshot_map: &SnapshotMap, + ) { if self.first_reflow.get() { for stylesheet in &ua_stylesheets.user_or_user_agent_stylesheets { self.stylist - .append_stylesheet(stylesheet.clone(), &ua_or_user_guard); - self.load_all_web_fonts_from_stylesheet_with_guard(stylesheet, &ua_or_user_guard); + .append_stylesheet(stylesheet.clone(), guards.ua_or_user); + self.load_all_web_fonts_from_stylesheet_with_guard(stylesheet, guards.ua_or_user); } if self.stylist.quirks_mode() != QuirksMode::NoQuirks { self.stylist.append_stylesheet( ua_stylesheets.quirks_mode_stylesheet.clone(), - &ua_or_user_guard, + guards.ua_or_user, ); self.load_all_web_fonts_from_stylesheet_with_guard( &ua_stylesheets.quirks_mode_stylesheet, - &ua_or_user_guard, + guards.ua_or_user, ); } } @@ -678,185 +719,105 @@ impl LayoutThread { // Flush shadow roots stylesheets if dirty. document.flush_shadow_roots_stylesheets(&mut self.stylist, guards.author); - let restyles = std::mem::take(&mut reflow_request.pending_restyles); - debug!("Draining restyles: {}", restyles.len()); - - let mut map = SnapshotMap::new(); - let elements_with_snapshot: Vec<_> = restyles - .iter() - .filter(|r| r.1.snapshot.is_some()) - .map(|r| unsafe { ServoLayoutNode::new(&r.0).as_element().unwrap() }) - .collect(); - - for (el, restyle) in restyles { - let el = unsafe { ServoLayoutNode::new(&el).as_element().unwrap() }; - - // If we haven't styled this node yet, we don't need to track a - // restyle. - let mut style_data = match el.mutate_data() { - Some(d) => d, - None => { - unsafe { el.unset_snapshot_flags() }; - continue; - }, - }; - - if let Some(s) = restyle.snapshot { - unsafe { el.set_has_snapshot() }; - map.insert(el.as_node().opaque(), s); - } - - // Stash the data on the element for processing by the style system. - style_data.hint.insert(restyle.hint); - style_data.damage = restyle.damage; - debug!("Noting restyle for {:?}: {:?}", el, style_data); - } - - self.stylist.flush(&guards, Some(root_element), Some(&map)); - - let rayon_pool = STYLE_THREAD_POOL.lock(); - let rayon_pool = rayon_pool.pool(); - let rayon_pool = rayon_pool.as_ref(); - - // Create a layout context for use throughout the following passes. - let mut layout_context = self.build_layout_context( - guards.clone(), - &map, - &mut reflow_request, - rayon_pool.is_some(), - ); + self.stylist + .flush(guards, Some(root_element), Some(snapshot_map)); + } + #[cfg_attr( + feature = "tracing", + tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") + )] + fn restyle_and_build_trees( + &self, + reflow_request: &ReflowRequest, + root_element: ServoLayoutElement<'_>, + rayon_pool: Option<&ThreadPool>, + layout_context: &mut LayoutContext<'_>, + ) { let dirty_root = unsafe { ServoLayoutNode::new(&reflow_request.dirty_root.unwrap()) .as_element() .unwrap() }; - let traversal = RecalcStyle::new(layout_context); + let recalc_style_traversal = RecalcStyle::new(layout_context); let token = { - let shared = DomTraversal::<ServoLayoutElement>::shared_context(&traversal); + let shared = + DomTraversal::<ServoLayoutElement>::shared_context(&recalc_style_traversal); RecalcStyle::pre_traverse(dirty_root, shared) }; - if token.should_traverse() { - #[cfg(feature = "tracing")] - let _span = - tracing::trace_span!("driver::traverse_dom", servo_profiling = true).entered(); - let dirty_root: ServoLayoutNode = - driver::traverse_dom(&traversal, token, rayon_pool).as_node(); - - let root_node = root_element.as_node(); - let mut box_tree = self.box_tree.borrow_mut(); - let box_tree = &mut *box_tree; - let mut build_box_tree = || { - if !BoxTree::update(traversal.context(), dirty_root) { - *box_tree = Some(Arc::new(BoxTree::construct(traversal.context(), root_node))); - } - }; - if let Some(pool) = rayon_pool { - pool.install(build_box_tree) - } else { - build_box_tree() - }; - - let viewport_size = Size2D::new( - self.viewport_size.width.to_f32_px(), - self.viewport_size.height.to_f32_px(), - ); - let run_layout = || { - box_tree - .as_ref() - .unwrap() - .layout(traversal.context(), viewport_size) - }; - let fragment_tree = Arc::new(if let Some(pool) = rayon_pool { - pool.install(run_layout) - } else { - run_layout() - }); - *self.fragment_tree.borrow_mut() = Some(fragment_tree); + if !token.should_traverse() { + layout_context.style_context.stylist.rule_tree().maybe_gc(); + return; } - layout_context = traversal.destroy(); + let dirty_root: ServoLayoutNode = + driver::traverse_dom(&recalc_style_traversal, token, rayon_pool).as_node(); + + let root_node = root_element.as_node(); + let mut box_tree = self.box_tree.borrow_mut(); + let box_tree = &mut *box_tree; + let mut build_box_tree = || { + if !BoxTree::update(recalc_style_traversal.context(), dirty_root) { + *box_tree = Some(Arc::new(BoxTree::construct( + recalc_style_traversal.context(), + root_node, + ))); + } + }; + if let Some(pool) = rayon_pool { + pool.install(build_box_tree) + } else { + build_box_tree() + }; + + let viewport_size = Size2D::new( + self.viewport_size.width.to_f32_px(), + self.viewport_size.height.to_f32_px(), + ); + let run_layout = || { + box_tree + .as_ref() + .unwrap() + .layout(recalc_style_traversal.context(), viewport_size) + }; + let fragment_tree = Arc::new(if let Some(pool) = rayon_pool { + pool.install(run_layout) + } else { + run_layout() + }); - for element in elements_with_snapshot { - unsafe { element.unset_snapshot_flags() } - } + *self.fragment_tree.borrow_mut() = Some(fragment_tree); if self.debug.dump_style_tree { println!( "{:?}", - style::dom::ShowSubtreeDataAndPrimaryValues(root_element.as_node()) + ShowSubtreeDataAndPrimaryValues(root_element.as_node()) ); } - if self.debug.dump_rule_tree { - layout_context + recalc_style_traversal + .context() .style_context .stylist .rule_tree() - .dump_stdout(&guards); + .dump_stdout(&layout_context.shared_context().guards); } // GC the rule tree if some heuristics are met. layout_context.style_context.stylist.rule_tree().maybe_gc(); - - // Perform post-style recalculation layout passes. - if let Some(root) = &*self.fragment_tree.borrow() { - self.perform_post_style_recalc_layout_passes( - root.clone(), - &reflow_request.reflow_goal, - &mut layout_context, - ); - } - - self.first_reflow.set(false); - - if let ReflowGoal::UpdateScrollNode(scroll_state) = reflow_request.reflow_goal { - self.update_scroll_node_state(&scroll_state); - } - - let pending_images = std::mem::take(&mut *layout_context.pending_images.lock()); - let iframe_sizes = std::mem::take(&mut *layout_context.iframe_sizes.lock()); - let node_to_image_animation_map = - std::mem::take(&mut *layout_context.node_image_animation_map.write()); - Some(ReflowResult { - pending_images, - iframe_sizes, - node_to_image_animation_map, - }) } - fn update_scroll_node_state(&self, state: &ScrollState) { - self.scroll_offsets - .borrow_mut() - .insert(state.scroll_id, state.scroll_offset); - let point = Point2D::new(-state.scroll_offset.x, -state.scroll_offset.y); - self.compositor_api.send_scroll_node( - self.webview_id, - self.id.into(), - LayoutPoint::from_untyped(point), - state.scroll_id, - ); - } - - fn perform_post_style_recalc_layout_passes( + fn build_display_list( &self, - fragment_tree: Arc<FragmentTree>, - reflow_goal: &ReflowGoal, - context: &mut LayoutContext, + reflow_request: &ReflowRequest, + layout_context: &mut LayoutContext<'_>, ) { - Self::cancel_animations_for_nodes_not_in_fragment_tree( - &context.style_context.animations, - &fragment_tree, - ); - - Self::cancel_image_animation_for_nodes_not_in_fragment_tree( - context.node_image_animation_map.clone(), - &fragment_tree, - ); - - if !reflow_goal.needs_display_list() { + let Some(fragment_tree) = &*self.fragment_tree.borrow() else { + return; + }; + if !reflow_request.reflow_goal.needs_display_list() { return; } @@ -890,10 +851,10 @@ impl LayoutThread { // tree of fragments in CSS painting order and also creates all // applicable spatial and clip nodes. let root_stacking_context = - display_list.build_stacking_context_tree(&fragment_tree, &self.debug); + display_list.build_stacking_context_tree(fragment_tree, &self.debug); // Build the rest of the display list which inclues all of the WebRender primitives. - display_list.build(context, &fragment_tree, &root_stacking_context); + display_list.build(layout_context, fragment_tree, &root_stacking_context); if self.debug.dump_flow_tree { fragment_tree.print(); @@ -901,9 +862,8 @@ impl LayoutThread { if self.debug.dump_stacking_context_tree { root_stacking_context.debug_print(); } - debug!("Layout done!"); - if reflow_goal.needs_display() { + if reflow_request.reflow_goal.needs_display() { self.compositor_api.send_display_list( self.webview_id, display_list.compositor_info, @@ -918,6 +878,19 @@ impl LayoutThread { } } + fn update_scroll_node_state(&self, state: &ScrollState) { + self.scroll_offsets + .borrow_mut() + .insert(state.scroll_id, state.scroll_offset); + let point = Point2D::new(-state.scroll_offset.x, -state.scroll_offset.y); + self.compositor_api.send_scroll_node( + self.webview_id, + self.id.into(), + LayoutPoint::from_untyped(point), + state.scroll_id, + ); + } + /// Returns profiling information which is passed to the time profiler. fn profiler_metadata(&self) -> Option<TimerMetadata> { Some(TimerMetadata { @@ -935,42 +908,6 @@ impl LayoutThread { }) } - /// Cancel animations for any nodes which have been removed from fragment tree. - /// TODO(mrobinson): We should look into a way of doing this during flow tree construction. - /// This also doesn't yet handles nodes that have been reparented. - fn cancel_animations_for_nodes_not_in_fragment_tree( - animations: &DocumentAnimationSet, - root: &FragmentTree, - ) { - // Assume all nodes have been removed until proven otherwise. - let mut animations = animations.sets.write(); - let mut invalid_nodes = animations.keys().cloned().collect(); - root.remove_nodes_in_fragment_tree_from_set(&mut invalid_nodes); - - // Cancel animations for any nodes that are no longer in the fragment tree. - for node in &invalid_nodes { - if let Some(state) = animations.get_mut(node) { - state.cancel_all_animations(); - } - } - } - - fn cancel_image_animation_for_nodes_not_in_fragment_tree( - image_animation_set: Arc<RwLock<FxHashMap<OpaqueNode, ImageAnimationState>>>, - root: &FragmentTree, - ) { - let mut image_animations = image_animation_set.write().to_owned(); - let mut invalid_nodes: FxHashSet<AnimationSetKey> = image_animations - .keys() - .cloned() - .map(|node| AnimationSetKey::new(node, None)) - .collect(); - root.remove_nodes_in_fragment_tree_from_set(&mut invalid_nodes); - for node in &invalid_nodes { - image_animations.remove(&node.node); - } - } - fn viewport_did_change(&mut self, viewport_details: ViewportDetails) -> bool { let new_pixel_ratio = viewport_details.hidpi_scale_factor.get(); let new_viewport_size = Size2D::new( @@ -1230,6 +1167,54 @@ impl Debug for LayoutFontMetricsProvider { } } -thread_local!(static SEEN_POINTERS: LazyCell<RefCell<HashSet<*const c_void>>> = const { - LazyCell::new(|| RefCell::new(HashSet::new())) -}); +struct SnapshotSetter<'dom> { + elements_with_snapshot: Vec<ServoLayoutElement<'dom>>, +} + +impl SnapshotSetter<'_> { + fn new(reflow_request: &mut ReflowRequest, snapshot_map: &mut SnapshotMap) -> Self { + debug!( + "Draining restyles: {}", + reflow_request.pending_restyles.len() + ); + let restyles = std::mem::take(&mut reflow_request.pending_restyles); + + let elements_with_snapshot: Vec<_> = restyles + .iter() + .filter(|r| r.1.snapshot.is_some()) + .map(|r| unsafe { ServoLayoutNode::new(&r.0).as_element().unwrap() }) + .collect(); + + for (element, restyle) in restyles { + let element = unsafe { ServoLayoutNode::new(&element).as_element().unwrap() }; + + // If we haven't styled this node yet, we don't need to track a + // restyle. + let Some(mut style_data) = element.mutate_data() else { + unsafe { element.unset_snapshot_flags() }; + continue; + }; + + debug!("Noting restyle for {:?}: {:?}", element, style_data); + if let Some(s) = restyle.snapshot { + unsafe { element.set_has_snapshot() }; + snapshot_map.insert(element.as_node().opaque(), s); + } + + // Stash the data on the element for processing by the style system. + style_data.hint.insert(restyle.hint); + style_data.damage = restyle.damage; + } + Self { + elements_with_snapshot, + } + } +} + +impl Drop for SnapshotSetter<'_> { + fn drop(&mut self) { + for element in &self.elements_with_snapshot { + unsafe { element.unset_snapshot_flags() } + } + } +} diff --git a/components/layout/query.rs b/components/layout/query.rs index 08b264deea9..e78acdd0ca8 100644 --- a/components/layout/query.rs +++ b/components/layout/query.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use app_units::Au; use euclid::default::{Point2D, Rect}; -use euclid::{SideOffsets2D, Size2D, Vector2D}; +use euclid::{SideOffsets2D, Size2D}; use itertools::Itertools; use script_layout_interface::wrapper_traits::{ LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, @@ -38,59 +38,60 @@ use style::values::specified::GenericGridTemplateComponent; use style::values::specified::box_::DisplayInside; use style_traits::{ParsingMode, ToCss}; +use crate::ArcRefCell; use crate::dom::NodeExt; use crate::flow::inline::construct::{TextTransformation, WhitespaceCollapse}; use crate::fragment_tree::{ - BoxFragment, Fragment, FragmentFlags, FragmentTree, SpecificLayoutInfo, Tag, + BoxFragment, Fragment, FragmentFlags, FragmentTree, SpecificLayoutInfo, }; -use crate::geom::{PhysicalRect, PhysicalVec}; use crate::taffy::SpecificTaffyGridInfo; -pub fn process_content_box_request( - requested_node: OpaqueNode, - fragment_tree: Option<Arc<FragmentTree>>, -) -> Option<Rect<Au>> { - let rects = fragment_tree?.get_content_boxes_for_node(requested_node); +pub fn process_content_box_request<'dom>(node: impl LayoutNode<'dom> + 'dom) -> Option<Rect<Au>> { + let rects: Vec<_> = node + .fragments_for_pseudo(None) + .iter() + .filter_map(Fragment::cumulative_border_box_rect) + .collect(); if rects.is_empty() { return None; } - Some( - rects - .iter() - .fold(Rect::zero(), |unioned_rect, rect| rect.union(&unioned_rect)), - ) + Some(rects.iter().fold(Rect::zero(), |unioned_rect, rect| { + rect.to_untyped().union(&unioned_rect) + })) } -pub fn process_content_boxes_request( - requested_node: OpaqueNode, - fragment_tree: Option<Arc<FragmentTree>>, -) -> Vec<Rect<Au>> { - fragment_tree - .map(|tree| tree.get_content_boxes_for_node(requested_node)) - .unwrap_or_default() +pub fn process_content_boxes_request<'dom>(node: impl LayoutNode<'dom> + 'dom) -> Vec<Rect<Au>> { + node.fragments_for_pseudo(None) + .iter() + .filter_map(Fragment::cumulative_border_box_rect) + .map(|rect| rect.to_untyped()) + .collect() } -pub fn process_node_geometry_request( - requested_node: OpaqueNode, - fragment_tree: Option<Arc<FragmentTree>>, -) -> Rect<i32> { - if let Some(fragment_tree) = fragment_tree { - fragment_tree.get_border_dimensions_for_node(requested_node) - } else { - Rect::zero() - } +pub fn process_client_rect_request<'dom>(node: impl LayoutNode<'dom> + 'dom) -> Rect<i32> { + node.fragments_for_pseudo(None) + .first() + .map(Fragment::client_rect) + .unwrap_or_default() } /// <https://drafts.csswg.org/cssom-view/#scrolling-area> -pub fn process_node_scroll_area_request( - requested_node: Option<OpaqueNode>, +pub fn process_node_scroll_area_request<'dom>( + requested_node: Option<impl LayoutNode<'dom> + 'dom>, fragment_tree: Option<Arc<FragmentTree>>, ) -> Rect<i32> { - let rect = match (fragment_tree, requested_node) { - (Some(tree), Some(node)) => tree.get_scrolling_area_for_node(node), - (Some(tree), None) => tree.get_scrolling_area_for_viewport(), - _ => return Rect::zero(), + let Some(tree) = fragment_tree else { + return Rect::zero(); + }; + + let rect = match requested_node { + Some(node) => node + .fragments_for_pseudo(None) + .first() + .map(Fragment::scrolling_area) + .unwrap_or_default(), + None => tree.get_scrolling_area_for_viewport(), }; Rect::new( @@ -109,7 +110,6 @@ pub fn process_resolved_style_request<'dom>( node: impl LayoutNode<'dom> + 'dom, pseudo: &Option<PseudoElement>, property: &PropertyId, - fragment_tree: Option<Arc<FragmentTree>>, ) -> String { if !node.as_element().unwrap().has_data() { return process_resolved_style_request_for_unstyled_node(context, node, pseudo, property); @@ -161,8 +161,6 @@ pub fn process_resolved_style_request<'dom>( _ => style.computed_value_to_string(PropertyDeclarationId::Longhand(longhand_id)), }; - let tag_to_find = Tag::new_pseudo(node.opaque(), *pseudo); - // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle // Here we are trying to conform to the specification that says that getComputedStyle // should return the used values in certain circumstances. For size and positional @@ -191,107 +189,87 @@ pub fn process_resolved_style_request<'dom>( return computed_style(None); } - let resolve_for_fragment = - |fragment: &Fragment, containing_block: Option<&PhysicalRect<Au>>| { - let (content_rect, margins, padding, specific_layout_info) = match fragment { - Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => { - let box_fragment = box_fragment.borrow(); - if style.get_box().position != Position::Static { - let resolved_insets = || { - box_fragment - .calculate_resolved_insets_if_positioned(containing_block.unwrap()) - }; - match longhand_id { - LonghandId::Top => return resolved_insets().top.to_css_string(), - LonghandId::Right => { - return resolved_insets().right.to_css_string(); - }, - LonghandId::Bottom => { - return resolved_insets().bottom.to_css_string(); - }, - LonghandId::Left => { - return resolved_insets().left.to_css_string(); - }, - _ => {}, - } - } - let content_rect = box_fragment.content_rect; - let margins = box_fragment.margin; - let padding = box_fragment.padding; - let specific_layout_info = box_fragment.specific_layout_info.clone(); - (content_rect, margins, padding, specific_layout_info) - }, - Fragment::Positioning(positioning_fragment) => { - let content_rect = positioning_fragment.borrow().rect; - ( - content_rect, - SideOffsets2D::zero(), - SideOffsets2D::zero(), - None, - ) - }, - _ => return computed_style(Some(fragment)), - }; - - // https://drafts.csswg.org/css-grid/#resolved-track-list - // > The grid-template-rows and grid-template-columns properties are - // > resolved value special case properties. - // - // > When an element generates a grid container box... - if display.inside() == DisplayInside::Grid { - if let Some(SpecificLayoutInfo::Grid(info)) = specific_layout_info { - if let Some(value) = resolve_grid_template(&info, style, longhand_id) { - return value; + let resolve_for_fragment = |fragment: &Fragment| { + let (content_rect, margins, padding, specific_layout_info) = match fragment { + Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => { + let box_fragment = box_fragment.borrow(); + if style.get_box().position != Position::Static { + let resolved_insets = || box_fragment.calculate_resolved_insets_if_positioned(); + match longhand_id { + LonghandId::Top => return resolved_insets().top.to_css_string(), + LonghandId::Right => { + return resolved_insets().right.to_css_string(); + }, + LonghandId::Bottom => { + return resolved_insets().bottom.to_css_string(); + }, + LonghandId::Left => { + return resolved_insets().left.to_css_string(); + }, + _ => {}, } } - } + let content_rect = box_fragment.content_rect; + let margins = box_fragment.margin; + let padding = box_fragment.padding; + let specific_layout_info = box_fragment.specific_layout_info.clone(); + (content_rect, margins, padding, specific_layout_info) + }, + Fragment::Positioning(positioning_fragment) => { + let content_rect = positioning_fragment.borrow().rect; + ( + content_rect, + SideOffsets2D::zero(), + SideOffsets2D::zero(), + None, + ) + }, + _ => return computed_style(Some(fragment)), + }; - // https://drafts.csswg.org/cssom/#resolved-value-special-case-property-like-height - // > If the property applies to the element or pseudo-element and the resolved value of the - // > display property is not none or contents, then the resolved value is the used value. - // > Otherwise the resolved value is the computed value. - // - // However, all browsers ignore that for margin and padding properties, and resolve to a length - // even if the property doesn't apply: https://github.com/w3c/csswg-drafts/issues/10391 - match longhand_id { - LonghandId::Width if resolved_size_should_be_used_value(fragment) => { - content_rect.size.width - }, - LonghandId::Height if resolved_size_should_be_used_value(fragment) => { - content_rect.size.height - }, - LonghandId::MarginBottom => margins.bottom, - LonghandId::MarginTop => margins.top, - LonghandId::MarginLeft => margins.left, - LonghandId::MarginRight => margins.right, - LonghandId::PaddingBottom => padding.bottom, - LonghandId::PaddingTop => padding.top, - LonghandId::PaddingLeft => padding.left, - LonghandId::PaddingRight => padding.right, - _ => return computed_style(Some(fragment)), + // https://drafts.csswg.org/css-grid/#resolved-track-list + // > The grid-template-rows and grid-template-columns properties are + // > resolved value special case properties. + // + // > When an element generates a grid container box... + if display.inside() == DisplayInside::Grid { + if let Some(SpecificLayoutInfo::Grid(info)) = specific_layout_info { + if let Some(value) = resolve_grid_template(&info, style, longhand_id) { + return value; + } } - .to_css_string() - }; + } - if !matches!( - longhand_id, - LonghandId::Top | LonghandId::Bottom | LonghandId::Left | LonghandId::Right - ) { - if let Some(fragment) = node.fragments_for_pseudo(*pseudo).first() { - return resolve_for_fragment(fragment, None); + // https://drafts.csswg.org/cssom/#resolved-value-special-case-property-like-height + // > If the property applies to the element or pseudo-element and the resolved value of the + // > display property is not none or contents, then the resolved value is the used value. + // > Otherwise the resolved value is the computed value. + // + // However, all browsers ignore that for margin and padding properties, and resolve to a length + // even if the property doesn't apply: https://github.com/w3c/csswg-drafts/issues/10391 + match longhand_id { + LonghandId::Width if resolved_size_should_be_used_value(fragment) => { + content_rect.size.width + }, + LonghandId::Height if resolved_size_should_be_used_value(fragment) => { + content_rect.size.height + }, + LonghandId::MarginBottom => margins.bottom, + LonghandId::MarginTop => margins.top, + LonghandId::MarginLeft => margins.left, + LonghandId::MarginRight => margins.right, + LonghandId::PaddingBottom => padding.bottom, + LonghandId::PaddingTop => padding.top, + LonghandId::PaddingLeft => padding.left, + LonghandId::PaddingRight => padding.right, + _ => return computed_style(Some(fragment)), } - } + .to_css_string() + }; - fragment_tree - .and_then(|fragment_tree| { - fragment_tree.find(|fragment, _, containing_block| { - if Some(tag_to_find) == fragment.tag() { - Some(resolve_for_fragment(fragment, Some(containing_block))) - } else { - None - } - }) - }) + node.fragments_for_pseudo(*pseudo) + .first() + .map(resolve_for_fragment) .unwrap_or_else(|| computed_style(None)) } @@ -450,231 +428,157 @@ fn shorthand_to_css_string( } } -pub fn process_offset_parent_query( - node: OpaqueNode, - fragment_tree: Option<Arc<FragmentTree>>, -) -> OffsetParentResponse { - process_offset_parent_query_inner(node, fragment_tree).unwrap_or_default() +struct OffsetParentFragments { + parent: ArcRefCell<BoxFragment>, + grandparent: Option<Fragment>, } -#[inline] -fn process_offset_parent_query_inner( - node: OpaqueNode, - fragment_tree: Option<Arc<FragmentTree>>, -) -> Option<OffsetParentResponse> { - let fragment_tree = fragment_tree?; - - struct NodeOffsetBoxInfo { - border_box: Rect<Au>, - offset_parent_node_address: Option<OpaqueNode>, - is_static_body_element: bool, +/// <https://www.w3.org/TR/2016/WD-cssom-view-1-20160317/#dom-htmlelement-offsetparent> +fn offset_parent_fragments<'dom>( + node: impl LayoutNode<'dom> + 'dom, +) -> Option<OffsetParentFragments> { + // 1. If any of the following holds true return null and terminate this algorithm: + // * The element does not have an associated CSS layout box. + // * The element is the root element. + // * The element is the HTML body element. + // * The element’s computed value of the position property is fixed. + let fragment = node.fragments_for_pseudo(None).first().cloned()?; + let flags = fragment.base()?.flags; + if flags.intersects( + FragmentFlags::IS_ROOT_ELEMENT | FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT, + ) { + return None; + } + if matches!( + fragment, Fragment::Box(fragment) if fragment.borrow().style.get_box().position == Position::Fixed + ) { + return None; } - // https://www.w3.org/TR/2016/WD-cssom-view-1-20160317/#extensions-to-the-htmlelement-interface - let mut parent_node_addresses: Vec<Option<(OpaqueNode, bool)>> = Vec::new(); - let tag_to_find = Tag::new(node); - let node_offset_box = fragment_tree.find(|fragment, level, containing_block| { - let base = fragment.base()?; - let is_body_element = base - .flags - .contains(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT); - - if fragment.tag() == Some(tag_to_find) { - // Only consider the first fragment of the node found as per a - // possible interpretation of the specification: "[...] return the - // y-coordinate of the top border edge of the first CSS layout box - // associated with the element [...]" - // - // FIXME: Browsers implement this all differently (e.g., [1]) - - // Firefox does returns the union of all layout elements of some - // sort. Chrome returns the first fragment for a block element (the - // same as ours) or the union of all associated fragments in the - // first containing block fragment for an inline element. We could - // implement Chrome's behavior, but our fragment tree currently - // provides insufficient information. - // - // [1]: https://github.com/w3c/csswg-drafts/issues/4541 - let fragment_relative_rect = match fragment { - Fragment::Box(fragment) | Fragment::Float(fragment) => fragment.borrow().border_rect(), - Fragment::Text(fragment) => fragment.borrow().rect, - Fragment::Positioning(fragment) => fragment.borrow().rect, - Fragment::AbsoluteOrFixedPositioned(_) | - Fragment::Image(_) | - Fragment::IFrame(_) => unreachable!(), + // 2. Return the nearest ancestor element of the element for which at least one of + // the following is true and terminate this algorithm if such an ancestor is found: + // * The computed value of the position property is not static. + // * It is the HTML body element. + // * The computed value of the position property of the element is static and the + // ancestor is one of the following HTML elements: td, th, or table. + let mut maybe_parent_node = node.parent_node(); + while let Some(parent_node) = maybe_parent_node { + maybe_parent_node = parent_node.parent_node(); + + if let Some(parent_fragment) = parent_node.fragments_for_pseudo(None).first() { + let parent_fragment = match parent_fragment { + Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => box_fragment, + _ => continue, }; - let mut border_box = fragment_relative_rect.translate(containing_block.origin.to_vector()).to_untyped(); - - // "If any of the following holds true return null and terminate - // this algorithm: [...] The element’s computed value of the - // `position` property is `fixed`." - let is_fixed = matches!( - fragment, Fragment::Box(fragment) if fragment.borrow().style.get_box().position == Position::Fixed - ); + let grandparent_fragment = + maybe_parent_node.and_then(|node| node.fragments_for_pseudo(None).first().cloned()); - if is_body_element { - // "If the element is the HTML body element or [...] return zero - // and terminate this algorithm." - border_box.origin = Point2D::zero(); + if parent_fragment.borrow().style.get_box().position != Position::Static { + return Some(OffsetParentFragments { + parent: parent_fragment.clone(), + grandparent: grandparent_fragment, + }); } - let offset_parent_node = if is_fixed { - None - } else { - // Find the nearest ancestor element eligible as `offsetParent`. - parent_node_addresses[..level] - .iter() - .rev() - .cloned() - .find_map(std::convert::identity) - }; - - Some(NodeOffsetBoxInfo { - border_box, - offset_parent_node_address: offset_parent_node.map(|node| node.0), - is_static_body_element: offset_parent_node.is_some_and(|node| node.1), - }) - } else { - // Record the paths of the nodes being traversed. - let parent_node_address = match fragment { - Fragment::Box(fragment) | Fragment::Float(fragment) => { - let fragment = &*fragment.borrow(); - let is_eligible_parent = is_eligible_parent(fragment); - let is_static_body_element = is_body_element && - fragment.style.get_box().position == Position::Static; - match base.tag { - Some(tag) if is_eligible_parent && !tag.is_pseudo() => { - Some((tag.node, is_static_body_element)) - }, - _ => None, - } - }, - Fragment::AbsoluteOrFixedPositioned(_) | - Fragment::IFrame(_) | - Fragment::Image(_) | - Fragment::Positioning(_) | - Fragment::Text(_) => None, - }; - - while parent_node_addresses.len() <= level { - parent_node_addresses.push(None); + let flags = parent_fragment.borrow().base.flags; + if flags.intersects( + FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT | + FragmentFlags::IS_TABLE_TH_OR_TD_ELEMENT, + ) { + return Some(OffsetParentFragments { + parent: parent_fragment.clone(), + grandparent: grandparent_fragment, + }); } - parent_node_addresses[level] = parent_node_address; - None } - }); + } - // Bail out if the element doesn't have an associated fragment. - // "If any of the following holds true return null and terminate this - // algorithm: [...] The element does not have an associated CSS layout box." - // (`offsetParent`) "If the element is the HTML body element [...] return - // zero and terminate this algorithm." (others) - let node_offset_box = node_offset_box?; + None +} - let offset_parent_padding_box_corner = if let Some(offset_parent_node_address) = - node_offset_box.offset_parent_node_address - { - // The spec (https://www.w3.org/TR/cssom-view-1/#extensions-to-the-htmlelement-interface) - // says that offsetTop/offsetLeft are always relative to the padding box of the offsetParent. - // However, in practice this is not true in major browsers in the case that the offsetParent is the body - // element and the body element is position:static. In that case offsetLeft/offsetTop are computed - // relative to the root node's border box. - if node_offset_box.is_static_body_element { - fn extract_box_fragment( - fragment: &Fragment, - containing_block: &PhysicalRect<Au>, - ) -> PhysicalVec<Au> { - let (Fragment::Box(fragment) | Fragment::Float(fragment)) = fragment else { - unreachable!(); - }; - // Again, take the *first* associated CSS layout box. - fragment.borrow().border_rect().origin.to_vector() + - containing_block.origin.to_vector() - } +#[inline] +pub fn process_offset_parent_query<'dom>( + node: impl LayoutNode<'dom> + 'dom, +) -> Option<OffsetParentResponse> { + // Only consider the first fragment of the node found as per a + // possible interpretation of the specification: "[...] return the + // y-coordinate of the top border edge of the first CSS layout box + // associated with the element [...]" + // + // FIXME: Browsers implement this all differently (e.g., [1]) - + // Firefox does returns the union of all layout elements of some + // sort. Chrome returns the first fragment for a block element (the + // same as ours) or the union of all associated fragments in the + // first containing block fragment for an inline element. We could + // implement Chrome's behavior, but our fragment tree currently + // provides insufficient information. + // + // [1]: https://github.com/w3c/csswg-drafts/issues/4541 + // > 1. If the element is the HTML body element or does not have any associated CSS + // layout box return zero and terminate this algorithm. + let fragment = node.fragments_for_pseudo(None).first().cloned()?; + let mut border_box = fragment.cumulative_border_box_rect()?; + + // 2. If the offsetParent of the element is null return the x-coordinate of the left + // border edge of the first CSS layout box associated with the element, relative to + // the initial containing block origin, ignoring any transforms that apply to the + // element and its ancestors, and terminate this algorithm. + let Some(offset_parent_fragment) = offset_parent_fragments(node) else { + return Some(OffsetParentResponse { + node_address: None, + rect: border_box.to_untyped(), + }); + }; - let containing_block = &fragment_tree.initial_containing_block; - let fragment = &fragment_tree.root_fragments[0]; - if let Fragment::AbsoluteOrFixedPositioned(shared_fragment) = fragment { - let shared_fragment = &*shared_fragment.borrow(); - let fragment = shared_fragment.fragment.as_ref().unwrap(); - extract_box_fragment(fragment, containing_block) - } else { - extract_box_fragment(fragment, containing_block) - } + let parent_fragment = offset_parent_fragment.parent.borrow(); + let parent_is_static_body_element = parent_fragment + .base + .flags + .contains(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT) && + parent_fragment.style.get_box().position == Position::Static; + + // For `offsetLeft`: + // 3. Return the result of subtracting the y-coordinate of the top padding edge of the + // first CSS layout box associated with the offsetParent of the element from the + // y-coordinate of the top border edge of the first CSS layout box associated with the + // element, relative to the initial containing block origin, ignoring any transforms + // that apply to the element and its ancestors. + // + // We generalize this for `offsetRight` as described in the specification. + let grandparent_box_fragment = || match offset_parent_fragment.grandparent { + Some(Fragment::Box(box_fragment)) | Some(Fragment::Float(box_fragment)) => { + Some(box_fragment) + }, + _ => None, + }; + + // The spec (https://www.w3.org/TR/cssom-view-1/#extensions-to-the-htmlelement-interface) + // says that offsetTop/offsetLeft are always relative to the padding box of the offsetParent. + // However, in practice this is not true in major browsers in the case that the offsetParent is the body + // element and the body element is position:static. In that case offsetLeft/offsetTop are computed + // relative to the root node's border box. + // + // See <https://github.com/w3c/csswg-drafts/issues/10549>. + let parent_offset_rect = if parent_is_static_body_element { + if let Some(grandparent_fragment) = grandparent_box_fragment() { + let grandparent_fragment = grandparent_fragment.borrow(); + grandparent_fragment.offset_by_containing_block(&grandparent_fragment.border_rect()) } else { - // Find the top and left padding edges of "the first CSS layout box - // associated with the `offsetParent` of the element". - // - // Since we saw `offset_parent_node_address` once, we should be able - // to find it again. - let offset_parent_node_tag = Tag::new(offset_parent_node_address); - fragment_tree - .find(|fragment, _, containing_block| { - match fragment { - Fragment::Box(fragment) | Fragment::Float(fragment) => { - let fragment = fragment.borrow(); - if fragment.base.tag == Some(offset_parent_node_tag) { - // Again, take the *first* associated CSS layout box. - let padding_box_corner = fragment.padding_rect().origin.to_vector() - + containing_block.origin.to_vector(); - Some(padding_box_corner) - } else { - None - } - }, - Fragment::AbsoluteOrFixedPositioned(_) - | Fragment::Text(_) - | Fragment::Image(_) - | Fragment::IFrame(_) - | Fragment::Positioning(_) => None, - } - }) - .unwrap() + parent_fragment.offset_by_containing_block(&parent_fragment.padding_rect()) } } else { - // "If the offsetParent of the element is null," subtract zero in the - // following step. - Vector2D::zero() + parent_fragment.offset_by_containing_block(&parent_fragment.padding_rect()) }; + border_box = border_box.translate(-parent_offset_rect.origin.to_vector()); + Some(OffsetParentResponse { - node_address: node_offset_box.offset_parent_node_address.map(Into::into), - // "Return the result of subtracting the x-coordinate of the left - // padding edge of the first CSS layout box associated with the - // `offsetParent` of the element from the x-coordinate of the left - // border edge of the first CSS layout box associated with the element, - // relative to the initial containing block origin, ignoring any - // transforms that apply to the element and its ancestors." (and vice - // versa for the top border edge) - rect: node_offset_box - .border_box - .translate(-offset_parent_padding_box_corner.to_untyped()), + node_address: parent_fragment.base.tag.map(|tag| tag.node.into()), + rect: border_box.to_untyped(), }) } -/// Returns whether or not the element with the given style and body element determination -/// is eligible to be a parent element for offset* queries. -/// -/// From <https://www.w3.org/TR/cssom-view-1/#dom-htmlelement-offsetparent>: -/// -/// > Return the nearest ancestor element of the element for which at least one of the following is -/// > true and terminate this algorithm if such an ancestor is found: -/// > 1. The computed value of the position property is not static. -/// > 2. It is the HTML body element. -/// > 3. The computed value of the position property of the element is static and the ancestor is -/// > one of the following HTML elements: td, th, or table. -fn is_eligible_parent(fragment: &BoxFragment) -> bool { - fragment - .base - .flags - .contains(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT) || - fragment.style.get_box().position != Position::Static || - fragment - .base - .flags - .contains(FragmentFlags::IS_TABLE_TH_OR_TD_ELEMENT) -} - /// <https://html.spec.whatwg.org/multipage/#get-the-text-steps> pub fn get_the_text_steps<'dom>(node: impl LayoutNode<'dom>) -> String { // Step 1: If element is not being rendered or if the user agent is a non-CSS user agent, then diff --git a/components/layout/table/layout.rs b/components/layout/table/layout.rs index 57b48ae0bca..2261f7d165c 100644 --- a/components/layout/table/layout.rs +++ b/components/layout/table/layout.rs @@ -2142,23 +2142,27 @@ impl<'a> TableLayout<'a> { for column_group in self.table.column_groups.iter() { let column_group = column_group.borrow(); if !column_group.is_empty() { - fragments.push(Fragment::Positioning(PositioningFragment::new_empty( + let fragment = Fragment::Positioning(PositioningFragment::new_empty( column_group.base.base_fragment_info, dimensions .get_column_group_rect(&column_group) .as_physical(None), column_group.base.style.clone(), - ))); + )); + column_group.base.set_fragment(fragment.clone()); + fragments.push(fragment); } } for (column_index, column) in self.table.columns.iter().enumerate() { let column = column.borrow(); - fragments.push(Fragment::Positioning(PositioningFragment::new_empty( + let fragment = Fragment::Positioning(PositioningFragment::new_empty( column.base.base_fragment_info, dimensions.get_column_rect(column_index).as_physical(None), column.base.style.clone(), - ))); + )); + column.base.set_fragment(fragment.clone()); + fragments.push(fragment); } } diff --git a/components/layout/table/mod.rs b/components/layout/table/mod.rs index 120270fc7cf..fe7f90437b8 100644 --- a/components/layout/table/mod.rs +++ b/components/layout/table/mod.rs @@ -346,6 +346,7 @@ pub(crate) struct TableLayoutStyle<'a> { /// Table parts that are stored in the DOM. This is used in order to map from /// the DOM to the box tree and will eventually be important for incremental /// layout. +#[derive(MallocSizeOf)] pub(crate) enum TableLevelBox { Caption(ArcRefCell<TableCaption>), Cell(ArcRefCell<TableSlotCell>), diff --git a/components/layout/traversal.rs b/components/layout/traversal.rs index 40281b640c9..bf60c41d6ba 100644 --- a/components/layout/traversal.rs +++ b/components/layout/traversal.rs @@ -12,19 +12,15 @@ use crate::context::LayoutContext; use crate::dom::DOMLayoutData; pub struct RecalcStyle<'a> { - context: LayoutContext<'a>, + context: &'a LayoutContext<'a>, } impl<'a> RecalcStyle<'a> { - pub fn new(context: LayoutContext<'a>) -> Self { + pub fn new(context: &'a LayoutContext<'a>) -> Self { RecalcStyle { context } } pub fn context(&self) -> &LayoutContext<'a> { - &self.context - } - - pub fn destroy(self) -> LayoutContext<'a> { self.context } } diff --git a/components/malloc_size_of/lib.rs b/components/malloc_size_of/lib.rs index b7f677f8044..52523af7cb1 100644 --- a/components/malloc_size_of/lib.rs +++ b/components/malloc_size_of/lib.rs @@ -746,6 +746,7 @@ malloc_size_of_is_0!(std::sync::atomic::AtomicUsize); malloc_size_of_is_0!(std::time::Duration); malloc_size_of_is_0!(std::time::Instant); malloc_size_of_is_0!(std::time::SystemTime); +malloc_size_of_is_0!(style::data::ElementData); malloc_size_of_is_0!(style::font_face::SourceList); malloc_size_of_is_0!(style::properties::ComputedValues); malloc_size_of_is_0!(style::queries::values::PrefersColorScheme); diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs index 697a46fedda..b1ad01b81e0 100644 --- a/components/net/fetch/methods.rs +++ b/components/net/fetch/methods.rs @@ -23,8 +23,8 @@ use net_traits::http_status::HttpStatus; use net_traits::policy_container::{PolicyContainer, RequestPolicyContainer}; use net_traits::request::{ BodyChunkRequest, BodyChunkResponse, CredentialsMode, Destination, Initiator, - InsecureRequestsPolicy, Origin, RedirectMode, Referrer, Request, RequestMode, ResponseTainting, - Window, is_cors_safelisted_method, is_cors_safelisted_request_header, + InsecureRequestsPolicy, Origin, ParserMetadata, RedirectMode, Referrer, Request, RequestMode, + ResponseTainting, Window, is_cors_safelisted_method, is_cors_safelisted_request_header, }; use net_traits::response::{Response, ResponseBody, ResponseType}; use net_traits::{ @@ -42,7 +42,7 @@ use crate::fetch::cors_cache::CorsCache; use crate::fetch::headers::determine_nosniff; use crate::filemanager_thread::FileManager; use crate::http_loader::{HttpState, determine_requests_referrer, http_fetch, set_default_accept}; -use crate::protocols::ProtocolRegistry; +use crate::protocols::{ProtocolRegistry, is_url_potentially_trustworthy}; use crate::request_interceptor::RequestInterceptor; use crate::subresource_integrity::is_response_integrity_valid; @@ -169,6 +169,29 @@ pub async fn fetch_with_cors_cache( // TODO: We don't implement fetchParams as defined in the spec } +fn convert_request_to_csp_request(request: &Request, origin: &ImmutableOrigin) -> csp::Request { + csp::Request { + url: request.url().into_url(), + origin: origin.clone().into_url_origin(), + redirect_count: request.redirect_count, + destination: request.destination, + initiator: match request.initiator { + Initiator::Download => csp::Initiator::Download, + Initiator::ImageSet => csp::Initiator::ImageSet, + Initiator::Manifest => csp::Initiator::Manifest, + Initiator::Prefetch => csp::Initiator::Prefetch, + _ => csp::Initiator::None, + }, + nonce: request.cryptographic_nonce_metadata.clone(), + integrity_metadata: request.integrity_metadata.clone(), + parser_metadata: match request.parser_metadata { + ParserMetadata::ParserInserted => csp::ParserMetadata::ParserInserted, + ParserMetadata::NotParserInserted => csp::ParserMetadata::NotParserInserted, + ParserMetadata::Default => csp::ParserMetadata::None, + }, + } +} + /// <https://www.w3.org/TR/CSP/#should-block-request> pub fn should_request_be_blocked_by_csp( request: &Request, @@ -178,17 +201,7 @@ pub fn should_request_be_blocked_by_csp( Origin::Client => return (csp::CheckResult::Allowed, Vec::new()), Origin::Origin(origin) => origin, }; - - let csp_request = csp::Request { - url: request.url().into_url(), - origin: origin.clone().into_url_origin(), - redirect_count: request.redirect_count, - destination: request.destination, - initiator: csp::Initiator::None, - nonce: request.cryptographic_nonce_metadata.clone(), - integrity_metadata: request.integrity_metadata.clone(), - parser_metadata: csp::ParserMetadata::None, - }; + let csp_request = convert_request_to_csp_request(request, origin); policy_container .csp_list @@ -197,6 +210,24 @@ pub fn should_request_be_blocked_by_csp( .unwrap_or((csp::CheckResult::Allowed, Vec::new())) } +/// <https://www.w3.org/TR/CSP/#report-for-request> +pub fn report_violations_for_request_by_csp( + request: &Request, + policy_container: &PolicyContainer, +) -> Vec<csp::Violation> { + let origin = match &request.origin { + Origin::Client => return Vec::new(), + Origin::Origin(origin) => origin, + }; + let csp_request = convert_request_to_csp_request(request, origin); + + policy_container + .csp_list + .as_ref() + .map(|c| c.report_violations_for_request(&csp_request)) + .unwrap_or_default() +} + /// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch) pub async fn main_fetch( fetch_params: &mut FetchParams, @@ -232,9 +263,6 @@ pub async fn main_fetch( ))); } - // Step 2.2. - // TODO: Report violations. - // The request should have a valid policy_container associated with it. // TODO: This should not be `Client` here let policy_container = match &request.policy_container { @@ -242,12 +270,19 @@ pub async fn main_fetch( RequestPolicyContainer::PolicyContainer(container) => container.to_owned(), }; + // Step 2.2. + let violations = report_violations_for_request_by_csp(request, &policy_container); + + if !violations.is_empty() { + target.process_csp_violations(request, violations); + } + // Step 3. // TODO: handle request abort. // Step 4. Upgrade request to a potentially trustworthy URL, if appropriate. if should_upgrade_request_to_potentially_trustworty(request, context) || - should_upgrade_mixed_content_request(request) + should_upgrade_mixed_content_request(request, &context.protocols) { trace!( "upgrading {} targeting {:?}", @@ -294,7 +329,7 @@ pub async fn main_fetch( "Request attempted on bad port".into(), ))); } - if should_request_be_blocked_as_mixed_content(request) { + if should_request_be_blocked_as_mixed_content(request, &context.protocols) { response = Some(Response::network_error(NetworkError::Internal( "Blocked as mixed content".into(), ))); @@ -359,13 +394,16 @@ pub async fn main_fetch( if (same_origin && request.response_tainting == ResponseTainting::Basic) || // request's current URL's scheme is "data" current_scheme == "data" || + // Note: Although it is not part of the specification, we make an exception here + // for custom protocols that are explicitly marked as active for fetch. + context.protocols.is_fetchable(current_scheme) || // request's mode is "navigate" or "websocket" matches!( request.mode, RequestMode::Navigate | RequestMode::WebSocket { .. } ) { - // Substep 1. Set request’s response tainting to "basic". + // Substep 1. Set request's response tainting to "basic". request.response_tainting = ResponseTainting::Basic; // Substep 2. Return the result of running scheme fetch given fetchParams. @@ -373,13 +411,13 @@ pub async fn main_fetch( } else if request.mode == RequestMode::SameOrigin { Response::network_error(NetworkError::Internal("Cross-origin response".into())) } else if request.mode == RequestMode::NoCors { - // Substep 1. If request’s redirect mode is not "follow", then return a network error. + // Substep 1. If request's redirect mode is not "follow", then return a network error. if request.redirect_mode != RedirectMode::Follow { Response::network_error(NetworkError::Internal( "NoCors requests must follow redirects".into(), )) } else { - // Substep 2. Set request’s response tainting to "opaque". + // Substep 2. Set request's response tainting to "opaque". request.response_tainting = ResponseTainting::Opaque; // Substep 3. Return the result of running scheme fetch given fetchParams. @@ -490,7 +528,7 @@ pub async fn main_fetch( let should_replace_with_mime_type_error = !response_is_network_error && should_be_blocked_due_to_mime_type(request.destination, &response.headers); let should_replace_with_mixed_content = !response_is_network_error && - should_response_be_blocked_as_mixed_content(request, &response); + should_response_be_blocked_as_mixed_content(request, &response, &context.protocols); // Step 15. let mut network_error_response = response @@ -933,7 +971,10 @@ pub fn should_request_be_blocked_due_to_a_bad_port(url: &ServoUrl) -> bool { } /// <https://w3c.github.io/webappsec-mixed-content/#should-block-fetch> -pub fn should_request_be_blocked_as_mixed_content(request: &Request) -> bool { +pub fn should_request_be_blocked_as_mixed_content( + request: &Request, + protocol_registry: &ProtocolRegistry, +) -> bool { // Step 1. Return allowed if one or more of the following conditions are met: // 1.1. Does settings prohibit mixed security contexts? // returns "Does Not Restrict Mixed Security Contexts" when applied to request’s client. @@ -944,7 +985,7 @@ pub fn should_request_be_blocked_as_mixed_content(request: &Request) -> bool { } // 1.2. request’s URL is a potentially trustworthy URL. - if request.url().is_potentially_trustworthy() { + if is_url_potentially_trustworthy(protocol_registry, &request.url()) { return false; } @@ -961,7 +1002,11 @@ pub fn should_request_be_blocked_as_mixed_content(request: &Request) -> bool { } /// <https://w3c.github.io/webappsec-mixed-content/#should-block-response> -pub fn should_response_be_blocked_as_mixed_content(request: &Request, response: &Response) -> bool { +pub fn should_response_be_blocked_as_mixed_content( + request: &Request, + response: &Response, + protocol_registry: &ProtocolRegistry, +) -> bool { // Step 1. Return allowed if one or more of the following conditions are met: // 1.1. Does settings prohibit mixed security contexts? returns Does Not Restrict Mixed Content // when applied to request’s client. @@ -975,7 +1020,7 @@ pub fn should_response_be_blocked_as_mixed_content(request: &Request, response: if response .actual_response() .url() - .is_some_and(|response_url| response_url.is_potentially_trustworthy()) + .is_some_and(|response_url| is_url_potentially_trustworthy(protocol_registry, response_url)) { return false; } @@ -1041,7 +1086,7 @@ fn should_upgrade_request_to_potentially_trustworty( // request’s header list if any of the following criteria are met: // * request’s URL is not a potentially trustworthy URL // * request’s URL's host is not a preloadable HSTS host - if !request.current_url().is_potentially_trustworthy() || + if !is_url_potentially_trustworthy(&context.protocols, &request.current_url()) || !request.current_url().host_str().is_some_and(|host| { !context.state.hsts_list.read().unwrap().is_host_secure(host) }) @@ -1094,10 +1139,13 @@ fn do_settings_prohibit_mixed_security_contexts(request: &Request) -> MixedSecur } /// <https://w3c.github.io/webappsec-mixed-content/#upgrade-algorithm> -fn should_upgrade_mixed_content_request(request: &Request) -> bool { +fn should_upgrade_mixed_content_request( + request: &Request, + protocol_registry: &ProtocolRegistry, +) -> bool { let url = request.url(); // Step 1.1 : request’s URL is a potentially trustworthy URL. - if url.is_potentially_trustworthy() { + if is_url_potentially_trustworthy(protocol_registry, &url) { return false; } diff --git a/components/net/protocols/mod.rs b/components/net/protocols/mod.rs index f8b989b9623..6dc58ceab64 100644 --- a/components/net/protocols/mod.rs +++ b/components/net/protocols/mod.rs @@ -14,6 +14,7 @@ use log::error; use net_traits::filemanager_thread::RelativePos; use net_traits::request::Request; use net_traits::response::Response; +use servo_url::ServoUrl; use crate::fetch::methods::{DoneChannel, FetchContext, RangeRequestBounds}; @@ -47,6 +48,15 @@ pub trait ProtocolHandler: Send + Sync { fn is_fetchable(&self) -> bool { false } + + /// Specify if this custom protocol can be used in a [secure context] + /// + /// Note: this only works for bypassing mixed content checks right now + /// + /// [secure context]: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts + fn is_secure(&self) -> bool { + false + } } #[derive(Default)] @@ -54,27 +64,45 @@ pub struct ProtocolRegistry { pub(crate) handlers: HashMap<String, Box<dyn ProtocolHandler>>, // Maps scheme -> handler } +#[derive(Clone, Copy, Debug)] +pub enum ProtocolRegisterError { + ForbiddenScheme, + SchemeAlreadyRegistered, +} + impl ProtocolRegistry { pub fn with_internal_protocols() -> Self { let mut registry = Self::default(); - registry.register("data", DataProtocolHander::default()); - registry.register("blob", BlobProtocolHander::default()); - registry.register("file", FileProtocolHander::default()); + // We just created a new registry, and know that we aren't using + // any forbidden schemes, so this should never panic. + registry + .register("data", DataProtocolHander::default()) + .expect("Infallible"); + registry + .register("blob", BlobProtocolHander::default()) + .expect("Infallible"); + registry + .register("file", FileProtocolHander::default()) + .expect("Infallible"); registry } - pub fn register(&mut self, scheme: &str, handler: impl ProtocolHandler + 'static) -> bool { + pub fn register( + &mut self, + scheme: &str, + handler: impl ProtocolHandler + 'static, + ) -> Result<(), ProtocolRegisterError> { if FORBIDDEN_SCHEMES.contains(&scheme) { error!("Protocol handler for '{scheme}' is not allowed to be registered."); - return false; + return Err(ProtocolRegisterError::ForbiddenScheme); } if let Entry::Vacant(entry) = self.handlers.entry(scheme.into()) { entry.insert(Box::new(handler)); - true + Ok(()) } else { error!("Protocol handler for '{scheme}' is already registered."); - false + Err(ProtocolRegisterError::SchemeAlreadyRegistered) } } @@ -96,9 +124,22 @@ impl ProtocolRegistry { pub fn is_fetchable(&self, scheme: &str) -> bool { self.handlers .get(scheme) - .map(|handler| handler.is_fetchable()) - .unwrap_or(false) + .is_some_and(|handler| handler.is_fetchable()) } + + pub fn is_secure(&self, scheme: &str) -> bool { + self.handlers + .get(scheme) + .is_some_and(|handler| handler.is_secure()) + } +} + +/// Test if the URL is potentially trustworthy or the custom protocol is registered as secure +pub fn is_url_potentially_trustworthy( + protocol_registry: &ProtocolRegistry, + url: &ServoUrl, +) -> bool { + url.is_potentially_trustworthy() || protocol_registry.is_secure(url.scheme()) } pub fn range_not_satisfiable_error(response: &mut Response) { diff --git a/components/pixels/lib.rs b/components/pixels/lib.rs index 7cba060ec74..24386ff830a 100644 --- a/components/pixels/lib.rs +++ b/components/pixels/lib.rs @@ -84,6 +84,7 @@ pub fn rgba8_premultiply_inplace(pixels: &mut [u8]) -> bool { is_opaque } +#[inline(always)] pub fn multiply_u8_color(a: u8, b: u8) -> u8 { (a as u32 * b as u32 / 255) as u8 } @@ -254,6 +255,69 @@ pub fn unmultiply_inplace<const SWAP_RB: bool>(pixels: &mut [u8]) { } } +#[repr(u8)] +pub enum Multiply { + None = 0, + PreMultiply = 1, + UnMultiply = 2, +} + +pub fn transform_inplace(pixels: &mut [u8], multiply: Multiply, swap_rb: bool, clear_alpha: bool) { + match (multiply, swap_rb, clear_alpha) { + (Multiply::None, true, true) => generic_transform_inplace::<0, true, true>(pixels), + (Multiply::None, true, false) => generic_transform_inplace::<0, true, false>(pixels), + (Multiply::None, false, true) => generic_transform_inplace::<0, false, true>(pixels), + (Multiply::None, false, false) => generic_transform_inplace::<0, false, false>(pixels), + (Multiply::PreMultiply, true, true) => generic_transform_inplace::<1, true, true>(pixels), + (Multiply::PreMultiply, true, false) => generic_transform_inplace::<1, true, false>(pixels), + (Multiply::PreMultiply, false, true) => generic_transform_inplace::<1, false, true>(pixels), + (Multiply::PreMultiply, false, false) => { + generic_transform_inplace::<1, false, false>(pixels) + }, + (Multiply::UnMultiply, true, true) => generic_transform_inplace::<2, true, true>(pixels), + (Multiply::UnMultiply, true, false) => generic_transform_inplace::<2, true, false>(pixels), + (Multiply::UnMultiply, false, true) => generic_transform_inplace::<2, false, true>(pixels), + (Multiply::UnMultiply, false, false) => { + generic_transform_inplace::<2, false, false>(pixels) + }, + } +} + +pub fn generic_transform_inplace< + const MULTIPLY: u8, // 1 premultiply, 2 unmultiply + const SWAP_RB: bool, + const CLEAR_ALPHA: bool, +>( + pixels: &mut [u8], +) { + for rgba in pixels.chunks_mut(4) { + match MULTIPLY { + 1 => { + let a = rgba[3]; + multiply_u8_color(rgba[0], a); + multiply_u8_color(rgba[1], a); + multiply_u8_color(rgba[2], a); + }, + 2 => { + let a = rgba[3] as u32; + + if a > 0 { + rgba[0] = (rgba[0] as u32 * 255 / a) as u8; + rgba[1] = (rgba[1] as u32 * 255 / a) as u8; + rgba[2] = (rgba[2] as u32 * 255 / a) as u8; + } + }, + _ => {}, + } + if SWAP_RB { + rgba.swap(0, 2); + } + if CLEAR_ALPHA { + rgba[3] = u8::MAX; + } + } +} + fn is_gif(buffer: &[u8]) -> bool { buffer.starts_with(b"GIF87a") || buffer.starts_with(b"GIF89a") } diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index 37e80d0e748..1aa821cdbd3 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -114,6 +114,7 @@ servo_config = { path = "../config" } servo_geometry = { path = "../geometry" } servo_rand = { path = "../rand" } servo_url = { path = "../url" } +snapshot = { workspace = true } smallvec = { workspace = true, features = ["union"] } strum = { workspace = true } strum_macros = { workspace = true } diff --git a/components/script/canvas_context.rs b/components/script/canvas_context.rs index 8bf188a5aa9..d49d31997e1 100644 --- a/components/script/canvas_context.rs +++ b/components/script/canvas_context.rs @@ -5,8 +5,8 @@ //! Common interfaces for Canvas Contexts use euclid::default::Size2D; -use ipc_channel::ipc::IpcSharedMemory; use script_layout_interface::{HTMLCanvasData, HTMLCanvasDataSource}; +use snapshot::Snapshot; use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanvas; use crate::dom::bindings::inheritance::Castable; @@ -30,11 +30,10 @@ pub(crate) trait CanvasContext { fn resize(&self); - fn get_image_data_as_shared_memory(&self) -> Option<IpcSharedMemory>; - - fn get_image_data(&self) -> Option<Vec<u8>> { - self.get_image_data_as_shared_memory().map(|sm| sm.to_vec()) - } + /// Returns none if area of canvas is zero. + /// + /// In case of other errors it returns cleared snapshot + fn get_image_data(&self) -> Option<Snapshot>; fn origin_is_clean(&self) -> bool { true diff --git a/components/script/canvas_state.rs b/components/script/canvas_state.rs index aea3012b365..408c94c124a 100644 --- a/components/script/canvas_state.rs +++ b/components/script/canvas_state.rs @@ -17,7 +17,7 @@ use cssparser::color::clamp_unit_f32; use cssparser::{Parser, ParserInput}; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; use euclid::vec2; -use ipc_channel::ipc::{self, IpcSender, IpcSharedMemory}; +use ipc_channel::ipc::{self, IpcSender}; use net_traits::image_cache::{ImageCache, ImageResponse}; use net_traits::request::CorsSettings; use pixels::PixelFormat; @@ -298,7 +298,7 @@ impl CanvasState { &self, url: ServoUrl, cors_setting: Option<CorsSettings>, - ) -> Option<(IpcSharedMemory, Size2D<u32>)> { + ) -> Option<snapshot::Snapshot> { let img = match self.request_image_from_cache(url, cors_setting) { ImageResponse::Loaded(img, _) => img, ImageResponse::PlaceholderLoaded(_, _) | @@ -308,13 +308,22 @@ impl CanvasState { }, }; - let image_size = Size2D::new(img.width, img.height); - let image_data = match img.format { - PixelFormat::BGRA8 => img.bytes(), + let size = Size2D::new(img.width, img.height); + let format = match img.format { + PixelFormat::BGRA8 => snapshot::PixelFormat::BGRA, + PixelFormat::RGBA8 => snapshot::PixelFormat::RGBA, pixel_format => unimplemented!("unsupported pixel format ({:?})", pixel_format), }; + let alpha_mode = snapshot::AlphaMode::Transparent { + premultiplied: false, + }; - Some((image_data, image_size)) + Some(snapshot::Snapshot::from_shared_memory( + size.cast(), + format, + alpha_mode, + img.bytes(), + )) } fn request_image_from_cache( @@ -341,13 +350,16 @@ impl CanvasState { assert!(Rect::from_size(canvas_size).contains_rect(&rect)); - let (sender, receiver) = ipc::bytes_channel().unwrap(); + let (sender, receiver) = ipc::channel().unwrap(); self.send_canvas_2d_msg(Canvas2dMsg::GetImageData(rect, canvas_size, sender)); - let mut pixels = receiver.recv().unwrap().to_vec(); - - pixels::unmultiply_inplace::<true>(&mut pixels); - - pixels + let mut snapshot = receiver.recv().unwrap().to_owned(); + snapshot.transform( + snapshot::AlphaMode::Transparent { + premultiplied: false, + }, + snapshot::PixelFormat::RGBA, + ); + snapshot.to_vec() } /// @@ -594,10 +606,10 @@ impl CanvasState { dh: Option<f64>, ) -> ErrorResult { debug!("Fetching image {}.", url); - let (image_data, image_size) = self + let snapshot = self .fetch_image_data(url, cors_setting) .ok_or(Error::InvalidState)?; - let image_size = image_size.to_f64(); + let image_size = snapshot.size().to_f64(); let dw = dw.unwrap_or(image_size.width); let dh = dh.unwrap_or(image_size.height); @@ -614,8 +626,7 @@ impl CanvasState { let smoothing_enabled = self.state.borrow().image_smoothing_enabled; self.send_canvas_2d_msg(Canvas2dMsg::DrawImage( - image_data, - image_size, + snapshot.as_ipc(), dest_rect, source_rect, smoothing_enabled, @@ -929,7 +940,7 @@ impl CanvasState { mut repetition: DOMString, can_gc: CanGc, ) -> Fallible<Option<DomRoot<CanvasPattern>>> { - let (image_data, image_size) = match image { + let snapshot = match image { CanvasImageSource::HTMLImageElement(ref image) => { // https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument if !image.is_usable()? { @@ -941,27 +952,17 @@ impl CanvasState { .and_then(|url| { self.fetch_image_data(url, cors_setting_for_element(image.upcast())) }) - .map(|data| (data.0.to_vec(), data.1)) .ok_or(Error::InvalidState)? }, CanvasImageSource::HTMLCanvasElement(ref canvas) => { - let (data, size) = canvas.fetch_all_data().ok_or(Error::InvalidState)?; - let data = data - .map(|data| data.to_vec()) - .unwrap_or_else(|| vec![0; size.area() as usize * 4]); - (data, size) + canvas.get_image_data().ok_or(Error::InvalidState)? }, CanvasImageSource::OffscreenCanvas(ref canvas) => { - let (data, size) = canvas.fetch_all_data().ok_or(Error::InvalidState)?; - let data = data - .map(|data| data.to_vec()) - .unwrap_or_else(|| vec![0; size.area() as usize * 4]); - (data, size) + canvas.get_image_data().ok_or(Error::InvalidState)? }, CanvasImageSource::CSSStyleValue(ref value) => value .get_url(self.base_url.clone()) .and_then(|url| self.fetch_image_data(url, None)) - .map(|data| (data.0.to_vec(), data.1)) .ok_or(Error::InvalidState)?, }; @@ -970,10 +971,11 @@ impl CanvasState { } if let Ok(rep) = RepetitionStyle::from_str(&repetition) { + let size = snapshot.size(); Ok(Some(CanvasPattern::new( global, - image_data, - image_size, + snapshot.to_vec(), + size.cast(), rep, self.is_origin_clean(image), can_gc, diff --git a/components/script/dom/bindings/error.rs b/components/script/dom/bindings/error.rs index b0fd301df6a..f5bd03cd8d7 100644 --- a/components/script/dom/bindings/error.rs +++ b/components/script/dom/bindings/error.rs @@ -93,6 +93,7 @@ pub(crate) fn throw_dom_exception( Error::NotReadable => DOMErrorName::NotReadableError, Error::Data => DOMErrorName::DataError, Error::Operation => DOMErrorName::OperationError, + Error::NotAllowed => DOMErrorName::NotAllowedError, Error::Type(message) => unsafe { assert!(!JS_IsExceptionPending(*cx)); throw_type_error(*cx, &message); diff --git a/components/script/dom/bindings/serializable.rs b/components/script/dom/bindings/serializable.rs index 4aa1a94a0a4..d0e851b4799 100644 --- a/components/script/dom/bindings/serializable.rs +++ b/components/script/dom/bindings/serializable.rs @@ -11,7 +11,7 @@ use base::id::{Index, NamespaceIndex, PipelineNamespaceId}; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::structuredclone::StructuredData; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::CanGc; @@ -65,13 +65,7 @@ where /// Returns the field of [StructuredDataReader]/[StructuredDataWriter] that /// should be used to read/store serialized instances of this type. - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<NamespaceIndex<Self::Index>, Self::Data>>; - - /// Returns the field of [StructuredDataReader] that should be used to store - /// deserialized instances of this type. - fn deserialized_storage( - reader: &mut StructuredDataReader, - ) -> &mut Option<HashMap<StorageKey, DomRoot<Self>>>; + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<NamespaceIndex<Self::Index>, Self::Data>>; } diff --git a/components/script/dom/bindings/structuredclone.rs b/components/script/dom/bindings/structuredclone.rs index 915a4951bb5..c9a49ba00c9 100644 --- a/components/script/dom/bindings/structuredclone.rs +++ b/components/script/dom/bindings/structuredclone.rs @@ -16,13 +16,14 @@ use constellation_traits::{ BlobImpl, DomException, DomPoint, MessagePortImpl, Serializable as SerializableInterface, StructuredSerializedData, Transferrable as TransferrableInterface, }; +use js::gc::RootedVec; use js::glue::{ CopyJSStructuredCloneData, DeleteJSAutoStructuredCloneBuffer, GetLengthOfJSStructuredCloneData, NewJSAutoStructuredCloneBuffer, WriteBytesToJSStructuredCloneData, }; use js::jsapi::{ - CloneDataPolicy, HandleObject as RawHandleObject, JS_ClearPendingException, JS_ReadUint32Pair, - JS_STRUCTURED_CLONE_VERSION, JS_WriteUint32Pair, JSContext, JSObject, + CloneDataPolicy, HandleObject as RawHandleObject, Heap, JS_ClearPendingException, + JS_ReadUint32Pair, JS_STRUCTURED_CLONE_VERSION, JS_WriteUint32Pair, JSContext, JSObject, JSStructuredCloneCallbacks, JSStructuredCloneReader, JSStructuredCloneWriter, MutableHandleObject as RawMutableHandleObject, StructuredCloneScope, TransferableOwnership, }; @@ -93,7 +94,7 @@ fn reader_for_type( ) -> unsafe fn( &GlobalScope, *mut JSStructuredCloneReader, - &mut StructuredDataReader, + &mut StructuredDataReader<'_>, CanGc, ) -> *mut JSObject { match val { @@ -107,7 +108,7 @@ fn reader_for_type( unsafe fn read_object<T: Serializable>( owner: &GlobalScope, r: *mut JSStructuredCloneReader, - sc_reader: &mut StructuredDataReader, + sc_reader: &mut StructuredDataReader<'_>, can_gc: CanGc, ) -> *mut JSObject { let mut name_space: u32 = 0; @@ -136,9 +137,8 @@ unsafe fn read_object<T: Serializable>( } if let Ok(obj) = T::deserialize(owner, serialized, can_gc) { - let destination = T::deserialized_storage(sc_reader).get_or_insert_with(HashMap::new); let reflector = obj.reflector().get_jsobject().get(); - destination.insert(storage_key, obj); + sc_reader.roots.push(Heap::boxed(reflector)); return reflector; } warn!("Reading structured data failed in {:?}.", owner.get_url()); @@ -191,7 +191,7 @@ unsafe extern "C" fn read_callback( "tag should be higher than StructuredCloneTags::Min" ); - let sc_reader = &mut *(closure as *mut StructuredDataReader); + let sc_reader = &mut *(closure as *mut StructuredDataReader<'_>); let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx)); let global = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof)); for serializable in SerializableInterface::iter() { @@ -259,7 +259,8 @@ unsafe extern "C" fn write_callback( fn receiver_for_type( val: TransferrableInterface, -) -> fn(&GlobalScope, &mut StructuredDataReader, u64, RawMutableHandleObject) -> Result<(), ()> { +) -> fn(&GlobalScope, &mut StructuredDataReader<'_>, u64, RawMutableHandleObject) -> Result<(), ()> +{ match val { TransferrableInterface::MessagePort => receive_object::<MessagePort>, TransferrableInterface::ReadableStream => receive_object::<ReadableStream>, @@ -269,7 +270,7 @@ fn receiver_for_type( fn receive_object<T: Transferable>( owner: &GlobalScope, - sc_reader: &mut StructuredDataReader, + sc_reader: &mut StructuredDataReader<'_>, extra_data: u64, return_object: RawMutableHandleObject, ) -> Result<(), ()> { @@ -305,13 +306,12 @@ fn receive_object<T: Transferable>( ); }; - if let Ok(received) = T::transfer_receive(owner, id, serialized) { - return_object.set(received.reflector().rootable().get()); - let storage = T::deserialized_storage(sc_reader).get_or_insert_with(Vec::new); - storage.push(received); - return Ok(()); - } - Err(()) + let Ok(received) = T::transfer_receive(owner, id, serialized) else { + return Err(()); + }; + return_object.set(received.reflector().rootable().get()); + sc_reader.roots.push(Heap::boxed(return_object.get())); + Ok(()) } unsafe extern "C" fn read_transfer_callback( @@ -324,7 +324,7 @@ unsafe extern "C" fn read_transfer_callback( closure: *mut raw::c_void, return_object: RawMutableHandleObject, ) -> bool { - let sc_reader = &mut *(closure as *mut StructuredDataReader); + let sc_reader = &mut *(closure as *mut StructuredDataReader<'_>); let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx)); let owner = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof)); @@ -489,8 +489,8 @@ static STRUCTURED_CLONE_CALLBACKS: JSStructuredCloneCallbacks = JSStructuredClon sabCloned: Some(sab_cloned_callback), }; -pub(crate) enum StructuredData<'a> { - Reader(&'a mut StructuredDataReader), +pub(crate) enum StructuredData<'a, 'b> { + Reader(&'a mut StructuredDataReader<'b>), Writer(&'a mut StructuredDataWriter), } @@ -503,19 +503,11 @@ pub(crate) struct DOMErrorRecord { /// Reader and writer structs for results from, and inputs to, structured-data read/write operations. /// <https://html.spec.whatwg.org/multipage/#safe-passing-of-structured-data> #[repr(C)] -pub(crate) struct StructuredDataReader { +pub(crate) struct StructuredDataReader<'a> { /// A struct of error message. - pub(crate) errors: DOMErrorRecord, - /// A map of deserialized blobs, stored temporarily here to keep them rooted. - pub(crate) blobs: Option<HashMap<StorageKey, DomRoot<Blob>>>, - /// A map of deserialized points, stored temporarily here to keep them rooted. - pub(crate) points_read_only: Option<HashMap<StorageKey, DomRoot<DOMPointReadOnly>>>, - pub(crate) dom_points: Option<HashMap<StorageKey, DomRoot<DOMPoint>>>, - /// A map of deserialized exceptions, stored temporarily here to keep them rooted. - pub(crate) dom_exceptions: Option<HashMap<StorageKey, DomRoot<DOMException>>>, - /// A vec of transfer-received DOM ports, - /// to be made available to script through a message event. - pub(crate) message_ports: Option<Vec<DomRoot<MessagePort>>>, + errors: DOMErrorRecord, + /// Rooted copies of every deserialized object to ensure they are not garbage collected. + roots: RootedVec<'a, Box<Heap<*mut JSObject>>>, /// A map of port implementations, /// used as part of the "transfer-receiving" steps of ports, /// to produce the DOM ports stored in `message_ports` above. @@ -528,12 +520,6 @@ pub(crate) struct StructuredDataReader { pub(crate) points: Option<HashMap<DomPointId, DomPoint>>, /// A map of serialized exceptions. pub(crate) exceptions: Option<HashMap<DomExceptionId, DomException>>, - - /// <https://streams.spec.whatwg.org/#rs-transfer> - pub(crate) readable_streams: Option<Vec<DomRoot<ReadableStream>>>, - - /// <https://streams.spec.whatwg.org/#ws-transfer> - pub(crate) writable_streams: Option<Vec<DomRoot<WritableStream>>>, } /// A data holder for transferred and serialized objects. @@ -618,19 +604,14 @@ pub(crate) fn read( ) -> Fallible<Vec<DomRoot<MessagePort>>> { let cx = GlobalScope::get_cx(); let _ac = enter_realm(global); + rooted_vec!(let mut roots); let mut sc_reader = StructuredDataReader { - blobs: None, - message_ports: None, - points_read_only: None, - dom_points: None, - dom_exceptions: None, + roots, port_impls: data.ports.take(), blob_impls: data.blobs.take(), points: data.points.take(), exceptions: data.exceptions.take(), errors: DOMErrorRecord { message: None }, - readable_streams: None, - writable_streams: None, }; let sc_reader_ptr = &mut sc_reader as *mut _; unsafe { @@ -666,8 +647,15 @@ pub(crate) fn read( DeleteJSAutoStructuredCloneBuffer(scbuf); + let mut message_ports = vec![]; + for reflector in sc_reader.roots.iter() { + let Ok(message_port) = root_from_object::<MessagePort>(reflector.get(), *cx) else { + continue; + }; + message_ports.push(message_port); + } // Any transfer-received port-impls should have been taken out. assert!(sc_reader.port_impls.is_none()); - Ok(sc_reader.message_ports.take().unwrap_or_default()) + Ok(message_ports) } } diff --git a/components/script/dom/bindings/transferable.rs b/components/script/dom/bindings/transferable.rs index b720c05ae37..e6b2f000f3a 100644 --- a/components/script/dom/bindings/transferable.rs +++ b/components/script/dom/bindings/transferable.rs @@ -12,7 +12,7 @@ use base::id::NamespaceIndex; use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::structuredclone::StructuredData; use crate::dom::globalscope::GlobalScope; pub(crate) trait Transferable: DomObject where @@ -32,8 +32,7 @@ where serialized: Self::Data, ) -> Result<DomRoot<Self>, ()>; - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<NamespaceIndex<Self::Index>, Self::Data>>; - fn deserialized_storage(reader: &mut StructuredDataReader) -> &mut Option<Vec<DomRoot<Self>>>; + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<NamespaceIndex<Self::Index>, Self::Data>>; } diff --git a/components/script/dom/blob.rs b/components/script/dom/blob.rs index df2afafd698..27aa382c3fc 100644 --- a/components/script/dom/blob.rs +++ b/components/script/dom/blob.rs @@ -24,9 +24,9 @@ use crate::dom::bindings::codegen::UnionTypes::ArrayBufferOrArrayBufferViewOrBlo use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::serializable::{Serializable, StorageKey}; +use crate::dom::bindings::serializable::Serializable; use crate::dom::bindings::str::DOMString; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::structuredclone::StructuredData; use crate::dom::globalscope::GlobalScope; use crate::dom::promise::Promise; use crate::dom::readablestream::ReadableStream; @@ -119,18 +119,14 @@ impl Serializable for Blob { Ok(deserialized_blob) } - fn serialized_storage(reader: StructuredData<'_>) -> &mut Option<HashMap<BlobId, Self::Data>> { + fn serialized_storage<'a>( + reader: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<BlobId, Self::Data>> { match reader { StructuredData::Reader(r) => &mut r.blob_impls, StructuredData::Writer(w) => &mut w.blobs, } } - - fn deserialized_storage( - data: &mut StructuredDataReader, - ) -> &mut Option<HashMap<StorageKey, DomRoot<Self>>> { - &mut data.blobs - } } /// Extract bytes from BlobParts, used by Blob and File constructor diff --git a/components/script/dom/canvasrenderingcontext2d.rs b/components/script/dom/canvasrenderingcontext2d.rs index f6bf432de69..73052e6906e 100644 --- a/components/script/dom/canvasrenderingcontext2d.rs +++ b/components/script/dom/canvasrenderingcontext2d.rs @@ -5,11 +5,11 @@ use canvas_traits::canvas::{Canvas2dMsg, CanvasId, CanvasMsg, FromScriptMsg}; use dom_struct::dom_struct; use euclid::default::{Point2D, Rect, Size2D}; -use ipc_channel::ipc::IpcSharedMemory; use profile_traits::ipc; use script_bindings::inheritance::Castable; use script_layout_interface::HTMLCanvasDataSource; use servo_url::ServoUrl; +use snapshot::Snapshot; use crate::canvas_context::{CanvasContext, CanvasHelpers, LayoutCanvasRenderingContextHelpers}; use crate::canvas_state::CanvasState; @@ -142,16 +142,18 @@ impl CanvasContext for CanvasRenderingContext2D { self.set_bitmap_dimensions(self.size().cast()) } - fn get_image_data_as_shared_memory(&self) -> Option<IpcSharedMemory> { + fn get_image_data(&self) -> Option<Snapshot> { + let size = self.size(); + + if size.is_empty() { + return None; + } + let (sender, receiver) = ipc::channel(self.global().time_profiler_chan().clone()).unwrap(); let msg = CanvasMsg::FromScript(FromScriptMsg::SendPixels(sender), self.get_canvas_id()); self.canvas_state.get_ipc_renderer().send(msg).unwrap(); - Some(receiver.recv().unwrap()) - } - - fn get_image_data(&self) -> Option<Vec<u8>> { - Some(self.get_rect(Rect::from_size(self.size().cast()))) + Some(receiver.recv().unwrap().to_owned()) } fn origin_is_clean(&self) -> bool { diff --git a/components/script/dom/cssstylesheet.rs b/components/script/dom/cssstylesheet.rs index 421e8f45523..a367c9943de 100644 --- a/components/script/dom/cssstylesheet.rs +++ b/components/script/dom/cssstylesheet.rs @@ -24,7 +24,7 @@ use crate::dom::bindings::reflector::{ DomGlobal, reflect_dom_object, reflect_dom_object_with_proto, }; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; -use crate::dom::bindings::str::DOMString; +use crate::dom::bindings::str::{DOMString, USVString}; use crate::dom::cssrulelist::{CSSRuleList, RulesSource}; use crate::dom::element::Element; use crate::dom::medialist::MediaList; @@ -290,4 +290,32 @@ impl CSSStyleSheetMethods<crate::DomTypeHolder> for CSSStyleSheet { // > 8. Return -1. Ok(-1) } + + /// <https://drafts.csswg.org/cssom/#synchronously-replace-the-rules-of-a-cssstylesheet> + fn ReplaceSync(&self, text: USVString) -> Result<(), Error> { + // Step 1. If the constructed flag is not set throw a NotAllowedError + if !self.is_constructed { + return Err(Error::NotAllowed); + } + + // Step 2. Let rules be the result of running parse a stylesheet’s contents from text. + let global = self.global(); + let window = global.as_window(); + + StyleStyleSheet::update_from_str( + &self.style_stylesheet, + &text, + UrlExtraData(window.get_url().get_arc()), + None, + window.css_error_reporter(), + AllowImportRules::No, // Step 3.If rules contains one or more @import rules, remove those rules from rules. + ); + + // Step 4. Set sheet’s CSS rules to rules. + // We reset our rule list, which will be initialized properly + // at the next getter access. + self.rulelist.set(None); + + Ok(()) + } } diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 852a12fc7c5..02bdd343d89 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -21,9 +21,7 @@ use base::id::WebViewId; use canvas_traits::canvas::CanvasId; use canvas_traits::webgl::{self, WebGLContextId, WebGLMsg}; use chrono::Local; -use constellation_traits::{ - AnimationTickType, NavigationHistoryBehavior, ScriptToConstellationMessage, -}; +use constellation_traits::{NavigationHistoryBehavior, ScriptToConstellationMessage}; use content_security_policy::{self as csp, CspList, PolicyDisposition}; use cookie::Cookie; use cssparser::match_ignore_ascii_case; @@ -516,10 +514,6 @@ pub(crate) struct Document { pending_input_events: DomRefCell<Vec<ConstellationInputEvent>>, /// The index of the last mouse move event in the pending compositor events queue. mouse_move_event_index: DomRefCell<Option<usize>>, - /// Pending animation ticks, to be handled at the next rendering opportunity. - #[no_trace] - #[ignore_malloc_size_of = "AnimationTickType contains data from an outside crate"] - pending_animation_ticks: DomRefCell<AnimationTickType>, /// <https://drafts.csswg.org/resize-observer/#dom-document-resizeobservers-slot> /// /// Note: we are storing, but never removing, resize observers. @@ -2397,10 +2391,6 @@ impl Document { pub(crate) fn run_the_animation_frame_callbacks(&self, can_gc: CanGc) { let _realm = enter_realm(self); - self.pending_animation_ticks - .borrow_mut() - .remove(AnimationTickType::REQUEST_ANIMATION_FRAME); - self.running_animation_callbacks.set(true); let was_faking_animation_frames = self.is_faking_animation_frames(); let timing = self.global().performance().Now(); @@ -3916,7 +3906,6 @@ impl Document { image_animation_manager: DomRefCell::new(ImageAnimationManager::new()), dirty_root: Default::default(), declarative_refresh: Default::default(), - pending_animation_ticks: Default::default(), pending_input_events: Default::default(), mouse_move_event_index: Default::default(), resize_observers: Default::default(), @@ -4689,18 +4678,6 @@ impl Document { .collect() } - /// Note a pending animation tick, to be processed at the next `update_the_rendering` task. - pub(crate) fn note_pending_animation_tick(&self, tick_type: AnimationTickType) { - self.pending_animation_ticks.borrow_mut().extend(tick_type); - } - - /// Whether this document has received an animation tick for rafs. - pub(crate) fn has_received_raf_tick(&self) -> bool { - self.pending_animation_ticks - .borrow() - .contains(AnimationTickType::REQUEST_ANIMATION_FRAME) - } - pub(crate) fn advance_animation_timeline_for_testing(&self, delta: f64) { self.animation_timeline.borrow_mut().advance_specific(delta); let current_timeline_value = self.current_animation_timeline_value(); @@ -6437,10 +6414,7 @@ impl FakeRequestAnimationFrameCallback { pub(crate) fn invoke(self, can_gc: CanGc) { // TODO: Once there is a more generic mechanism to trigger `update_the_rendering` when // not driven by the compositor, it should be used here. - self.document - .root() - .note_pending_animation_tick(AnimationTickType::REQUEST_ANIMATION_FRAME); - with_script_thread(|script_thread| script_thread.update_the_rendering(false, can_gc)) + with_script_thread(|script_thread| script_thread.update_the_rendering(true, can_gc)) } } diff --git a/components/script/dom/domexception.rs b/components/script/dom/domexception.rs index e63c3ff1f53..fbb807b7a95 100644 --- a/components/script/dom/domexception.rs +++ b/components/script/dom/domexception.rs @@ -17,9 +17,9 @@ use crate::dom::bindings::reflector::{ Reflector, reflect_dom_object, reflect_dom_object_with_proto, }; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::serializable::{Serializable, StorageKey}; +use crate::dom::bindings::serializable::Serializable; use crate::dom::bindings::str::DOMString; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::structuredclone::StructuredData; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::CanGc; @@ -53,6 +53,7 @@ pub(crate) enum DOMErrorName { NotReadableError, DataError, OperationError, + NotAllowedError, } impl DOMErrorName { @@ -84,6 +85,7 @@ impl DOMErrorName { "NotReadableError" => Some(DOMErrorName::NotReadableError), "DataError" => Some(DOMErrorName::DataError), "OperationError" => Some(DOMErrorName::OperationError), + "NotAllowedError" => Some(DOMErrorName::NotAllowedError), _ => None, } } @@ -135,6 +137,10 @@ impl DOMException { DOMErrorName::OperationError => { "The operation failed for an operation-specific reason." }, + DOMErrorName::NotAllowedError => { + r#"The request is not allowed by the user agent or the platform in the current context, + possibly because the user denied permission."# + }, }; ( @@ -252,18 +258,12 @@ impl Serializable for DOMException { )) } - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<DomExceptionId, Self::Data>> { + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<DomExceptionId, Self::Data>> { match data { StructuredData::Reader(reader) => &mut reader.exceptions, StructuredData::Writer(writer) => &mut writer.exceptions, } } - - fn deserialized_storage( - reader: &mut StructuredDataReader, - ) -> &mut Option<HashMap<StorageKey, DomRoot<Self>>> { - &mut reader.dom_exceptions - } } diff --git a/components/script/dom/dompoint.rs b/components/script/dom/dompoint.rs index 006abb50def..5e848c43c47 100644 --- a/components/script/dom/dompoint.rs +++ b/components/script/dom/dompoint.rs @@ -14,8 +14,8 @@ use crate::dom::bindings::codegen::Bindings::DOMPointReadOnlyBinding::DOMPointRe use crate::dom::bindings::error::Fallible; use crate::dom::bindings::reflector::reflect_dom_object_with_proto; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::serializable::{Serializable, StorageKey}; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::serializable::Serializable; +use crate::dom::bindings::structuredclone::StructuredData; use crate::dom::dompointreadonly::{DOMPointReadOnly, DOMPointWriteMethods}; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::CanGc; @@ -163,18 +163,12 @@ impl Serializable for DOMPoint { )) } - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<DomPointId, Self::Data>> { + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<DomPointId, Self::Data>> { match data { StructuredData::Reader(reader) => &mut reader.points, StructuredData::Writer(writer) => &mut writer.points, } } - - fn deserialized_storage( - reader: &mut StructuredDataReader, - ) -> &mut Option<HashMap<StorageKey, DomRoot<Self>>> { - &mut reader.dom_points - } } diff --git a/components/script/dom/dompointreadonly.rs b/components/script/dom/dompointreadonly.rs index 44e4a70b680..eb6bc9b93a9 100644 --- a/components/script/dom/dompointreadonly.rs +++ b/components/script/dom/dompointreadonly.rs @@ -15,8 +15,8 @@ use crate::dom::bindings::codegen::Bindings::DOMPointReadOnlyBinding::DOMPointRe use crate::dom::bindings::error::Fallible; use crate::dom::bindings::reflector::{Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::serializable::{Serializable, StorageKey}; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::serializable::Serializable; +use crate::dom::bindings::structuredclone::StructuredData; use crate::dom::globalscope::GlobalScope; use crate::script_runtime::CanGc; @@ -172,18 +172,12 @@ impl Serializable for DOMPointReadOnly { )) } - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<DomPointId, Self::Data>> { + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<DomPointId, Self::Data>> { match data { StructuredData::Reader(r) => &mut r.points, StructuredData::Writer(w) => &mut w.points, } } - - fn deserialized_storage( - reader: &mut StructuredDataReader, - ) -> &mut Option<HashMap<StorageKey, DomRoot<Self>>> { - &mut reader.points_read_only - } } diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs index 2658911c795..b3345b90fc0 100644 --- a/components/script/dom/globalscope.rs +++ b/components/script/dom/globalscope.rs @@ -2422,7 +2422,8 @@ impl GlobalScope { headers: &Option<Serde<HeaderMap>>, ) -> Option<CspList> { // TODO: Implement step 1 (local scheme special case) - let mut csp = headers.as_ref()?.get_all("content-security-policy").iter(); + let headers = headers.as_ref()?; + let mut csp = headers.get_all("content-security-policy").iter(); // This silently ignores the CSP if it contains invalid Unicode. // We should probably report an error somewhere. let c = csp.next().and_then(|c| c.to_str().ok())?; @@ -2435,6 +2436,19 @@ impl GlobalScope { PolicyDisposition::Enforce, )); } + let csp_report = headers + .get_all("content-security-policy-report-only") + .iter(); + // This silently ignores the CSP if it contains invalid Unicode. + // We should probably report an error somewhere. + for c in csp_report { + let c = c.to_str().ok()?; + csp_list.append(CspList::parse( + c, + PolicySource::Header, + PolicyDisposition::Report, + )); + } Some(csp_list) } @@ -2822,36 +2836,16 @@ impl GlobalScope { })) } - #[allow(unsafe_code)] - pub(crate) fn is_js_evaluation_allowed(&self, cx: SafeJSContext) -> bool { + pub(crate) fn is_js_evaluation_allowed(&self, source: &str) -> bool { let Some(csp_list) = self.get_csp_list() else { return true; }; - let scripted_caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default(); - let is_js_evaluation_allowed = csp_list.is_js_evaluation_allowed() == CheckResult::Allowed; - - if !is_js_evaluation_allowed { - // FIXME: Don't fire event if `script-src` and `default-src` - // were not passed. - for policy in csp_list.0 { - let report = CSPViolationReportBuilder::default() - .resource("eval".to_owned()) - .effective_directive("script-src".to_owned()) - .report_only(policy.disposition == PolicyDisposition::Report) - .source_file(scripted_caller.filename.clone()) - .line_number(scripted_caller.line) - .column_number(scripted_caller.col) - .build(self); - let task = CSPViolationReportTask::new(self, report); + let (is_js_evaluation_allowed, violations) = csp_list.is_js_evaluation_allowed(source); - self.task_manager() - .dom_manipulation_task_source() - .queue(task); - } - } + self.report_csp_violations(violations); - is_js_evaluation_allowed + is_js_evaluation_allowed == CheckResult::Allowed } pub(crate) fn create_image_bitmap( @@ -2880,15 +2874,11 @@ impl GlobalScope { return p; } - if let Some((data, size)) = canvas.fetch_all_data() { - let data = data - .map(|data| data.to_vec()) - .unwrap_or_else(|| vec![0; size.area() as usize * 4]); - + if let Some(snapshot) = canvas.get_image_data() { + let size = snapshot.size().cast(); let image_bitmap = ImageBitmap::new(self, size.width, size.height, can_gc).unwrap(); - - image_bitmap.set_bitmap_data(data); + image_bitmap.set_bitmap_data(snapshot.to_vec()); image_bitmap.set_origin_clean(canvas.origin_is_clean()); p.resolve_native(&(image_bitmap), can_gc); } @@ -2901,14 +2891,11 @@ impl GlobalScope { return p; } - if let Some((data, size)) = canvas.fetch_all_data() { - let data = data - .map(|data| data.to_vec()) - .unwrap_or_else(|| vec![0; size.area() as usize * 4]); - + if let Some(snapshot) = canvas.get_image_data() { + let size = snapshot.size().cast(); let image_bitmap = ImageBitmap::new(self, size.width, size.height, can_gc).unwrap(); - image_bitmap.set_bitmap_data(data); + image_bitmap.set_bitmap_data(snapshot.to_vec()); image_bitmap.set_origin_clean(canvas.origin_is_clean()); p.resolve_native(&(image_bitmap), can_gc); } @@ -3471,10 +3458,13 @@ impl GlobalScope { unreachable!(); } + #[allow(unsafe_code)] pub(crate) fn report_csp_violations(&self, violations: Vec<Violation>) { + let scripted_caller = + unsafe { describe_scripted_caller(*GlobalScope::get_cx()) }.unwrap_or_default(); for violation in violations { let (sample, resource) = match violation.resource { - ViolationResource::Inline { .. } => (None, "inline".to_owned()), + ViolationResource::Inline { sample } => (sample, "inline".to_owned()), ViolationResource::Url(url) => (None, url.into()), ViolationResource::TrustedTypePolicy { sample } => { (Some(sample), "trusted-types-policy".to_owned()) @@ -3482,6 +3472,8 @@ impl GlobalScope { ViolationResource::TrustedTypeSink { sample } => { (Some(sample), "trusted-types-sink".to_owned()) }, + ViolationResource::Eval { sample } => (sample, "eval".to_owned()), + ViolationResource::WasmEval => (None, "wasm-eval".to_owned()), }; let report = CSPViolationReportBuilder::default() .resource(resource) @@ -3489,6 +3481,9 @@ impl GlobalScope { .effective_directive(violation.directive.name) .original_policy(violation.policy.to_string()) .report_only(violation.policy.disposition == PolicyDisposition::Report) + .source_file(scripted_caller.filename.clone()) + .line_number(scripted_caller.line) + .column_number(scripted_caller.col + 1) .build(self); let task = CSPViolationReportTask::new(self, report); self.task_manager() diff --git a/components/script/dom/htmlcanvaselement.rs b/components/script/dom/htmlcanvaselement.rs index 9e20539ceca..bb27d28cea8 100644 --- a/components/script/dom/htmlcanvaselement.rs +++ b/components/script/dom/htmlcanvaselement.rs @@ -17,7 +17,6 @@ use image::codecs::jpeg::JpegEncoder; use image::codecs::png::PngEncoder; use image::codecs::webp::WebPEncoder; use image::{ColorType, ImageEncoder}; -use ipc_channel::ipc::IpcSharedMemory; #[cfg(feature = "webgpu")] use ipc_channel::ipc::{self as ipcchan}; use js::error::throw_type_error; @@ -25,6 +24,7 @@ use js::rust::{HandleObject, HandleValue}; use script_layout_interface::{HTMLCanvasData, HTMLCanvasDataSource}; use servo_media::streams::MediaStreamType; use servo_media::streams::registry::MediaStreamId; +use snapshot::Snapshot; use style::attr::AttrValue; use crate::canvas_context::CanvasContext as _; @@ -69,6 +69,7 @@ use crate::script_runtime::{CanGc, JSContext}; const DEFAULT_WIDTH: u32 = 300; const DEFAULT_HEIGHT: u32 = 150; +#[derive(PartialEq)] enum EncodedImageType { Png, Jpeg, @@ -375,42 +376,21 @@ impl HTMLCanvasElement { self.Height() != 0 && self.Width() != 0 } - pub(crate) fn fetch_all_data(&self) -> Option<(Option<IpcSharedMemory>, Size2D<u32>)> { - let size = self.get_size(); - - if size.width == 0 || size.height == 0 { - return None; - } - - let data = match self.context.borrow().as_ref() { - Some(CanvasContext::Context2d(context)) => context.get_image_data_as_shared_memory(), - Some(CanvasContext::WebGL(_context)) => { - // TODO: add a method in WebGLRenderingContext to get the pixels. - return None; - }, - Some(CanvasContext::WebGL2(_context)) => { - // TODO: add a method in WebGL2RenderingContext to get the pixels. - return None; - }, - #[cfg(feature = "webgpu")] - Some(CanvasContext::WebGPU(context)) => context.get_image_data_as_shared_memory(), - Some(CanvasContext::Placeholder(context)) => return context.fetch_all_data(), - None => None, - }; - - Some((data, size)) - } - - fn get_content(&self) -> Option<Vec<u8>> { - match *self.context.borrow() { - Some(CanvasContext::Context2d(ref context)) => context.get_image_data(), - Some(CanvasContext::WebGL(ref context)) => context.get_image_data(), - Some(CanvasContext::WebGL2(ref context)) => context.get_image_data(), + pub(crate) fn get_image_data(&self) -> Option<Snapshot> { + match self.context.borrow().as_ref() { + Some(CanvasContext::Context2d(context)) => context.get_image_data(), + Some(CanvasContext::WebGL(context)) => context.get_image_data(), + Some(CanvasContext::WebGL2(context)) => context.get_image_data(), #[cfg(feature = "webgpu")] - Some(CanvasContext::WebGPU(ref context)) => context.get_image_data(), - Some(CanvasContext::Placeholder(_)) | None => { - // Each pixel is fully-transparent black. - Some(vec![0; (self.Width() * self.Height() * 4) as usize]) + Some(CanvasContext::WebGPU(context)) => context.get_image_data(), + Some(CanvasContext::Placeholder(context)) => context.get_image_data(), + None => { + let size = self.get_size(); + if size.width == 0 || size.height == 0 { + None + } else { + Some(Snapshot::cleared(size.cast())) + } }, } } @@ -427,15 +407,23 @@ impl HTMLCanvasElement { &self, image_type: &EncodedImageType, quality: Option<f64>, - bytes: &[u8], + snapshot: &Snapshot, encoder: &mut W, ) { + // We can't use self.Width() or self.Height() here, since the size of the canvas + // may have changed since the snapshot was created. Truncating the dimensions to a + // u32 can't panic, since the data comes from a canvas which is always smaller than + // u32::MAX. + let canvas_data = snapshot.data(); + let width = snapshot.size().width as u32; + let height = snapshot.size().height as u32; + match image_type { EncodedImageType::Png => { // FIXME(nox): https://github.com/image-rs/image-png/issues/86 // FIXME(nox): https://github.com/image-rs/image-png/issues/87 PngEncoder::new(encoder) - .write_image(bytes, self.Width(), self.Height(), ColorType::Rgba8) + .write_image(canvas_data, width, height, ColorType::Rgba8) .unwrap(); }, EncodedImageType::Jpeg => { @@ -455,14 +443,14 @@ impl HTMLCanvasElement { }; jpeg_encoder - .write_image(bytes, self.Width(), self.Height(), ColorType::Rgba8) + .write_image(canvas_data, width, height, ColorType::Rgba8) .unwrap(); }, EncodedImageType::Webp => { // No quality support because of https://github.com/image-rs/image/issues/1984 WebPEncoder::new_lossless(encoder) - .write_image(bytes, self.Width(), self.Height(), ColorType::Rgba8) + .write_image(canvas_data, width, height, ColorType::Rgba8) .unwrap(); }, } @@ -560,11 +548,23 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { } // Step 3. - let Some(file) = self.get_content() else { + let Some(mut snapshot) = self.get_image_data() else { return Ok(USVString("data:,".into())); }; let image_type = EncodedImageType::from(mime_type); + snapshot.transform( + if image_type == EncodedImageType::Jpeg { + snapshot::AlphaMode::AsOpaque { + premultiplied: true, + } + } else { + snapshot::AlphaMode::Transparent { + premultiplied: false, + } + }, + snapshot::PixelFormat::RGBA, + ); let mut url = format!("data:{};base64,", image_type.as_mime_type()); let mut encoder = base64::write::EncoderStringWriter::from_consumer( @@ -575,7 +575,7 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { self.encode_for_mime_type( &image_type, Self::maybe_quality(quality), - &file, + &snapshot, &mut encoder, ); encoder.into_inner(); @@ -597,14 +597,14 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { return Err(Error::Security); } - // Step 2. and 3. - // If this canvas element's bitmap has pixels (i.e., neither its horizontal dimension + // Step 2. Let result be null. + // Step 3. If this canvas element's bitmap has pixels (i.e., neither its horizontal dimension // nor its vertical dimension is zero), // then set result to a copy of this canvas element's bitmap. let result = if self.Width() == 0 || self.Height() == 0 { None } else { - self.get_content() + self.get_image_data() }; let this = Trusted::new(self); @@ -625,18 +625,22 @@ impl HTMLCanvasElementMethods<crate::DomTypeHolder> for HTMLCanvasElement { return error!("Expected blob callback, but found none!"); }; - if let Some(bytes) = result { + if let Some(mut snapshot) = result { + snapshot.transform( + snapshot::AlphaMode::Transparent{ premultiplied: false }, + snapshot::PixelFormat::RGBA + ); // Step 4.1 // If result is non-null, then set result to a serialization of result as a file with // type and quality if given. let mut encoded: Vec<u8> = vec![]; - this.encode_for_mime_type(&image_type, quality, &bytes, &mut encoded); + this.encode_for_mime_type(&image_type, quality, &snapshot, &mut encoded); let blob_impl = BlobImpl::new_from_bytes(encoded, image_type.as_mime_type()); - // Step 4.2.1 & 4.2.2 - // Set result to a new Blob object, created in the relevant realm of this canvas element - // Invoke callback with « result » and "report". + // Step 4.2.1 Set result to a new Blob object, created in the relevant realm of this canvas element let blob = Blob::new(&this.global(), blob_impl, CanGc::note()); + + // Step 4.2.2 Invoke callback with « result » and "report". let _ = callback.Call__(Some(&blob), ExceptionHandling::Report, CanGc::note()); } else { let _ = callback.Call__(None, ExceptionHandling::Report, CanGc::note()); diff --git a/components/script/dom/messageport.rs b/components/script/dom/messageport.rs index fe590052db4..85d94c1aa7a 100644 --- a/components/script/dom/messageport.rs +++ b/components/script/dom/messageport.rs @@ -23,7 +23,7 @@ use crate::dom::bindings::error::{Error, ErrorResult, ErrorToJsval}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object}; use crate::dom::bindings::root::DomRoot; -use crate::dom::bindings::structuredclone::{self, StructuredData, StructuredDataReader}; +use crate::dom::bindings::structuredclone::{self, StructuredData}; use crate::dom::bindings::trace::RootedTraceableBox; use crate::dom::bindings::transferable::Transferable; use crate::dom::bindings::utils::set_dictionary_property; @@ -268,18 +268,14 @@ impl Transferable for MessagePort { Ok(transferred_port) } - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<MessagePortId, Self::Data>> { + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<MessagePortId, Self::Data>> { match data { StructuredData::Reader(r) => &mut r.port_impls, StructuredData::Writer(w) => &mut w.ports, } } - - fn deserialized_storage(reader: &mut StructuredDataReader) -> &mut Option<Vec<DomRoot<Self>>> { - &mut reader.message_ports - } } impl MessagePortMethods<crate::DomTypeHolder> for MessagePort { diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 2a01370085a..45a107ae673 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -167,7 +167,6 @@ pub struct Node { /// Layout data for this node. This is populated during layout and can /// be used for incremental relayout and script queries. - #[ignore_malloc_size_of = "trait object"] #[no_trace] layout_data: DomRefCell<Option<Box<GenericLayoutData>>>, } diff --git a/components/script/dom/offscreencanvas.rs b/components/script/dom/offscreencanvas.rs index 0587fbad12b..aabe5955e12 100644 --- a/components/script/dom/offscreencanvas.rs +++ b/components/script/dom/offscreencanvas.rs @@ -6,8 +6,8 @@ use std::cell::Cell; use dom_struct::dom_struct; use euclid::default::Size2D; -use ipc_channel::ipc::IpcSharedMemory; use js::rust::{HandleObject, HandleValue}; +use snapshot::Snapshot; use crate::dom::bindings::cell::{DomRefCell, Ref, ref_filter_map}; use crate::dom::bindings::codegen::Bindings::OffscreenCanvasBinding::{ @@ -88,21 +88,18 @@ impl OffscreenCanvas { ref_filter_map(self.context.borrow(), |ctx| ctx.as_ref()) } - pub(crate) fn fetch_all_data(&self) -> Option<(Option<IpcSharedMemory>, Size2D<u32>)> { - let size = self.get_size(); - - if size.width == 0 || size.height == 0 { - return None; - } - - let data = match self.context.borrow().as_ref() { - Some(OffscreenCanvasContext::OffscreenContext2d(context)) => { - context.get_image_data_as_shared_memory() + pub(crate) fn get_image_data(&self) -> Option<Snapshot> { + match self.context.borrow().as_ref() { + Some(OffscreenCanvasContext::OffscreenContext2d(context)) => context.get_image_data(), + None => { + let size = self.get_size(); + if size.width == 0 || size.height == 0 { + None + } else { + Some(Snapshot::cleared(size)) + } }, - None => None, - }; - - Some((data, size.to_u32())) + } } pub(crate) fn get_or_init_2d_context( diff --git a/components/script/dom/offscreencanvasrenderingcontext2d.rs b/components/script/dom/offscreencanvasrenderingcontext2d.rs index 69a1d41af2e..2f9b52640e6 100644 --- a/components/script/dom/offscreencanvasrenderingcontext2d.rs +++ b/components/script/dom/offscreencanvasrenderingcontext2d.rs @@ -8,7 +8,7 @@ use crate::dom::bindings::codegen::UnionTypes::HTMLCanvasElementOrOffscreenCanva use canvas_traits::canvas::Canvas2dMsg; use dom_struct::dom_struct; use euclid::default::Size2D; -use ipc_channel::ipc::IpcSharedMemory; +use snapshot::Snapshot; use crate::dom::bindings::codegen::Bindings::CanvasRenderingContext2DBinding::{ CanvasDirection, CanvasFillRule, CanvasImageSource, CanvasLineCap, CanvasLineJoin, @@ -76,8 +76,8 @@ impl OffscreenCanvasRenderingContext2D { self.context.origin_is_clean() } - pub(crate) fn get_image_data_as_shared_memory(&self) -> Option<IpcSharedMemory> { - self.context.get_image_data_as_shared_memory() + pub(crate) fn get_image_data(&self) -> Option<Snapshot> { + self.context.get_image_data() } } diff --git a/components/script/dom/readablestream.rs b/components/script/dom/readablestream.rs index b445fb4b9fc..37899f18fec 100644 --- a/components/script/dom/readablestream.rs +++ b/components/script/dom/readablestream.rs @@ -59,7 +59,7 @@ use crate::realms::{enter_realm, InRealm}; use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler}; use crate::dom::bindings::transferable::Transferable; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::structuredclone::StructuredData; use super::bindings::buffer_source::HeapBufferSource; use super::bindings::codegen::Bindings::ReadableStreamBYOBReaderBinding::ReadableStreamBYOBReaderReadOptions; @@ -2247,16 +2247,12 @@ impl Transferable for ReadableStream { } /// Note: we are relying on the port transfer, so the data returned here are related to the port. - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<MessagePortId, Self::Data>> { + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<MessagePortId, Self::Data>> { match data { StructuredData::Reader(r) => &mut r.port_impls, StructuredData::Writer(w) => &mut w.ports, } } - - fn deserialized_storage(reader: &mut StructuredDataReader) -> &mut Option<Vec<DomRoot<Self>>> { - &mut reader.readable_streams - } } diff --git a/components/script/dom/response.rs b/components/script/dom/response.rs index 283b7d615aa..cbdfbe94603 100644 --- a/components/script/dom/response.rs +++ b/components/script/dom/response.rs @@ -8,7 +8,7 @@ use std::str::FromStr; use dom_struct::dom_struct; use http::header::HeaderMap as HyperHeaders; use hyper_serde::Serde; -use js::rust::HandleObject; +use js::rust::{HandleObject, HandleValue}; use net_traits::http_status::HttpStatus; use servo_url::ServoUrl; use url::Position; @@ -24,13 +24,13 @@ use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; -use crate::dom::bindings::str::{ByteString, USVString}; +use crate::dom::bindings::str::{ByteString, USVString, serialize_jsval_to_json_utf8}; use crate::dom::globalscope::GlobalScope; use crate::dom::headers::{Guard, Headers, is_obs_text, is_vchar}; use crate::dom::promise::Promise; use crate::dom::readablestream::ReadableStream; use crate::dom::underlyingsourcecontainer::UnderlyingSourceType; -use crate::script_runtime::{CanGc, StreamConsumer}; +use crate::script_runtime::{CanGc, JSContext, StreamConsumer}; #[dom_struct] pub(crate) struct Response { @@ -72,7 +72,7 @@ impl Response { } } - // https://fetch.spec.whatwg.org/#dom-response + /// <https://fetch.spec.whatwg.org/#dom-response> pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Response> { Self::new_with_proto(global, None, can_gc) } @@ -142,92 +142,43 @@ fn is_null_body_status(status: u16) -> bool { } impl ResponseMethods<crate::DomTypeHolder> for Response { - // https://fetch.spec.whatwg.org/#initialize-a-response + /// <https://fetch.spec.whatwg.org/#dom-response> fn Constructor( global: &GlobalScope, proto: Option<HandleObject>, can_gc: CanGc, - body: Option<BodyInit>, + body_init: Option<BodyInit>, init: &ResponseBinding::ResponseInit, ) -> Fallible<DomRoot<Response>> { - // Step 1 - if init.status < 200 || init.status > 599 { - return Err(Error::Range(format!( - "init's status member should be in the range 200 to 599, inclusive, but is {}", - init.status - ))); - } - - // Step 2 - if !is_valid_status_text(&init.statusText) { - return Err(Error::Type( - "init's statusText member does not match the reason-phrase token production" - .to_string(), - )); - } - - let r = Response::new_with_proto(global, proto, can_gc); - - // Step 3 & 4 - *r.status.borrow_mut() = HttpStatus::new_raw(init.status, init.statusText.clone().into()); - - // Step 5 - if let Some(ref headers_member) = init.headers { - r.Headers(can_gc).fill(Some(headers_member.clone()))?; - } - - // Step 6 - if let Some(ref body) = body { - // Step 6.1 - if is_null_body_status(init.status) { - return Err(Error::Type( - "Body is non-null but init's status member is a null body status".to_string(), - )); - }; - - // Step 6.2 - let ExtractedBody { - stream, - total_bytes: _, - content_type, - source: _, - } = body.extract(global, can_gc)?; - - r.body_stream.set(Some(&*stream)); - - // Step 6.3 - if let Some(content_type_contents) = content_type { - if !r - .Headers(can_gc) - .Has(ByteString::new(b"Content-Type".to_vec())) - .unwrap() - { - r.Headers(can_gc).Append( - ByteString::new(b"Content-Type".to_vec()), - ByteString::new(content_type_contents.as_bytes().to_vec()), - )?; - } - }; - } else { - // Reset FetchResponse to an in-memory stream with empty byte sequence here for - // no-init-body case - let stream = ReadableStream::new_from_bytes(global, Vec::with_capacity(0), can_gc)?; - r.body_stream.set(Some(&*stream)); - } + // 1. Set this’s response to a new response. + // Our Response/Body types don't actually hold onto an internal fetch Response. + let response = Response::new_with_proto(global, proto, can_gc); + + // 2. Set this’s headers to a new Headers object with this’s relevant realm, + // whose header list is this’s response’s header list and guard is "response". + response.Headers(can_gc).set_guard(Guard::Response); + + // 3. Let bodyWithType be null. + // 4. If body is non-null, then set bodyWithType to the result of extracting body. + let body_with_type = match body_init { + Some(body) => Some(body.extract(global, can_gc)?), + None => None, + }; - Ok(r) + // 5. Perform *initialize a response* given this, init, and bodyWithType. + initialize_response(global, can_gc, body_with_type, init, response) } - // https://fetch.spec.whatwg.org/#dom-response-error + /// <https://fetch.spec.whatwg.org/#dom-response-error> fn Error(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Response> { - let r = Response::new(global, can_gc); - *r.response_type.borrow_mut() = DOMResponseType::Error; - r.Headers(can_gc).set_guard(Guard::Immutable); - *r.status.borrow_mut() = HttpStatus::new_error(); - r + let response = Response::new(global, can_gc); + *response.response_type.borrow_mut() = DOMResponseType::Error; + response.Headers(can_gc).set_guard(Guard::Immutable); + *response.status.borrow_mut() = HttpStatus::new_error(); + response } - // https://fetch.spec.whatwg.org/#dom-response-redirect + /// <https://fetch.spec.whatwg.org/#dom-response-redirect> fn Redirect( global: &GlobalScope, url: USVString, @@ -251,31 +202,60 @@ impl ResponseMethods<crate::DomTypeHolder> for Response { // Step 4 // see Step 4 continued - let r = Response::new(global, can_gc); + let response = Response::new(global, can_gc); // Step 5 - *r.status.borrow_mut() = HttpStatus::new_raw(status, vec![]); + *response.status.borrow_mut() = HttpStatus::new_raw(status, vec![]); // Step 6 let url_bytestring = ByteString::from_str(url.as_str()).unwrap_or(ByteString::new(b"".to_vec())); - r.Headers(can_gc) + response + .Headers(can_gc) .Set(ByteString::new(b"Location".to_vec()), url_bytestring)?; // Step 4 continued // Headers Guard is set to Immutable here to prevent error in Step 6 - r.Headers(can_gc).set_guard(Guard::Immutable); + response.Headers(can_gc).set_guard(Guard::Immutable); // Step 7 - Ok(r) + Ok(response) + } + + /// <https://fetch.spec.whatwg.org/#dom-response-json> + #[allow(unsafe_code)] + fn CreateFromJson( + cx: JSContext, + global: &GlobalScope, + data: HandleValue, + init: &ResponseBinding::ResponseInit, + can_gc: CanGc, + ) -> Fallible<DomRoot<Response>> { + // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. + let json_str = serialize_jsval_to_json_utf8(cx, data)?; + + // 2. Let body be the result of extracting bytes + // The spec's definition of JSON bytes is a UTF-8 encoding so using a DOMString here handles + // the encoding part. + let body_init = BodyInit::String(json_str); + let mut body = body_init.extract(global, can_gc)?; + + // 3. Let responseObject be the result of creating a Response object, given a new response, + // "response", and the current realm. + let response = Response::new(global, can_gc); + response.Headers(can_gc).set_guard(Guard::Response); + + // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). + body.content_type = Some("application/json".into()); + initialize_response(global, can_gc, Some(body), init, response) } - // https://fetch.spec.whatwg.org/#dom-response-type + /// <https://fetch.spec.whatwg.org/#dom-response-type> fn Type(&self) -> DOMResponseType { *self.response_type.borrow() //into() } - // https://fetch.spec.whatwg.org/#dom-response-url + /// <https://fetch.spec.whatwg.org/#dom-response-url> fn Url(&self) -> USVString { USVString(String::from( (*self.url.borrow()) @@ -285,33 +265,33 @@ impl ResponseMethods<crate::DomTypeHolder> for Response { )) } - // https://fetch.spec.whatwg.org/#dom-response-redirected + /// <https://fetch.spec.whatwg.org/#dom-response-redirected> fn Redirected(&self) -> bool { return *self.redirected.borrow(); } - // https://fetch.spec.whatwg.org/#dom-response-status + /// <https://fetch.spec.whatwg.org/#dom-response-status> fn Status(&self) -> u16 { self.status.borrow().raw_code() } - // https://fetch.spec.whatwg.org/#dom-response-ok + /// <https://fetch.spec.whatwg.org/#dom-response-ok> fn Ok(&self) -> bool { self.status.borrow().is_success() } - // https://fetch.spec.whatwg.org/#dom-response-statustext + /// <https://fetch.spec.whatwg.org/#dom-response-statustext> fn StatusText(&self) -> ByteString { ByteString::new(self.status.borrow().message().to_vec()) } - // https://fetch.spec.whatwg.org/#dom-response-headers + /// <https://fetch.spec.whatwg.org/#dom-response-headers> fn Headers(&self, can_gc: CanGc) -> DomRoot<Headers> { self.headers_reflector .or_init(|| Headers::for_response(&self.global(), can_gc)) } - // https://fetch.spec.whatwg.org/#dom-response-clone + /// <https://fetch.spec.whatwg.org/#dom-response-clone> fn Clone(&self, can_gc: CanGc) -> Fallible<DomRoot<Response>> { // Step 1 if self.is_locked() || self.is_disturbed() { @@ -352,7 +332,7 @@ impl ResponseMethods<crate::DomTypeHolder> for Response { Ok(new_response) } - // https://fetch.spec.whatwg.org/#dom-body-bodyused + /// <https://fetch.spec.whatwg.org/#dom-body-bodyused> fn BodyUsed(&self) -> bool { self.is_disturbed() } @@ -362,27 +342,27 @@ impl ResponseMethods<crate::DomTypeHolder> for Response { self.body() } - // https://fetch.spec.whatwg.org/#dom-body-text + /// <https://fetch.spec.whatwg.org/#dom-body-text> fn Text(&self, can_gc: CanGc) -> Rc<Promise> { consume_body(self, BodyType::Text, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-blob + /// <https://fetch.spec.whatwg.org/#dom-body-blob> fn Blob(&self, can_gc: CanGc) -> Rc<Promise> { consume_body(self, BodyType::Blob, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-formdata + /// <https://fetch.spec.whatwg.org/#dom-body-formdata> fn FormData(&self, can_gc: CanGc) -> Rc<Promise> { consume_body(self, BodyType::FormData, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-json + /// <https://fetch.spec.whatwg.org/#dom-body-json> fn Json(&self, can_gc: CanGc) -> Rc<Promise> { consume_body(self, BodyType::Json, can_gc) } - // https://fetch.spec.whatwg.org/#dom-body-arraybuffer + /// <https://fetch.spec.whatwg.org/#dom-body-arraybuffer> fn ArrayBuffer(&self, can_gc: CanGc) -> Rc<Promise> { consume_body(self, BodyType::ArrayBuffer, can_gc) } @@ -393,6 +373,80 @@ impl ResponseMethods<crate::DomTypeHolder> for Response { } } +/// <https://fetch.spec.whatwg.org/#initialize-a-response> +fn initialize_response( + global: &GlobalScope, + can_gc: CanGc, + body: Option<ExtractedBody>, + init: &ResponseBinding::ResponseInit, + response: DomRoot<Response>, +) -> Result<DomRoot<Response>, Error> { + // 1. If init["status"] is not in the range 200 to 599, inclusive, then throw a RangeError. + if init.status < 200 || init.status > 599 { + return Err(Error::Range(format!( + "init's status member should be in the range 200 to 599, inclusive, but is {}", + init.status + ))); + } + + // 2. If init["statusText"] is not the empty string and does not match the reason-phrase token production, + // then throw a TypeError. + if !is_valid_status_text(&init.statusText) { + return Err(Error::Type( + "init's statusText member does not match the reason-phrase token production" + .to_string(), + )); + } + + // 3. Set response’s response’s status to init["status"]. + // 4. Set response’s response’s status message to init["statusText"]. + *response.status.borrow_mut() = + HttpStatus::new_raw(init.status, init.statusText.clone().into()); + + // 5. If init["headers"] exists, then fill response’s headers with init["headers"]. + if let Some(ref headers_member) = init.headers { + response + .Headers(can_gc) + .fill(Some(headers_member.clone()))?; + } + + // 6. If body is non-null, then: + if let Some(ref body) = body { + // 6.1 If response’s status is a null body status, then throw a TypeError. + if is_null_body_status(init.status) { + return Err(Error::Type( + "Body is non-null but init's status member is a null body status".to_string(), + )); + }; + + // 6.2 Set response’s body to body’s body. + response.body_stream.set(Some(&*body.stream)); + + // 6.3 If body’s type is non-null and response’s header list does not contain `Content-Type`, + // then append (`Content-Type`, body’s type) to response’s header list. + if let Some(content_type_contents) = &body.content_type { + if !response + .Headers(can_gc) + .Has(ByteString::new(b"Content-Type".to_vec())) + .unwrap() + { + response.Headers(can_gc).Append( + ByteString::new(b"Content-Type".to_vec()), + ByteString::new(content_type_contents.as_bytes().to_vec()), + )?; + } + }; + } else { + // Reset FetchResponse to an in-memory stream with empty byte sequence here for + // no-init-body case. This is because the Response/Body types here do not hold onto a + // fetch Response object. + let stream = ReadableStream::new_from_bytes(global, Vec::with_capacity(0), can_gc)?; + response.body_stream.set(Some(&*stream)); + } + + Ok(response) +} + fn serialize_without_fragment(url: &ServoUrl) -> &str { &url[..Position::AfterQuery] } diff --git a/components/script/dom/webgl2renderingcontext.rs b/components/script/dom/webgl2renderingcontext.rs index bb6ffa11849..416454d8719 100644 --- a/components/script/dom/webgl2renderingcontext.rs +++ b/components/script/dom/webgl2renderingcontext.rs @@ -24,6 +24,7 @@ use js::typedarray::{ArrayBufferView, CreateWith, Float32, Int32Array, Uint32, U use script_bindings::interfaces::WebGL2RenderingContextHelpers; use script_layout_interface::HTMLCanvasDataSource; use servo_config::pref; +use snapshot::Snapshot; use url::Host; use crate::canvas_context::CanvasContext; @@ -549,11 +550,11 @@ impl WebGL2RenderingContext { return ); - let (sender, receiver) = ipc::bytes_channel().unwrap(); + let (sender, receiver) = ipc::channel().unwrap(); self.base.send_command(WebGLCommand::ReadPixels( src_rect, format, pixel_type, sender, )); - let src = receiver.recv().unwrap(); + let (src, _) = receiver.recv().unwrap(); for i in 0..src_rect.size.height as usize { let src_start = i * src_row_bytes as usize; @@ -916,11 +917,7 @@ impl CanvasContext for WebGL2RenderingContext { self.base.resize(); } - fn get_image_data_as_shared_memory(&self) -> Option<IpcSharedMemory> { - self.base.get_image_data_as_shared_memory() - } - - fn get_image_data(&self) -> Option<Vec<u8>> { + fn get_image_data(&self) -> Option<Snapshot> { self.base.get_image_data() } diff --git a/components/script/dom/webglrenderingcontext.rs b/components/script/dom/webglrenderingcontext.rs index 1e51ac4baf9..9996a3cf504 100644 --- a/components/script/dom/webglrenderingcontext.rs +++ b/components/script/dom/webglrenderingcontext.rs @@ -34,6 +34,7 @@ use pixels::{self, PixelFormat}; use script_layout_interface::HTMLCanvasDataSource; use serde::{Deserialize, Serialize}; use servo_config::pref; +use snapshot::Snapshot; use webrender_api::ImageKey; use crate::canvas_context::CanvasContext; @@ -628,11 +629,15 @@ impl WebGLRenderingContext { if !canvas.origin_is_clean() { return Err(Error::Security); } - if let Some((data, size)) = canvas.fetch_all_data() { - let data = data.unwrap_or_else(|| { - IpcSharedMemory::from_bytes(&vec![0; size.area() as usize * 4]) - }); - TexPixels::new(data, size, PixelFormat::BGRA8, true) + if let Some(snapshot) = canvas.get_image_data() { + let snapshot = snapshot.as_ipc(); + let size = snapshot.size().cast(); + let format = match snapshot.format() { + snapshot::PixelFormat::RGBA => PixelFormat::RGBA8, + snapshot::PixelFormat::BGRA => PixelFormat::BGRA8, + }; + let premultiply = snapshot.alpha_mode().is_premultiplied(); + TexPixels::new(snapshot.to_ipc_shared_memory(), size, format, premultiply) } else { return Ok(None); } @@ -1922,18 +1927,13 @@ impl CanvasContext for WebGLRenderingContext { } } - fn get_image_data_as_shared_memory(&self) -> Option<IpcSharedMemory> { - // TODO: add a method in WebGLRenderingContext to get the pixels. - None - } - // Used by HTMLCanvasElement.toDataURL // // This emits errors quite liberally, but the spec says that this operation // can fail and that it is UB what happens in that case. // // https://www.khronos.org/registry/webgl/specs/latest/1.0/#2.2 - fn get_image_data(&self) -> Option<Vec<u8>> { + fn get_image_data(&self) -> Option<Snapshot> { handle_potential_webgl_error!(self, self.validate_framebuffer(), return None); let mut size = self.size().cast(); @@ -1945,14 +1945,20 @@ impl CanvasContext for WebGLRenderingContext { size.width = cmp::min(size.width, fb_width as u32); size.height = cmp::min(size.height, fb_height as u32); - let (sender, receiver) = ipc::bytes_channel().unwrap(); + let (sender, receiver) = ipc::channel().unwrap(); self.send_command(WebGLCommand::ReadPixels( Rect::from_size(size), constants::RGBA, constants::UNSIGNED_BYTE, sender, )); - Some(receiver.recv().unwrap()) + let (data, alpha_mode) = receiver.recv().unwrap(); + Some(Snapshot::from_vec( + size.cast(), + snapshot::PixelFormat::RGBA, + alpha_mode, + data.to_vec(), + )) } fn mark_as_dirty(&self) { @@ -3826,11 +3832,11 @@ impl WebGLRenderingContextMethods<crate::DomTypeHolder> for WebGLRenderingContex dest_offset += -y * row_len; } - let (sender, receiver) = ipc::bytes_channel().unwrap(); + let (sender, receiver) = ipc::channel().unwrap(); self.send_command(WebGLCommand::ReadPixels( src_rect, format, pixel_type, sender, )); - let src = receiver.recv().unwrap(); + let (src, _) = receiver.recv().unwrap(); let src_row_len = src_rect.size.width as usize * bytes_per_pixel as usize; for i in 0..src_rect.size.height { diff --git a/components/script/dom/webgpu/gpucanvascontext.rs b/components/script/dom/webgpu/gpucanvascontext.rs index 595b54c58d7..c81f96f651f 100644 --- a/components/script/dom/webgpu/gpucanvascontext.rs +++ b/components/script/dom/webgpu/gpucanvascontext.rs @@ -7,8 +7,9 @@ use std::cell::RefCell; use arrayvec::ArrayVec; use dom_struct::dom_struct; -use ipc_channel::ipc::{self, IpcSharedMemory}; +use ipc_channel::ipc::{self}; use script_layout_interface::HTMLCanvasDataSource; +use snapshot::Snapshot; use webgpu_traits::{ ContextConfiguration, PRESENTATION_BUFFER_COUNT, WebGPU, WebGPUContextId, WebGPURequest, WebGPUTexture, @@ -277,10 +278,10 @@ impl CanvasContext for GPUCanvasContext { } /// <https://gpuweb.github.io/gpuweb/#ref-for-abstract-opdef-get-a-copy-of-the-image-contents-of-a-context%E2%91%A5> - fn get_image_data_as_shared_memory(&self) -> Option<IpcSharedMemory> { + fn get_image_data(&self) -> Option<Snapshot> { // 1. Return a copy of the image contents of context. Some(if self.drawing_buffer.borrow().cleared { - IpcSharedMemory::from_byte(0, self.size().area() as usize * 4) + Snapshot::cleared(self.size()) } else { let (sender, receiver) = ipc::channel().unwrap(); self.channel @@ -290,7 +291,7 @@ impl CanvasContext for GPUCanvasContext { sender, }) .unwrap(); - receiver.recv().unwrap() + receiver.recv().unwrap().to_owned() }) } diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 96176132b6b..932a9ec7f2d 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -1216,15 +1216,26 @@ impl WindowMethods<crate::DomTypeHolder> for Window { } } - #[allow(unsafe_code)] - fn WebdriverCallback(&self, cx: JSContext, val: HandleValue) { - let rv = unsafe { jsval_to_webdriver(*cx, &self.globalscope, val) }; + fn WebdriverCallback(&self, cx: JSContext, val: HandleValue, realm: InRealm, can_gc: CanGc) { + let rv = jsval_to_webdriver(cx, &self.globalscope, val, realm, can_gc); let opt_chan = self.webdriver_script_chan.borrow_mut().take(); if let Some(chan) = opt_chan { chan.send(rv).unwrap(); } } + fn WebdriverException(&self, cx: JSContext, val: HandleValue, realm: InRealm, can_gc: CanGc) { + let rv = jsval_to_webdriver(cx, &self.globalscope, val, realm, can_gc); + let opt_chan = self.webdriver_script_chan.borrow_mut().take(); + if let Some(chan) = opt_chan { + if let Ok(rv) = rv { + chan.send(Err(WebDriverJSError::JSException(rv))).unwrap(); + } else { + chan.send(rv).unwrap(); + } + } + } + fn WebdriverTimeout(&self) { let opt_chan = self.webdriver_script_chan.borrow_mut().take(); if let Some(chan) = opt_chan { @@ -2250,7 +2261,9 @@ impl Window { // Query content box without considering any reflow pub(crate) fn content_box_query_unchecked(&self, node: &Node) -> Option<UntypedRect<Au>> { - self.layout.borrow().query_content_box(node.to_opaque()) + self.layout + .borrow() + .query_content_box(node.to_trusted_node_address()) } pub(crate) fn content_box_query(&self, node: &Node, can_gc: CanGc) -> Option<UntypedRect<Au>> { @@ -2264,14 +2277,18 @@ impl Window { if !self.layout_reflow(QueryMsg::ContentBoxes, can_gc) { return vec![]; } - self.layout.borrow().query_content_boxes(node.to_opaque()) + self.layout + .borrow() + .query_content_boxes(node.to_trusted_node_address()) } pub(crate) fn client_rect_query(&self, node: &Node, can_gc: CanGc) -> UntypedRect<i32> { if !self.layout_reflow(QueryMsg::ClientRectQuery, can_gc) { return Rect::zero(); } - self.layout.borrow().query_client_rect(node.to_opaque()) + self.layout + .borrow() + .query_client_rect(node.to_trusted_node_address()) } /// Find the scroll area of the given node, if it is not None. If the node @@ -2281,11 +2298,12 @@ impl Window { node: Option<&Node>, can_gc: CanGc, ) -> UntypedRect<i32> { - let opaque = node.map(|node| node.to_opaque()); if !self.layout_reflow(QueryMsg::ScrollingAreaQuery, can_gc) { return Rect::zero(); } - self.layout.borrow().query_scrolling_area(opaque) + self.layout + .borrow() + .query_scrolling_area(node.map(Node::to_trusted_node_address)) } pub(crate) fn scroll_offset_query(&self, node: &Node) -> Vector2D<f32, LayoutPixel> { @@ -2374,7 +2392,10 @@ impl Window { return (None, Rect::zero()); } - let response = self.layout.borrow().query_offset_parent(node.to_opaque()); + let response = self + .layout + .borrow() + .query_offset_parent(node.to_trusted_node_address()); let element = response.node_address.and_then(|parent_node_address| { let node = unsafe { from_untrusted_node_address(parent_node_address) }; DomRoot::downcast(node) diff --git a/components/script/dom/writablestream.rs b/components/script/dom/writablestream.rs index 1490fc694ef..e7e9ce906a6 100644 --- a/components/script/dom/writablestream.rs +++ b/components/script/dom/writablestream.rs @@ -27,7 +27,7 @@ use crate::dom::bindings::conversions::ConversionResult; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; -use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader}; +use crate::dom::bindings::structuredclone::StructuredData; use crate::dom::bindings::transferable::Transferable; use crate::dom::countqueuingstrategy::{extract_high_water_mark, extract_size_algorithm}; use crate::dom::domexception::{DOMErrorName, DOMException}; @@ -1182,16 +1182,12 @@ impl Transferable for WritableStream { } /// Note: we are relying on the port transfer, so the data returned here are related to the port. - fn serialized_storage( - data: StructuredData<'_>, - ) -> &mut Option<HashMap<MessagePortId, Self::Data>> { + fn serialized_storage<'a>( + data: StructuredData<'a, '_>, + ) -> &'a mut Option<HashMap<MessagePortId, Self::Data>> { match data { StructuredData::Reader(r) => &mut r.port_impls, StructuredData::Writer(w) => &mut w.ports, } } - - fn deserialized_storage(reader: &mut StructuredDataReader) -> &mut Option<Vec<DomRoot<Self>>> { - &mut reader.writable_streams - } } diff --git a/components/script/layout_image.rs b/components/script/layout_image.rs index 7fd23804ffd..df542b4b759 100644 --- a/components/script/layout_image.rs +++ b/components/script/layout_image.rs @@ -119,7 +119,10 @@ pub(crate) fn fetch_image_for_layout( ) .origin(document.origin().immutable().clone()) .destination(Destination::Image) - .pipeline_id(Some(document.global().pipeline_id())); + .pipeline_id(Some(document.global().pipeline_id())) + .insecure_requests_policy(document.insecure_requests_policy()) + .has_trustworthy_ancestor_origin(document.has_trustworthy_ancestor_origin()) + .policy_container(document.policy_container().to_owned()); // Layout image loads do not delay the document load event. document.fetch_background(request, context); diff --git a/components/script/messaging.rs b/components/script/messaging.rs index 808b338e709..7d0b7aabe05 100644 --- a/components/script/messaging.rs +++ b/components/script/messaging.rs @@ -73,7 +73,7 @@ impl MixedMessage { ScriptThreadMessage::RemoveHistoryStates(id, ..) => Some(*id), ScriptThreadMessage::FocusIFrame(id, ..) => Some(*id), ScriptThreadMessage::WebDriverScriptCommand(id, ..) => Some(*id), - ScriptThreadMessage::TickAllAnimations(id, ..) => Some(*id), + ScriptThreadMessage::TickAllAnimations(..) => None, ScriptThreadMessage::WebFontLoaded(id, ..) => Some(*id), ScriptThreadMessage::DispatchIFrameLoadEvent { target: _, diff --git a/components/script/script_runtime.rs b/components/script/script_runtime.rs index d6832a644ec..1f05c15d74e 100644 --- a/components/script/script_runtime.rs +++ b/components/script/script_runtime.rs @@ -19,7 +19,7 @@ use std::time::{Duration, Instant}; use std::{os, ptr, thread}; use background_hang_monitor_api::ScriptHangAnnotation; -use content_security_policy::{CheckResult, PolicyDisposition}; +use content_security_policy::CheckResult; use js::conversions::jsstr_to_string; use js::glue::{ CollectServoSizes, CreateJobQueue, DeleteJobQueue, DispatchableRun, JobQueueTraps, @@ -45,7 +45,7 @@ pub(crate) use js::rust::ThreadSafeJSContext; use js::rust::wrappers::{GetPromiseIsHandled, JS_GetPromiseResult}; use js::rust::{ Handle, HandleObject as RustHandleObject, IntoHandle, JSEngine, JSEngineHandle, ParentRuntime, - Runtime as RustRuntime, describe_scripted_caller, + Runtime as RustRuntime, }; use malloc_size_of::MallocSizeOfOps; use malloc_size_of_derive::MallocSizeOf; @@ -82,7 +82,6 @@ use crate::microtask::{EnqueuedPromiseCallback, Microtask, MicrotaskQueue}; use crate::realms::{AlreadyInRealm, InRealm}; use crate::script_module::EnsureModuleHooksInitialized; use crate::script_thread::trace_thread; -use crate::security_manager::{CSPViolationReportBuilder, CSPViolationReportTask}; use crate::task_source::SendableTaskSource; static JOB_QUEUE_TRAPS: JobQueueTraps = JobQueueTraps { @@ -373,10 +372,6 @@ unsafe extern "C" fn content_security_policy_allows( let cx = JSContext::from_ptr(cx); wrap_panic(&mut || { // SpiderMonkey provides null pointer when executing webassembly. - let sample = match sample { - sample if !sample.is_null() => Some(jsstr_to_string(*cx, *sample)), - _ => None, - }; let in_realm_proof = AlreadyInRealm::assert_for_cx(cx); let global = GlobalScope::from_context(*cx, InRealm::Already(&in_realm_proof)); let Some(csp_list) = global.get_csp_list() else { @@ -384,43 +379,19 @@ unsafe extern "C" fn content_security_policy_allows( return; }; - let is_js_evaluation_allowed = csp_list.is_js_evaluation_allowed() == CheckResult::Allowed; - let is_wasm_evaluation_allowed = - csp_list.is_wasm_evaluation_allowed() == CheckResult::Allowed; - let scripted_caller = describe_scripted_caller(*cx).unwrap_or_default(); - - let resource = match runtime_code { - RuntimeCode::JS => "eval".to_owned(), - RuntimeCode::WASM => "wasm-eval".to_owned(), - }; - - allowed = match runtime_code { - RuntimeCode::JS if is_js_evaluation_allowed => true, - RuntimeCode::WASM if is_wasm_evaluation_allowed => true, - _ => false, + let (is_evaluation_allowed, violations) = match runtime_code { + RuntimeCode::JS => { + let source = match sample { + sample if !sample.is_null() => &jsstr_to_string(*cx, *sample), + _ => "", + }; + csp_list.is_js_evaluation_allowed(source) + }, + RuntimeCode::WASM => csp_list.is_wasm_evaluation_allowed(), }; - if !allowed { - // FIXME: Don't fire event if `script-src` and `default-src` - // were not passed. - for policy in csp_list.0 { - let report = CSPViolationReportBuilder::default() - .resource(resource.clone()) - .sample(sample.clone()) - .report_only(policy.disposition == PolicyDisposition::Report) - .source_file(scripted_caller.filename.clone()) - .line_number(scripted_caller.line) - .column_number(scripted_caller.col) - .effective_directive("script-src".to_owned()) - .build(&global); - let task = CSPViolationReportTask::new(&global, report); - - global - .task_manager() - .dom_manipulation_task_source() - .queue(task); - } - } + global.report_csp_violations(violations); + allowed = is_evaluation_allowed == CheckResult::Allowed; }); allowed } diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index c9b27bb6c56..f78b5bf281b 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -1147,14 +1147,6 @@ impl ScriptThread { return; } - // Run rafs for all pipeline, if a raf tick was received for any. - // This ensures relative ordering of rafs between parent doc and iframes. - let should_run_rafs = self - .documents - .borrow() - .iter() - .any(|(_, doc)| doc.is_fully_active() && doc.has_received_raf_tick()); - let any_animations_running = self.documents.borrow().iter().any(|(_, document)| { document.is_fully_active() && document.animations().running_animation_count() != 0 }); @@ -1242,7 +1234,7 @@ impl ScriptThread { // > 14. For each doc of docs, run the animation frame callbacks for doc, passing // > in the relative high resolution time given frameTimestamp and doc's // > relevant global object as the timestamp. - if should_run_rafs { + if requested_by_compositor { document.run_the_animation_frame_callbacks(can_gc); } @@ -1421,18 +1413,9 @@ impl ScriptThread { self.handle_viewport(id, rect); }), MixedMessage::FromConstellation(ScriptThreadMessage::TickAllAnimations( - pipeline_id, - tick_type, + _webviews, )) => { - if let Some(document) = self.documents.borrow().find_document(pipeline_id) { - document.note_pending_animation_tick(tick_type); - compositor_requested_update_the_rendering = true; - } else { - warn!( - "Trying to note pending animation tick for closed pipeline {}.", - pipeline_id - ) - } + compositor_requested_update_the_rendering = true; }, MixedMessage::FromConstellation(ScriptThreadMessage::SendInputEvent(id, event)) => { self.handle_input_event(id, event) @@ -2291,6 +2274,7 @@ impl ScriptThread { node_id, name, reply, + can_gc, ) }, WebDriverScriptCommand::GetElementCSS(node_id, name, reply) => { @@ -2439,8 +2423,6 @@ impl ScriptThread { let mut reports = vec![]; perform_memory_report(|ops| { - let prefix = format!("url({urls})"); - reports.extend(self.get_cx().get_reports(prefix.clone(), ops)); for (_, document) in documents.iter() { document .window() @@ -2448,6 +2430,9 @@ impl ScriptThread { .collect_reports(&mut reports, ops); } + let prefix = format!("url({urls})"); + reports.extend(self.get_cx().get_reports(prefix.clone(), ops)); + reports.push(self.image_cache.memory_report(&prefix, ops)); }); diff --git a/components/script/timers.rs b/components/script/timers.rs index 244aa2df4ed..0afc3da164a 100644 --- a/components/script/timers.rs +++ b/components/script/timers.rs @@ -421,8 +421,7 @@ impl JsTimers { ) -> i32 { let callback = match callback { TimerCallback::StringTimerCallback(code_str) => { - let cx = GlobalScope::get_cx(); - if global.is_js_evaluation_allowed(cx) { + if global.is_js_evaluation_allowed(code_str.as_ref()) { InternalTimerCallback::StringTimerCallback(code_str) } else { return 0; diff --git a/components/script/webdriver_handlers.rs b/components/script/webdriver_handlers.rs index c6303ca89e0..781ac53f415 100644 --- a/components/script/webdriver_handlers.rs +++ b/components/script/webdriver_handlers.rs @@ -3,7 +3,7 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cmp; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::ffi::CString; use std::ptr::NonNull; @@ -48,7 +48,7 @@ use crate::dom::bindings::conversions::{ ConversionBehavior, ConversionResult, FromJSValConvertible, StringificationBehavior, get_property, get_property_jsval, jsid_to_string, jsstring_to_str, root_from_object, }; -use crate::dom::bindings::error::{Error, throw_dom_exception}; +use crate::dom::bindings::error::{Error, report_pending_exception, throw_dom_exception}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::{DomGlobal, DomObject}; use crate::dom::bindings::root::DomRoot; @@ -67,7 +67,7 @@ use crate::dom::node::{Node, NodeTraits, ShadowIncluding}; use crate::dom::nodelist::NodeList; use crate::dom::window::Window; use crate::dom::xmlserializer::XMLSerializer; -use crate::realms::enter_realm; +use crate::realms::{AlreadyInRealm, InRealm, enter_realm}; use crate::script_module::ScriptFetchOptions; use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; use crate::script_thread::ScriptThread; @@ -183,12 +183,44 @@ unsafe fn is_arguments_object(cx: *mut JSContext, value: HandleValue) -> bool { jsstring_to_str(cx, class_name) == "[object Arguments]" } +#[derive(Eq, Hash, PartialEq)] +struct HashableJSVal(u64); + +impl From<HandleValue<'_>> for HashableJSVal { + fn from(v: HandleValue<'_>) -> HashableJSVal { + HashableJSVal(v.get().asBits_) + } +} + #[allow(unsafe_code)] -pub(crate) unsafe fn jsval_to_webdriver( +pub(crate) fn jsval_to_webdriver( + cx: SafeJSContext, + global_scope: &GlobalScope, + val: HandleValue, + realm: InRealm, + can_gc: CanGc, +) -> WebDriverJSResult { + let mut seen = HashSet::new(); + let result = unsafe { jsval_to_webdriver_inner(*cx, global_scope, val, &mut seen) }; + if result.is_err() { + report_pending_exception(cx, true, realm, can_gc); + } + result +} + +#[allow(unsafe_code)] +unsafe fn jsval_to_webdriver_inner( cx: *mut JSContext, global_scope: &GlobalScope, val: HandleValue, + seen: &mut HashSet<HashableJSVal>, ) -> WebDriverJSResult { + let hashable = val.into(); + if seen.contains(&hashable) { + return Err(WebDriverJSError::JSError); + } + seen.insert(hashable); + let _ac = enter_realm(global_scope); if val.get().is_undefined() { Ok(WebDriverJSValue::Undefined) @@ -254,9 +286,11 @@ pub(crate) unsafe fn jsval_to_webdriver( for i in 0..length { rooted!(in(cx) let mut item = UndefinedValue()); match get_property_jsval(cx, object.handle(), &i.to_string(), item.handle_mut()) { - Ok(_) => match jsval_to_webdriver(cx, global_scope, item.handle()) { - Ok(converted_item) => result.push(converted_item), - err @ Err(_) => return err, + Ok(_) => { + match jsval_to_webdriver_inner(cx, global_scope, item.handle(), seen) { + Ok(converted_item) => result.push(converted_item), + err @ Err(_) => return err, + } }, Err(error) => { throw_dom_exception( @@ -298,7 +332,7 @@ pub(crate) unsafe fn jsval_to_webdriver( &HandleValueArray::empty(), value.handle_mut(), ) { - jsval_to_webdriver(cx, global_scope, value.handle()) + jsval_to_webdriver_inner(cx, global_scope, value.handle(), seen) } else { throw_dom_exception( SafeJSContext::from_ptr(cx), @@ -349,7 +383,9 @@ pub(crate) unsafe fn jsval_to_webdriver( return Err(WebDriverJSError::JSError); }; - if let Ok(value) = jsval_to_webdriver(cx, global_scope, property.handle()) { + if let Ok(value) = + jsval_to_webdriver_inner(cx, global_scope, property.handle(), seen) + { result.insert(name.into(), value); } else { return Err(WebDriverJSError::JSError); @@ -373,18 +409,22 @@ pub(crate) fn handle_execute_script( ) { match window { Some(window) => { - let result = unsafe { - let cx = window.get_cx(); - rooted!(in(*cx) let mut rval = UndefinedValue()); - let global = window.as_global_scope(); - global.evaluate_js_on_global_with_result( - &eval, - rval.handle_mut(), - ScriptFetchOptions::default_classic_script(global), - global.api_base_url(), - can_gc, - ); - jsval_to_webdriver(*cx, global, rval.handle()) + let cx = window.get_cx(); + let realm = AlreadyInRealm::assert_for_cx(cx); + let realm = InRealm::already(&realm); + + rooted!(in(*cx) let mut rval = UndefinedValue()); + let global = window.as_global_scope(); + let result = if global.evaluate_js_on_global_with_result( + &eval, + rval.handle_mut(), + ScriptFetchOptions::default_classic_script(global), + global.api_base_url(), + can_gc, + ) { + jsval_to_webdriver(cx, global, rval.handle(), realm, can_gc) + } else { + Err(WebDriverJSError::JSError) }; reply.send(result).unwrap(); @@ -406,17 +446,20 @@ pub(crate) fn handle_execute_async_script( match window { Some(window) => { let cx = window.get_cx(); + let reply_sender = reply.clone(); window.set_webdriver_script_chan(Some(reply)); rooted!(in(*cx) let mut rval = UndefinedValue()); let global_scope = window.as_global_scope(); - global_scope.evaluate_js_on_global_with_result( + if !global_scope.evaluate_js_on_global_with_result( &eval, rval.handle_mut(), ScriptFetchOptions::default_classic_script(global_scope), global_scope.api_base_url(), can_gc, - ); + ) { + reply_sender.send(Err(WebDriverJSError::JSError)).unwrap(); + } }, None => { reply @@ -1136,12 +1179,13 @@ pub(crate) fn handle_get_property( node_id: String, name: String, reply: IpcSender<Result<WebDriverJSValue, ErrorStatus>>, + can_gc: CanGc, ) { reply .send( find_node_by_unique_id(documents, pipeline, node_id).map(|node| { let document = documents.find_document(pipeline).unwrap(); - let _ac = enter_realm(&*document); + let realm = enter_realm(&*document); let cx = document.window().get_cx(); rooted!(in(*cx) let mut property = UndefinedValue()); @@ -1154,14 +1198,19 @@ pub(crate) fn handle_get_property( ) } { Ok(_) => { - match unsafe { jsval_to_webdriver(*cx, &node.global(), property.handle()) } - { + match jsval_to_webdriver( + cx, + &node.global(), + property.handle(), + InRealm::entered(&realm), + can_gc, + ) { Ok(property) => property, Err(_) => WebDriverJSValue::Undefined, } }, Err(error) => { - throw_dom_exception(cx, &node.global(), error, CanGc::note()); + throw_dom_exception(cx, &node.global(), error, can_gc); WebDriverJSValue::Undefined }, } diff --git a/components/script_bindings/codegen/Bindings.conf b/components/script_bindings/codegen/Bindings.conf index 4946177e0b3..c457bf70b85 100644 --- a/components/script_bindings/codegen/Bindings.conf +++ b/components/script_bindings/codegen/Bindings.conf @@ -551,7 +551,7 @@ DOMInterfaces = { }, 'Response': { - 'canGc': ['Error', 'Redirect', 'Clone', 'Text', 'Blob', 'FormData', 'Json', 'ArrayBuffer', 'Headers', 'Bytes'], + 'canGc': ['Error', 'Redirect', 'Clone', 'CreateFromJson', 'Text', 'Blob', 'FormData', 'Json', 'ArrayBuffer', 'Headers', 'Bytes'], }, 'RTCPeerConnection': { @@ -642,8 +642,8 @@ DOMInterfaces = { }, 'Window': { - 'canGc': ['Stop', 'Fetch', 'Scroll', 'Scroll_','ScrollBy', 'ScrollBy_', 'Stop', 'Fetch', 'Open', 'CreateImageBitmap', 'TrustedTypes'], - 'inRealms': ['Fetch', 'GetOpener'], + 'canGc': ['Stop', 'Fetch', 'Scroll', 'Scroll_','ScrollBy', 'ScrollBy_', 'Stop', 'Fetch', 'Open', 'CreateImageBitmap', 'TrustedTypes', 'WebdriverCallback', 'WebdriverException'], + 'inRealms': ['Fetch', 'GetOpener', 'WebdriverCallback', 'WebdriverException'], 'additionalTraits': ['crate::interfaces::WindowHelpers'], }, diff --git a/components/script_bindings/error.rs b/components/script_bindings/error.rs index 8424ff0fa95..a95d0b0b78c 100644 --- a/components/script_bindings/error.rs +++ b/components/script_bindings/error.rs @@ -59,6 +59,8 @@ pub enum Error { Data, /// OperationError DOMException Operation, + /// NotAllowedError DOMException + NotAllowed, /// TypeError JavaScript Error Type(String), diff --git a/components/script_bindings/str.rs b/components/script_bindings/str.rs index 09d48512f3e..0ef6e0c528a 100644 --- a/components/script_bindings/str.rs +++ b/components/script_bindings/str.rs @@ -10,14 +10,19 @@ use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::str::FromStr; use std::sync::LazyLock; -use std::{fmt, ops, str}; +use std::{fmt, ops, slice, str}; use cssparser::CowRcStr; use html5ever::{LocalName, Namespace}; +use js::rust::wrappers::ToJSON; +use js::rust::{HandleObject, HandleValue}; use num_traits::Zero; use regex::Regex; use stylo_atoms::Atom; +use crate::error::Error; +use crate::script_runtime::JSContext as SafeJSContext; + /// Encapsulates the IDL `ByteString` type. #[derive(Clone, Debug, Default, Eq, JSTraceable, MallocSizeOf, PartialEq)] pub struct ByteString(Vec<u8>); @@ -293,6 +298,64 @@ impl DOMString { } } +/// Because this converts to a DOMString it becomes UTF-8 encoded which is closer to +/// the spec definition of <https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-json-bytes> +/// but we generally do not operate on anything that is truly a WTF-16 string. +/// +/// <https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string> +pub fn serialize_jsval_to_json_utf8( + cx: SafeJSContext, + data: HandleValue, +) -> Result<DOMString, Error> { + #[repr(C)] + struct ToJSONCallbackData { + string: Option<String>, + } + + let mut out_str = ToJSONCallbackData { string: None }; + + #[allow(unsafe_code)] + unsafe extern "C" fn write_callback( + string: *const u16, + len: u32, + data: *mut std::ffi::c_void, + ) -> bool { + let data = data as *mut ToJSONCallbackData; + let string_chars = slice::from_raw_parts(string, len as usize); + (*data) + .string + .get_or_insert_with(Default::default) + .push_str(&String::from_utf16_lossy(string_chars)); + true + } + + // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »). + unsafe { + let stringify_result = ToJSON( + *cx, + data, + HandleObject::null(), + HandleValue::null(), + Some(write_callback), + &mut out_str as *mut ToJSONCallbackData as *mut _, + ); + // Note: ToJSON returns false when a JS error is thrown, so we need to return + // JSFailed to propagate the raised exception + if !stringify_result { + return Err(Error::JSFailed); + } + } + + // 2. If result is undefined, then throw a TypeError. + // Note: ToJSON will not call the callback if the data cannot be serialized. + // 3. Assert: result is a string. + // 4. Return result. + out_str + .string + .map(Into::into) + .ok_or_else(|| Error::Type("unable to serialize JSON".to_owned())) +} + impl Borrow<str> for DOMString { #[inline] fn borrow(&self) -> &str { diff --git a/components/script_bindings/webidls/CSSStyleSheet.webidl b/components/script_bindings/webidls/CSSStyleSheet.webidl index 1241b5c2769..302e7433300 100644 --- a/components/script_bindings/webidls/CSSStyleSheet.webidl +++ b/components/script_bindings/webidls/CSSStyleSheet.webidl @@ -11,6 +11,7 @@ interface CSSStyleSheet : StyleSheet { [Throws, SameObject] readonly attribute CSSRuleList cssRules; [Throws] unsigned long insertRule(DOMString rule, optional unsigned long index = 0); [Throws] undefined deleteRule(unsigned long index); + [Throws] undefined replaceSync(USVString text); }; dictionary CSSStyleSheetInit { diff --git a/components/script_bindings/webidls/Response.webidl b/components/script_bindings/webidls/Response.webidl index 0ced0c13794..d37538d4b6b 100644 --- a/components/script_bindings/webidls/Response.webidl +++ b/components/script_bindings/webidls/Response.webidl @@ -9,6 +9,7 @@ interface Response { [Throws] constructor(optional BodyInit? body = null, optional ResponseInit init = {}); [NewObject] static Response error(); [NewObject, Throws] static Response redirect(USVString url, optional unsigned short status = 302); + [NewObject, Throws, BinaryName="createFromJson"] static Response json(any data, optional ResponseInit init = {}); readonly attribute ResponseType type; diff --git a/components/script_bindings/webidls/Window.webidl b/components/script_bindings/webidls/Window.webidl index d42ba22ea66..81c442b119f 100644 --- a/components/script_bindings/webidls/Window.webidl +++ b/components/script_bindings/webidls/Window.webidl @@ -148,6 +148,7 @@ partial interface Window { partial interface Window { // Shouldn't be public, but just to make things work for now undefined webdriverCallback(optional any result); + undefined webdriverException(optional any result); undefined webdriverTimeout(); }; diff --git a/components/servo/Cargo.toml b/components/servo/Cargo.toml index 0c4eaf40904..498d170492d 100644 --- a/components/servo/Cargo.toml +++ b/components/servo/Cargo.toml @@ -104,6 +104,7 @@ serde = { workspace = true } servo-media = { workspace = true } servo-media-dummy = { workspace = true } servo-media-gstreamer = { workspace = true, optional = true } +servo-tracing = { workspace = true } servo_allocator = { path = "../allocator" } servo_config = { path = "../config" } servo_geometry = { path = "../geometry" } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index be56ffd5c1f..7fb990527ec 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -246,10 +246,7 @@ impl webrender_api::RenderNotifier for RenderNotifier { } impl Servo { - #[cfg_attr( - feature = "tracing", - tracing::instrument(skip(builder), fields(servo_profiling = true), level = "trace",) - )] + #[servo_tracing::instrument(skip(builder))] fn new(builder: ServoBuilder) -> Self { // Global configuration options, parsed from the command line. let opts = builder.opts.map(|opts| *opts); diff --git a/components/servo_tracing/Cargo.toml b/components/servo_tracing/Cargo.toml new file mode 100644 index 00000000000..1660aa7f691 --- /dev/null +++ b/components/servo_tracing/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "servo-tracing" +edition.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true +publish.workspace = true +rust-version.workspace = true + +[dependencies] +quote = { workspace = true } +proc-macro2 = { workspace = true } +syn = { version = "2", features = ["full"] } + +[lib] +path = "lib.rs" +proc-macro = true +doctest = false + +[dev-dependencies] +prettyplease = "0.2.32" diff --git a/components/servo_tracing/lib.rs b/components/servo_tracing/lib.rs new file mode 100644 index 00000000000..04e87ee6cc0 --- /dev/null +++ b/components/servo_tracing/lib.rs @@ -0,0 +1,394 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::Punct; +use quote::{ToTokens, TokenStreamExt, quote}; +use syn::parse::{Parse, Parser}; +use syn::punctuated::Punctuated; +use syn::token::Comma; +use syn::{Expr, ItemFn, Meta, MetaList, Token, parse_quote, parse2}; + +struct Fields(MetaList); +impl From<MetaList> for Fields { + fn from(value: MetaList) -> Self { + Fields(value) + } +} + +impl Fields { + fn create_with_servo_profiling() -> Self { + Fields(parse_quote! { fields(servo_profiling = true) }) + } + + fn inject_servo_profiling(&mut self) -> syn::Result<()> { + let metalist = std::mem::replace(&mut self.0, parse_quote! {field()}); + + let arguments: Punctuated<Meta, Comma> = + Punctuated::parse_terminated.parse2(metalist.tokens)?; + + let servo_profile_given = arguments + .iter() + .any(|arg| arg.path().is_ident("servo_profiling")); + + let metalist = if servo_profile_given { + parse_quote! { + fields(#arguments) + } + } else { + parse_quote! { + fields(servo_profiling=true, #arguments) + } + }; + + let _ = std::mem::replace(&mut self.0, metalist); + + Ok(()) + } +} + +impl ToTokens for Fields { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let items = &self.0; + tokens.append_all(quote! { #items }); + } +} +enum Directive { + Passthrough(Meta), + Level(Expr), + Fields(Fields), +} + +impl From<Fields> for Directive { + fn from(value: Fields) -> Self { + Directive::Fields(value) + } +} + +impl Directive { + fn is_level(&self) -> bool { + matches!(self, Directive::Level(..)) + } + + fn fields_mut(&mut self) -> Option<&mut Fields> { + match self { + Directive::Fields(fields) => Some(fields), + _ => None, + } + } +} + +impl ToTokens for Directive { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Directive::Passthrough(meta) => tokens.append_all(quote! { #meta }), + Directive::Level(level) => tokens.append_all(quote! { level = #level }), + Directive::Fields(fields) => tokens.append_all(quote! { #fields }), + }; + } +} + +impl ToTokens for InstrumentConfiguration { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.append_terminated(&self.0, Punct::new(',', proc_macro2::Spacing::Joint)); + } +} + +struct InstrumentConfiguration(Vec<Directive>); + +impl InstrumentConfiguration { + fn inject_servo_profiling(&mut self) -> syn::Result<()> { + let fields = self.0.iter_mut().find_map(Directive::fields_mut); + match fields { + None => { + self.0 + .push(Directive::from(Fields::create_with_servo_profiling())); + Ok(()) + }, + Some(fields) => fields.inject_servo_profiling(), + } + } + + fn inject_level(&mut self) { + if self.0.iter().any(|a| a.is_level()) { + return; + } + self.0.push(Directive::Level(parse_quote! { "trace" })); + } +} + +impl Parse for InstrumentConfiguration { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?; + let mut components = vec![]; + + for arg in args { + match arg { + Meta::List(meta_list) if meta_list.path.is_ident("fields") => { + components.push(Directive::Fields(meta_list.into())); + }, + Meta::NameValue(meta_name_value) if meta_name_value.path.is_ident("level") => { + components.push(Directive::Level(meta_name_value.value)); + }, + _ => { + components.push(Directive::Passthrough(arg)); + }, + } + } + Ok(InstrumentConfiguration(components)) + } +} + +fn instrument_internal( + attr: proc_macro2::TokenStream, + item: proc_macro2::TokenStream, +) -> syn::Result<proc_macro2::TokenStream> { + // Prepare passthrough arguments for tracing::instrument + let mut configuration: InstrumentConfiguration = parse2(attr)?; + let input_fn: ItemFn = parse2(item)?; + + configuration.inject_servo_profiling()?; + configuration.inject_level(); + + let output = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( + #configuration + ) + )] + #input_fn + }; + + Ok(output) +} + +#[proc_macro_attribute] +/// Instruments a function with some sane defaults by automatically: +/// - setting the attribute behind the "tracing" flag +/// - adding `servo_profiling = true` in the `tracing::instrument(fields(...))` argument. +/// - setting `level = "trace"` if it is not given. +/// +/// This macro assumes the consuming crate has a `tracing` feature flag. +/// +/// We need to be able to set the following +/// ``` +/// #[cfg_attr( +/// feature = "tracing", +/// tracing::instrument( +/// name = "MyCustomName", +/// skip_all, +/// fields(servo_profiling = true), +/// level = "trace", +/// ) +/// )] +/// fn my_fn() { /* .... */ } +/// ``` +/// from a simpler macro, such as: +/// +/// ``` +/// #[servo_tracing::instrument(name = "MyCustomName", skip_all)] +/// fn my_fn() { /* .... */ } +/// ``` +pub fn instrument(attr: TokenStream, item: TokenStream) -> TokenStream { + match instrument_internal(attr.into(), item.into()) { + Ok(stream) => stream.into(), + Err(err) => err.to_compile_error().into(), + } +} + +#[cfg(test)] +mod test { + use proc_macro2::TokenStream; + use quote::{ToTokens, quote}; + use syn::{Attribute, ItemFn}; + + use crate::instrument_internal; + + fn extract_instrument_attribute(item_fn: &mut ItemFn) -> TokenStream { + let attr: &Attribute = item_fn + .attrs + .iter() + .find(|attr| { + // because this is a very nested structure, it is easier to check + // by constructing the full path, and then doing a string comparison. + let p = attr.path().to_token_stream().to_string(); + p == "servo_tracing :: instrument" + }) + .expect("Attribute `servo_tracing::instrument` not found"); + + // we create a tokenstream of the actual internal contents of the attribute + let attr_args = attr + .parse_args::<TokenStream>() + .expect("Failed to parse attribute args"); + + // we remove the tracing attribute, this is to avoid passing it as an actual attribute to itself. + item_fn.attrs.retain(|attr| { + attr.path().to_token_stream().to_string() != "servo_tracing :: instrument" + }); + + attr_args + } + + /// To make test case generation easy, we parse a test_case as a function item + /// with its own attributes, including [`servo_tracing::instrument`]. + /// + /// We extract the [`servo_tracing::instrument`] attribute, and pass it as the first argument to + /// [`servo_tracing::instrument_internal`], + fn evaluate(function: TokenStream, test_case: TokenStream, expected: TokenStream) { + let test_case = quote! { + #test_case + #function + }; + let expected = quote! { + #expected + #function + }; + let function_str = function.to_string(); + let function_str = syn::parse_file(&function_str).expect("function to have valid syntax"); + let function_str = prettyplease::unparse(&function_str); + + let mut item_fn: ItemFn = + syn::parse2(test_case).expect("Failed to parse input as function"); + + let attr_args = extract_instrument_attribute(&mut item_fn); + let item_fn = item_fn.to_token_stream(); + + let generated = instrument_internal(attr_args, item_fn).expect("Generation to not fail."); + + let generated = syn::parse_file(generated.to_string().as_str()) + .expect("to have generated a valid function"); + let generated = prettyplease::unparse(&generated); + let expected = syn::parse_file(expected.to_string().as_str()) + .expect("to have been given a valid expected function"); + let expected = prettyplease::unparse(&expected); + + eprintln!( + "Generated:---------:\n{}--------\nExpected:----------\n{}", + &generated, &expected + ); + assert_eq!(generated, expected); + assert!( + generated.contains(&function_str), + "Expected generated code: {generated} to contain the function code: {function_str}" + ); + } + + fn function1() -> TokenStream { + quote! { + pub fn start( + state: (), + layout_factory: (), + random_pipeline_closure_probability: (), + random_pipeline_closure_seed: (), + hard_fail: (), + canvas_create_sender: (), + canvas_ipc_sender: (), + ) { + } + } + } + + fn function2() -> TokenStream { + quote! { + fn layout( + mut self, + layout_context: &LayoutContext, + positioning_context: &mut PositioningContext, + containing_block_for_children: &ContainingBlock, + containing_block_for_table: &ContainingBlock, + depends_on_block_constraints: bool, + ) { + } + } + } + + #[test] + fn passing_servo_profiling_and_level_and_aux() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument(skip(state, layout_factory), fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(skip(state, layout_factory),fields(servo_profiling = true),level = "trace",)] + }; + + evaluate(function, test_case, expected); + } + + #[test] + fn passing_servo_profiling_and_level() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(fields(servo_profiling = true),level = "trace",)] + }; + evaluate(function, test_case, expected); + } + + #[test] + fn passing_servo_profiling() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(fields(servo_profiling = true))] + }; + evaluate(function, test_case, expected); + } + + #[test] + fn inject_level_and_servo_profiling() { + let function = function1(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument(fields(servo_profiling = true), level = "trace",) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument()] + }; + evaluate(function, test_case, expected); + } + + #[test] + fn instrument_with_name() { + let function = function2(); + let expected = quote! { + #[cfg_attr( + feature = "tracing", + tracing::instrument( + name = "Table::layout", + skip_all, + fields(servo_profiling = true), + level = "trace", + ) + )] + }; + + let test_case = quote! { + #[servo_tracing::instrument(name="Table::layout", skip_all)] + }; + + evaluate(function, test_case, expected); + } +} diff --git a/components/shared/canvas/Cargo.toml b/components/shared/canvas/Cargo.toml index d6e96711e1d..c77399ef847 100644 --- a/components/shared/canvas/Cargo.toml +++ b/components/shared/canvas/Cargo.toml @@ -26,6 +26,7 @@ pixels = { path = "../../pixels" } serde = { workspace = true } serde_bytes = { workspace = true } servo_config = { path = "../../config" } +snapshot = { workspace = true } stylo = { workspace = true } webrender_api = { workspace = true } webxr-api = { workspace = true, features = ["ipc"] } diff --git a/components/shared/canvas/canvas.rs b/components/shared/canvas/canvas.rs index 90ba569b5eb..850f5f9bd9a 100644 --- a/components/shared/canvas/canvas.rs +++ b/components/shared/canvas/canvas.rs @@ -6,10 +6,11 @@ use std::default::Default; use std::str::FromStr; use euclid::default::{Point2D, Rect, Size2D, Transform2D}; -use ipc_channel::ipc::{IpcBytesReceiver, IpcBytesSender, IpcSender, IpcSharedMemory}; +use ipc_channel::ipc::{IpcBytesReceiver, IpcSender}; use malloc_size_of_derive::MallocSizeOf; use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; +use snapshot::IpcSnapshot; use style::color::AbsoluteColor; use style::properties::style_structs::Font as FontStyleStruct; @@ -87,7 +88,7 @@ pub enum CanvasMsg { pub enum Canvas2dMsg { Arc(Point2D<f32>, f32, f32, f32, bool), ArcTo(Point2D<f32>, Point2D<f32>, f32), - DrawImage(IpcSharedMemory, Size2D<f64>, Rect<f64>, Rect<f64>, bool), + DrawImage(IpcSnapshot, Rect<f64>, Rect<f64>, bool), DrawEmptyImage(Size2D<f64>, Rect<f64>, Rect<f64>), DrawImageInOther(CanvasId, Size2D<f64>, Rect<f64>, Rect<f64>, bool), BeginPath, @@ -101,7 +102,7 @@ pub enum Canvas2dMsg { FillPath(FillOrStrokeStyle, Vec<PathSegment>), FillText(String, f64, f64, Option<f64>, FillOrStrokeStyle, bool), FillRect(Rect<f32>, FillOrStrokeStyle), - GetImageData(Rect<u64>, Size2D<u64>, IpcBytesSender), + GetImageData(Rect<u64>, Size2D<u64>, IpcSender<IpcSnapshot>), GetTransform(IpcSender<Transform2D<f32>>), IsPointInCurrentPath(f64, f64, FillRule, IpcSender<bool>), IsPointInPath(Vec<PathSegment>, f64, f64, FillRule, IpcSender<bool>), @@ -137,7 +138,7 @@ pub enum Canvas2dMsg { #[derive(Clone, Debug, Deserialize, Serialize)] pub enum FromScriptMsg { - SendPixels(IpcSender<IpcSharedMemory>), + SendPixels(IpcSender<IpcSnapshot>), } #[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize)] diff --git a/components/shared/canvas/webgl.rs b/components/shared/canvas/webgl.rs index eaa934fc145..7ffae5d5fc5 100644 --- a/components/shared/canvas/webgl.rs +++ b/components/shared/canvas/webgl.rs @@ -298,7 +298,12 @@ pub enum WebGLCommand { PolygonOffset(f32, f32), RenderbufferStorage(u32, u32, i32, i32), RenderbufferStorageMultisample(u32, i32, u32, i32, i32), - ReadPixels(Rect<u32>, u32, u32, IpcBytesSender), + ReadPixels( + Rect<u32>, + u32, + u32, + IpcSender<(IpcSharedMemory, snapshot::AlphaMode)>, + ), ReadPixelsPP(Rect<i32>, u32, u32, usize), SampleCoverage(f32, bool), Scissor(i32, i32, u32, u32), diff --git a/components/shared/compositing/Cargo.toml b/components/shared/compositing/Cargo.toml index e3ff81615e6..2e39ef5397b 100644 --- a/components/shared/compositing/Cargo.toml +++ b/components/shared/compositing/Cargo.toml @@ -38,4 +38,3 @@ stylo = { workspace = true } stylo_traits = { workspace = true } surfman = { workspace = true, features = ["sm-x11"] } webrender_api = { workspace = true } - diff --git a/components/shared/constellation/lib.rs b/components/shared/constellation/lib.rs index 548e17b532c..b3d4fe525a1 100644 --- a/components/shared/constellation/lib.rs +++ b/components/shared/constellation/lib.rs @@ -18,7 +18,6 @@ use std::time::Duration; use base::Epoch; use base::cross_process_instant::CrossProcessInstant; use base::id::{MessagePortId, PipelineId, WebViewId}; -use bitflags::bitflags; use embedder_traits::{ CompositorHitTestResult, Cursor, InputEvent, MediaSessionActionType, Theme, ViewportDetails, WebDriverCommandMsg, @@ -57,8 +56,9 @@ pub enum EmbedderToConstellationMessage { ChangeViewportDetails(WebViewId, ViewportDetails, WindowSizeType), /// Inform the constellation of a theme change. ThemeChange(Theme), - /// Requests that the constellation instruct layout to begin a new tick of the animation. - TickAnimation(PipelineId, AnimationTickType), + /// Requests that the constellation instruct script/layout to try to layout again and tick + /// animations. + TickAnimation(Vec<WebViewId>), /// Dispatch a webdriver command WebDriverCommand(WebDriverCommandMsg), /// Reload a top-level browsing context. @@ -130,17 +130,6 @@ pub enum WindowSizeType { Resize, } -bitflags! { - #[derive(Debug, Default, Deserialize, Serialize)] - /// Specifies if rAF should be triggered and/or CSS Animations and Transitions. - pub struct AnimationTickType: u8 { - /// Trigger a call to requestAnimationFrame. - const REQUEST_ANIMATION_FRAME = 0b001; - /// Trigger restyles for CSS Animations and Transitions. - const CSS_ANIMATIONS_AND_TRANSITIONS = 0b010; - } -} - /// The scroll state of a stacking context. #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] pub struct ScrollState { diff --git a/components/shared/embedder/webdriver.rs b/components/shared/embedder/webdriver.rs index 324906c2584..9577163411e 100644 --- a/components/shared/embedder/webdriver.rs +++ b/components/shared/embedder/webdriver.rs @@ -170,6 +170,7 @@ pub enum WebDriverJSError { /// Occurs when handler received an event message for a layout channel that is not /// associated with the current script thread BrowsingContextNotFound, + JSException(WebDriverJSValue), JSError, StaleElementReference, Timeout, diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index a39be739fd5..7323907cba3 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -20,7 +20,7 @@ use bluetooth_traits::BluetoothRequest; use canvas_traits::webgl::WebGLPipeline; use compositing_traits::CrossProcessCompositorApi; use constellation_traits::{ - AnimationTickType, LoadData, NavigationHistoryBehavior, ScriptToConstellationChan, ScrollState, + LoadData, NavigationHistoryBehavior, ScriptToConstellationChan, ScrollState, StructuredSerializedData, WindowSizeType, }; use crossbeam_channel::{RecvTimeoutError, Sender}; @@ -195,7 +195,7 @@ pub enum ScriptThreadMessage { /// Passes a webdriver command to the script thread for execution WebDriverScriptCommand(PipelineId, WebDriverScriptCommand), /// Notifies script thread that all animations are done - TickAllAnimations(PipelineId, AnimationTickType), + TickAllAnimations(Vec<WebViewId>), /// Notifies the script thread that a new Web font has been loaded, and thus the page should be /// reflowed. WebFontLoaded(PipelineId, bool /* success */), diff --git a/components/shared/script_layout/lib.rs b/components/shared/script_layout/lib.rs index 9b052642c32..a40b8c403c1 100644 --- a/components/shared/script_layout/lib.rs +++ b/components/shared/script_layout/lib.rs @@ -27,7 +27,7 @@ use fonts::{FontContext, SystemFontServiceProxy}; use fxhash::FxHashMap; use ipc_channel::ipc::IpcSender; use libc::c_void; -use malloc_size_of::MallocSizeOfOps; +use malloc_size_of::{MallocSizeOf as MallocSizeOfTrait, MallocSizeOfOps}; use malloc_size_of_derive::MallocSizeOf; use net_traits::image_cache::{ImageCache, PendingImageId}; use pixels::Image; @@ -51,7 +51,11 @@ use style::selector_parser::{PseudoElement, RestyleDamage, Snapshot}; use style::stylesheets::Stylesheet; use webrender_api::ImageKey; -pub type GenericLayoutData = dyn Any + Send + Sync; +pub trait GenericLayoutDataTrait: Any + MallocSizeOfTrait { + fn as_any(&self) -> &dyn Any; +} + +pub type GenericLayoutData = dyn GenericLayoutDataTrait + Send + Sync; #[derive(MallocSizeOf)] pub struct StyleData { @@ -59,7 +63,6 @@ pub struct StyleData { /// style system is being used standalone, this is all that hangs /// off the node. This must be first to permit the various /// transmutations between ElementData and PersistentLayoutData. - #[ignore_malloc_size_of = "This probably should not be ignored"] pub element_data: AtomicRefCell<ElementData>, /// Information needed during parallel traversals. @@ -240,16 +243,16 @@ pub trait Layout { /// Set the scroll states of this layout after a compositor scroll. fn set_scroll_offsets(&mut self, scroll_states: &[ScrollState]); - fn query_content_box(&self, node: OpaqueNode) -> Option<Rect<Au>>; - fn query_content_boxes(&self, node: OpaqueNode) -> Vec<Rect<Au>>; - fn query_client_rect(&self, node: OpaqueNode) -> Rect<i32>; + fn query_content_box(&self, node: TrustedNodeAddress) -> Option<Rect<Au>>; + fn query_content_boxes(&self, node: TrustedNodeAddress) -> Vec<Rect<Au>>; + fn query_client_rect(&self, node: TrustedNodeAddress) -> Rect<i32>; fn query_element_inner_outer_text(&self, node: TrustedNodeAddress) -> String; fn query_nodes_from_point( &self, point: Point2D<f32>, query_type: NodesFromPointQueryType, ) -> Vec<UntrustedNodeAddress>; - fn query_offset_parent(&self, node: OpaqueNode) -> OffsetParentResponse; + fn query_offset_parent(&self, node: TrustedNodeAddress) -> OffsetParentResponse; fn query_resolved_style( &self, node: TrustedNodeAddress, @@ -265,7 +268,7 @@ pub trait Layout { animations: DocumentAnimationSet, animation_timeline_value: f64, ) -> Option<ServoArc<Font>>; - fn query_scrolling_area(&self, node: Option<OpaqueNode>) -> Rect<i32>; + fn query_scrolling_area(&self, node: Option<TrustedNodeAddress>) -> Rect<i32>; fn query_text_indext(&self, node: OpaqueNode, point: Point2D<f32>) -> Option<usize>; } diff --git a/components/shared/script_layout/wrapper_traits.rs b/components/shared/script_layout/wrapper_traits.rs index be27050a42f..6c4de339c1b 100644 --- a/components/shared/script_layout/wrapper_traits.rs +++ b/components/shared/script_layout/wrapper_traits.rs @@ -25,11 +25,11 @@ use style::selector_parser::{PseudoElement, PseudoElementCascadeType, SelectorIm use style::stylist::RuleInclusion; use crate::{ - FragmentType, GenericLayoutData, HTMLCanvasData, HTMLMediaData, LayoutNodeType, SVGSVGData, - StyleData, + FragmentType, GenericLayoutData, GenericLayoutDataTrait, HTMLCanvasData, HTMLMediaData, + LayoutNodeType, SVGSVGData, StyleData, }; -pub trait LayoutDataTrait: Default + Send + Sync + 'static {} +pub trait LayoutDataTrait: GenericLayoutDataTrait + Default + Send + Sync + 'static {} /// A wrapper so that layout can access only the methods that it should have access to. Layout must /// only ever see these and must never see instances of `LayoutDom`. diff --git a/components/shared/snapshot/Cargo.toml b/components/shared/snapshot/Cargo.toml new file mode 100644 index 00000000000..9d5bd555623 --- /dev/null +++ b/components/shared/snapshot/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "snapshot" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +publish.workspace = true +rust-version.workspace = true + +[lib] +name = "snapshot" +path = "lib.rs" + + +[dependencies] +euclid = { workspace = true } +ipc-channel = { workspace = true } +serde = { workspace = true } +pixels = { path = "../../pixels" } diff --git a/components/shared/snapshot/lib.rs b/components/shared/snapshot/lib.rs new file mode 100644 index 00000000000..5a8c1a5fbc0 --- /dev/null +++ b/components/shared/snapshot/lib.rs @@ -0,0 +1,305 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::ops::{Deref, DerefMut}; + +use euclid::default::Size2D; +use ipc_channel::ipc::IpcSharedMemory; +use pixels::Multiply; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub enum PixelFormat { + #[default] + RGBA, + BGRA, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum AlphaMode { + /// Internal data is opaque (alpha is cleared to 1) + Opaque, + /// Internal data should be threated as opaque (does not mean it actually is) + AsOpaque { premultiplied: bool }, + /// Data is not opaque + Transparent { premultiplied: bool }, +} + +impl Default for AlphaMode { + fn default() -> Self { + Self::Transparent { + premultiplied: true, + } + } +} + +impl AlphaMode { + pub const fn is_premultiplied(&self) -> bool { + match self { + AlphaMode::Opaque => true, + AlphaMode::AsOpaque { premultiplied } => *premultiplied, + AlphaMode::Transparent { premultiplied } => *premultiplied, + } + } + + pub const fn is_opaque(&self) -> bool { + matches!(self, AlphaMode::Opaque | AlphaMode::AsOpaque { .. }) + } +} + +#[derive(Debug)] +pub enum Data { + // TODO: https://github.com/servo/servo/issues/36594 + //IPC(IpcSharedMemory), + Owned(Vec<u8>), +} + +impl Deref for Data { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match &self { + //Data::IPC(ipc_shared_memory) => ipc_shared_memory, + Data::Owned(items) => items, + } + } +} + +impl DerefMut for Data { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + //Data::IPC(ipc_shared_memory) => unsafe { ipc_shared_memory.deref_mut() }, + Data::Owned(items) => items, + } + } +} + +pub type IpcSnapshot = Snapshot<IpcSharedMemory>; + +/// Represents image bitmap with metadata, usually as snapshot of canvas +/// +/// This allows us to hold off conversions (BGRA <-> RGBA, (un)premultiply) +/// to when/if they are actually needed (WebGL/WebGPU can load both BGRA and RGBA). +/// +/// Inspired by snapshot for concept in WebGPU spec: +/// <https://gpuweb.github.io/gpuweb/#abstract-opdef-get-a-copy-of-the-image-contents-of-a-context> +#[derive(Debug, Deserialize, Serialize)] +pub struct Snapshot<T = Data> { + size: Size2D<u64>, + /// internal data (can be any format it will be converted on use if needed) + data: T, + /// RGBA/BGRA (reflect internal data) + format: PixelFormat, + /// How to treat alpha channel + alpha_mode: AlphaMode, +} + +impl<T> Snapshot<T> { + pub const fn size(&self) -> Size2D<u64> { + self.size + } + + pub const fn format(&self) -> PixelFormat { + self.format + } + + pub const fn alpha_mode(&self) -> AlphaMode { + self.alpha_mode + } + + pub const fn is_premultiplied(&self) -> bool { + self.alpha_mode().is_premultiplied() + } + + pub const fn is_opaque(&self) -> bool { + self.alpha_mode().is_opaque() + } +} + +impl Snapshot<Data> { + pub fn empty() -> Self { + Self { + size: Size2D::zero(), + data: Data::Owned(vec![]), + format: PixelFormat::RGBA, + alpha_mode: AlphaMode::Transparent { + premultiplied: true, + }, + } + } + + /// Returns snapshot with provided size that is black transparent alpha + pub fn cleared(size: Size2D<u64>) -> Self { + Self { + size, + data: Data::Owned(vec![0; size.area() as usize * 4]), + format: PixelFormat::RGBA, + alpha_mode: AlphaMode::Transparent { + premultiplied: true, + }, + } + } + + pub fn from_vec( + size: Size2D<u64>, + format: PixelFormat, + alpha_mode: AlphaMode, + data: Vec<u8>, + ) -> Self { + Self { + size, + data: Data::Owned(data), + format, + alpha_mode, + } + } + + pub fn from_shared_memory( + size: Size2D<u64>, + format: PixelFormat, + alpha_mode: AlphaMode, + ism: IpcSharedMemory, + ) -> Self { + Self { + size, + data: Data::Owned(ism.to_vec()), + format, + alpha_mode, + } + } + + // TODO: https://github.com/servo/servo/issues/36594 + /* + /// # Safety + /// + /// This is safe if data is owned by this process only + /// (ownership is transferred on send) + pub unsafe fn from_shared_memory( + size: Size2D<u64>, + format: PixelFormat, + alpha_mode: AlphaMode, + ism: IpcSharedMemory, + ) -> Self { + Self { + size, + data: Data::IPC(ism), + format, + alpha_mode, + } + } + */ + + pub fn data(&self) -> &[u8] { + &self.data + } + + /// Convert inner data of snapshot to target format and alpha mode. + /// If data is already in target format and alpha mode no work will be done. + pub fn transform(&mut self, target_alpha_mode: AlphaMode, target_format: PixelFormat) { + let swap_rb = target_format != self.format; + let multiply = match (self.alpha_mode, target_alpha_mode) { + (AlphaMode::Opaque, _) => Multiply::None, + (alpha_mode, AlphaMode::Opaque) => { + if alpha_mode.is_premultiplied() { + Multiply::UnMultiply + } else { + Multiply::None + } + }, + ( + AlphaMode::Transparent { premultiplied } | AlphaMode::AsOpaque { premultiplied }, + AlphaMode::Transparent { + premultiplied: target_premultiplied, + } | + AlphaMode::AsOpaque { + premultiplied: target_premultiplied, + }, + ) => { + if premultiplied == target_premultiplied { + Multiply::None + } else if target_premultiplied { + Multiply::PreMultiply + } else { + Multiply::UnMultiply + } + }, + }; + let clear_alpha = !matches!(self.alpha_mode, AlphaMode::Opaque) && + matches!(target_alpha_mode, AlphaMode::Opaque); + pixels::transform_inplace(self.data.deref_mut(), multiply, swap_rb, clear_alpha); + self.alpha_mode = target_alpha_mode; + self.format = target_format; + } + + pub fn as_ipc(self) -> Snapshot<IpcSharedMemory> { + let Snapshot { + size, + data, + format, + alpha_mode, + } = self; + let data = match data { + //Data::IPC(ipc_shared_memory) => ipc_shared_memory, + Data::Owned(items) => IpcSharedMemory::from_bytes(&items), + }; + Snapshot { + size, + data, + format, + alpha_mode, + } + } + + pub fn to_vec(self) -> Vec<u8> { + match self.data { + Data::Owned(data) => data, + } + } +} + +impl Snapshot<IpcSharedMemory> { + // TODO: https://github.com/servo/servo/issues/36594 + /* + /// # Safety + /// + /// This is safe if data is owned by this process only + /// (ownership is transferred on send) + pub unsafe fn to_data(self) -> Snapshot<Data> { + let Snapshot { + size, + data, + format, + alpha_mode, + } = self; + Snapshot { + size, + data: Data::IPC(data), + format, + alpha_mode, + } + } + */ + pub fn to_owned(self) -> Snapshot<Data> { + let Snapshot { + size, + data, + format, + alpha_mode, + } = self; + Snapshot { + size, + data: Data::Owned(data.to_vec()), + format, + alpha_mode, + } + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn to_ipc_shared_memory(self) -> IpcSharedMemory { + self.data + } +} diff --git a/components/shared/webgpu/Cargo.toml b/components/shared/webgpu/Cargo.toml index 0a4aa4e7ff3..04efea0cb9c 100644 --- a/components/shared/webgpu/Cargo.toml +++ b/components/shared/webgpu/Cargo.toml @@ -20,3 +20,4 @@ serde = { workspace = true } webrender_api = { workspace = true } wgpu-core = { workspace = true, features = ["serde", "wgsl"] } wgpu-types = { workspace = true } +snapshot = { workspace = true } diff --git a/components/shared/webgpu/messages/recv.rs b/components/shared/webgpu/messages/recv.rs index 47c32437e45..4a30177cf0a 100644 --- a/components/shared/webgpu/messages/recv.rs +++ b/components/shared/webgpu/messages/recv.rs @@ -9,6 +9,7 @@ use arrayvec::ArrayVec; use base::id::PipelineId; use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; use serde::{Deserialize, Serialize}; +use snapshot::IpcSnapshot; use webrender_api::ImageKey; use webrender_api::units::DeviceIntSize; use wgpu_core::Label; @@ -165,7 +166,7 @@ pub enum WebGPURequest { /// Obtains image from latest presentation buffer (same as wr update) GetImage { context_id: WebGPUContextId, - sender: IpcSender<IpcSharedMemory>, + sender: IpcSender<IpcSnapshot>, }, ValidateTextureDescriptor { device_id: DeviceId, diff --git a/components/webdriver_server/lib.rs b/components/webdriver_server/lib.rs index e9bec44afaa..ce83a8f3cc1 100644 --- a/components/webdriver_server/lib.rs +++ b/components/webdriver_server/lib.rs @@ -1505,7 +1505,7 @@ impl Handler { .iter() .map(webdriver_value_to_js_argument) .collect(); - args_string.push("window.webdriverCallback".to_string()); + args_string.push("resolve".to_string()); let timeout_script = if let Some(script_timeout) = self.session()?.script_timeout { format!("setTimeout(webdriverTimeout, {});", script_timeout) @@ -1513,7 +1513,17 @@ impl Handler { "".into() }; let script = format!( - "{} (function() {{ {}\n }})({})", + r#"(function() {{ + let webdriverPromise = new Promise(function(resolve, reject) {{ + {} + (async function() {{ + {} + }})({}) + .then((v) => {{}}, (err) => reject(err)) + }}) + .then((v) => window.webdriverCallback(v), (r) => window.webdriverException(r)) + .catch((r) => window.webdriverException(r)); + }})();"#, timeout_script, func_body, args_string.join(", "), @@ -1541,15 +1551,21 @@ impl Handler { ErrorStatus::NoSuchWindow, "Pipeline id not found in browsing context", )), - Err(WebDriverJSError::JSError) => Err(WebDriverError::new( + Err(WebDriverJSError::JSException(_e)) => Err(WebDriverError::new( ErrorStatus::JavascriptError, "JS evaluation raised an exception", )), + Err(WebDriverJSError::JSError) => Err(WebDriverError::new( + ErrorStatus::JavascriptError, + "JS evaluation raised an unknown exception", + )), Err(WebDriverJSError::StaleElementReference) => Err(WebDriverError::new( ErrorStatus::StaleElementReference, "Stale element", )), - Err(WebDriverJSError::Timeout) => Err(WebDriverError::new(ErrorStatus::Timeout, "")), + Err(WebDriverJSError::Timeout) => { + Err(WebDriverError::new(ErrorStatus::ScriptTimeout, "")) + }, Err(WebDriverJSError::UnknownType) => Err(WebDriverError::new( ErrorStatus::UnsupportedOperation, "Unsupported return type", diff --git a/components/webgpu/Cargo.toml b/components/webgpu/Cargo.toml index f25f160b1b0..39ca562affe 100644 --- a/components/webgpu/Cargo.toml +++ b/components/webgpu/Cargo.toml @@ -22,6 +22,7 @@ malloc_size_of = { workspace = true } serde = { workspace = true, features = ["serde_derive"] } servo_config = { path = "../config" } webgpu_traits = { workspace = true } +snapshot = { workspace = true } webrender = { workspace = true } webrender_api = { workspace = true } wgpu-core = { workspace = true, features = ["serde", "wgsl"] } diff --git a/components/webgpu/swapchain.rs b/components/webgpu/swapchain.rs index 6d61540581b..a3ca15b638d 100644 --- a/components/webgpu/swapchain.rs +++ b/components/webgpu/swapchain.rs @@ -10,9 +10,10 @@ use std::sync::{Arc, Mutex}; use arrayvec::ArrayVec; use compositing_traits::{WebrenderExternalImageApi, WebrenderImageSource}; use euclid::default::Size2D; -use ipc_channel::ipc::{IpcSender, IpcSharedMemory}; +use ipc_channel::ipc::IpcSender; use log::{error, warn}; use serde::{Deserialize, Serialize}; +use snapshot::{IpcSnapshot, Snapshot}; use webgpu_traits::{ ContextConfiguration, Error, PRESENTATION_BUFFER_COUNT, WebGPUContextId, WebGPUMsg, }; @@ -364,20 +365,34 @@ impl crate::WGPU { ); } - pub(crate) fn get_image(&self, context_id: WebGPUContextId) -> IpcSharedMemory { + pub(crate) fn get_image(&self, context_id: WebGPUContextId) -> IpcSnapshot { let webgpu_contexts = self.wgpu_image_map.lock().unwrap(); let context_data = webgpu_contexts.get(&context_id).unwrap(); - let buffer_size = context_data.image_desc.buffer_size(); + let size = context_data.image_desc.size().cast().cast_unit(); let data = if let Some(present_buffer) = context_data .swap_chain .as_ref() .and_then(|swap_chain| swap_chain.data.as_ref()) { - IpcSharedMemory::from_bytes(present_buffer.slice()) + let format = match context_data.image_desc.0.format { + ImageFormat::RGBA8 => snapshot::PixelFormat::RGBA, + ImageFormat::BGRA8 => snapshot::PixelFormat::BGRA, + _ => unimplemented!(), + }; + let alpha_mode = if context_data.image_desc.0.is_opaque() { + snapshot::AlphaMode::AsOpaque { + premultiplied: false, + } + } else { + snapshot::AlphaMode::Transparent { + premultiplied: true, + } + }; + Snapshot::from_vec(size, format, alpha_mode, present_buffer.slice().to_vec()) } else { - IpcSharedMemory::from_byte(0, buffer_size as usize) + Snapshot::cleared(size) }; - data + data.as_ipc() } pub(crate) fn update_context( |