diff options
Diffstat (limited to 'components')
73 files changed, 1988 insertions, 644 deletions
diff --git a/components/canvas/Cargo.toml b/components/canvas/Cargo.toml index 7e7b00efe11..6084fc6e434 100644 --- a/components/canvas/Cargo.toml +++ b/components/canvas/Cargo.toml @@ -11,24 +11,15 @@ rust-version.workspace = true name = "canvas" path = "lib.rs" -[features] -webgl_backtrace = ["canvas_traits/webgl_backtrace"] -webxr = ["dep:webxr", "dep:webxr-api"] - [dependencies] app_units = { workspace = true } -bitflags = { workspace = true } -byteorder = { workspace = true } canvas_traits = { workspace = true } compositing_traits = { workspace = true } crossbeam-channel = { workspace = true } cssparser = { workspace = true } euclid = { workspace = true } -fnv = { workspace = true } font-kit = "0.14" fonts = { path = "../fonts" } -glow = { workspace = true } -half = "2" ipc-channel = { workspace = true } log = { workspace = true } lyon_geom = "1.0.4" @@ -40,9 +31,5 @@ raqote = "0.8.5" servo_arc = { workspace = true } snapshot = { workspace = true } stylo = { workspace = true } -surfman = { workspace = true } unicode-script = { workspace = true } -webrender = { workspace = true } webrender_api = { workspace = true } -webxr = { path = "../webxr", features = ["ipc"], optional = true } -webxr-api = { workspace = true, features = ["ipc"], optional = true } diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index 99d6273813e..2667b7f6b44 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -28,6 +28,10 @@ use webrender_api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat, ImageKey use crate::raqote_backend::Repetition; +// Asserts on WR texture cache update for zero sized image with raw data. +// https://github.com/servo/webrender/blob/main/webrender/src/texture_cache.rs#L1475 +const MIN_WR_IMAGE_SIZE: Size2D<u64> = Size2D::new(1, 1); + fn to_path(path: &[PathSegment], mut builder: Box<dyn GenericPathBuilder>) -> Path { let mut build_ref = PathBuilderRef { builder: &mut builder, @@ -595,6 +599,7 @@ impl<'a> CanvasData<'a> { compositor_api: CrossProcessCompositorApi, font_context: Arc<FontContext>, ) -> CanvasData<'a> { + let size = size.max(MIN_WR_IMAGE_SIZE); let backend = create_backend(); let draw_target = backend.create_drawtarget(size); let image_key = compositor_api.generate_image_key().unwrap(); @@ -1402,7 +1407,9 @@ impl<'a> CanvasData<'a> { } pub fn recreate(&mut self, size: Option<Size2D<u64>>) { - let size = size.unwrap_or_else(|| self.drawtarget.get_size().to_u64()); + let size = size + .unwrap_or_else(|| self.drawtarget.get_size().to_u64()) + .max(MIN_WR_IMAGE_SIZE); self.drawtarget = self .backend .create_drawtarget(Size2D::new(size.width, size.height)); diff --git a/components/canvas/lib.rs b/components/canvas/lib.rs index d2c62c1d8b6..86c291fdc87 100644 --- a/components/canvas/lib.rs +++ b/components/canvas/lib.rs @@ -6,12 +6,5 @@ mod raqote_backend; -pub use webgl_mode::WebGLComm; - pub mod canvas_data; pub mod canvas_paint_thread; -mod webgl_limits; -mod webgl_mode; -pub mod webgl_thread; -#[cfg(feature = "webxr")] -mod webxr; diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 2175028a81b..2f9345c416f 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -127,10 +127,10 @@ use devtools_traits::{ use embedder_traits::resources::{self, Resource}; use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::{ - AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy, ImeEvent, - InputEvent, MediaSessionActionType, MediaSessionEvent, MediaSessionPlaybackState, MouseButton, - MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverCommandMsg, - WebDriverLoadStatus, + AnimationState, CompositorHitTestResult, Cursor, EmbedderMsg, EmbedderProxy, + FocusSequenceNumber, ImeEvent, InputEvent, MediaSessionActionType, MediaSessionEvent, + MediaSessionPlaybackState, MouseButton, MouseButtonAction, MouseButtonEvent, Theme, + ViewportDetails, WebDriverCommandMsg, WebDriverLoadStatus, }; use euclid::Size2D; use euclid::default::Size2D as UntypedSize2D; @@ -1043,6 +1043,44 @@ where } } + /// Enumerate the specified browsing context's ancestor pipelines up to + /// the top-level pipeline. + fn ancestor_pipelines_of_browsing_context_iter( + &self, + browsing_context_id: BrowsingContextId, + ) -> impl Iterator<Item = &Pipeline> + '_ { + let mut state: Option<PipelineId> = self + .browsing_contexts + .get(&browsing_context_id) + .and_then(|browsing_context| browsing_context.parent_pipeline_id); + std::iter::from_fn(move || { + if let Some(pipeline_id) = state { + let pipeline = self.pipelines.get(&pipeline_id)?; + let browsing_context = self.browsing_contexts.get(&pipeline.browsing_context_id)?; + state = browsing_context.parent_pipeline_id; + Some(pipeline) + } else { + None + } + }) + } + + /// Enumerate the specified browsing context's ancestor-or-self pipelines up + /// to the top-level pipeline. + fn ancestor_or_self_pipelines_of_browsing_context_iter( + &self, + browsing_context_id: BrowsingContextId, + ) -> impl Iterator<Item = &Pipeline> + '_ { + let this_pipeline = self + .browsing_contexts + .get(&browsing_context_id) + .map(|browsing_context| browsing_context.pipeline_id) + .and_then(|pipeline_id| self.pipelines.get(&pipeline_id)); + this_pipeline + .into_iter() + .chain(self.ancestor_pipelines_of_browsing_context_iter(browsing_context_id)) + } + /// Create a new browsing context and update the internal bookkeeping. #[allow(clippy::too_many_arguments)] fn new_browsing_context( @@ -1621,8 +1659,15 @@ where data, ); }, - ScriptToConstellationMessage::Focus => { - self.handle_focus_msg(source_pipeline_id); + ScriptToConstellationMessage::Focus(focused_child_browsing_context_id, sequence) => { + self.handle_focus_msg( + source_pipeline_id, + focused_child_browsing_context_id, + sequence, + ); + }, + ScriptToConstellationMessage::FocusRemoteDocument(focused_browsing_context_id) => { + self.handle_focus_remote_document_msg(focused_browsing_context_id); }, ScriptToConstellationMessage::SetThrottledComplete(throttled) => { self.handle_set_throttled_complete(source_pipeline_id, throttled); @@ -4070,6 +4115,7 @@ where } new_pipeline.set_throttled(false); + self.notify_focus_state(new_pipeline_id); } self.update_activity(old_pipeline_id); @@ -4275,66 +4321,231 @@ where feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn handle_focus_msg(&mut self, pipeline_id: PipelineId) { - let (browsing_context_id, webview_id) = match self.pipelines.get(&pipeline_id) { - Some(pipeline) => (pipeline.browsing_context_id, pipeline.webview_id), + fn handle_focus_msg( + &mut self, + pipeline_id: PipelineId, + focused_child_browsing_context_id: Option<BrowsingContextId>, + sequence: FocusSequenceNumber, + ) { + let (browsing_context_id, webview_id) = match self.pipelines.get_mut(&pipeline_id) { + Some(pipeline) => { + pipeline.focus_sequence = sequence; + (pipeline.browsing_context_id, pipeline.webview_id) + }, None => return warn!("{}: Focus parent after closure", pipeline_id), }; + // Ignore if the pipeline isn't fully active. + if self.get_activity(pipeline_id) != DocumentActivity::FullyActive { + debug!( + "Ignoring the focus request because pipeline {} is not \ + fully active", + pipeline_id + ); + return; + } + // Focus the top-level browsing context. self.webviews.focus(webview_id); self.embedder_proxy .send(EmbedderMsg::WebViewFocused(webview_id)); - // Update the webview’s focused browsing context. - match self.webviews.get_mut(webview_id) { - Some(webview) => { - webview.focused_browsing_context_id = browsing_context_id; - }, - None => { - return warn!( - "{}: Browsing context for focus msg does not exist", - webview_id - ); - }, + // If a container with a non-null nested browsing context is focused, + // the nested browsing context's active document becomes the focused + // area of the top-level browsing context instead. + let focused_browsing_context_id = + focused_child_browsing_context_id.unwrap_or(browsing_context_id); + + // Send focus messages to the affected pipelines, except + // `pipeline_id`, which has already its local focus state + // updated. + self.focus_browsing_context(Some(pipeline_id), focused_browsing_context_id); + } + + fn handle_focus_remote_document_msg(&mut self, focused_browsing_context_id: BrowsingContextId) { + let pipeline_id = match self.browsing_contexts.get(&focused_browsing_context_id) { + Some(browsing_context) => browsing_context.pipeline_id, + None => return warn!("Browsing context {} not found", focused_browsing_context_id), }; - // Focus parent iframes recursively - self.focus_parent_pipeline(browsing_context_id); + // Ignore if its active document isn't fully active. + if self.get_activity(pipeline_id) != DocumentActivity::FullyActive { + debug!( + "Ignoring the remote focus request because pipeline {} of \ + browsing context {} is not fully active", + pipeline_id, focused_browsing_context_id, + ); + return; + } + + self.focus_browsing_context(None, focused_browsing_context_id); } + /// Perform [the focusing steps][1] for the active document of + /// `focused_browsing_context_id`. + /// + /// If `initiator_pipeline_id` is specified, this method avoids sending + /// a message to `initiator_pipeline_id`, assuming its local focus state has + /// already been updated. This is necessary for performing the focusing + /// steps for an object that is not the document itself but something that + /// belongs to the document. + /// + /// [1]: https://html.spec.whatwg.org/multipage/#focusing-steps #[cfg_attr( feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] - fn focus_parent_pipeline(&mut self, browsing_context_id: BrowsingContextId) { - let parent_pipeline_id = match self.browsing_contexts.get(&browsing_context_id) { - Some(ctx) => ctx.parent_pipeline_id, - None => { - return warn!("{}: Focus parent after closure", browsing_context_id); - }, + fn focus_browsing_context( + &mut self, + initiator_pipeline_id: Option<PipelineId>, + focused_browsing_context_id: BrowsingContextId, + ) { + let webview_id = match self.browsing_contexts.get(&focused_browsing_context_id) { + Some(browsing_context) => browsing_context.top_level_id, + None => return warn!("Browsing context {} not found", focused_browsing_context_id), }; - let parent_pipeline_id = match parent_pipeline_id { - Some(parent_id) => parent_id, + + // Update the webview’s focused browsing context. + let old_focused_browsing_context_id = match self.webviews.get_mut(webview_id) { + Some(browser) => replace( + &mut browser.focused_browsing_context_id, + focused_browsing_context_id, + ), None => { - return debug!("{}: Focus has no parent", browsing_context_id); + return warn!( + "{}: Browsing context for focus msg does not exist", + webview_id + ); }, }; - // Send a message to the parent of the provided browsing context (if it - // exists) telling it to mark the iframe element as focused. - let msg = ScriptThreadMessage::FocusIFrame(parent_pipeline_id, browsing_context_id); - let (result, parent_browsing_context_id) = match self.pipelines.get(&parent_pipeline_id) { - Some(pipeline) => { - let result = pipeline.event_loop.send(msg); - (result, pipeline.browsing_context_id) + // The following part is similar to [the focus update steps][1] except + // that only `Document`s in the given focus chains are considered. It's + // ultimately up to the script threads to fire focus events at the + // affected objects. + // + // [1]: https://html.spec.whatwg.org/multipage/#focus-update-steps + let mut old_focus_chain_pipelines: Vec<&Pipeline> = self + .ancestor_or_self_pipelines_of_browsing_context_iter(old_focused_browsing_context_id) + .collect(); + let mut new_focus_chain_pipelines: Vec<&Pipeline> = self + .ancestor_or_self_pipelines_of_browsing_context_iter(focused_browsing_context_id) + .collect(); + + debug!( + "old_focus_chain_pipelines = {:?}", + old_focus_chain_pipelines + .iter() + .map(|p| p.id.to_string()) + .collect::<Vec<_>>() + ); + debug!( + "new_focus_chain_pipelines = {:?}", + new_focus_chain_pipelines + .iter() + .map(|p| p.id.to_string()) + .collect::<Vec<_>>() + ); + + // At least the last entries should match. Otherwise something is wrong, + // and we don't want to proceed and crash the top-level pipeline by + // sending an impossible `Unfocus` message to it. + match ( + &old_focus_chain_pipelines[..], + &new_focus_chain_pipelines[..], + ) { + ([.., p1], [.., p2]) if p1.id == p2.id => {}, + _ => { + warn!("Aborting the focus operation - focus chain sanity check failed"); + return; }, - None => return warn!("{}: Focus after closure", parent_pipeline_id), - }; - if let Err(e) = result { - self.handle_send_error(parent_pipeline_id, e); } - self.focus_parent_pipeline(parent_browsing_context_id); + + // > If the last entry in `old chain` and the last entry in `new chain` + // > are the same, pop the last entry from `old chain` and the last + // > entry from `new chain` and redo this step. + let mut first_common_pipeline_in_chain = None; + while let ([.., p1], [.., p2]) = ( + &old_focus_chain_pipelines[..], + &new_focus_chain_pipelines[..], + ) { + if p1.id != p2.id { + break; + } + old_focus_chain_pipelines.pop(); + first_common_pipeline_in_chain = new_focus_chain_pipelines.pop(); + } + + let mut send_errors = Vec::new(); + + // > For each entry `entry` in `old chain`, in order, run these + // > substeps: [...] + for &pipeline in old_focus_chain_pipelines.iter() { + if Some(pipeline.id) != initiator_pipeline_id { + let msg = ScriptThreadMessage::Unfocus(pipeline.id, pipeline.focus_sequence); + trace!("Sending {:?} to {}", msg, pipeline.id); + if let Err(e) = pipeline.event_loop.send(msg) { + send_errors.push((pipeline.id, e)); + } + } else { + trace!( + "Not notifying {} - it's the initiator of this focus operation", + pipeline.id + ); + } + } + + // > For each entry entry in `new chain`, in reverse order, run these + // > substeps: [...] + let mut child_browsing_context_id = None; + for &pipeline in new_focus_chain_pipelines.iter().rev() { + // Don't send a message to the browsing context that initiated this + // focus operation. It already knows that it has gotten focus. + if Some(pipeline.id) != initiator_pipeline_id { + let msg = if let Some(child_browsing_context_id) = child_browsing_context_id { + // Focus the container element of `child_browsing_context_id`. + ScriptThreadMessage::FocusIFrame( + pipeline.id, + child_browsing_context_id, + pipeline.focus_sequence, + ) + } else { + // Focus the document. + ScriptThreadMessage::FocusDocument(pipeline.id, pipeline.focus_sequence) + }; + trace!("Sending {:?} to {}", msg, pipeline.id); + if let Err(e) = pipeline.event_loop.send(msg) { + send_errors.push((pipeline.id, e)); + } + } else { + trace!( + "Not notifying {} - it's the initiator of this focus operation", + pipeline.id + ); + } + child_browsing_context_id = Some(pipeline.browsing_context_id); + } + + if let (Some(pipeline), Some(child_browsing_context_id)) = + (first_common_pipeline_in_chain, child_browsing_context_id) + { + if Some(pipeline.id) != initiator_pipeline_id { + // Focus the container element of `child_browsing_context_id`. + let msg = ScriptThreadMessage::FocusIFrame( + pipeline.id, + child_browsing_context_id, + pipeline.focus_sequence, + ); + trace!("Sending {:?} to {}", msg, pipeline.id); + if let Err(e) = pipeline.event_loop.send(msg) { + send_errors.push((pipeline.id, e)); + } + } + } + + for (pipeline_id, e) in send_errors { + self.handle_send_error(pipeline_id, e); + } } #[cfg_attr( @@ -4929,10 +5140,42 @@ where self.trim_history(top_level_id); } + self.notify_focus_state(change.new_pipeline_id); + self.notify_history_changed(change.webview_id); self.update_webview_in_compositor(change.webview_id); } + /// Update the focus state of the specified pipeline that recently became + /// active (thus doesn't have a focused container element) and may have + /// out-dated information. + fn notify_focus_state(&mut self, pipeline_id: PipelineId) { + let pipeline = match self.pipelines.get(&pipeline_id) { + Some(pipeline) => pipeline, + None => return warn!("Pipeline {} is closed", pipeline_id), + }; + + let is_focused = match self.webviews.get(pipeline.webview_id) { + Some(webview) => webview.focused_browsing_context_id == pipeline.browsing_context_id, + None => { + return warn!( + "Pipeline {}'s top-level browsing context {} is closed", + pipeline_id, pipeline.webview_id + ); + }, + }; + + // If the browsing context is focused, focus the document + let msg = if is_focused { + ScriptThreadMessage::FocusDocument(pipeline_id, pipeline.focus_sequence) + } else { + ScriptThreadMessage::Unfocus(pipeline_id, pipeline.focus_sequence) + }; + if let Err(e) = pipeline.event_loop.send(msg) { + self.handle_send_error(pipeline_id, e); + } + } + #[cfg_attr( feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") @@ -5382,7 +5625,29 @@ where None => { warn!("{parent_pipeline_id}: Child closed after parent"); }, - Some(parent_pipeline) => parent_pipeline.remove_child(browsing_context_id), + Some(parent_pipeline) => { + parent_pipeline.remove_child(browsing_context_id); + + // If `browsing_context_id` has focus, focus the parent + // browsing context + if let Some(webview) = self.webviews.get_mut(browsing_context.top_level_id) { + if webview.focused_browsing_context_id == browsing_context_id { + trace!( + "About-to-be-closed browsing context {} is currently focused, so \ + focusing its parent {}", + browsing_context_id, parent_pipeline.browsing_context_id + ); + webview.focused_browsing_context_id = + parent_pipeline.browsing_context_id; + } + } else { + warn!( + "Browsing context {} contains a reference to \ + a non-existent top-level browsing context {}", + browsing_context_id, browsing_context.top_level_id + ); + } + }, }; } debug!("{}: Closed", browsing_context_id); diff --git a/components/constellation/pipeline.rs b/components/constellation/pipeline.rs index 2e139578ffe..556ef9bd60f 100644 --- a/components/constellation/pipeline.rs +++ b/components/constellation/pipeline.rs @@ -25,7 +25,7 @@ use constellation_traits::{LoadData, SWManagerMsg, ScriptToConstellationChan}; use crossbeam_channel::{Sender, unbounded}; use devtools_traits::{DevtoolsControlMsg, ScriptToDevtoolsControlMsg}; use embedder_traits::user_content_manager::UserContentManager; -use embedder_traits::{AnimationState, ViewportDetails}; +use embedder_traits::{AnimationState, FocusSequenceNumber, ViewportDetails}; use fonts::{SystemFontServiceProxy, SystemFontServiceProxySender}; use ipc_channel::Error; use ipc_channel::ipc::{self, IpcReceiver, IpcSender}; @@ -102,6 +102,8 @@ pub struct Pipeline { /// The last compositor [`Epoch`] that was laid out in this pipeline if "exit after load" is /// enabled. pub layout_epoch: Epoch, + + pub focus_sequence: FocusSequenceNumber, } /// Initial setup data needed to construct a pipeline. @@ -370,6 +372,7 @@ impl Pipeline { completely_loaded: false, title: String::new(), layout_epoch: Epoch(0), + focus_sequence: FocusSequenceNumber::default(), }; pipeline.set_throttled(throttled); diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index a939bbafc48..5c9a09e1f13 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -138,7 +138,8 @@ mod from_script { Self::BroadcastStorageEvent(..) => target!("BroadcastStorageEvent"), Self::ChangeRunningAnimationsState(..) => target!("ChangeRunningAnimationsState"), Self::CreateCanvasPaintThread(..) => target!("CreateCanvasPaintThread"), - Self::Focus => target!("Focus"), + Self::Focus(..) => target!("Focus"), + Self::FocusRemoteDocument(..) => target!("FocusRemoteDocument"), Self::GetTopForBrowsingContext(..) => target!("GetTopForBrowsingContext"), Self::GetBrowsingContextInfo(..) => target!("GetBrowsingContextInfo"), Self::GetChildBrowsingContextId(..) => target!("GetChildBrowsingContextId"), diff --git a/components/devtools/actors/watcher.rs b/components/devtools/actors/watcher.rs index 6a84499b6dd..b0b2c755fd8 100644 --- a/components/devtools/actors/watcher.rs +++ b/components/devtools/actors/watcher.rs @@ -20,8 +20,10 @@ use serde_json::{Map, Value}; use self::network_parent::{NetworkParentActor, NetworkParentActorMsg}; use super::thread::ThreadActor; +use super::worker::WorkerMsg; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::actors::browsing_context::{BrowsingContextActor, BrowsingContextActorMsg}; +use crate::actors::root::RootActor; use crate::actors::watcher::target_configuration::{ TargetConfigurationActor, TargetConfigurationActorMsg, }; @@ -29,8 +31,8 @@ use crate::actors::watcher::thread_configuration::{ ThreadConfigurationActor, ThreadConfigurationActorMsg, }; use crate::protocol::JsonPacketStream; -use crate::resource::ResourceAvailable; -use crate::{EmptyReplyMsg, StreamId}; +use crate::resource::{ResourceAvailable, ResourceAvailableReply}; +use crate::{EmptyReplyMsg, StreamId, WorkerActor}; pub mod network_parent; pub mod target_configuration; @@ -55,7 +57,7 @@ impl SessionContext { supported_targets: HashMap::from([ ("frame", true), ("process", false), - ("worker", false), + ("worker", true), ("service_worker", false), ("shared_worker", false), ]), @@ -103,11 +105,18 @@ pub enum SessionContextType { } #[derive(Serialize)] +#[serde(untagged)] +enum TargetActorMsg { + BrowsingContext(BrowsingContextActorMsg), + Worker(WorkerMsg), +} + +#[derive(Serialize)] struct WatchTargetsReply { from: String, #[serde(rename = "type")] type_: String, - target: BrowsingContextActorMsg, + target: TargetActorMsg, } #[derive(Serialize)] @@ -212,16 +221,38 @@ impl Actor for WatcherActor { _id: StreamId, ) -> Result<ActorMessageStatus, ()> { let target = registry.find::<BrowsingContextActor>(&self.browsing_context_actor); + let root = registry.find::<RootActor>("root"); Ok(match msg_type { "watchTargets" => { - let msg = WatchTargetsReply { - from: self.name(), - type_: "target-available-form".into(), - target: target.encodable(), - }; - let _ = stream.write_json_packet(&msg); + // As per logs we either get targetType as "frame" or "worker" + let target_type = msg + .get("targetType") + .and_then(Value::as_str) + .unwrap_or("frame"); // default to "frame" + + if target_type == "frame" { + let msg = WatchTargetsReply { + from: self.name(), + type_: "target-available-form".into(), + target: TargetActorMsg::BrowsingContext(target.encodable()), + }; + let _ = stream.write_json_packet(&msg); - target.frame_update(stream); + target.frame_update(stream); + } else if target_type == "worker" { + for worker_name in &root.workers { + let worker = registry.find::<WorkerActor>(worker_name); + let worker_msg = WatchTargetsReply { + from: self.name(), + type_: "target-available-form".into(), + target: TargetActorMsg::Worker(worker.encodable()), + }; + let _ = stream.write_json_packet(&worker_msg); + } + } else { + warn!("Unexpected target_type: {}", target_type); + return Ok(ActorMessageStatus::Ignored); + } // Messages that contain a `type` field are used to send event callbacks, but they // don't count as a reply. Since every message needs to be responded, we send an @@ -267,6 +298,22 @@ impl Actor for WatcherActor { let thread_actor = registry.find::<ThreadActor>(&target.thread); let sources = thread_actor.source_manager.sources(); target.resources_available(sources.iter().collect(), "source".into()); + + for worker_name in &root.workers { + let worker = registry.find::<WorkerActor>(worker_name); + let thread = registry.find::<ThreadActor>(&worker.thread); + let worker_sources = thread.source_manager.sources(); + + let msg = ResourceAvailableReply { + from: worker.name(), + type_: "resources-available-array".into(), + array: vec![( + "source".to_string(), + worker_sources.iter().cloned().collect(), + )], + }; + let _ = stream.write_json_packet(&msg); + } }, "console-message" | "error-message" => {}, _ => warn!("resource {} not handled yet", resource), diff --git a/components/devtools/actors/worker.rs b/components/devtools/actors/worker.rs index 42c9d9a9c28..68ff56fb3b2 100644 --- a/components/devtools/actors/worker.rs +++ b/components/devtools/actors/worker.rs @@ -17,7 +17,7 @@ use servo_url::ServoUrl; use crate::StreamId; use crate::actor::{Actor, ActorMessageStatus, ActorRegistry}; use crate::protocol::JsonPacketStream; -use crate::resource::ResourceAvailable; +use crate::resource::{ResourceAvailable, ResourceAvailableReply}; #[derive(Clone, Copy)] #[allow(dead_code)] @@ -48,8 +48,10 @@ impl WorkerActor { url: self.url.to_string(), traits: WorkerTraits { is_parent_intercept_enabled: false, + supports_top_level_target_flag: false, }, type_: self.type_ as u32, + target_type: "worker".to_string(), } } } @@ -131,6 +133,28 @@ impl Actor for WorkerActor { } } +impl WorkerActor { + 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); + } + } +} + #[derive(Serialize)] struct DetachedReply { from: String, @@ -160,6 +184,7 @@ struct ConnectReply { #[serde(rename_all = "camelCase")] struct WorkerTraits { is_parent_intercept_enabled: bool, + supports_top_level_target_flag: bool, } #[derive(Serialize)] @@ -173,4 +198,6 @@ pub(crate) struct WorkerMsg { traits: WorkerTraits, #[serde(rename = "type")] type_: u32, + #[serde(rename = "targetType")] + target_type: String, } diff --git a/components/devtools/lib.rs b/components/devtools/lib.rs index 4d1e0222177..5fb9485e9d3 100644 --- a/components/devtools/lib.rs +++ b/components/devtools/lib.rs @@ -510,35 +510,54 @@ impl DevtoolsInstance { fn handle_script_source_info(&mut self, pipeline_id: PipelineId, source_info: SourceInfo) { let mut actors = self.actors.lock().unwrap(); - let browsing_context_id = match self.pipelines.get(&pipeline_id) { - Some(id) => id, - None => return, - }; + if let Some(worker_id) = source_info.worker_id { + let Some(worker_actor_name) = self.actor_workers.get(&worker_id) else { + return; + }; - let actor_name = match self.browsing_contexts.get(browsing_context_id) { - Some(name) => name, - None => return, - }; + let thread_actor_name = actors.find::<WorkerActor>(worker_actor_name).thread.clone(); - let thread_actor_name = actors - .find::<BrowsingContextActor>(actor_name) - .thread - .clone(); - - let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name); - thread_actor - .source_manager - .add_source(source_info.url.clone()); - - let source = SourceData { - actor: thread_actor_name.clone(), - url: source_info.url.to_string(), - is_black_boxed: false, - }; + let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name); + thread_actor + .source_manager + .add_source(source_info.url.clone()); + + let source = SourceData { + actor: thread_actor_name.clone(), + url: source_info.url.to_string(), + is_black_boxed: false, + }; - // Notify browsing context about the new source - let browsing_context = actors.find::<BrowsingContextActor>(actor_name); - browsing_context.resource_available(source, "source".into()); + let worker_actor = actors.find::<WorkerActor>(worker_actor_name); + worker_actor.resource_available(source, "source".into()); + } else { + let Some(browsing_context_id) = self.pipelines.get(&pipeline_id) else { + return; + }; + let Some(actor_name) = self.browsing_contexts.get(browsing_context_id) else { + return; + }; + + let thread_actor_name = actors + .find::<BrowsingContextActor>(actor_name) + .thread + .clone(); + + let thread_actor = actors.find_mut::<ThreadActor>(&thread_actor_name); + thread_actor + .source_manager + .add_source(source_info.url.clone()); + + let source = SourceData { + actor: thread_actor_name.clone(), + url: source_info.url.to_string(), + is_black_boxed: false, + }; + + // Notify browsing context about the new source + let browsing_context = actors.find::<BrowsingContextActor>(actor_name); + browsing_context.resource_available(source, "source".into()); + } } } diff --git a/components/layout/context.rs b/components/layout/context.rs index 71372ffe224..62f8a8cdae9 100644 --- a/components/layout/context.rs +++ b/components/layout/context.rs @@ -61,6 +61,20 @@ impl Drop for LayoutContext<'_> { } } +#[derive(Debug)] +pub enum ResolveImageError { + LoadError, + ImagePending, + ImageRequested, + OnlyMetadata, + InvalidUrl, + MissingNode, + ImageMissingFromImageSet, + FailedToResolveImageFromImageSet, + NotImplementedYet(&'static str), + None, +} + impl LayoutContext<'_> { #[inline(always)] pub fn shared_context(&self) -> &SharedStyleContext { @@ -72,7 +86,7 @@ impl LayoutContext<'_> { node: OpaqueNode, url: ServoUrl, use_placeholder: UsePlaceholder, - ) -> Option<ImageOrMetadataAvailable> { + ) -> Result<ImageOrMetadataAvailable, ResolveImageError> { // Check for available image or start tracking. let cache_result = self.image_cache.get_cached_image_status( url.clone(), @@ -82,7 +96,7 @@ impl LayoutContext<'_> { ); match cache_result { - ImageCacheResult::Available(img_or_meta) => Some(img_or_meta), + ImageCacheResult::Available(img_or_meta) => Ok(img_or_meta), // Image has been requested, is still pending. Return no image for this paint loop. // When the image loads it will trigger a reflow and/or repaint. ImageCacheResult::Pending(id) => { @@ -93,7 +107,7 @@ impl LayoutContext<'_> { origin: self.origin.clone(), }; self.pending_images.lock().push(image); - None + Result::Err(ResolveImageError::ImagePending) }, // Not yet requested - request image or metadata from the cache ImageCacheResult::ReadyForRequest(id) => { @@ -104,10 +118,10 @@ impl LayoutContext<'_> { origin: self.origin.clone(), }; self.pending_images.lock().push(image); - None + Result::Err(ResolveImageError::ImageRequested) }, // Image failed to load, so just return nothing - ImageCacheResult::LoadError => None, + ImageCacheResult::LoadError => Result::Err(ResolveImageError::LoadError), } } @@ -136,31 +150,34 @@ impl LayoutContext<'_> { node: OpaqueNode, url: ServoUrl, use_placeholder: UsePlaceholder, - ) -> Option<WebRenderImageInfo> { + ) -> Result<WebRenderImageInfo, ResolveImageError> { if let Some(existing_webrender_image) = self .webrender_image_cache .read() .get(&(url.clone(), use_placeholder)) { - return Some(*existing_webrender_image); + return Ok(*existing_webrender_image); } - - match self.get_or_request_image_or_meta(node, url.clone(), use_placeholder) { - Some(ImageOrMetadataAvailable::ImageAvailable { image, .. }) => { + let image_or_meta = + self.get_or_request_image_or_meta(node, url.clone(), use_placeholder)?; + match image_or_meta { + ImageOrMetadataAvailable::ImageAvailable { image, .. } => { self.handle_animated_image(node, image.clone()); let image_info = WebRenderImageInfo { size: Size2D::new(image.width, image.height), key: image.id, }; if image_info.key.is_none() { - Some(image_info) + Ok(image_info) } else { let mut webrender_image_cache = self.webrender_image_cache.write(); webrender_image_cache.insert((url, use_placeholder), image_info); - Some(image_info) + Ok(image_info) } }, - None | Some(ImageOrMetadataAvailable::MetadataAvailable(..)) => None, + ImageOrMetadataAvailable::MetadataAvailable(..) => { + Result::Err(ResolveImageError::OnlyMetadata) + }, } } @@ -168,11 +185,15 @@ impl LayoutContext<'_> { &self, node: Option<OpaqueNode>, image: &'a Image, - ) -> Option<ResolvedImage<'a>> { + ) -> Result<ResolvedImage<'a>, ResolveImageError> { match image { // TODO: Add support for PaintWorklet and CrossFade rendering. - Image::None | Image::CrossFade(_) | Image::PaintWorklet(_) => None, - Image::Gradient(gradient) => Some(ResolvedImage::Gradient(gradient)), + Image::None => Result::Err(ResolveImageError::None), + Image::CrossFade(_) => Result::Err(ResolveImageError::NotImplementedYet("CrossFade")), + Image::PaintWorklet(_) => { + Result::Err(ResolveImageError::NotImplementedYet("PaintWorklet")) + }, + Image::Gradient(gradient) => Ok(ResolvedImage::Gradient(gradient)), Image::Url(image_url) => { // FIXME: images won’t always have in intrinsic width or // height when support for SVG is added, or a WebRender @@ -180,18 +201,20 @@ impl LayoutContext<'_> { // // FIXME: It feels like this should take into account the pseudo // element and not just the node. - let image_url = image_url.url()?; + let image_url = image_url.url().ok_or(ResolveImageError::InvalidUrl)?; + let node = node.ok_or(ResolveImageError::MissingNode)?; let webrender_info = self.get_webrender_image_for_url( - node?, + node, image_url.clone().into(), UsePlaceholder::No, )?; - Some(ResolvedImage::Image(webrender_info)) + Ok(ResolvedImage::Image(webrender_info)) }, Image::ImageSet(image_set) => { image_set .items .get(image_set.selected_index) + .ok_or(ResolveImageError::ImageMissingFromImageSet) .and_then(|image| { self.resolve_image(node, &image.image) .map(|info| match info { diff --git a/components/layout/display_list/mod.rs b/components/layout/display_list/mod.rs index fa313b306f4..3908da69ce1 100644 --- a/components/layout/display_list/mod.rs +++ b/components/layout/display_list/mod.rs @@ -39,7 +39,7 @@ use webrender_api::{ use wr::units::LayoutVector2D; use crate::context::{LayoutContext, ResolvedImage}; -use crate::display_list::conversions::ToWebRender; +pub use crate::display_list::conversions::ToWebRender; use crate::display_list::stacking_context::StackingContextSection; use crate::fragment_tree::{ BackgroundMode, BoxFragment, Fragment, FragmentFlags, FragmentTree, SpecificLayoutInfo, Tag, @@ -711,7 +711,12 @@ impl<'a> BuilderForBoxFragment<'a> { fn build(&mut self, builder: &mut DisplayListBuilder, section: StackingContextSection) { if self.is_hit_test_for_scrollable_overflow { - self.build_hit_test(builder, self.fragment.scrollable_overflow().to_webrender()); + self.build_hit_test( + builder, + self.fragment + .reachable_scrollable_overflow_region() + .to_webrender(), + ); return; } @@ -838,8 +843,8 @@ impl<'a> BuilderForBoxFragment<'a> { // Reverse because the property is top layer first, we want to paint bottom layer first. for (index, image) in b.background_image.0.iter().enumerate().rev() { match builder.context.resolve_image(node, image) { - None => {}, - Some(ResolvedImage::Gradient(gradient)) => { + Err(_) => {}, + Ok(ResolvedImage::Gradient(gradient)) => { let intrinsic = NaturalSizes::empty(); let Some(layer) = &background::layout_layer(self, painter, builder, index, intrinsic) @@ -875,7 +880,7 @@ impl<'a> BuilderForBoxFragment<'a> { }, } }, - Some(ResolvedImage::Image(image_info)) => { + Ok(ResolvedImage::Image(image_info)) => { // FIXME: https://drafts.csswg.org/css-images-4/#the-image-resolution let dppx = 1.0; let intrinsic = NaturalSizes::from_width_and_height( @@ -1063,8 +1068,8 @@ impl<'a> BuilderForBoxFragment<'a> { .context .resolve_image(node, &border.border_image_source) { - None => return false, - Some(ResolvedImage::Image(image_info)) => { + Err(_) => return false, + Ok(ResolvedImage::Image(image_info)) => { let Some(key) = image_info.key else { return false; }; @@ -1073,7 +1078,7 @@ impl<'a> BuilderForBoxFragment<'a> { height = image_info.size.height as f32; NinePatchBorderSource::Image(key, ImageRendering::Auto) }, - Some(ResolvedImage::Gradient(gradient)) => { + Ok(ResolvedImage::Gradient(gradient)) => { match gradient::build(&self.fragment.style, gradient, border_image_size, builder) { WebRenderGradient::Linear(gradient) => { NinePatchBorderSource::Gradient(gradient) diff --git a/components/layout/display_list/stacking_context.rs b/components/layout/display_list/stacking_context.rs index 0c0def9a563..b044b713260 100644 --- a/components/layout/display_list/stacking_context.rs +++ b/components/layout/display_list/stacking_context.rs @@ -530,7 +530,7 @@ impl StackingContext { if effects.filter.0.is_empty() && effects.opacity == 1.0 && effects.mix_blend_mode == ComputedMixBlendMode::Normal && - !style.has_transform_or_perspective(FragmentFlags::empty()) && + !style.has_effective_transform_or_perspective(FragmentFlags::empty()) && style.clone_clip_path() == ClipPath::None { return false; @@ -1477,7 +1477,7 @@ impl BoxFragment { y: overflow.y.into(), }; - let content_rect = self.scrollable_overflow().to_webrender(); + let content_rect = self.reachable_scrollable_overflow_region().to_webrender(); let scroll_tree_node_id = display_list.define_scroll_frame( parent_scroll_node_id, @@ -1584,7 +1584,10 @@ impl BoxFragment { &self, containing_block_rect: &PhysicalRect<Au>, ) -> Option<ReferenceFrameData> { - if !self.style.has_transform_or_perspective(self.base.flags) { + if !self + .style + .has_effective_transform_or_perspective(self.base.flags) + { return None; } diff --git a/components/layout/flexbox/layout.rs b/components/layout/flexbox/layout.rs index 77069236787..a5540123681 100644 --- a/components/layout/flexbox/layout.rs +++ b/components/layout/flexbox/layout.rs @@ -1774,7 +1774,9 @@ impl FlexItem<'_> { non_stretch_layout_result: Option<&mut FlexItemLayoutResult>, ) -> Option<FlexItemLayoutResult> { let containing_block = flex_context.containing_block; - let mut positioning_context = PositioningContext::new_for_style(self.box_.style()) + let independent_formatting_context = &self.box_.independent_formatting_context; + let mut positioning_context = independent_formatting_context + .new_positioning_context() .unwrap_or_else(|| { PositioningContext::new_for_subtree( flex_context @@ -1783,7 +1785,6 @@ impl FlexItem<'_> { ) }); - let independent_formatting_context = &self.box_.independent_formatting_context; let item_writing_mode = independent_formatting_context.style().writing_mode; let item_is_horizontal = item_writing_mode.is_horizontal(); let flex_axis = flex_context.config.flex_axis; @@ -2616,7 +2617,9 @@ impl FlexItemBox { cross_size_stretches_to_container_size: bool, intrinsic_sizing_mode: IntrinsicSizingMode, ) -> Au { - let mut positioning_context = PositioningContext::new_for_style(self.style()) + let mut positioning_context = self + .independent_formatting_context + .new_positioning_context() .unwrap_or_else(|| { PositioningContext::new_for_subtree( flex_context diff --git a/components/layout/flow/float.rs b/components/layout/flow/float.rs index 0570ce0d0f4..dbc50c07603 100644 --- a/components/layout/flow/float.rs +++ b/components/layout/flow/float.rs @@ -913,11 +913,10 @@ impl FloatBox { positioning_context: &mut PositioningContext, containing_block: &ContainingBlock, ) -> BoxFragment { - let style = self.contents.style().clone(); positioning_context.layout_maybe_position_relative_fragment( layout_context, containing_block, - &style, + &self.contents.base, |positioning_context| { self.contents .layout_float_or_atomic_inline( diff --git a/components/layout/flow/inline/line.rs b/components/layout/flow/inline/line.rs index c42f32c9242..e65eaed2367 100644 --- a/components/layout/flow/inline/line.rs +++ b/components/layout/flow/inline/line.rs @@ -326,13 +326,12 @@ impl LineItemLayout<'_, '_> { let inline_box = self.layout.ifc.inline_boxes.get(identifier); let inline_box = &*(inline_box.borrow()); - let style = &inline_box.base.style; let space_above_baseline = inline_box_state.calculate_space_above_baseline(); let block_start_offset = self.calculate_inline_box_block_start(inline_box_state, space_above_baseline); let positioning_context_or_start_offset_in_parent = - match PositioningContext::new_for_style(style) { + match inline_box.base.new_positioning_context() { Some(positioning_context) => Either::Left(positioning_context), None => Either::Right(self.current_positioning_context_mut().len()), }; diff --git a/components/layout/flow/inline/mod.rs b/components/layout/flow/inline/mod.rs index dabb9773410..25fbaa324b1 100644 --- a/components/layout/flow/inline/mod.rs +++ b/components/layout/flow/inline/mod.rs @@ -2004,7 +2004,8 @@ impl IndependentFormattingContext { bidi_level: Level, ) { // We need to know the inline size of the atomic before deciding whether to do the line break. - let mut child_positioning_context = PositioningContext::new_for_style(self.style()) + let mut child_positioning_context = self + .new_positioning_context() .unwrap_or_else(|| PositioningContext::new_for_subtree(true)); let IndependentFloatOrAtomicLayoutResult { mut fragment, diff --git a/components/layout/flow/mod.rs b/components/layout/flow/mod.rs index d983e8592c4..983282dc389 100644 --- a/components/layout/flow/mod.rs +++ b/components/layout/flow/mod.rs @@ -779,7 +779,7 @@ impl BlockLevelBox { ArcRefCell::new(positioning_context.layout_maybe_position_relative_fragment( layout_context, containing_block, - &base.style, + base, |positioning_context| { layout_in_flow_non_replaced_block_level_same_formatting_context( layout_context, @@ -798,7 +798,7 @@ impl BlockLevelBox { positioning_context.layout_maybe_position_relative_fragment( layout_context, containing_block, - independent.style(), + &independent.base, |positioning_context| { independent.layout_in_flow_block_level( layout_context, diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs index 187726595f8..c6498eeed63 100644 --- a/components/layout/flow/root.rs +++ b/components/layout/flow/root.rs @@ -411,7 +411,7 @@ impl BoxTree { let scrollable_overflow = root_fragments .iter() .fold(PhysicalRect::zero(), |acc, child| { - let child_overflow = child.scrollable_overflow(); + let child_overflow = child.scrollable_overflow_for_parent(); // https://drafts.csswg.org/css-overflow/#scrolling-direction // We want to clip scrollable overflow on box-start and inline-start diff --git a/components/layout/fragment_tree/box_fragment.rs b/components/layout/fragment_tree/box_fragment.rs index e87826ec3ca..65ad1c4aa93 100644 --- a/components/layout/fragment_tree/box_fragment.rs +++ b/components/layout/fragment_tree/box_fragment.rs @@ -2,11 +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 app_units::Au; +use app_units::{Au, MAX_AU, MIN_AU}; use atomic_refcell::AtomicRefCell; use base::print_tree::PrintTree; use malloc_size_of_derive::MallocSizeOf; use servo_arc::Arc as ServoArc; +use servo_geometry::f32_rect_to_au_rect; use style::Zero; use style::computed_values::border_collapse::T as BorderCollapse; use style::computed_values::overflow_x::T as ComputedOverflow; @@ -16,6 +17,7 @@ use style::properties::ComputedValues; use style::values::specified::box_::DisplayOutside; use super::{BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment}; +use crate::display_list::ToWebRender; use crate::formatting_contexts::Baselines; use crate::geom::{ AuOrAuto, LengthPercentageOrAuto, PhysicalPoint, PhysicalRect, PhysicalSides, ToLogical, @@ -116,7 +118,7 @@ impl BoxFragment { ) -> BoxFragment { let scrollable_overflow_from_children = children.iter().fold(PhysicalRect::zero(), |acc, child| { - acc.union(&child.scrollable_overflow()) + acc.union(&child.scrollable_overflow_for_parent()) }); BoxFragment { @@ -267,30 +269,96 @@ impl BoxFragment { pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { let mut overflow = self.border_rect(); - if self.style.establishes_scroll_container(self.base.flags) { - return overflow; + if !self.style.establishes_scroll_container(self.base.flags) { + // https://www.w3.org/TR/css-overflow-3/#scrollable + // Only include the scrollable overflow of a child box if it has overflow: visible. + let scrollable_overflow = self.scrollable_overflow(); + let bottom_right = PhysicalPoint::new( + overflow.max_x().max(scrollable_overflow.max_x()), + overflow.max_y().max(scrollable_overflow.max_y()), + ); + + let overflow_style = self.style.effective_overflow(self.base.flags); + if overflow_style.y == ComputedOverflow::Visible { + overflow.origin.y = overflow.origin.y.min(scrollable_overflow.origin.y); + overflow.size.height = bottom_right.y - overflow.origin.y; + } + + if overflow_style.x == ComputedOverflow::Visible { + overflow.origin.x = overflow.origin.x.min(scrollable_overflow.origin.x); + overflow.size.width = bottom_right.x - overflow.origin.x; + } } - // https://www.w3.org/TR/css-overflow-3/#scrollable - // Only include the scrollable overflow of a child box if it has overflow: visible. - let scrollable_overflow = self.scrollable_overflow(); - let bottom_right = PhysicalPoint::new( - overflow.max_x().max(scrollable_overflow.max_x()), - overflow.max_y().max(scrollable_overflow.max_y()), - ); + // <https://drafts.csswg.org/css-overflow-3/#scrollable-overflow-region> + // > ...accounting for transforms by projecting each box onto the plane of + // > the element that establishes its 3D rendering context. [CSS3-TRANSFORMS] + // Both boxes and its scrollable overflow (if it is included) should be transformed accordingly. + // + // TODO(stevennovaryo): We are supposed to handle perspective transform and 3d context, but it is yet to happen. + if self + .style + .has_effective_transform_or_perspective(self.base.flags) + { + if let Some(transform) = + self.calculate_transform_matrix(&self.border_rect().to_untyped()) + { + if let Some(transformed_overflow_box) = + transform.outer_transformed_rect(&overflow.to_webrender().to_rect()) + { + overflow = + f32_rect_to_au_rect(transformed_overflow_box.to_untyped()).cast_unit(); + } + } + } + + overflow + } - let overflow_style = self.style.effective_overflow(self.base.flags); - if overflow_style.y == ComputedOverflow::Visible { - overflow.origin.y = overflow.origin.y.min(scrollable_overflow.origin.y); - overflow.size.height = bottom_right.y - overflow.origin.y; + /// <https://drafts.csswg.org/css-overflow/#unreachable-scrollable-overflow-region> + /// > area beyond the scroll origin in either axis is considered the unreachable scrollable overflow region + /// + /// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction. + /// For an element, the clip rect is the padding rect and for viewport, it is the initial containing block. + pub fn clip_unreachable_scrollable_overflow_region( + &self, + scrollable_overflow: PhysicalRect<Au>, + clipping_rect: PhysicalRect<Au>, + ) -> PhysicalRect<Au> { + let scrolling_direction = self.style.overflow_direction(); + let mut scrollable_overflow_box = scrollable_overflow.to_box2d(); + let mut clipping_box = clipping_rect.to_box2d(); + + if scrolling_direction.rightward { + clipping_box.max.x = MAX_AU; + } else { + clipping_box.min.x = MIN_AU; } - if overflow_style.x == ComputedOverflow::Visible { - overflow.origin.x = overflow.origin.x.min(scrollable_overflow.origin.x); - overflow.size.width = bottom_right.x - overflow.origin.x; + if scrolling_direction.downward { + clipping_box.max.y = MAX_AU; + } else { + clipping_box.min.y = MIN_AU; } - overflow + scrollable_overflow_box = scrollable_overflow_box.intersection_unchecked(&clipping_box); + + match scrollable_overflow_box.is_negative() { + true => PhysicalRect::zero(), + false => scrollable_overflow_box.to_rect(), + } + } + + /// <https://drafts.csswg.org/css-overflow/#unreachable-scrollable-overflow-region> + /// > area beyond the scroll origin in either axis is considered the unreachable scrollable overflow region + /// + /// Return the clipped the scrollable overflow based on its scroll origin, determined by overflow direction. + /// This will coincides with the scrollport if the fragment is a scroll container. + pub fn reachable_scrollable_overflow_region(&self) -> PhysicalRect<Au> { + self.clip_unreachable_scrollable_overflow_region( + self.scrollable_overflow(), + self.padding_rect(), + ) } pub(crate) fn calculate_resolved_insets_if_positioned(&self) -> PhysicalSides<AuOrAuto> { diff --git a/components/layout/fragment_tree/fragment.rs b/components/layout/fragment_tree/fragment.rs index 7708b0893ee..1c5324fa1c4 100644 --- a/components/layout/fragment_tree/fragment.rs +++ b/components/layout/fragment_tree/fragment.rs @@ -170,17 +170,28 @@ impl Fragment { } } - pub fn scrolling_area(&self) -> PhysicalRect<Au> { + pub fn unclipped_scrolling_area(&self) -> PhysicalRect<Au> { match self { Fragment::Box(fragment) | Fragment::Float(fragment) => { let fragment = fragment.borrow(); fragment.offset_by_containing_block(&fragment.scrollable_overflow()) }, - _ => self.scrollable_overflow(), + _ => self.scrollable_overflow_for_parent(), + } + } + + pub fn scrolling_area(&self) -> PhysicalRect<Au> { + match self { + Fragment::Box(fragment) | Fragment::Float(fragment) => { + let fragment = fragment.borrow(); + fragment + .offset_by_containing_block(&fragment.reachable_scrollable_overflow_region()) + }, + _ => self.scrollable_overflow_for_parent(), } } - pub fn scrollable_overflow(&self) -> PhysicalRect<Au> { + pub fn scrollable_overflow_for_parent(&self) -> PhysicalRect<Au> { match self { Fragment::Box(fragment) | Fragment::Float(fragment) => { fragment.borrow().scrollable_overflow_for_parent() diff --git a/components/layout/fragment_tree/fragment_tree.rs b/components/layout/fragment_tree/fragment_tree.rs index 3a082c99389..1499a50dacf 100644 --- a/components/layout/fragment_tree/fragment_tree.rs +++ b/components/layout/fragment_tree/fragment_tree.rs @@ -138,11 +138,26 @@ impl FragmentTree { .find_map(|child| child.find(&info, 0, &mut process_func)) } + /// <https://drafts.csswg.org/cssom-view/#scrolling-area> + /// + /// Scrolling area for a viewport that is clipped according to overflow direction of root element. 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().union(&scroll_area); + if let Some(root_fragment) = self.root_fragments.first() { + for fragment in self.root_fragments.iter() { + scroll_area = fragment.unclipped_scrolling_area().union(&scroll_area); + } + match root_fragment { + Fragment::Box(fragment) | Fragment::Float(fragment) => fragment + .borrow() + .clip_unreachable_scrollable_overflow_region( + scroll_area, + self.initial_containing_block, + ), + _ => scroll_area, + } + } else { + scroll_area } - scroll_area } } diff --git a/components/layout/fragment_tree/positioning_fragment.rs b/components/layout/fragment_tree/positioning_fragment.rs index 1fe968eb484..0cf525a3479 100644 --- a/components/layout/fragment_tree/positioning_fragment.rs +++ b/components/layout/fragment_tree/positioning_fragment.rs @@ -56,7 +56,7 @@ impl PositioningFragment { let scrollable_overflow = children.iter().fold(PhysicalRect::zero(), |acc, child| { acc.union( &child - .scrollable_overflow() + .scrollable_overflow_for_parent() .translate(content_origin.to_vector()), ) }); diff --git a/components/layout/positioned.rs b/components/layout/positioned.rs index 5f08e4e86c5..6bfe2af87ef 100644 --- a/components/layout/positioned.rs +++ b/components/layout/positioned.rs @@ -29,6 +29,7 @@ use crate::geom::{ PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, PhysicalVec, Size, Sizes, ToLogical, ToLogicalWithContainingBlock, }; +use crate::layout_box_base::LayoutBoxBase; use crate::sizing::ContentSizes; use crate::style_ext::{Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, DisplayInside}; use crate::{ @@ -103,6 +104,20 @@ impl AbsolutelyPositionedBox { } } +impl IndependentFormattingContext { + #[inline] + pub(crate) fn new_positioning_context(&self) -> Option<PositioningContext> { + self.base.new_positioning_context() + } +} + +impl LayoutBoxBase { + #[inline] + pub(crate) fn new_positioning_context(&self) -> Option<PositioningContext> { + PositioningContext::new_for_style(&self.style, &self.base_fragment_info.flags) + } +} + impl PositioningContext { pub(crate) fn new_for_containing_block_for_all_descendants() -> Self { Self { @@ -130,14 +145,10 @@ impl PositioningContext { self.for_nearest_positioned_ancestor.is_some() } - pub(crate) fn new_for_style(style: &ComputedValues) -> Option<Self> { - // NB: We never make PositioningContexts for replaced elements, which is why we always - // pass false here. - if style.establishes_containing_block_for_all_descendants(FragmentFlags::empty()) { + fn new_for_style(style: &ComputedValues, flags: &FragmentFlags) -> Option<Self> { + if style.establishes_containing_block_for_all_descendants(*flags) { Some(Self::new_for_containing_block_for_all_descendants()) - } else if style - .establishes_containing_block_for_absolute_descendants(FragmentFlags::empty()) - { + } else if style.establishes_containing_block_for_absolute_descendants(*flags) { Some(Self { for_nearest_positioned_ancestor: Some(Vec::new()), for_nearest_containing_block_for_all_descendants: Vec::new(), @@ -213,12 +224,12 @@ impl PositioningContext { &mut self, layout_context: &LayoutContext, containing_block: &ContainingBlock, - style: &ComputedValues, + base: &LayoutBoxBase, fragment_layout_fn: impl FnOnce(&mut Self) -> BoxFragment, ) -> BoxFragment { // Try to create a context, but if one isn't necessary, simply create the fragment // using the given closure and the current `PositioningContext`. - let mut new_context = match Self::new_for_style(style) { + let mut new_context = match base.new_positioning_context() { Some(new_context) => new_context, None => return fragment_layout_fn(self), }; @@ -229,9 +240,8 @@ impl PositioningContext { // If the new context has any hoisted boxes for the nearest containing block for // pass them up the tree. self.append(new_context); - - if style.clone_position() == Position::Relative { - new_fragment.content_rect.origin += relative_adjustement(style, containing_block) + if base.style.clone_position() == Position::Relative { + new_fragment.content_rect.origin += relative_adjustement(&base.style, containing_block) .to_physical_vector(containing_block.style.writing_mode) } @@ -586,7 +596,7 @@ impl HoistedAbsolutelyPositionedBox { .sizes })); - let mut positioning_context = PositioningContext::new_for_style(&style).unwrap(); + let mut positioning_context = context.new_positioning_context().unwrap(); let mut new_fragment = { let content_size: LogicalVec2<Au>; let fragments; diff --git a/components/layout/replaced.rs b/components/layout/replaced.rs index 6a6b1979ff9..b82fb947074 100644 --- a/components/layout/replaced.rs +++ b/components/layout/replaced.rs @@ -220,13 +220,13 @@ impl ReplacedContents { image_url.clone().into(), UsePlaceholder::No, ) { - Some(ImageOrMetadataAvailable::ImageAvailable { image, .. }) => { + Ok(ImageOrMetadataAvailable::ImageAvailable { image, .. }) => { (Some(image.clone()), image.width as f32, image.height as f32) }, - Some(ImageOrMetadataAvailable::MetadataAvailable(metadata, _id)) => { + Ok(ImageOrMetadataAvailable::MetadataAvailable(metadata, _id)) => { (None, metadata.width as f32, metadata.height as f32) }, - None => return None, + Err(_) => return None, }; return Some(Self { diff --git a/components/layout/style_ext.rs b/components/layout/style_ext.rs index c28511766b2..023db6b07f1 100644 --- a/components/layout/style_ext.rs +++ b/components/layout/style_ext.rs @@ -12,7 +12,7 @@ use style::computed_values::mix_blend_mode::T as ComputedMixBlendMode; use style::computed_values::position::T as ComputedPosition; use style::computed_values::transform_style::T as ComputedTransformStyle; use style::computed_values::unicode_bidi::T as UnicodeBidi; -use style::logical_geometry::{Direction as AxisDirection, WritingMode}; +use style::logical_geometry::{Direction as AxisDirection, PhysicalSide, WritingMode}; use style::properties::ComputedValues; use style::properties::longhands::backface_visibility::computed_value::T as BackfaceVisiblity; use style::properties::longhands::box_sizing::computed_value::T as BoxSizing; @@ -280,6 +280,16 @@ impl Default for BorderStyleColor { } } +/// <https://drafts.csswg.org/cssom-view/#overflow-directions> +/// > A scrolling box of a viewport or element has two overflow directions, +/// > which are the block-end and inline-end directions for that viewport or element. +pub(crate) struct OverflowDirection { + /// Whether block-end or inline-end direction is [PhysicalSide::Right]. + pub rightward: bool, + /// Whether block-end or inline-end direction is [PhysicalSide::Bottom]. + pub downward: bool, +} + pub(crate) trait ComputedValuesExt { fn physical_box_offsets(&self) -> PhysicalSides<LengthPercentageOrAuto<'_>>; fn box_offsets(&self, writing_mode: WritingMode) -> LogicalSides<LengthPercentageOrAuto<'_>>; @@ -320,7 +330,8 @@ pub(crate) trait ComputedValuesExt { containing_block_writing_mode: WritingMode, ) -> LogicalSides<LengthPercentageOrAuto<'_>>; fn is_transformable(&self, fragment_flags: FragmentFlags) -> bool; - fn has_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool; + fn has_transform_or_perspective_style(&self) -> bool; + fn has_effective_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool; fn z_index_applies(&self, fragment_flags: FragmentFlags) -> bool; fn effective_z_index(&self, fragment_flags: FragmentFlags) -> i32; fn effective_overflow(&self, fragment_flags: FragmentFlags) -> AxesOverflow; @@ -353,6 +364,7 @@ pub(crate) trait ComputedValuesExt { writing_mode: WritingMode, ) -> bool; fn is_inline_box(&self, fragment_flags: FragmentFlags) -> bool; + fn overflow_direction(&self) -> OverflowDirection; } impl ComputedValuesExt for ComputedValues { @@ -522,15 +534,20 @@ impl ComputedValuesExt for ComputedValues { !self.is_inline_box(fragment_flags) } - /// Returns true if this style has a transform, or perspective property set and + /// Returns true if this style has a transform or perspective property set. + fn has_transform_or_perspective_style(&self) -> bool { + !self.get_box().transform.0.is_empty() || + self.get_box().scale != GenericScale::None || + self.get_box().rotate != GenericRotate::None || + self.get_box().translate != GenericTranslate::None || + self.get_box().perspective != Perspective::None + } + + /// Returns true if this style has a transform or perspective property set, and /// it applies to this element. - fn has_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool { - self.is_transformable(fragment_flags) && - (!self.get_box().transform.0.is_empty() || - self.get_box().scale != GenericScale::None || - self.get_box().rotate != GenericRotate::None || - self.get_box().translate != GenericTranslate::None || - self.get_box().perspective != Perspective::None) + #[inline] + fn has_effective_transform_or_perspective(&self, fragment_flags: FragmentFlags) -> bool { + self.is_transformable(fragment_flags) && self.has_transform_or_perspective_style() } /// Whether the `z-index` property applies to this fragment. @@ -705,7 +722,6 @@ impl ComputedValuesExt for ComputedValues { if self.is_transformable(fragment_flags) && (!self.get_box().transform.0.is_empty() || self.get_box().transform_style == ComputedTransformStyle::Preserve3d || - self.get_box().perspective != Perspective::None || will_change_bits .intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE)) { @@ -795,29 +811,43 @@ impl ComputedValuesExt for ComputedValues { &self, fragment_flags: FragmentFlags, ) -> bool { - if self.has_transform_or_perspective(fragment_flags) { - return true; - } - - if !self.get_effects().filter.0.is_empty() { - return true; - } - - // See <https://drafts.csswg.org/css-transforms-2/#transform-style-property>. - if self.is_transformable(fragment_flags) && - self.get_box().transform_style == ComputedTransformStyle::Preserve3d - { - return true; - } // From <https://www.w3.org/TR/css-will-change/#valdef-will-change-custom-ident>: // > If any non-initial value of a property would cause the element to generate a // > containing block for fixed positioned elements, specifying that property in will-change // > must cause the element to generate a containing block for fixed positioned elements. let will_change_bits = self.clone_will_change().bits; - if will_change_bits.intersects(WillChangeBits::FIXPOS_CB_NON_SVG) || - (will_change_bits - .intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE) && - self.is_transformable(fragment_flags)) + + // From <https://drafts.csswg.org/css-transforms-1/#transform-rendering>: + // > any value other than `none` for the `transform` property also causes the element + // > to establish a containing block for all descendants. + // + // From <https://www.w3.org/TR/css-transforms-2/#individual-transforms> + // > all other values […] create a stacking context and containing block for all + // > descendants, per usual for transforms. + // + // From <https://drafts.csswg.org/css-transforms-2/#perspective-property>: + // > The use of this property with any value other than `none` […] establishes a + // > containing block for all descendants, just like the `transform` property does. + // + // From <https://drafts.csswg.org/css-transforms-2/#transform-style-property>: + // > A computed value of `preserve-3d` for `transform-style` on a transformable element + // > establishes both a stacking context and a containing block for all descendants. + if self.is_transformable(fragment_flags) && + (self.has_transform_or_perspective_style() || + self.get_box().transform_style == ComputedTransformStyle::Preserve3d || + will_change_bits + .intersects(WillChangeBits::TRANSFORM | WillChangeBits::PERSPECTIVE)) + { + return true; + } + + // From <https://www.w3.org/TR/filter-effects-1/#propdef-filter>: + // > A value other than none for the filter property results in the creation of a containing + // > block for absolute and fixed positioned descendants unless the element it applies to is + // > a document root element in the current browsing context. + if !fragment_flags.contains(FragmentFlags::IS_ROOT_ELEMENT) && + (!self.get_effects().filter.0.is_empty() || + will_change_bits.intersects(WillChangeBits::FIXPOS_CB_NON_SVG)) { return true; } @@ -961,6 +991,23 @@ impl ComputedValuesExt for ComputedValues { }; has_percentage(box_offsets.block_start) || has_percentage(box_offsets.block_end) } + + // <https://drafts.csswg.org/cssom-view/#overflow-directions> + fn overflow_direction(&self) -> OverflowDirection { + let inline_end_direction = self.writing_mode.inline_end_physical_side(); + let block_end_direction = self.writing_mode.block_end_physical_side(); + + let rightward = inline_end_direction == PhysicalSide::Right || + block_end_direction == PhysicalSide::Right; + let downward = inline_end_direction == PhysicalSide::Bottom || + block_end_direction == PhysicalSide::Bottom; + + // TODO(stevennovaryo): We should consider the flex-container's CSS (e.g. flow-direction: column-reverse). + OverflowDirection { + rightward, + downward, + } + } } pub(crate) enum LayoutStyle<'a> { diff --git a/components/layout/table/layout.rs b/components/layout/table/layout.rs index 2261f7d165c..0cbe3e9ca76 100644 --- a/components/layout/table/layout.rs +++ b/components/layout/table/layout.rs @@ -1503,7 +1503,7 @@ impl<'a> TableLayout<'a> { layout_context: &LayoutContext, parent_positioning_context: &mut PositioningContext, ) -> BoxFragment { - let mut positioning_context = PositioningContext::new_for_style(caption.context.style()); + let mut positioning_context = caption.context.new_positioning_context(); let containing_block = &ContainingBlock { size: ContainingBlockSize { inline: self.table_width + self.pbm.padding_border_sums.inline, @@ -2325,7 +2325,7 @@ impl<'a> RowFragmentLayout<'a> { Self { row: table_row, rect, - positioning_context: PositioningContext::new_for_style(&table_row.base.style), + positioning_context: table_row.base.new_positioning_context(), containing_block, fragments: Vec::new(), } @@ -2410,7 +2410,7 @@ impl RowGroupFragmentLayout { let row_group = row_group.borrow(); ( dimensions.get_row_group_rect(&row_group), - PositioningContext::new_for_style(&row_group.base.style), + row_group.base.new_positioning_context(), ) }; Self { diff --git a/components/layout/taffy/layout.rs b/components/layout/taffy/layout.rs index a7581136bf2..3777c902053 100644 --- a/components/layout/taffy/layout.rs +++ b/components/layout/taffy/layout.rs @@ -251,8 +251,9 @@ impl taffy::LayoutPartialTree for TaffyContainerContext<'_> { style, }; let layout = { - let mut child_positioning_context = - PositioningContext::new_for_style(style).unwrap_or_else(|| { + let mut child_positioning_context = independent_context + .new_positioning_context() + .unwrap_or_else(|| { PositioningContext::new_for_subtree( self.positioning_context .collects_for_nearest_positioned_ancestor(), diff --git a/components/net/async_runtime.rs b/components/net/async_runtime.rs index c99068b1076..909bdef8fb0 100644 --- a/components/net/async_runtime.rs +++ b/components/net/async_runtime.rs @@ -2,31 +2,27 @@ * 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::cmp::Ord; +use std::sync::LazyLock; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{LazyLock, Mutex}; use std::thread; use tokio::runtime::{Builder, Runtime}; -pub static HANDLE: LazyLock<Mutex<Option<Runtime>>> = LazyLock::new(|| { - Mutex::new(Some( - Builder::new_multi_thread() - .thread_name_fn(|| { - static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); - let id = ATOMIC_ID.fetch_add(1, Ordering::Relaxed); - format!("tokio-runtime-{}", id) - }) - .worker_threads( - thread::available_parallelism() - .map(|i| i.get()) - .unwrap_or(servo_config::pref!(threadpools_fallback_worker_num) as usize) - .min( - servo_config::pref!(threadpools_async_runtime_workers_max).max(1) as usize, - ), - ) - .enable_io() - .enable_time() - .build() - .unwrap(), - )) +pub static HANDLE: LazyLock<Runtime> = LazyLock::new(|| { + Builder::new_multi_thread() + .thread_name_fn(|| { + static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); + let id = ATOMIC_ID.fetch_add(1, Ordering::Relaxed); + format!("tokio-runtime-{}", id) + }) + .worker_threads( + thread::available_parallelism() + .map(|i| i.get()) + .unwrap_or(servo_config::pref!(threadpools_fallback_worker_num) as usize) + .min(servo_config::pref!(threadpools_async_runtime_workers_max).max(1) as usize), + ) + .enable_io() + .enable_time() + .build() + .expect("Unable to build tokio-runtime runtime") }); diff --git a/components/net/connector.rs b/components/net/connector.rs index 12d0638d84d..e02ff8971e3 100644 --- a/components/net/connector.rs +++ b/components/net/connector.rs @@ -165,7 +165,7 @@ where F: Future<Output = ()> + 'static + std::marker::Send, { fn execute(&self, fut: F) { - HANDLE.lock().unwrap().as_ref().unwrap().spawn(fut); + HANDLE.spawn(fut); } } diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs index 35624bb8645..e0867b8d07f 100644 --- a/components/net/http_loader.rs +++ b/components/net/http_loader.rs @@ -493,7 +493,7 @@ impl BodySink { match self { BodySink::Chunked(sender) => { let sender = sender.clone(); - HANDLE.lock().unwrap().as_mut().unwrap().spawn(async move { + HANDLE.spawn(async move { let _ = sender.send(Ok(Frame::data(bytes.into()))).await; }); }, @@ -2016,7 +2016,7 @@ async fn http_network_fetch( let url1 = request.url(); let url2 = url1.clone(); - HANDLE.lock().unwrap().as_ref().unwrap().spawn( + HANDLE.spawn( res.into_body() .map_err(|e| { warn!("Error streaming response body: {:?}", e); diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs index b6f885f29b7..5d1ede28c32 100644 --- a/components/net/resource_thread.rs +++ b/components/net/resource_thread.rs @@ -771,7 +771,7 @@ impl CoreResourceManager { _ => (FileTokenCheck::NotRequired, None), }; - HANDLE.lock().unwrap().as_ref().unwrap().spawn(async move { + HANDLE.spawn(async move { // XXXManishearth: Check origin against pipeline id (also ensure that the mode is allowed) // todo load context / mimesniff in fetch // todo referrer policy? diff --git a/components/net/websocket_loader.rs b/components/net/websocket_loader.rs index 95f66558482..128436ac47c 100644 --- a/components/net/websocket_loader.rs +++ b/components/net/websocket_loader.rs @@ -418,24 +418,21 @@ fn connect( tls_config.alpn_protocols = vec!["http/1.1".to_string().into()]; let resource_event_sender2 = resource_event_sender.clone(); - match HANDLE.lock().unwrap().as_mut() { - Some(handle) => handle.spawn( - start_websocket( - http_state, - req_url.clone(), - resource_event_sender, - protocols, - client, - tls_config, - dom_action_receiver, - ) - .map_err(move |e| { - warn!("Failed to establish a WebSocket connection: {:?}", e); - let _ = resource_event_sender2.send(WebSocketNetworkEvent::Fail); - }), - ), - None => return Err("No runtime available".to_string()), - }; + HANDLE.spawn( + start_websocket( + http_state, + req_url.clone(), + resource_event_sender, + protocols, + client, + tls_config, + dom_action_receiver, + ) + .map_err(move |e| { + warn!("Failed to establish a WebSocket connection: {:?}", e); + let _ = resource_event_sender2.send(WebSocketNetworkEvent::Fail); + }), + ); Ok(()) } diff --git a/components/script/canvas_state.rs b/components/script/canvas_state.rs index 408c94c124a..e9892818e92 100644 --- a/components/script/canvas_state.rs +++ b/components/script/canvas_state.rs @@ -152,6 +152,8 @@ pub(crate) struct CanvasState { canvas_id: CanvasId, #[no_trace] image_key: ImageKey, + #[no_trace] + size: Cell<Size2D<u64>>, state: DomRefCell<CanvasContextState>, origin_clean: Cell<bool>, #[ignore_malloc_size_of = "Arc"] @@ -176,6 +178,7 @@ impl CanvasState { profiled_ipc::channel(global.time_profiler_chan().clone()).unwrap(); let script_to_constellation_chan = global.script_to_constellation_chan(); debug!("Asking constellation to create new canvas thread."); + let size = adjust_canvas_size(size); script_to_constellation_chan .send(ScriptToConstellationMessage::CreateCanvasPaintThread( size, sender, @@ -194,6 +197,7 @@ impl CanvasState { CanvasState { ipc_renderer, canvas_id, + size: Cell::new(size), state: DomRefCell::new(CanvasContextState::new()), origin_clean: Cell::new(true), image_cache: global.image_cache(), @@ -221,7 +225,15 @@ impl CanvasState { self.canvas_id } + pub(crate) fn is_paintable(&self) -> bool { + !self.size.get().is_empty() + } + pub(crate) fn send_canvas_2d_msg(&self, msg: Canvas2dMsg) { + if !self.is_paintable() { + return; + } + self.ipc_renderer .send(CanvasMsg::Canvas2d(msg, self.get_canvas_id())) .unwrap() @@ -229,6 +241,10 @@ impl CanvasState { /// Updates WR image and blocks on completion pub(crate) fn update_rendering(&self) { + if !self.is_paintable() { + return; + } + let (sender, receiver) = ipc::channel().unwrap(); self.ipc_renderer .send(CanvasMsg::Canvas2d( @@ -239,16 +255,27 @@ impl CanvasState { receiver.recv().unwrap(); } - // https://html.spec.whatwg.org/multipage/#concept-canvas-set-bitmap-dimensions + /// <https://html.spec.whatwg.org/multipage/#concept-canvas-set-bitmap-dimensions> pub(crate) fn set_bitmap_dimensions(&self, size: Size2D<u64>) { self.reset_to_initial_state(); + + self.size.replace(adjust_canvas_size(size)); + self.ipc_renderer - .send(CanvasMsg::Recreate(Some(size), self.get_canvas_id())) + .send(CanvasMsg::Recreate( + Some(self.size.get()), + self.get_canvas_id(), + )) .unwrap(); } pub(crate) fn reset(&self) { self.reset_to_initial_state(); + + if !self.is_paintable() { + return; + } + self.ipc_renderer .send(CanvasMsg::Recreate(None, self.get_canvas_id())) .unwrap(); @@ -347,7 +374,6 @@ impl CanvasState { pub(crate) fn get_rect(&self, canvas_size: Size2D<u64>, rect: Rect<u64>) -> Vec<u8> { assert!(self.origin_is_clean()); - assert!(Rect::from_size(canvas_size).contains_rect(&rect)); let (sender, receiver) = ipc::channel().unwrap(); @@ -398,18 +424,22 @@ impl CanvasState { dw: Option<f64>, dh: Option<f64>, ) -> ErrorResult { + if !self.is_paintable() { + return Ok(()); + } + let result = match image { CanvasImageSource::HTMLCanvasElement(ref canvas) => { - // https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument - if !canvas.is_valid() { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if canvas.get_size().is_empty() { return Err(Error::InvalidState); } self.draw_html_canvas_element(canvas, htmlcanvas, sx, sy, sw, sh, dx, dy, dw, dh) }, CanvasImageSource::OffscreenCanvas(ref canvas) => { - // https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument - if !canvas.is_valid() { + // <https://html.spec.whatwg.org/multipage/#check-the-usability-of-the-image-argument> + if canvas.get_size().is_empty() { return Err(Error::InvalidState); } @@ -528,11 +558,6 @@ impl CanvasState { dw: Option<f64>, dh: Option<f64>, ) -> ErrorResult { - // 1. Check the usability of the image argument - if !canvas.is_valid() { - return Err(Error::InvalidState); - } - let canvas_size = canvas.get_size(); let dw = dw.unwrap_or(canvas_size.width as f64); let dh = dh.unwrap_or(canvas_size.height as f64); @@ -1403,13 +1428,13 @@ impl CanvasState { }, }; - ImageData::new( - global, - size.width, - size.height, - Some(self.get_rect(canvas_size, read_rect)), - can_gc, - ) + let data = if self.is_paintable() { + Some(self.get_rect(canvas_size, read_rect)) + } else { + None + }; + + ImageData::new(global, size.width, size.height, data, can_gc) } // https://html.spec.whatwg.org/multipage/#dom-context-2d-putimagedata @@ -1445,6 +1470,10 @@ impl CanvasState { dirty_width: i32, dirty_height: i32, ) { + if !self.is_paintable() { + return; + } + // FIXME(nox): There are many arithmetic operations here that can // overflow or underflow, this should probably be audited. @@ -2013,3 +2042,23 @@ where style.font_family.to_css_string() ) } + +fn adjust_canvas_size(size: Size2D<u64>) -> Size2D<u64> { + // Firefox limits width/height to 32767 pixels and Chromium to 65535 pixels, + // but slows down dramatically before it reaches that limit. + // We limit by area instead, giving us larger maximum dimensions, + // in exchange for a smaller maximum canvas size. + const MAX_CANVAS_AREA: u64 = 32768 * 8192; + // Max width/height to 65535 in CSS pixels. + const MAX_CANVAS_SIZE: u64 = 65535; + + if !size.is_empty() && + size.greater_than(Size2D::new(MAX_CANVAS_SIZE, MAX_CANVAS_SIZE)) + .none() && + size.area() < MAX_CANVAS_AREA + { + size + } else { + Size2D::zero() + } +} diff --git a/components/script/dom/canvasrenderingcontext2d.rs b/components/script/dom/canvasrenderingcontext2d.rs index 73052e6906e..38bd38ad511 100644 --- a/components/script/dom/canvasrenderingcontext2d.rs +++ b/components/script/dom/canvasrenderingcontext2d.rs @@ -4,7 +4,7 @@ use canvas_traits::canvas::{Canvas2dMsg, CanvasId, CanvasMsg, FromScriptMsg}; use dom_struct::dom_struct; -use euclid::default::{Point2D, Rect, Size2D}; +use euclid::default::Size2D; use profile_traits::ipc; use script_bindings::inheritance::Castable; use script_layout_interface::HTMLCanvasDataSource; @@ -74,23 +74,12 @@ impl CanvasRenderingContext2D { reflect_dom_object(boxed, global, can_gc) } - // https://html.spec.whatwg.org/multipage/#concept-canvas-set-bitmap-dimensions - pub(crate) fn set_bitmap_dimensions(&self, size: Size2D<u32>) { - self.reset_to_initial_state(); - self.canvas_state - .get_ipc_renderer() - .send(CanvasMsg::Recreate( - Some(size.to_u64()), - self.canvas_state.get_canvas_id(), - )) - .unwrap(); - } - // https://html.spec.whatwg.org/multipage/#reset-the-rendering-context-to-its-default-state fn reset_to_initial_state(&self) { self.canvas_state.reset_to_initial_state(); } + /// <https://html.spec.whatwg.org/multipage/#concept-canvas-set-bitmap-dimensions> pub(crate) fn set_canvas_bitmap_dimensions(&self, size: Size2D<u64>) { self.canvas_state.set_bitmap_dimensions(size); } @@ -106,20 +95,17 @@ impl CanvasRenderingContext2D { pub(crate) fn send_canvas_2d_msg(&self, msg: Canvas2dMsg) { self.canvas_state.send_canvas_2d_msg(msg) } - - pub(crate) fn get_rect(&self, rect: Rect<u32>) -> Vec<u8> { - let rect = Rect::new( - Point2D::new(rect.origin.x as u64, rect.origin.y as u64), - Size2D::new(rect.size.width as u64, rect.size.height as u64), - ); - self.canvas_state.get_rect(self.canvas.size(), rect) - } } impl LayoutCanvasRenderingContextHelpers for LayoutDom<'_, CanvasRenderingContext2D> { fn canvas_data_source(self) -> HTMLCanvasDataSource { let canvas_state = &self.unsafe_get().canvas_state; - HTMLCanvasDataSource::Image(canvas_state.image_key()) + + if canvas_state.is_paintable() { + HTMLCanvasDataSource::Image(canvas_state.image_key()) + } else { + HTMLCanvasDataSource::Empty + } } } @@ -139,13 +125,11 @@ impl CanvasContext for CanvasRenderingContext2D { } fn resize(&self) { - self.set_bitmap_dimensions(self.size().cast()) + self.set_canvas_bitmap_dimensions(self.size().cast()) } fn get_image_data(&self) -> Option<Snapshot> { - let size = self.size(); - - if size.is_empty() { + if !self.canvas_state.is_paintable() { return None; } diff --git a/components/script/dom/defaultteereadrequest.rs b/components/script/dom/defaultteereadrequest.rs index debc084e068..94e285da72b 100644 --- a/components/script/dom/defaultteereadrequest.rs +++ b/components/script/dom/defaultteereadrequest.rs @@ -21,7 +21,7 @@ use crate::dom::globalscope::GlobalScope; use crate::dom::promise::Promise; use crate::dom::readablestream::ReadableStream; use crate::microtask::Microtask; -use crate::script_runtime::CanGc; +use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, allow(crown::unrooted_must_root))] @@ -32,8 +32,8 @@ pub(crate) struct DefaultTeeReadRequestMicrotask { } impl DefaultTeeReadRequestMicrotask { - pub(crate) fn microtask_chunk_steps(&self, can_gc: CanGc) { - self.tee_read_request.chunk_steps(&self.chunk, can_gc) + pub(crate) fn microtask_chunk_steps(&self, cx: SafeJSContext, can_gc: CanGc) { + self.tee_read_request.chunk_steps(cx, &self.chunk, can_gc) } } @@ -94,8 +94,14 @@ impl DefaultTeeReadRequest { } /// Call into cancel of the stream, /// <https://streams.spec.whatwg.org/#readable-stream-cancel> - pub(crate) fn stream_cancel(&self, reason: SafeHandleValue, can_gc: CanGc) { - self.stream.cancel(reason, can_gc); + pub(crate) fn stream_cancel( + &self, + cx: SafeJSContext, + global: &GlobalScope, + reason: SafeHandleValue, + can_gc: CanGc, + ) { + self.stream.cancel(cx, global, reason, can_gc); } /// Enqueue a microtask to perform the chunk steps /// <https://streams.spec.whatwg.org/#ref-for-read-request-chunk-steps%E2%91%A2> @@ -115,13 +121,13 @@ impl DefaultTeeReadRequest { } /// <https://streams.spec.whatwg.org/#ref-for-read-request-chunk-steps%E2%91%A2> #[allow(clippy::borrowed_box)] - pub(crate) fn chunk_steps(&self, chunk: &Box<Heap<JSVal>>, can_gc: CanGc) { + pub(crate) fn chunk_steps(&self, cx: SafeJSContext, chunk: &Box<Heap<JSVal>>, can_gc: CanGc) { + let global = &self.stream.global(); // Set readAgain to false. self.read_again.set(false); // Let chunk1 and chunk2 be chunk. let chunk1 = chunk; let chunk2 = chunk; - let cx = GlobalScope::get_cx(); rooted!(in(*cx) let chunk1_value = chunk1.get()); rooted!(in(*cx) let chunk2_value = chunk2.get()); @@ -131,9 +137,7 @@ impl DefaultTeeReadRequest { rooted!(in(*cx) let mut clone_result = UndefinedValue()); let data = structuredclone::write(cx, chunk2_value.handle(), None).unwrap(); // If cloneResult is an abrupt completion, - if structuredclone::read(&self.stream.global(), data, clone_result.handle_mut()) - .is_err() - { + if structuredclone::read(global, data, clone_result.handle_mut()).is_err() { // Perform ! ReadableStreamDefaultControllerError(branch_1.[[controller]], cloneResult.[[Value]]). self.readable_stream_default_controller_error( &self.branch_1, @@ -148,7 +152,7 @@ impl DefaultTeeReadRequest { can_gc, ); // Resolve cancelPromise with ! ReadableStreamCancel(stream, cloneResult.[[Value]]). - self.stream_cancel(clone_result.handle(), can_gc); + self.stream_cancel(cx, global, clone_result.handle(), can_gc); // Return. return; } else { diff --git a/components/script/dom/defaultteeunderlyingsource.rs b/components/script/dom/defaultteeunderlyingsource.rs index 5895297d982..7935c388842 100644 --- a/components/script/dom/defaultteeunderlyingsource.rs +++ b/components/script/dom/defaultteeunderlyingsource.rs @@ -19,7 +19,7 @@ use crate::dom::defaultteereadrequest::DefaultTeeReadRequest; use crate::dom::globalscope::GlobalScope; use crate::dom::promise::Promise; use crate::dom::readablestreamdefaultreader::ReadRequest; -use crate::script_runtime::CanGc; +use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; #[derive(JSTraceable, MallocSizeOf)] pub(crate) enum TeeCancelAlgorithm { @@ -156,6 +156,8 @@ impl DefaultTeeUnderlyingSource { #[allow(unsafe_code)] pub(crate) fn cancel_algorithm( &self, + cx: SafeJSContext, + global: &GlobalScope, reason: SafeHandleValue, can_gc: CanGc, ) -> Option<Result<Rc<Promise>, Error>> { @@ -169,7 +171,7 @@ impl DefaultTeeUnderlyingSource { // If canceled_2 is true, if self.canceled_2.get() { - self.resolve_cancel_promise(can_gc); + self.resolve_cancel_promise(cx, global, can_gc); } // Return cancelPromise. Some(Ok(self.cancel_promise.clone())) @@ -183,7 +185,7 @@ impl DefaultTeeUnderlyingSource { // If canceled_1 is true, if self.canceled_1.get() { - self.resolve_cancel_promise(can_gc); + self.resolve_cancel_promise(cx, global, can_gc); } // Return cancelPromise. Some(Ok(self.cancel_promise.clone())) @@ -192,9 +194,8 @@ impl DefaultTeeUnderlyingSource { } #[allow(unsafe_code)] - fn resolve_cancel_promise(&self, can_gc: CanGc) { + fn resolve_cancel_promise(&self, cx: SafeJSContext, global: &GlobalScope, can_gc: CanGc) { // Let compositeReason be ! CreateArrayFromList(« reason_1, reason_2 »). - let cx = GlobalScope::get_cx(); rooted_vec!(let mut reasons_values); reasons_values.push(self.reason_1.get()); reasons_values.push(self.reason_2.get()); @@ -204,7 +205,9 @@ impl DefaultTeeUnderlyingSource { rooted!(in(*cx) let reasons_value = ObjectValue(reasons.get())); // Let cancelResult be ! ReadableStreamCancel(stream, compositeReason). - let cancel_result = self.stream.cancel(reasons_value.handle(), can_gc); + let cancel_result = self + .stream + .cancel(cx, global, reasons_value.handle(), can_gc); // Resolve cancelPromise with cancelResult. self.cancel_promise.resolve_native(&cancel_result, can_gc); diff --git a/components/script/dom/dissimilaroriginwindow.rs b/components/script/dom/dissimilaroriginwindow.rs index b7fbe0855fe..70c384db822 100644 --- a/components/script/dom/dissimilaroriginwindow.rs +++ b/components/script/dom/dissimilaroriginwindow.rs @@ -181,12 +181,13 @@ impl DissimilarOriginWindowMethods<crate::DomTypeHolder> for DissimilarOriginWin // https://html.spec.whatwg.org/multipage/#dom-window-blur fn Blur(&self) { - // TODO: Implement x-origin blur + // > User agents are encouraged to ignore calls to this `blur()` method + // > entirely. } - // https://html.spec.whatwg.org/multipage/#dom-focus + // https://html.spec.whatwg.org/multipage/#dom-window-focus fn Focus(&self) { - // TODO: Implement x-origin focus + self.window_proxy().focus(); } // https://html.spec.whatwg.org/multipage/#dom-location diff --git a/components/script/dom/document.rs b/components/script/dom/document.rs index 1e2a3747751..2baab15e1b8 100644 --- a/components/script/dom/document.rs +++ b/components/script/dom/document.rs @@ -30,8 +30,8 @@ use devtools_traits::ScriptToDevtoolsControlMsg; use dom_struct::dom_struct; use embedder_traits::{ AllowOrDeny, AnimationState, CompositorHitTestResult, ContextMenuResult, EditingActionEvent, - EmbedderMsg, ImeEvent, InputEvent, LoadStatus, MouseButton, MouseButtonAction, - MouseButtonEvent, TouchEvent, TouchEventType, TouchId, WheelEvent, + EmbedderMsg, FocusSequenceNumber, ImeEvent, InputEvent, LoadStatus, MouseButton, + MouseButtonAction, MouseButtonEvent, TouchEvent, TouchEventType, TouchId, WheelEvent, }; use encoding_rs::{Encoding, UTF_8}; use euclid::default::{Point2D, Rect, Size2D}; @@ -270,12 +270,11 @@ pub(crate) enum IsHTMLDocument { #[derive(JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] -enum FocusTransaction { - /// No focus operation is in effect. - NotInTransaction, - /// A focus operation is in effect. - /// Contains the element that has most recently requested focus for itself. - InTransaction(Option<Dom<Element>>), +struct FocusTransaction { + /// The focused element of this document. + element: Option<Dom<Element>>, + /// See [`Document::has_focus`]. + has_focus: bool, } /// Information about a declarative refresh @@ -341,9 +340,16 @@ pub(crate) struct Document { /// Whether the DOMContentLoaded event has already been dispatched. domcontentloaded_dispatched: Cell<bool>, /// The state of this document's focus transaction. - focus_transaction: DomRefCell<FocusTransaction>, + focus_transaction: DomRefCell<Option<FocusTransaction>>, /// The element that currently has the document focus context. focused: MutNullableDom<Element>, + /// The last sequence number sent to the constellation. + #[no_trace] + focus_sequence: Cell<FocusSequenceNumber>, + /// Indicates whether the container is included in the top-level browsing + /// context's focus chain (not considering system focus). Permanently `true` + /// for a top-level document. + has_focus: Cell<bool>, /// The script element that is currently executing. current_script: MutNullableDom<HTMLScriptElement>, /// <https://html.spec.whatwg.org/multipage/#pending-parsing-blocking-script> @@ -1120,122 +1126,318 @@ impl Document { self.focused.get() } + /// Get the last sequence number sent to the constellation. + /// + /// Received focus-related messages with sequence numbers less than the one + /// returned by this method must be discarded. + pub fn get_focus_sequence(&self) -> FocusSequenceNumber { + self.focus_sequence.get() + } + + /// Generate the next sequence number for focus-related messages. + fn increment_fetch_focus_sequence(&self) -> FocusSequenceNumber { + self.focus_sequence.set(FocusSequenceNumber( + self.focus_sequence + .get() + .0 + .checked_add(1) + .expect("too many focus messages have been sent"), + )); + self.focus_sequence.get() + } + /// Initiate a new round of checking for elements requesting focus. The last element to call /// `request_focus` before `commit_focus_transaction` is called will receive focus. fn begin_focus_transaction(&self) { - *self.focus_transaction.borrow_mut() = FocusTransaction::InTransaction(Default::default()); + // Initialize it with the current state + *self.focus_transaction.borrow_mut() = Some(FocusTransaction { + element: self.focused.get().as_deref().map(Dom::from_ref), + has_focus: self.has_focus.get(), + }); } /// <https://html.spec.whatwg.org/multipage/#focus-fixup-rule> pub(crate) fn perform_focus_fixup_rule(&self, not_focusable: &Element, can_gc: CanGc) { + // Return if `not_focusable` is not the designated focused area of the + // `Document`. if Some(not_focusable) != self.focused.get().as_deref() { return; } - self.request_focus( - self.GetBody().as_ref().map(|e| e.upcast()), - FocusType::Element, - can_gc, - ) + + let implicit_transaction = self.focus_transaction.borrow().is_none(); + + if implicit_transaction { + self.begin_focus_transaction(); + } + + // Designate the viewport as the new focused area of the `Document`, but + // do not run the focusing steps. + { + let mut focus_transaction = self.focus_transaction.borrow_mut(); + focus_transaction.as_mut().unwrap().element = None; + } + + if implicit_transaction { + self.commit_focus_transaction(FocusInitiator::Local, can_gc); + } } - /// Request that the given element receive focus once the current transaction is complete. - /// If None is passed, then whatever element is currently focused will no longer be focused - /// once the transaction is complete. + /// Request that the given element receive focus once the current + /// transaction is complete. `None` specifies to focus the document. + /// + /// If there's no ongoing transaction, this method automatically starts and + /// commits an implicit transaction. pub(crate) fn request_focus( &self, elem: Option<&Element>, - focus_type: FocusType, + focus_initiator: FocusInitiator, can_gc: CanGc, ) { - let implicit_transaction = matches!( - *self.focus_transaction.borrow(), - FocusTransaction::NotInTransaction - ); + // If an element is specified, and it's non-focusable, ignore the + // request. + if elem.is_some_and(|e| !e.is_focusable_area()) { + return; + } + + let implicit_transaction = self.focus_transaction.borrow().is_none(); + if implicit_transaction { self.begin_focus_transaction(); } - if elem.is_none_or(|e| e.is_focusable_area()) { - *self.focus_transaction.borrow_mut() = - FocusTransaction::InTransaction(elem.map(Dom::from_ref)); + + { + let mut focus_transaction = self.focus_transaction.borrow_mut(); + let focus_transaction = focus_transaction.as_mut().unwrap(); + focus_transaction.element = elem.map(Dom::from_ref); + focus_transaction.has_focus = true; } + if implicit_transaction { - self.commit_focus_transaction(focus_type, can_gc); + self.commit_focus_transaction(focus_initiator, can_gc); + } + } + + /// Update the local focus state accordingly after being notified that the + /// document's container is removed from the top-level browsing context's + /// focus chain (not considering system focus). + pub(crate) fn handle_container_unfocus(&self, can_gc: CanGc) { + assert!( + self.window().parent_info().is_some(), + "top-level document cannot be unfocused", + ); + + // Since this method is called from an event loop, there mustn't be + // an in-progress focus transaction + assert!( + self.focus_transaction.borrow().is_none(), + "there mustn't be an in-progress focus transaction at this point" + ); + + // Start an implicit focus transaction + self.begin_focus_transaction(); + + // Update the transaction + { + let mut focus_transaction = self.focus_transaction.borrow_mut(); + focus_transaction.as_mut().unwrap().has_focus = false; } + + // Commit the implicit focus transaction + self.commit_focus_transaction(FocusInitiator::Remote, can_gc); } /// Reassign the focus context to the element that last requested focus during this - /// transaction, or none if no elements requested it. - fn commit_focus_transaction(&self, focus_type: FocusType, can_gc: CanGc) { - let possibly_focused = match *self.focus_transaction.borrow() { - FocusTransaction::NotInTransaction => unreachable!(), - FocusTransaction::InTransaction(ref elem) => { - elem.as_ref().map(|e| DomRoot::from_ref(&**e)) - }, + /// transaction, or the document if no elements requested it. + fn commit_focus_transaction(&self, focus_initiator: FocusInitiator, can_gc: CanGc) { + let (mut new_focused, new_focus_state) = { + let focus_transaction = self.focus_transaction.borrow(); + let focus_transaction = focus_transaction + .as_ref() + .expect("no focus transaction in progress"); + ( + focus_transaction + .element + .as_ref() + .map(|e| DomRoot::from_ref(&**e)), + focus_transaction.has_focus, + ) }; - *self.focus_transaction.borrow_mut() = FocusTransaction::NotInTransaction; - if self.focused == possibly_focused.as_deref() { - return; + *self.focus_transaction.borrow_mut() = None; + + if !new_focus_state { + // In many browsers, a document forgets its focused area when the + // document is removed from the top-level BC's focus chain + if new_focused.take().is_some() { + trace!( + "Forgetting the document's focused area because the \ + document's container was removed from the top-level BC's \ + focus chain" + ); + } } - if let Some(ref elem) = self.focused.get() { - let node = elem.upcast::<Node>(); - elem.set_focus_state(false); - // FIXME: pass appropriate relatedTarget - self.fire_focus_event(FocusEventType::Blur, node, None, can_gc); - // Notify the embedder to hide the input method. - if elem.input_method_type().is_some() { - self.send_to_embedder(EmbedderMsg::HideIME(self.webview_id())); + let old_focused = self.focused.get(); + let old_focus_state = self.has_focus.get(); + + debug!( + "Committing focus transaction: {:?} → {:?}", + (&old_focused, old_focus_state), + (&new_focused, new_focus_state), + ); + + // `*_focused_filtered` indicates the local element (if any) included in + // the top-level BC's focus chain. + let old_focused_filtered = old_focused.as_ref().filter(|_| old_focus_state); + let new_focused_filtered = new_focused.as_ref().filter(|_| new_focus_state); + + let trace_focus_chain = |name, element, doc| { + trace!( + "{} local focus chain: {}", + name, + match (element, doc) { + (Some(e), _) => format!("[{:?}, document]", e), + (None, true) => "[document]".to_owned(), + (None, false) => "[]".to_owned(), + } + ); + }; + + trace_focus_chain("Old", old_focused_filtered, old_focus_state); + trace_focus_chain("New", new_focused_filtered, new_focus_state); + + if old_focused_filtered != new_focused_filtered { + if let Some(elem) = &old_focused_filtered { + let node = elem.upcast::<Node>(); + elem.set_focus_state(false); + // FIXME: pass appropriate relatedTarget + if node.is_connected() { + self.fire_focus_event(FocusEventType::Blur, node.upcast(), None, can_gc); + } + + // Notify the embedder to hide the input method. + if elem.input_method_type().is_some() { + self.send_to_embedder(EmbedderMsg::HideIME(self.webview_id())); + } } } - self.focused.set(possibly_focused.as_deref()); + if old_focus_state != new_focus_state && !new_focus_state { + self.fire_focus_event(FocusEventType::Blur, self.global().upcast(), None, can_gc); + } - if let Some(ref elem) = self.focused.get() { - elem.set_focus_state(true); - let node = elem.upcast::<Node>(); - // FIXME: pass appropriate relatedTarget - self.fire_focus_event(FocusEventType::Focus, node, None, can_gc); - // Update the focus state for all elements in the focus chain. - // https://html.spec.whatwg.org/multipage/#focus-chain - if focus_type == FocusType::Element { - self.window() - .send_to_constellation(ScriptToConstellationMessage::Focus); + self.focused.set(new_focused.as_deref()); + self.has_focus.set(new_focus_state); + + if old_focus_state != new_focus_state && new_focus_state { + self.fire_focus_event(FocusEventType::Focus, self.global().upcast(), None, can_gc); + } + + if old_focused_filtered != new_focused_filtered { + if let Some(elem) = &new_focused_filtered { + elem.set_focus_state(true); + let node = elem.upcast::<Node>(); + // FIXME: pass appropriate relatedTarget + self.fire_focus_event(FocusEventType::Focus, node.upcast(), None, can_gc); + + // Notify the embedder to display an input method. + if let Some(kind) = elem.input_method_type() { + let rect = elem.upcast::<Node>().bounding_content_box_or_zero(can_gc); + let rect = Rect::new( + Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()), + Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()), + ); + let (text, multiline) = if let Some(input) = elem.downcast::<HTMLInputElement>() + { + ( + Some(( + (input.Value()).to_string(), + input.GetSelectionEnd().unwrap_or(0) as i32, + )), + false, + ) + } else if let Some(textarea) = elem.downcast::<HTMLTextAreaElement>() { + ( + Some(( + (textarea.Value()).to_string(), + textarea.GetSelectionEnd().unwrap_or(0) as i32, + )), + true, + ) + } else { + (None, false) + }; + self.send_to_embedder(EmbedderMsg::ShowIME( + self.webview_id(), + kind, + text, + multiline, + DeviceIntRect::from_untyped(&rect.to_box2d()), + )); + } } + } + + if focus_initiator != FocusInitiator::Local { + return; + } - // Notify the embedder to display an input method. - if let Some(kind) = elem.input_method_type() { - let rect = elem.upcast::<Node>().bounding_content_box_or_zero(can_gc); - let rect = Rect::new( - Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()), - Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()), + // We are the initiator of the focus operation, so we must broadcast + // the change we intend to make. + match (old_focus_state, new_focus_state) { + (_, true) => { + // Advertise the change in the focus chain. + // <https://html.spec.whatwg.org/multipage/#focus-chain> + // <https://html.spec.whatwg.org/multipage/#focusing-steps> + // + // If the top-level BC doesn't have system focus, this won't + // have an immediate effect, but it will when we gain system + // focus again. Therefore we still have to send `ScriptMsg:: + // Focus`. + // + // When a container with a non-null nested browsing context is + // focused, its active document becomes the focused area of the + // top-level browsing context instead. Therefore we need to let + // the constellation know if such a container is focused. + // + // > The focusing steps for an object `new focus target` [...] + // > + // > 3. If `new focus target` is a browsing context container + // > with non-null nested browsing context, then set + // > `new focus target` to the nested browsing context's + // > active document. + let child_browsing_context_id = new_focused + .as_ref() + .and_then(|elem| elem.downcast::<HTMLIFrameElement>()) + .and_then(|iframe| iframe.browsing_context_id()); + + let sequence = self.increment_fetch_focus_sequence(); + + debug!( + "Advertising the focus request to the constellation \ + with sequence number {} and child BC ID {}", + sequence, + child_browsing_context_id + .as_ref() + .map(|id| id as &dyn std::fmt::Display) + .unwrap_or(&"(none)"), ); - let (text, multiline) = if let Some(input) = elem.downcast::<HTMLInputElement>() { - ( - Some(( - input.Value().to_string(), - input.GetSelectionEnd().unwrap_or(0) as i32, - )), - false, - ) - } else if let Some(textarea) = elem.downcast::<HTMLTextAreaElement>() { - ( - Some(( - textarea.Value().to_string(), - textarea.GetSelectionEnd().unwrap_or(0) as i32, - )), - true, - ) - } else { - (None, false) - }; - self.send_to_embedder(EmbedderMsg::ShowIME( - self.webview_id(), - kind, - text, - multiline, - DeviceIntRect::from_untyped(&rect.to_box2d()), - )); - } + + self.window() + .send_to_constellation(ScriptToConstellationMessage::Focus( + child_browsing_context_id, + sequence, + )); + }, + (false, false) => { + // Our `Document` doesn't have focus, and we intend to keep it + // this way. + }, + (true, false) => { + unreachable!( + "Can't lose the document's focus without specifying \ + another one to focus" + ); + }, } } @@ -1350,7 +1552,10 @@ impl Document { } self.begin_focus_transaction(); - self.request_focus(Some(&*el), FocusType::Element, can_gc); + // Try to focus `el`. If it's not focusable, focus the document + // instead. + self.request_focus(None, FocusInitiator::Local, can_gc); + self.request_focus(Some(&*el), FocusInitiator::Local, can_gc); } let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event( @@ -1388,7 +1593,9 @@ impl Document { } if let MouseButtonAction::Click = event.action { - self.commit_focus_transaction(FocusType::Element, can_gc); + if self.focus_transaction.borrow().is_some() { + self.commit_focus_transaction(FocusInitiator::Local, can_gc); + } self.maybe_fire_dblclick( hit_test_result.point_in_viewport, node, @@ -2215,7 +2422,7 @@ impl Document { ImeEvent::Dismissed => { self.request_focus( self.GetBody().as_ref().map(|e| e.upcast()), - FocusType::Element, + FocusInitiator::Local, can_gc, ); return; @@ -3194,7 +3401,7 @@ impl Document { fn fire_focus_event( &self, focus_event_type: FocusEventType, - node: &Node, + event_target: &EventTarget, related_target: Option<&EventTarget>, can_gc: CanGc, ) { @@ -3214,8 +3421,7 @@ impl Document { ); let event = event.upcast::<Event>(); event.set_trusted(true); - let target = node.upcast(); - event.fire(target, can_gc); + event.fire(event_target, can_gc); } /// <https://html.spec.whatwg.org/multipage/#cookie-averse-document-object> @@ -3795,6 +4001,8 @@ impl Document { .and_then(|charset| Encoding::for_label(charset.as_bytes())) .unwrap_or(UTF_8); + let has_focus = window.parent_info().is_none(); + let has_browsing_context = has_browsing_context == HasBrowsingContext::Yes; Document { @@ -3842,8 +4050,10 @@ impl Document { stylesheet_list: MutNullableDom::new(None), ready_state: Cell::new(ready_state), domcontentloaded_dispatched: Cell::new(domcontentloaded_dispatched), - focus_transaction: DomRefCell::new(FocusTransaction::NotInTransaction), + focus_transaction: DomRefCell::new(None), focused: Default::default(), + focus_sequence: Cell::new(FocusSequenceNumber::default()), + has_focus: Cell::new(has_focus), current_script: Default::default(), pending_parsing_blocking_script: Default::default(), script_blocking_stylesheets_count: Cell::new(0u32), @@ -4989,12 +5199,34 @@ impl DocumentMethods<crate::DomTypeHolder> for Document { // https://html.spec.whatwg.org/multipage/#dom-document-hasfocus fn HasFocus(&self) -> bool { - // Step 1-2. - if self.window().parent_info().is_none() && self.is_fully_active() { - return true; + // <https://html.spec.whatwg.org/multipage/#has-focus-steps> + // + // > The has focus steps, given a `Document` object `target`, are as + // > follows: + // > + // > 1. If `target`'s browsing context's top-level browsing context does + // > not have system focus, then return false. + + // > 2. Let `candidate` be `target`'s browsing context's top-level + // > browsing context's active document. + // > + // > 3. While true: + // > + // > 3.1. If `candidate` is target, then return true. + // > + // > 3.2. If the focused area of `candidate` is a browsing context + // > container with a non-null nested browsing context, then set + // > `candidate` to the active document of that browsing context + // > container's nested browsing context. + // > + // > 3.3. Otherwise, return false. + if self.window().parent_info().is_none() { + // 2 → 3 → (3.1 || ⋯ → 3.3) + self.is_fully_active() + } else { + // 2 → 3 → 3.2 → (⋯ → 3.1 || ⋯ → 3.3) + self.is_fully_active() && self.has_focus.get() } - // TODO Step 3. - false } // https://html.spec.whatwg.org/multipage/#dom-document-domain @@ -6397,6 +6629,17 @@ pub(crate) enum FocusType { Parent, // Focusing a parent element (an iframe) } +/// Specifies the initiator of a focus operation. +#[derive(Clone, Copy, PartialEq)] +pub enum FocusInitiator { + /// The operation is initiated by this document and to be broadcasted + /// through the constellation. + Local, + /// The operation is initiated somewhere else, and we are updating our + /// internal state accordingly. + Remote, +} + /// Focus events pub(crate) enum FocusEventType { Focus, // Element gained focus. Doesn't bubble. diff --git a/components/script/dom/htmlelement.rs b/components/script/dom/htmlelement.rs index 9505d5182c7..e7efbde9b1d 100644 --- a/components/script/dom/htmlelement.rs +++ b/components/script/dom/htmlelement.rs @@ -32,7 +32,7 @@ use crate::dom::bindings::str::DOMString; use crate::dom::characterdata::CharacterData; use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner}; use crate::dom::customelementregistry::CallbackReaction; -use crate::dom::document::{Document, FocusType}; +use crate::dom::document::{Document, FocusInitiator}; use crate::dom::documentfragment::DocumentFragment; use crate::dom::domstringmap::DOMStringMap; use crate::dom::element::{AttributeMutation, Element}; @@ -415,18 +415,19 @@ impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement { // TODO: Mark the element as locked for focus and run the focusing steps. // https://html.spec.whatwg.org/multipage/#focusing-steps let document = self.owner_document(); - document.request_focus(Some(self.upcast()), FocusType::Element, can_gc); + document.request_focus(Some(self.upcast()), FocusInitiator::Local, can_gc); } // https://html.spec.whatwg.org/multipage/#dom-blur fn Blur(&self, can_gc: CanGc) { - // TODO: Run the unfocusing steps. + // TODO: Run the unfocusing steps. Focus the top-level document, not + // the current document. if !self.as_element().focus_state() { return; } // https://html.spec.whatwg.org/multipage/#unfocusing-steps let document = self.owner_document(); - document.request_focus(None, FocusType::Element, can_gc); + document.request_focus(None, FocusInitiator::Local, can_gc); } // https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent diff --git a/components/script/dom/htmlformelement.rs b/components/script/dom/htmlformelement.rs index ce6dcca66f3..2421b683bf7 100644 --- a/components/script/dom/htmlformelement.rs +++ b/components/script/dom/htmlformelement.rs @@ -1270,8 +1270,14 @@ impl HTMLFormElement { return; } - let controls = self.controls.borrow(); - for child in controls.iter() { + let controls: Vec<_> = self + .controls + .borrow() + .iter() + .map(|c| c.as_rooted()) + .collect(); + + for child in controls { let child = child.upcast::<Node>(); match child.type_id() { diff --git a/components/script/dom/htmlscriptelement.rs b/components/script/dom/htmlscriptelement.rs index 9452dcb17a6..58853f600d2 100644 --- a/components/script/dom/htmlscriptelement.rs +++ b/components/script/dom/htmlscriptelement.rs @@ -1008,12 +1008,12 @@ impl HTMLScriptElement { Ok(script) => script, }; - // TODO: we need to handle this for worker if let Some(chan) = self.global().devtools_chan() { let pipeline_id = self.global().pipeline_id(); let source_info = SourceInfo { url: script.url.clone(), external: script.external, + worker_id: None, }; let _ = chan.send(ScriptToDevtoolsControlMsg::ScriptSourceLoaded( pipeline_id, diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 45a107ae673..b56126076da 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -2246,9 +2246,6 @@ impl Node { }; // Step 4. - Node::adopt(node, &parent.owner_document(), can_gc); - - // Step 5. Node::insert( node, parent, @@ -2257,7 +2254,7 @@ impl Node { can_gc, ); - // Step 6. + // Step 5. Ok(DomRoot::from_ref(node)) } @@ -2269,49 +2266,64 @@ impl Node { suppress_observers: SuppressObserver, can_gc: CanGc, ) { - node.owner_doc().add_script_and_layout_blocker(); - debug_assert!(*node.owner_doc() == *parent.owner_doc()); debug_assert!(child.is_none_or(|child| Some(parent) == child.GetParentNode().as_deref())); - // Step 1. - let count = if node.is::<DocumentFragment>() { - node.children_count() + // Step 1. Let nodes be node’s children, if node is a DocumentFragment node; otherwise « node ». + rooted_vec!(let mut new_nodes); + let new_nodes = if let NodeTypeId::DocumentFragment(_) = node.type_id() { + new_nodes.extend(node.children().map(|node| Dom::from_ref(&*node))); + new_nodes.r() } else { - 1 + from_ref(&node) }; - // Step 2. - if let Some(child) = child { - if !parent.ranges_is_empty() { - let index = child.index(); - // Steps 2.1-2. - parent.ranges().increase_above(parent, index, count); - } + + // Step 2. Let count be nodes’s size. + let count = new_nodes.len(); + + // Step 3. If count is 0, then return. + if count == 0 { + return; } - rooted_vec!(let mut new_nodes); - let new_nodes = if let NodeTypeId::DocumentFragment(_) = node.type_id() { - // Step 3. - new_nodes.extend(node.children().map(|kid| Dom::from_ref(&*kid))); - // Step 4. - for kid in &*new_nodes { + + // Script and layout blockers must be added after any early return. + // `node.owner_doc()` may change during the algorithm. + let parent_document = parent.owner_doc(); + let from_document = node.owner_doc(); + from_document.add_script_and_layout_blocker(); + parent_document.add_script_and_layout_blocker(); + + // Step 4. If node is a DocumentFragment node: + if let NodeTypeId::DocumentFragment(_) = node.type_id() { + // Step 4.1. Remove its children with the suppress observers flag set. + for kid in new_nodes { Node::remove(kid, node, SuppressObserver::Suppressed, can_gc); } - // Step 5. - vtable_for(node).children_changed(&ChildrenMutation::replace_all(new_nodes.r(), &[])); + vtable_for(node).children_changed(&ChildrenMutation::replace_all(new_nodes, &[])); + // Step 4.2. Queue a tree mutation record for node with « », nodes, null, and null. let mutation = LazyCell::new(|| Mutation::ChildList { added: None, - removed: Some(new_nodes.r()), + removed: Some(new_nodes), prev: None, next: None, }); MutationObserver::queue_a_mutation_record(node, mutation); + } - new_nodes.r() - } else { - // Step 3. - from_ref(&node) - }; - // Step 6. + // Step 5. If child is non-null: + // 1. For each live range whose start node is parent and start offset is + // greater than child’s index, increase its start offset by count. + // 2. For each live range whose end node is parent and end offset is + // greater than child’s index, increase its end offset by count. + if let Some(child) = child { + if !parent.ranges_is_empty() { + parent + .ranges() + .increase_above(parent, child.index(), count.try_into().unwrap()); + } + } + + // Step 6. Let previousSibling be child’s previous sibling or parent’s last child if child is null. let previous_sibling = match suppress_observers { SuppressObserver::Unsuppressed => match child { Some(child) => child.GetPreviousSibling(), @@ -2319,9 +2331,14 @@ impl Node { }, SuppressObserver::Suppressed => None, }; - // Step 7. + + // Step 7. For each node in nodes, in tree order: for kid in new_nodes { - // Step 7.1. + // Step 7.1. Adopt node into parent’s node document. + Node::adopt(kid, &parent.owner_document(), can_gc); + + // Step 7.2. If child is null, then append node to parent’s children. + // Step 7.3. Otherwise, insert node into parent’s children before child’s index. parent.add_child(kid, child, can_gc); // Step 7.4 If parent is a shadow host whose shadow root’s slot assignment is "named" @@ -2350,12 +2367,17 @@ impl Node { kid.GetRootNode(&GetRootNodeOptions::empty()) .assign_slottables_for_a_tree(); - // Step 7.7. + // Step 7.7. For each shadow-including inclusive descendant inclusiveDescendant of node, + // in shadow-including tree order: for descendant in kid .traverse_preorder(ShadowIncluding::Yes) .filter_map(DomRoot::downcast::<Element>) { + // Step 7.7.1. Run the insertion steps with inclusiveDescendant. + // This is done in `parent.add_child()`. + // Step 7.7.2, whatwg/dom#833 + // Enqueue connected reactions for custom elements or try upgrade. if descendant.is_custom() { if descendant.is_connected() { ScriptThread::enqueue_callback_reaction( @@ -2369,13 +2391,18 @@ impl Node { } } } + if let SuppressObserver::Unsuppressed = suppress_observers { + // Step 9. Run the children changed steps for parent. + // TODO(xiaochengh): If we follow the spec and move it out of the if block, some WPT fail. Investigate. vtable_for(parent).children_changed(&ChildrenMutation::insert( previous_sibling.as_deref(), new_nodes, child, )); + // Step 8. If suppress observers flag is unset, then queue a tree mutation record for parent + // with nodes, « », previousSibling, and child. let mutation = LazyCell::new(|| Mutation::ChildList { added: Some(new_nodes), removed: None, @@ -2408,7 +2435,7 @@ impl Node { // 2) post_connection_steps from Node::insert, // we use a delayed task that will run as soon as Node::insert removes its // script/layout blocker. - node.owner_doc().add_delayed_task(task!(PostConnectionSteps: move || { + parent_document.add_delayed_task(task!(PostConnectionSteps: move || { // Step 12. For each node of staticNodeList, if node is connected, then run the // post-connection steps with node. for node in static_node_list.iter().map(Trusted::root).filter(|n| n.is_connected()) { @@ -2416,7 +2443,8 @@ impl Node { } })); - node.owner_doc().remove_script_and_layout_blocker(); + parent_document.remove_script_and_layout_blocker(); + from_document.remove_script_and_layout_blocker(); } /// <https://dom.spec.whatwg.org/#concept-node-replace-all> @@ -3239,10 +3267,16 @@ impl NodeMethods<crate::DomTypeHolder> for Node { // Step 9. let previous_sibling = child.GetPreviousSibling(); - // Step 10. + // NOTE: All existing browsers assume that adoption is performed here, which does not follow the DOM spec. + // However, if we follow the spec and delay adoption to inside `Node::insert()`, then the mutation records will + // be different, and we will fail WPT dom/nodes/MutationObserver-childList.html. let document = self.owner_document(); Node::adopt(node, &document, can_gc); + // Step 10. Let removedNodes be the empty set. + // Step 11. If child’s parent is non-null: + // 1. Set removedNodes to « child ». + // 2. Remove child with the suppress observers flag set. let removed_child = if node != child { // Step 11. Node::remove(child, self, SuppressObserver::Suppressed, can_gc); diff --git a/components/script/dom/offscreencanvasrenderingcontext2d.rs b/components/script/dom/offscreencanvasrenderingcontext2d.rs index 2f9b52640e6..b2d0f3201ca 100644 --- a/components/script/dom/offscreencanvasrenderingcontext2d.rs +++ b/components/script/dom/offscreencanvasrenderingcontext2d.rs @@ -65,7 +65,7 @@ impl OffscreenCanvasRenderingContext2D { } pub(crate) fn set_canvas_bitmap_dimensions(&self, size: Size2D<u64>) { - self.context.set_bitmap_dimensions(size.cast()); + self.context.set_canvas_bitmap_dimensions(size.cast()); } pub(crate) fn send_canvas_2d_msg(&self, msg: Canvas2dMsg) { diff --git a/components/script/dom/readablebytestreamcontroller.rs b/components/script/dom/readablebytestreamcontroller.rs index 340e6d04eab..8f28a9a1215 100644 --- a/components/script/dom/readablebytestreamcontroller.rs +++ b/components/script/dom/readablebytestreamcontroller.rs @@ -1612,7 +1612,7 @@ impl ReadableByteStreamController { let realm = enter_realm(&*global); let comp = InRealm::Entered(&realm); let result = underlying_source - .call_pull_algorithm(controller, can_gc) + .call_pull_algorithm(controller, &global, can_gc) .unwrap_or_else(|| { let promise = Promise::new(&global, can_gc); promise.resolve_native(&(), can_gc); @@ -1781,6 +1781,8 @@ impl ReadableByteStreamController { /// <https://streams.spec.whatwg.org/#rbs-controller-private-cancel> pub(crate) fn perform_cancel_steps( &self, + cx: SafeJSContext, + global: &GlobalScope, reason: SafeHandleValue, can_gc: CanGc, ) -> Rc<Promise> { @@ -1794,13 +1796,12 @@ impl ReadableByteStreamController { .underlying_source .get() .expect("Controller should have a source when the cancel steps are called into."); - let global = self.global(); // Let result be the result of performing this.[[cancelAlgorithm]], passing in reason. let result = underlying_source - .call_cancel_algorithm(reason, can_gc) + .call_cancel_algorithm(cx, global, reason, can_gc) .unwrap_or_else(|| { - let promise = Promise::new(&global, can_gc); + let promise = Promise::new(global, can_gc); promise.resolve_native(&(), can_gc); Ok(promise) }); @@ -1808,11 +1809,10 @@ impl ReadableByteStreamController { let promise = result.unwrap_or_else(|error| { let cx = GlobalScope::get_cx(); rooted!(in(*cx) let mut rval = UndefinedValue()); - // TODO: check if `self.global()` is the right globalscope. error .clone() - .to_jsval(cx, &self.global(), rval.handle_mut(), can_gc); - let promise = Promise::new(&global, can_gc); + .to_jsval(cx, global, rval.handle_mut(), can_gc); + let promise = Promise::new(global, can_gc); promise.reject_native(&rval.handle(), can_gc); promise }); diff --git a/components/script/dom/readablestream.rs b/components/script/dom/readablestream.rs index 37899f18fec..51393ab33ae 100644 --- a/components/script/dom/readablestream.rs +++ b/components/script/dom/readablestream.rs @@ -3,10 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, RefCell}; +use std::collections::HashMap; use std::collections::VecDeque; use std::ptr::{self}; use std::rc::Rc; -use std::collections::HashMap; use base::id::{MessagePortId, MessagePortIndex}; use constellation_traits::MessagePortImpl; @@ -22,12 +22,14 @@ use js::typedarray::ArrayBufferViewU8; use crate::dom::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategy; use crate::dom::bindings::codegen::Bindings::ReadableStreamBinding::{ - ReadableStreamGetReaderOptions, ReadableStreamMethods, ReadableStreamReaderMode, StreamPipeOptions + ReadableStreamGetReaderOptions, ReadableStreamMethods, ReadableStreamReaderMode, + StreamPipeOptions, }; use script_bindings::str::DOMString; use crate::dom::domexception::{DOMErrorName, DOMException}; use script_bindings::conversions::StringificationBehavior; +use super::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategySize; use crate::dom::bindings::codegen::Bindings::ReadableStreamDefaultReaderBinding::ReadableStreamDefaultReaderMethods; use crate::dom::bindings::codegen::Bindings::ReadableStreamDefaultControllerBinding::ReadableStreamDefaultController_Binding::ReadableStreamDefaultControllerMethods; use crate::dom::bindings::codegen::Bindings::UnderlyingSourceBinding::UnderlyingSource as JsUnderlyingSource; @@ -640,7 +642,7 @@ impl PipeTo { .reader .get_stream() .expect("Reader should have a stream."); - source.cancel(error.handle(), can_gc) + source.cancel(cx, global, error.handle(), can_gc) }, ShutdownAction::WritableStreamDefaultWriterCloseWithErrorPropagation => { self.writer.close_with_error_propagation(cx, global, can_gc) @@ -766,19 +768,19 @@ impl PartialEq for ReaderType { /// <https://streams.spec.whatwg.org/#create-readable-stream> #[cfg_attr(crown, allow(crown::unrooted_must_root))] -fn create_readable_stream( +pub(crate) fn create_readable_stream( global: &GlobalScope, underlying_source_type: UnderlyingSourceType, - queuing_strategy: QueuingStrategy, + queuing_strategy: Option<Rc<QueuingStrategySize>>, + high_water_mark: Option<f64>, can_gc: CanGc, ) -> DomRoot<ReadableStream> { // If highWaterMark was not passed, set it to 1. - let high_water_mark = queuing_strategy.highWaterMark.unwrap_or(1.0); + let high_water_mark = high_water_mark.unwrap_or(1.0); // If sizeAlgorithm was not passed, set it to an algorithm that returns 1. - let size_algorithm = queuing_strategy - .size - .unwrap_or(extract_size_algorithm(&QueuingStrategy::empty(), can_gc)); + let size_algorithm = + queuing_strategy.unwrap_or(extract_size_algorithm(&QueuingStrategy::empty(), can_gc)); // Assert: ! IsNonNegativeNumber(highWaterMark) is true. assert!(high_water_mark >= 0.0); @@ -1437,19 +1439,24 @@ impl ReadableStream { /// <https://streams.spec.whatwg.org/#readable-stream-cancel> #[allow(unsafe_code)] - pub(crate) fn cancel(&self, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { + pub(crate) fn cancel( + &self, + cx: SafeJSContext, + global: &GlobalScope, + reason: SafeHandleValue, + can_gc: CanGc, + ) -> Rc<Promise> { // Set stream.[[disturbed]] to true. self.disturbed.set(true); // If stream.[[state]] is "closed", return a promise resolved with undefined. if self.is_closed() { - return Promise::new_resolved(&self.global(), GlobalScope::get_cx(), (), can_gc); + return Promise::new_resolved(global, cx, (), can_gc); } // If stream.[[state]] is "errored", return a promise rejected with stream.[[storedError]]. if self.is_errored() { - let promise = Promise::new(&self.global(), can_gc); + let promise = Promise::new(global, can_gc); unsafe { - let cx = GlobalScope::get_cx(); rooted!(in(*cx) let mut rval = UndefinedValue()); self.stored_error.to_jsval(*cx, rval.handle_mut()); promise.reject_native(&rval.handle(), can_gc); @@ -1473,11 +1480,11 @@ impl ReadableStream { Some(ControllerType::Default(controller)) => controller .get() .expect("Stream should have controller.") - .perform_cancel_steps(reason, can_gc), + .perform_cancel_steps(cx, global, reason, can_gc), Some(ControllerType::Byte(controller)) => controller .get() .expect("Stream should have controller.") - .perform_cancel_steps(reason, can_gc), + .perform_cancel_steps(cx, global, reason, can_gc), None => { panic!("Stream does not have a controller."); }, @@ -1587,7 +1594,8 @@ impl ReadableStream { let branch_1 = create_readable_stream( &self.global(), underlying_source_type_branch_1, - QueuingStrategy::empty(), + None, + None, can_gc, ); tee_source_1.set_branch_1(&branch_1); @@ -1597,7 +1605,8 @@ impl ReadableStream { let branch_2 = create_readable_stream( &self.global(), underlying_source_type_branch_2, - QueuingStrategy::empty(), + None, + None, can_gc, ); tee_source_1.set_branch_2(&branch_2); @@ -1908,16 +1917,17 @@ impl ReadableStreamMethods<crate::DomTypeHolder> for ReadableStream { } /// <https://streams.spec.whatwg.org/#rs-cancel> - fn Cancel(&self, _cx: SafeJSContext, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { + fn Cancel(&self, cx: SafeJSContext, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { + let global = self.global(); if self.is_locked() { // If ! IsReadableStreamLocked(this) is true, // return a promise rejected with a TypeError exception. - let promise = Promise::new(&self.global(), can_gc); + let promise = Promise::new(&global, can_gc); promise.reject_error(Error::Type("stream is not locked".to_owned()), can_gc); promise } else { // Return ! ReadableStreamCancel(this, reason). - self.cancel(reason, can_gc) + self.cancel(cx, &global, reason, can_gc) } } diff --git a/components/script/dom/readablestreambyobreader.rs b/components/script/dom/readablestreambyobreader.rs index 16827c1add6..3ccfb255009 100644 --- a/components/script/dom/readablestreambyobreader.rs +++ b/components/script/dom/readablestreambyobreader.rs @@ -401,8 +401,8 @@ impl ReadableStreamBYOBReaderMethods<crate::DomTypeHolder> for ReadableStreamBYO } /// <https://streams.spec.whatwg.org/#generic-reader-cancel> - fn Cancel(&self, _cx: SafeJSContext, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { - self.generic_cancel(&self.global(), reason, can_gc) + fn Cancel(&self, cx: SafeJSContext, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { + self.generic_cancel(cx, &self.global(), reason, can_gc) } } diff --git a/components/script/dom/readablestreamdefaultcontroller.rs b/components/script/dom/readablestreamdefaultcontroller.rs index 66ba3d209c7..c52fb712a03 100644 --- a/components/script/dom/readablestreamdefaultcontroller.rs +++ b/components/script/dom/readablestreamdefaultcontroller.rs @@ -540,7 +540,7 @@ impl ReadableStreamDefaultController { let realm = enter_realm(&*global); let comp = InRealm::Entered(&realm); let result = underlying_source - .call_pull_algorithm(controller, can_gc) + .call_pull_algorithm(controller, &global, can_gc) .unwrap_or_else(|| { let promise = Promise::new(&global, can_gc); promise.resolve_native(&(), can_gc); @@ -563,6 +563,8 @@ impl ReadableStreamDefaultController { /// <https://streams.spec.whatwg.org/#rs-default-controller-private-cancel> pub(crate) fn perform_cancel_steps( &self, + cx: SafeJSContext, + global: &GlobalScope, reason: SafeHandleValue, can_gc: CanGc, ) -> Rc<Promise> { @@ -573,24 +575,21 @@ impl ReadableStreamDefaultController { .underlying_source .get() .expect("Controller should have a source when the cancel steps are called into."); - let global = self.global(); - // Let result be the result of performing this.[[cancelAlgorithm]], passing reason. let result = underlying_source - .call_cancel_algorithm(reason, can_gc) + .call_cancel_algorithm(cx, global, reason, can_gc) .unwrap_or_else(|| { - let promise = Promise::new(&global, can_gc); + let promise = Promise::new(global, can_gc); promise.resolve_native(&(), can_gc); Ok(promise) }); let promise = result.unwrap_or_else(|error| { - let cx = GlobalScope::get_cx(); rooted!(in(*cx) let mut rval = UndefinedValue()); - // TODO: check if `self.global()` is the right globalscope. + error .clone() - .to_jsval(cx, &self.global(), rval.handle_mut(), can_gc); - let promise = Promise::new(&global, can_gc); + .to_jsval(cx, global, rval.handle_mut(), can_gc); + let promise = Promise::new(global, can_gc); promise.reject_native(&rval.handle(), can_gc); promise }); @@ -812,7 +811,7 @@ impl ReadableStreamDefaultController { } /// <https://streams.spec.whatwg.org/#readable-stream-default-controller-get-desired-size> - fn get_desired_size(&self) -> Option<f64> { + pub(crate) fn get_desired_size(&self) -> Option<f64> { let stream = self.stream.get()?; // If state is "errored", return null. @@ -832,7 +831,7 @@ impl ReadableStreamDefaultController { } /// <https://streams.spec.whatwg.org/#readable-stream-default-controller-can-close-or-enqueue> - fn can_close_or_enqueue(&self) -> bool { + pub(crate) fn can_close_or_enqueue(&self) -> bool { let Some(stream) = self.stream.get() else { return false; }; @@ -865,6 +864,14 @@ impl ReadableStreamDefaultController { stream.error(e, can_gc); } + + /// <https://streams.spec.whatwg.org/#rs-default-controller-has-backpressure> + #[allow(unused)] + pub(crate) fn has_backpressure(&self) -> bool { + // If ! ReadableStreamDefaultControllerShouldCallPull(controller) is true, return false. + // Otherwise, return true. + !self.should_call_pull() + } } impl ReadableStreamDefaultControllerMethods<crate::DomTypeHolder> diff --git a/components/script/dom/readablestreamdefaultreader.rs b/components/script/dom/readablestreamdefaultreader.rs index f490627a2ee..7fd243b0b56 100644 --- a/components/script/dom/readablestreamdefaultreader.rs +++ b/components/script/dom/readablestreamdefaultreader.rs @@ -605,8 +605,8 @@ impl ReadableStreamDefaultReaderMethods<crate::DomTypeHolder> for ReadableStream } /// <https://streams.spec.whatwg.org/#generic-reader-cancel> - fn Cancel(&self, _cx: SafeJSContext, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { - self.generic_cancel(&self.global(), reason, can_gc) + fn Cancel(&self, cx: SafeJSContext, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { + self.generic_cancel(cx, &self.global(), reason, can_gc) } } diff --git a/components/script/dom/readablestreamgenericreader.rs b/components/script/dom/readablestreamgenericreader.rs index b437605953b..8ba1149bcb5 100644 --- a/components/script/dom/readablestreamgenericreader.rs +++ b/components/script/dom/readablestreamgenericreader.rs @@ -16,7 +16,7 @@ use crate::dom::globalscope::GlobalScope; use crate::dom::promise::Promise; use crate::dom::readablestreambyobreader::ReadableStreamBYOBReader; use crate::dom::readablestreamdefaultreader::ReadableStreamDefaultReader; -use crate::script_runtime::CanGc; +use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; /// <https://streams.spec.whatwg.org/#readablestreamgenericreader> pub(crate) trait ReadableStreamGenericReader { @@ -61,7 +61,13 @@ pub(crate) trait ReadableStreamGenericReader { } /// <https://streams.spec.whatwg.org/#readable-stream-reader-generic-cancel> - fn reader_generic_cancel(&self, reason: SafeHandleValue, can_gc: CanGc) -> Rc<Promise> { + fn reader_generic_cancel( + &self, + cx: SafeJSContext, + global: &GlobalScope, + reason: SafeHandleValue, + can_gc: CanGc, + ) -> Rc<Promise> { // Let stream be reader.[[stream]]. let stream = self.get_stream(); @@ -70,7 +76,7 @@ pub(crate) trait ReadableStreamGenericReader { stream.expect("Reader should have a stream when generic cancel is called into."); // Return ! ReadableStreamCancel(stream, reason). - stream.cancel(reason, can_gc) + stream.cancel(cx, global, reason, can_gc) } /// <https://streams.spec.whatwg.org/#readable-stream-reader-generic-release> @@ -135,6 +141,7 @@ pub(crate) trait ReadableStreamGenericReader { // <https://streams.spec.whatwg.org/#generic-reader-cancel> fn generic_cancel( &self, + cx: SafeJSContext, global: &GlobalScope, reason: SafeHandleValue, can_gc: CanGc, @@ -147,7 +154,7 @@ pub(crate) trait ReadableStreamGenericReader { promise } else { // Return ! ReadableStreamReaderGenericCancel(this, reason). - self.reader_generic_cancel(reason, can_gc) + self.reader_generic_cancel(cx, global, reason, can_gc) } } diff --git a/components/script/dom/underlyingsourcecontainer.rs b/components/script/dom/underlyingsourcecontainer.rs index cf396825d4f..541a831693a 100644 --- a/components/script/dom/underlyingsourcecontainer.rs +++ b/components/script/dom/underlyingsourcecontainer.rs @@ -20,7 +20,7 @@ use crate::dom::defaultteeunderlyingsource::DefaultTeeUnderlyingSource; use crate::dom::globalscope::GlobalScope; use crate::dom::messageport::MessagePort; use crate::dom::promise::Promise; -use crate::script_runtime::CanGc; +use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; /// <https://streams.spec.whatwg.org/#underlying-source-api> /// The `Js` variant corresponds to @@ -43,6 +43,11 @@ pub(crate) enum UnderlyingSourceType { Tee(Dom<DefaultTeeUnderlyingSource>), /// Transfer, with the port used in some of the algorithms. Transfer(Dom<MessagePort>), + /// A struct representing a JS object as underlying source, + /// and the actual JS object for use as `thisArg` in callbacks. + /// This is used for the `TransformStream` API. + #[allow(unused)] + Transform(/* Dom<TransformStream>, Rc<Promise>*/), } impl UnderlyingSourceType { @@ -110,6 +115,8 @@ impl UnderlyingSourceContainer { #[allow(unsafe_code)] pub(crate) fn call_cancel_algorithm( &self, + cx: SafeJSContext, + global: &GlobalScope, reason: SafeHandleValue, can_gc: CanGc, ) -> Option<Result<Rc<Promise>, Error>> { @@ -128,9 +135,13 @@ impl UnderlyingSourceContainer { } None }, - UnderlyingSourceType::Tee(tee_underlyin_source) => { + UnderlyingSourceType::Tee(tee_underlying_source) => { // Call the cancel algorithm for the appropriate branch. - tee_underlyin_source.cancel_algorithm(reason, can_gc) + tee_underlying_source.cancel_algorithm(cx, global, reason, can_gc) + }, + UnderlyingSourceType::Transform() => { + // Return ! TransformStreamDefaultSourceCancelAlgorithm(stream, reason). + todo!(); }, UnderlyingSourceType::Transfer(port) => { // Let cancelAlgorithm be the following steps, taking a reason argument: @@ -163,6 +174,7 @@ impl UnderlyingSourceContainer { pub(crate) fn call_pull_algorithm( &self, controller: Controller, + _global: &GlobalScope, can_gc: CanGc, ) -> Option<Result<Rc<Promise>, Error>> { match &self.underlying_source_type { @@ -180,9 +192,9 @@ impl UnderlyingSourceContainer { } None }, - UnderlyingSourceType::Tee(tee_underlyin_source) => { + UnderlyingSourceType::Tee(tee_underlying_source) => { // Call the pull algorithm for the appropriate branch. - Some(Ok(tee_underlyin_source.pull_algorithm(can_gc))) + Some(Ok(tee_underlying_source.pull_algorithm(can_gc))) }, UnderlyingSourceType::Transfer(port) => { // Let pullAlgorithm be the following steps: @@ -201,6 +213,10 @@ impl UnderlyingSourceContainer { Some(Ok(promise)) }, // Note: other source type have no pull steps for now. + UnderlyingSourceType::Transform() => { + // Return ! TransformStreamDefaultSourcePullAlgorithm(stream). + todo!(); + }, _ => None, } } @@ -264,6 +280,10 @@ impl UnderlyingSourceContainer { // from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable None }, + UnderlyingSourceType::Transform() => { + // Some(transform_underlying_source.start_algorithm()) + todo!(); + }, _ => None, } } diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index e210476a5df..a685bbb25f2 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -787,6 +787,32 @@ impl WindowMethods<crate::DomTypeHolder> for Window { doc.abort(can_gc); } + /// <https://html.spec.whatwg.org/multipage/#dom-window-focus> + fn Focus(&self) { + // > 1. Let `current` be this `Window` object's browsing context. + // > + // > 2. If `current` is null, then return. + let current = match self.undiscarded_window_proxy() { + Some(proxy) => proxy, + None => return, + }; + + // > 3. Run the focusing steps with `current`. + current.focus(); + + // > 4. If current is a top-level browsing context, user agents are + // > encouraged to trigger some sort of notification to indicate to + // > the user that the page is attempting to gain focus. + // + // TODO: Step 4 + } + + // https://html.spec.whatwg.org/multipage/#dom-window-blur + fn Blur(&self) { + // > User agents are encouraged to ignore calls to this `blur()` method + // > entirely. + } + // https://html.spec.whatwg.org/multipage/#dom-open fn Open( &self, diff --git a/components/script/dom/windowproxy.rs b/components/script/dom/windowproxy.rs index e3fc81bf7ec..dc02f9feb49 100644 --- a/components/script/dom/windowproxy.rs +++ b/components/script/dom/windowproxy.rs @@ -620,6 +620,23 @@ impl WindowProxy { result } + /// Run [the focusing steps] with this browsing context. + /// + /// [the focusing steps]: https://html.spec.whatwg.org/multipage/#focusing-steps + pub fn focus(&self) { + debug!( + "Requesting the constellation to initiate a focus operation for \ + browsing context {}", + self.browsing_context_id() + ); + self.global() + .script_to_constellation_chan() + .send(ScriptToConstellationMessage::FocusRemoteDocument( + self.browsing_context_id(), + )) + .unwrap(); + } + #[allow(unsafe_code)] /// Change the Window that this WindowProxy resolves to. // TODO: support setting the window proxy to a dummy value, diff --git a/components/script/dom/worker.rs b/components/script/dom/worker.rs index 429234c7a8e..6ad56026db6 100644 --- a/components/script/dom/worker.rs +++ b/components/script/dom/worker.rs @@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use constellation_traits::{StructuredSerializedData, WorkerScriptLoadOrigin}; use crossbeam_channel::{Sender, unbounded}; -use devtools_traits::{DevtoolsPageInfo, ScriptToDevtoolsControlMsg, WorkerId}; +use devtools_traits::{DevtoolsPageInfo, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId}; use dom_struct::dom_struct; use ipc_channel::ipc; use js::jsapi::{Heap, JSObject}; @@ -213,6 +213,16 @@ impl WorkerMethods<crate::DomTypeHolder> for Worker { devtools_sender.clone(), page_info, )); + + let source_info = SourceInfo { + url: worker_url.clone(), + external: true, // Worker scripts are always external. + worker_id: Some(worker_id), + }; + let _ = chan.send(ScriptToDevtoolsControlMsg::ScriptSourceLoaded( + pipeline_id, + source_info, + )); } } diff --git a/components/script/dom/writablestream.rs b/components/script/dom/writablestream.rs index e7e9ce906a6..8c2b2434cd2 100644 --- a/components/script/dom/writablestream.rs +++ b/components/script/dom/writablestream.rs @@ -19,6 +19,7 @@ use js::rust::{ }; use script_bindings::codegen::GenericBindings::MessagePortBinding::MessagePortMethods; +use super::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategySize; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategy; use crate::dom::bindings::codegen::Bindings::UnderlyingSinkBinding::UnderlyingSink; @@ -209,6 +210,11 @@ impl WritableStream { self.controller.set(Some(controller)); } + #[allow(unused)] + pub(crate) fn get_default_controller(&self) -> DomRoot<WritableStreamDefaultController> { + self.controller.get().expect("Controller should be set.") + } + pub(crate) fn is_writable(&self) -> bool { matches!(self.state.get(), WritableStreamState::Writable) } @@ -873,7 +879,6 @@ impl WritableStream { backpressure_promise: backpressure_promise.clone(), port: Dom::from_ref(port), }, - &UnderlyingSink::empty(), 1.0, size_algorithm, can_gc, @@ -892,9 +897,102 @@ impl WritableStream { // Perform ! SetUpWritableStreamDefaultController controller - .setup(cx, &global, self, &None, can_gc) + .setup(cx, &global, self, can_gc) .expect("Setup for transfer cannot fail"); } + /// <https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller-from-underlying-sink> + #[allow(clippy::too_many_arguments)] + pub(crate) fn setup_from_underlying_sink( + &self, + cx: SafeJSContext, + global: &GlobalScope, + stream: &WritableStream, + underlying_sink_obj: SafeHandleObject, + underlying_sink: &UnderlyingSink, + strategy_hwm: f64, + strategy_size: Rc<QueuingStrategySize>, + can_gc: CanGc, + ) -> Result<(), Error> { + // Let controller be a new WritableStreamDefaultController. + + // Let startAlgorithm be an algorithm that returns undefined. + + // Let writeAlgorithm be an algorithm that returns a promise resolved with undefined. + + // Let closeAlgorithm be an algorithm that returns a promise resolved with undefined. + + // Let abortAlgorithm be an algorithm that returns a promise resolved with undefined. + + // If underlyingSinkDict["start"] exists, then set startAlgorithm to an algorithm which + // returns the result of invoking underlyingSinkDict["start"] with argument + // list « controller », exception behavior "rethrow", and callback this value underlyingSink. + + // If underlyingSinkDict["write"] exists, then set writeAlgorithm to an algorithm which + // takes an argument chunk and returns the result of invoking underlyingSinkDict["write"] + // with argument list « chunk, controller » and callback this value underlyingSink. + + // If underlyingSinkDict["close"] exists, then set closeAlgorithm to an algorithm which + // returns the result of invoking underlyingSinkDict["close"] with argument + // list «» and callback this value underlyingSink. + + // If underlyingSinkDict["abort"] exists, then set abortAlgorithm to an algorithm which + // takes an argument reason and returns the result of invoking underlyingSinkDict["abort"] + // with argument list « reason » and callback this value underlyingSink. + let controller = WritableStreamDefaultController::new( + global, + UnderlyingSinkType::new_js( + underlying_sink.abort.clone(), + underlying_sink.start.clone(), + underlying_sink.close.clone(), + underlying_sink.write.clone(), + ), + strategy_hwm, + strategy_size, + can_gc, + ); + + // Note: this must be done before `setup`, + // otherwise `thisOb` is null in the start callback. + controller.set_underlying_sink_this_object(underlying_sink_obj); + + // Perform ? SetUpWritableStreamDefaultController + controller.setup(cx, global, stream, can_gc) + } +} + +/// <https://streams.spec.whatwg.org/#create-writable-stream> +#[cfg_attr(crown, allow(crown::unrooted_must_root))] +#[allow(unused)] +pub(crate) fn create_writable_stream( + cx: SafeJSContext, + global: &GlobalScope, + can_gc: CanGc, + writable_high_water_mark: f64, + writable_size_algorithm: Rc<QueuingStrategySize>, + underlying_sink_type: UnderlyingSinkType, +) -> Fallible<DomRoot<WritableStream>> { + // Assert: ! IsNonNegativeNumber(highWaterMark) is true. + assert!(writable_high_water_mark >= 0.0); + + // Let stream be a new WritableStream. + // Perform ! InitializeWritableStream(stream). + let stream = WritableStream::new_with_proto(global, None, can_gc); + + // Let controller be a new WritableStreamDefaultController. + let controller = WritableStreamDefaultController::new( + global, + underlying_sink_type, + writable_high_water_mark, + writable_size_algorithm, + can_gc, + ); + + // Perform ? SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, + // closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm). + controller.setup(cx, global, &stream, can_gc)?; + + // Return stream. + Ok(stream) } impl WritableStreamMethods<crate::DomTypeHolder> for WritableStream { @@ -939,22 +1037,18 @@ impl WritableStreamMethods<crate::DomTypeHolder> for WritableStream { // Let highWaterMark be ? ExtractHighWaterMark(strategy, 1). let high_water_mark = extract_high_water_mark(strategy, 1.0)?; - // Perform ? SetUpWritableStreamDefaultControllerFromUnderlyingSink - let controller = WritableStreamDefaultController::new( + // Perform ? SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, + // underlyingSinkDict, highWaterMark, sizeAlgorithm). + stream.setup_from_underlying_sink( + cx, global, - UnderlyingSinkType::Js, + &stream, + underlying_sink_obj.handle(), &underlying_sink_dict, high_water_mark, size_algorithm, can_gc, - ); - - // Note: this must be done before `setup`, - // otherwise `thisOb` is null in the start callback. - controller.set_underlying_sink_this_object(underlying_sink_obj.handle()); - - // Perform ? SetUpWritableStreamDefaultController - controller.setup(cx, global, &stream, &underlying_sink_dict.start, can_gc)?; + )?; Ok(stream) } diff --git a/components/script/dom/writablestreamdefaultcontroller.rs b/components/script/dom/writablestreamdefaultcontroller.rs index 751f5d8d976..301404ffdb2 100644 --- a/components/script/dom/writablestreamdefaultcontroller.rs +++ b/components/script/dom/writablestreamdefaultcontroller.rs @@ -14,11 +14,11 @@ use js::rust::{HandleObject as SafeHandleObject, HandleValue as SafeHandleValue, use super::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategySize; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::codegen::Bindings::UnderlyingSinkBinding::{ - UnderlyingSink, UnderlyingSinkAbortCallback, UnderlyingSinkCloseCallback, - UnderlyingSinkStartCallback, UnderlyingSinkWriteCallback, + UnderlyingSinkAbortCallback, UnderlyingSinkCloseCallback, UnderlyingSinkStartCallback, + UnderlyingSinkWriteCallback, }; use crate::dom::bindings::codegen::Bindings::WritableStreamDefaultControllerBinding::WritableStreamDefaultControllerMethods; -use crate::dom::bindings::error::{Error, ErrorToJsval}; +use crate::dom::bindings::error::{Error, ErrorToJsval, Fallible}; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::globalscope::GlobalScope; @@ -268,15 +268,46 @@ impl Callback for WriteAlgorithmRejectionHandler { /// The type of sink algorithms we are using. #[derive(JSTraceable, PartialEq)] +#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] pub enum UnderlyingSinkType { /// Algorithms are provided by Js callbacks. - Js, + Js { + /// <https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-abortalgorithm> + abort: RefCell<Option<Rc<UnderlyingSinkAbortCallback>>>, + + start: RefCell<Option<Rc<UnderlyingSinkStartCallback>>>, + + /// <https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-closealgorithm> + close: RefCell<Option<Rc<UnderlyingSinkCloseCallback>>>, + + /// <https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-writealgorithm> + write: RefCell<Option<Rc<UnderlyingSinkWriteCallback>>>, + }, /// Algorithms supporting streams transfer are implemented in Rust. /// The promise and port used in those algorithms are stored here. Transfer { backpressure_promise: Rc<RefCell<Option<Rc<Promise>>>>, port: Dom<MessagePort>, }, + /// Algorithms supporting transform streams are implemented in Rust. + #[allow(unused)] + Transform(/*Dom<TransformStream>, Rc<Promise>*/), +} + +impl UnderlyingSinkType { + pub(crate) fn new_js( + abort: Option<Rc<UnderlyingSinkAbortCallback>>, + start: Option<Rc<UnderlyingSinkStartCallback>>, + close: Option<Rc<UnderlyingSinkCloseCallback>>, + write: Option<Rc<UnderlyingSinkWriteCallback>>, + ) -> Self { + UnderlyingSinkType::Js { + abort: RefCell::new(abort), + start: RefCell::new(start), + close: RefCell::new(close), + write: RefCell::new(write), + } + } } /// <https://streams.spec.whatwg.org/#ws-default-controller-class> @@ -284,21 +315,11 @@ pub enum UnderlyingSinkType { pub struct WritableStreamDefaultController { reflector_: Reflector, - #[ignore_malloc_size_of = "Rc is hard"] + /// The type of underlying sink used. Besides the default JS one, + /// there will be others for stream transfer, and for transform stream. + #[ignore_malloc_size_of = "underlying_sink_type"] underlying_sink_type: UnderlyingSinkType, - /// <https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-abortalgorithm> - #[ignore_malloc_size_of = "Rc is hard"] - abort: RefCell<Option<Rc<UnderlyingSinkAbortCallback>>>, - - /// <https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-closealgorithm> - #[ignore_malloc_size_of = "Rc is hard"] - close: RefCell<Option<Rc<UnderlyingSinkCloseCallback>>>, - - /// <https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-writealgorithm> - #[ignore_malloc_size_of = "Rc is hard"] - write: RefCell<Option<Rc<UnderlyingSinkWriteCallback>>>, - /// The JS object used as `this` when invoking sink algorithms. #[ignore_malloc_size_of = "mozjs"] underlying_sink_obj: Heap<*mut JSObject>, @@ -325,7 +346,6 @@ impl WritableStreamDefaultController { #[cfg_attr(crown, allow(crown::unrooted_must_root))] fn new_inherited( underlying_sink_type: UnderlyingSinkType, - underlying_sink: &UnderlyingSink, strategy_hwm: f64, strategy_size: Rc<QueuingStrategySize>, ) -> WritableStreamDefaultController { @@ -334,9 +354,6 @@ impl WritableStreamDefaultController { underlying_sink_type, queue: Default::default(), stream: Default::default(), - abort: RefCell::new(underlying_sink.abort.clone()), - close: RefCell::new(underlying_sink.close.clone()), - write: RefCell::new(underlying_sink.write.clone()), underlying_sink_obj: Default::default(), strategy_hwm, strategy_size: RefCell::new(Some(strategy_size)), @@ -344,10 +361,10 @@ impl WritableStreamDefaultController { } } + #[cfg_attr(crown, allow(crown::unrooted_must_root))] pub(crate) fn new( global: &GlobalScope, underlying_sink_type: UnderlyingSinkType, - underlying_sink: &UnderlyingSink, strategy_hwm: f64, strategy_size: Rc<QueuingStrategySize>, can_gc: CanGc, @@ -355,7 +372,6 @@ impl WritableStreamDefaultController { reflect_dom_object( Box::new(WritableStreamDefaultController::new_inherited( underlying_sink_type, - underlying_sink, strategy_hwm, strategy_size, )), @@ -375,27 +391,44 @@ impl WritableStreamDefaultController { /// <https://streams.spec.whatwg.org/#writable-stream-default-controller-clear-algorithms> fn clear_algorithms(&self) { - // Set controller.[[writeAlgorithm]] to undefined. - self.write.borrow_mut().take(); + match &self.underlying_sink_type { + UnderlyingSinkType::Js { + abort, + start: _, + close, + write, + } => { + // Set controller.[[writeAlgorithm]] to undefined. + write.borrow_mut().take(); - // Set controller.[[closeAlgorithm]] to undefined. - self.close.borrow_mut().take(); + // Set controller.[[closeAlgorithm]] to undefined. + close.borrow_mut().take(); - // Set controller.[[abortAlgorithm]] to undefined. - self.abort.borrow_mut().take(); + // Set controller.[[abortAlgorithm]] to undefined. + abort.borrow_mut().take(); + }, + UnderlyingSinkType::Transfer { + backpressure_promise, + .. + } => { + backpressure_promise.borrow_mut().take(); + }, + UnderlyingSinkType::Transform() => { + return; + }, + } // Set controller.[[strategySizeAlgorithm]] to undefined. self.strategy_size.borrow_mut().take(); } - /// <https://streams.spec.whatwg.org/#set-up-writable-stream-default-controllerr> + /// <https://streams.spec.whatwg.org/#set-up-writable-stream-default-controller> #[allow(unsafe_code)] pub(crate) fn setup( &self, cx: SafeJSContext, global: &GlobalScope, stream: &WritableStream, - start: &Option<Rc<UnderlyingSinkStartCallback>>, can_gc: CanGc, ) -> Result<(), Error> { // Assert: stream implements WritableStream. @@ -436,40 +469,7 @@ impl WritableStreamDefaultController { // Let startResult be the result of performing startAlgorithm. (This may throw an exception.) // Let startPromise be a promise resolved with startResult. - let start_promise = if let Some(start) = start { - rooted!(in(*cx) let mut result_object = ptr::null_mut::<JSObject>()); - rooted!(in(*cx) let mut result: JSVal); - rooted!(in(*cx) let this_object = self.underlying_sink_obj.get()); - start.Call_( - &this_object.handle(), - self, - result.handle_mut(), - ExceptionHandling::Rethrow, - can_gc, - )?; - let is_promise = unsafe { - if result.is_object() { - result_object.set(result.to_object()); - IsPromiseObject(result_object.handle().into_handle()) - } else { - false - } - }; - if is_promise { - let promise = Promise::new_with_js_promise(result_object.handle(), cx); - promise - } else { - Promise::new_resolved(global, cx, result.get(), can_gc) - } - } else { - // Note: we are either here because the Js algorithm is none, - // or because we are suppporting a stream transfer as - // part of #abstract-opdef-setupcrossrealmtransformwritable - // and the logic is the same for both. - - // Let startAlgorithm be an algorithm that returns undefined. - Promise::new_resolved(global, cx, (), can_gc) - }; + let start_promise = self.start_algorithm(cx, global, can_gc)?; let rooted_default_controller = DomRoot::from_ref(self); @@ -509,6 +509,64 @@ impl WritableStreamDefaultController { self.advance_queue_if_needed(cx, global, can_gc); } + #[allow(unsafe_code)] + fn start_algorithm( + &self, + cx: SafeJSContext, + global: &GlobalScope, + can_gc: CanGc, + ) -> Fallible<Rc<Promise>> { + match &self.underlying_sink_type { + UnderlyingSinkType::Js { + start, + abort: _, + close: _, + write: _, + } => { + let algo = start.borrow().clone(); + let start_promise = if let Some(start) = algo { + rooted!(in(*cx) let mut result_object = ptr::null_mut::<JSObject>()); + rooted!(in(*cx) let mut result: JSVal); + rooted!(in(*cx) let this_object = self.underlying_sink_obj.get()); + start.Call_( + &this_object.handle(), + self, + result.handle_mut(), + ExceptionHandling::Rethrow, + can_gc, + )?; + let is_promise = unsafe { + if result.is_object() { + result_object.set(result.to_object()); + IsPromiseObject(result_object.handle().into_handle()) + } else { + false + } + }; + if is_promise { + let promise = Promise::new_with_js_promise(result_object.handle(), cx); + promise + } else { + Promise::new_resolved(global, cx, result.get(), can_gc) + } + } else { + // Let startAlgorithm be an algorithm that returns undefined. + Promise::new_resolved(global, cx, (), can_gc) + }; + + Ok(start_promise) + }, + UnderlyingSinkType::Transfer { .. } => { + // Let startAlgorithm be an algorithm that returns undefined. + Ok(Promise::new_resolved(global, cx, (), can_gc)) + }, + UnderlyingSinkType::Transform() => { + // Let startAlgorithm be an algorithm that returns startPromise. + todo!() + }, + } + } + /// <https://streams.spec.whatwg.org/#ref-for-abstract-opdef-writablestreamcontroller-abortsteps> pub(crate) fn abort_steps( &self, @@ -517,10 +575,15 @@ impl WritableStreamDefaultController { reason: SafeHandleValue, can_gc: CanGc, ) -> Rc<Promise> { - let result = match self.underlying_sink_type { - UnderlyingSinkType::Js => { + let result = match &self.underlying_sink_type { + UnderlyingSinkType::Js { + abort, + start: _, + close: _, + write: _, + } => { rooted!(in(*cx) let this_object = self.underlying_sink_obj.get()); - let algo = self.abort.borrow().clone(); + let algo = abort.borrow().clone(); // Let result be the result of performing this.[[abortAlgorithm]], passing reason. let result = if let Some(algo) = algo { algo.Call_( @@ -538,7 +601,7 @@ impl WritableStreamDefaultController { promise }) }, - UnderlyingSinkType::Transfer { ref port, .. } => { + UnderlyingSinkType::Transfer { port, .. } => { // The steps from the `abortAlgorithm` at // <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable> @@ -559,6 +622,10 @@ impl WritableStreamDefaultController { } promise }, + UnderlyingSinkType::Transform() => { + // Return ! TransformStreamDefaultSinkAbortAlgorithm(stream, reason). + todo!() + }, }; // Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). @@ -575,10 +642,15 @@ impl WritableStreamDefaultController { global: &GlobalScope, can_gc: CanGc, ) -> Rc<Promise> { - match self.underlying_sink_type { - UnderlyingSinkType::Js => { + match &self.underlying_sink_type { + UnderlyingSinkType::Js { + abort: _, + start: _, + close: _, + write, + } => { rooted!(in(*cx) let this_object = self.underlying_sink_obj.get()); - let algo = self.write.borrow().clone(); + let algo = write.borrow().clone(); let result = if let Some(algo) = algo { algo.Call_( &this_object.handle(), @@ -597,9 +669,8 @@ impl WritableStreamDefaultController { }) }, UnderlyingSinkType::Transfer { - ref backpressure_promise, - ref port, - .. + backpressure_promise, + port, } => { // The steps from the `writeAlgorithm` at // <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable> @@ -636,6 +707,10 @@ impl WritableStreamDefaultController { .append_native_handler(&handler, comp, can_gc); result_promise }, + UnderlyingSinkType::Transform() => { + // Return ! TransformStreamDefaultSinkWriteAlgorithm(stream, chunk). + todo!() + }, } } @@ -646,11 +721,16 @@ impl WritableStreamDefaultController { global: &GlobalScope, can_gc: CanGc, ) -> Rc<Promise> { - match self.underlying_sink_type { - UnderlyingSinkType::Js => { + match &self.underlying_sink_type { + UnderlyingSinkType::Js { + abort: _, + start: _, + close, + write: _, + } => { rooted!(in(*cx) let mut this_object = ptr::null_mut::<JSObject>()); this_object.set(self.underlying_sink_obj.get()); - let algo = self.close.borrow().clone(); + let algo = close.borrow().clone(); let result = if let Some(algo) = algo { algo.Call_(&this_object.handle(), ExceptionHandling::Rethrow, can_gc) } else { @@ -662,7 +742,7 @@ impl WritableStreamDefaultController { promise }) }, - UnderlyingSinkType::Transfer { ref port, .. } => { + UnderlyingSinkType::Transfer { port, .. } => { // The steps from the `closeAlgorithm` at // <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformwritable> @@ -677,6 +757,10 @@ impl WritableStreamDefaultController { // Return a promise resolved with undefined. Promise::new_resolved(global, cx, (), can_gc) }, + UnderlyingSinkType::Transform() => { + // Return ! TransformStreamDefaultSinkCloseAlgorithm(stream). + todo!() + }, } } diff --git a/components/script/messaging.rs b/components/script/messaging.rs index 7d0b7aabe05..e0ea9e30af2 100644 --- a/components/script/messaging.rs +++ b/components/script/messaging.rs @@ -72,6 +72,8 @@ impl MixedMessage { ScriptThreadMessage::UpdateHistoryState(id, ..) => Some(*id), ScriptThreadMessage::RemoveHistoryStates(id, ..) => Some(*id), ScriptThreadMessage::FocusIFrame(id, ..) => Some(*id), + ScriptThreadMessage::FocusDocument(id, ..) => Some(*id), + ScriptThreadMessage::Unfocus(id, ..) => Some(*id), ScriptThreadMessage::WebDriverScriptCommand(id, ..) => Some(*id), ScriptThreadMessage::TickAllAnimations(..) => None, ScriptThreadMessage::WebFontLoaded(id, ..) => Some(*id), diff --git a/components/script/microtask.rs b/components/script/microtask.rs index 57f558c1aac..453e587e892 100644 --- a/components/script/microtask.rs +++ b/components/script/microtask.rs @@ -147,7 +147,7 @@ impl MicrotaskQueue { MutationObserver::notify_mutation_observers(can_gc); }, Microtask::ReadableStreamTeeReadRequest(ref task) => { - task.microtask_chunk_steps(can_gc) + task.microtask_chunk_steps(cx, can_gc) }, } } diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index f78b5bf281b..2129979ad42 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -50,8 +50,9 @@ use devtools_traits::{ }; use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::{ - CompositorHitTestResult, EmbedderMsg, InputEvent, MediaSessionActionType, MouseButton, - MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, WebDriverScriptCommand, + CompositorHitTestResult, EmbedderMsg, FocusSequenceNumber, InputEvent, MediaSessionActionType, + MouseButton, MouseButtonAction, MouseButtonEvent, Theme, ViewportDetails, + WebDriverScriptCommand, }; use euclid::Point2D; use euclid::default::Rect; @@ -124,7 +125,7 @@ use crate::dom::customelementregistry::{ CallbackReaction, CustomElementDefinition, CustomElementReactionStack, }; use crate::dom::document::{ - Document, DocumentSource, FocusType, HasBrowsingContext, IsHTMLDocument, TouchEventResult, + Document, DocumentSource, FocusInitiator, HasBrowsingContext, IsHTMLDocument, TouchEventResult, }; use crate::dom::element::Element; use crate::dom::globalscope::GlobalScope; @@ -331,8 +332,7 @@ pub struct ScriptThread { #[no_trace] layout_factory: Arc<dyn LayoutFactory>, - // Mouse down point. - // In future, this shall be mouse_down_point for primary button + /// The screen coordinates where the primary mouse button was pressed. #[no_trace] relative_mouse_down_point: Cell<Point2D<f32, DevicePixel>>, } @@ -1804,8 +1804,14 @@ impl ScriptThread { ScriptThreadMessage::RemoveHistoryStates(pipeline_id, history_states) => { self.handle_remove_history_states(pipeline_id, history_states) }, - ScriptThreadMessage::FocusIFrame(parent_pipeline_id, frame_id) => { - self.handle_focus_iframe_msg(parent_pipeline_id, frame_id, can_gc) + ScriptThreadMessage::FocusIFrame(parent_pipeline_id, frame_id, sequence) => { + self.handle_focus_iframe_msg(parent_pipeline_id, frame_id, sequence, can_gc) + }, + ScriptThreadMessage::FocusDocument(pipeline_id, sequence) => { + self.handle_focus_document_msg(pipeline_id, sequence, can_gc) + }, + ScriptThreadMessage::Unfocus(pipeline_id, sequence) => { + self.handle_unfocus_msg(pipeline_id, sequence, can_gc) }, ScriptThreadMessage::WebDriverScriptCommand(pipeline_id, msg) => { self.handle_webdriver_msg(pipeline_id, msg, can_gc) @@ -2514,6 +2520,7 @@ impl ScriptThread { &self, parent_pipeline_id: PipelineId, browsing_context_id: BrowsingContextId, + sequence: FocusSequenceNumber, can_gc: CanGc, ) { let document = self @@ -2533,7 +2540,65 @@ impl ScriptThread { return; }; - document.request_focus(Some(&iframe_element_root), FocusType::Parent, can_gc); + if document.get_focus_sequence() > sequence { + debug!( + "Disregarding the FocusIFrame message because the contained sequence number is \ + too old ({:?} < {:?})", + sequence, + document.get_focus_sequence() + ); + return; + } + + document.request_focus(Some(&iframe_element_root), FocusInitiator::Remote, can_gc); + } + + fn handle_focus_document_msg( + &self, + pipeline_id: PipelineId, + sequence: FocusSequenceNumber, + can_gc: CanGc, + ) { + if let Some(doc) = self.documents.borrow().find_document(pipeline_id) { + if doc.get_focus_sequence() > sequence { + debug!( + "Disregarding the FocusDocument message because the contained sequence number is \ + too old ({:?} < {:?})", + sequence, + doc.get_focus_sequence() + ); + return; + } + doc.request_focus(None, FocusInitiator::Remote, can_gc); + } else { + warn!( + "Couldn't find document by pipleline_id:{pipeline_id:?} when handle_focus_document_msg." + ); + } + } + + fn handle_unfocus_msg( + &self, + pipeline_id: PipelineId, + sequence: FocusSequenceNumber, + can_gc: CanGc, + ) { + if let Some(doc) = self.documents.borrow().find_document(pipeline_id) { + if doc.get_focus_sequence() > sequence { + debug!( + "Disregarding the Unfocus message because the contained sequence number is \ + too old ({:?} < {:?})", + sequence, + doc.get_focus_sequence() + ); + return; + } + doc.handle_container_unfocus(can_gc); + } else { + warn!( + "Couldn't find document by pipleline_id:{pipeline_id:?} when handle_unfocus_msg." + ); + } } fn handle_post_message_msg( diff --git a/components/script_bindings/codegen/Bindings.conf b/components/script_bindings/codegen/Bindings.conf index c457bf70b85..c50bc31a7f5 100644 --- a/components/script_bindings/codegen/Bindings.conf +++ b/components/script_bindings/codegen/Bindings.conf @@ -764,6 +764,10 @@ DOMInterfaces = { 'inRealms': ['Abort', 'Close', 'Write'], }, +'TransformStreamDefaultController': { + 'canGc': ['Enqueue', 'Error', 'Terminate'], +}, + 'WorkerNavigator': { 'canGc': ['Languages'], }, diff --git a/components/script_bindings/webidls/Window.webidl b/components/script_bindings/webidls/Window.webidl index 81c442b119f..eb7c3e1d03d 100644 --- a/components/script_bindings/webidls/Window.webidl +++ b/components/script_bindings/webidls/Window.webidl @@ -27,8 +27,8 @@ [CrossOriginCallable] undefined close(); [CrossOriginReadable] readonly attribute boolean closed; undefined stop(); - //[CrossOriginCallable] void focus(); - //[CrossOriginCallable] void blur(); + [CrossOriginCallable] undefined focus(); + [CrossOriginCallable] undefined blur(); // other browsing contexts [Replaceable, CrossOriginReadable] readonly attribute WindowProxy frames; diff --git a/components/servo/Cargo.toml b/components/servo/Cargo.toml index 498d170492d..b49f60e742a 100644 --- a/components/servo/Cargo.toml +++ b/components/servo/Cargo.toml @@ -46,14 +46,14 @@ tracing = [ webdriver = ["webdriver_server"] webgl_backtrace = [ "script/webgl_backtrace", - "canvas/webgl_backtrace", + "webgl/webgl_backtrace", "canvas_traits/webgl_backtrace", ] webxr = [ "dep:webxr", "dep:webxr-api", "compositing/webxr", - "canvas/webxr", + "webgl/webxr", "script/webxr", ] webgpu = [ @@ -68,7 +68,8 @@ base = { workspace = true } bincode = { workspace = true } bluetooth = { path = "../bluetooth", optional = true } bluetooth_traits = { workspace = true, optional = true } -canvas = { path = "../canvas", default-features = false } +canvas = { path = "../canvas" } +webgl = { path = "../webgl", default-features = false } canvas_traits = { workspace = true } cfg-if = { workspace = true } compositing = { path = "../compositing" } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 7fb990527ec..6f39e773e4e 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -38,7 +38,6 @@ use base::id::{PipelineNamespace, PipelineNamespaceId}; use bluetooth::BluetoothThreadFactory; #[cfg(feature = "bluetooth")] use bluetooth_traits::BluetoothRequest; -use canvas::WebGLComm; use canvas::canvas_paint_thread::CanvasPaintThread; use canvas_traits::webgl::{GlType, WebGLThreads}; use clipboard_delegate::StringRequest; @@ -99,6 +98,7 @@ use servo_delegate::DefaultServoDelegate; use servo_media::ServoMedia; use servo_media::player::context::GlContext; use servo_url::ServoUrl; +use webgl::WebGLComm; #[cfg(feature = "webgpu")] pub use webgpu; #[cfg(feature = "webgpu")] diff --git a/components/shared/constellation/from_script_message.rs b/components/shared/constellation/from_script_message.rs index 8346551fd15..625d642033e 100644 --- a/components/shared/constellation/from_script_message.rs +++ b/components/shared/constellation/from_script_message.rs @@ -15,7 +15,8 @@ use base::id::{ use canvas_traits::canvas::{CanvasId, CanvasMsg}; use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, WorkerId}; use embedder_traits::{ - AnimationState, EmbedderMsg, MediaSessionEvent, TouchEventResult, ViewportDetails, + AnimationState, EmbedderMsg, FocusSequenceNumber, MediaSessionEvent, TouchEventResult, + ViewportDetails, }; use euclid::default::Size2D as UntypedSize2D; use http::{HeaderMap, Method}; @@ -519,8 +520,21 @@ pub enum ScriptToConstellationMessage { UntypedSize2D<u64>, IpcSender<(IpcSender<CanvasMsg>, CanvasId, ImageKey)>, ), - /// Notifies the constellation that this frame has received focus. - Focus, + /// Notifies the constellation that this pipeline is requesting focus. + /// + /// When this message is sent, the sender pipeline has already its local + /// focus state updated. The constellation, after receiving this message, + /// will broadcast messages to other pipelines that are affected by this + /// focus operation. + /// + /// The first field contains the browsing context ID of the container + /// element if one was focused. + /// + /// The second field is a sequence number that the constellation should use + /// when sending a focus-related message to the sender pipeline next time. + Focus(Option<BrowsingContextId>, FocusSequenceNumber), + /// Requests the constellation to focus the specified browsing context. + FocusRemoteDocument(BrowsingContextId), /// Get the top-level browsing context info for a given browsing context. GetTopForBrowsingContext(BrowsingContextId, IpcSender<Option<WebViewId>>), /// Get the browsing context id of the browsing context in which pipeline is diff --git a/components/shared/devtools/lib.rs b/components/shared/devtools/lib.rs index 59857dc0d00..0cf99d22658 100644 --- a/components/shared/devtools/lib.rs +++ b/components/shared/devtools/lib.rs @@ -551,4 +551,5 @@ impl fmt::Display for ShadowRootMode { pub struct SourceInfo { pub url: ServoUrl, pub external: bool, + pub worker_id: Option<WorkerId>, } diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 5f1171859dc..c87fa9019ef 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -14,7 +14,7 @@ pub mod user_content_manager; mod webdriver; use std::ffi::c_void; -use std::fmt::{Debug, Error, Formatter}; +use std::fmt::{Debug, Display, Error, Formatter}; use std::path::PathBuf; use std::sync::Arc; @@ -784,3 +784,76 @@ pub enum AnimationState { /// No animations are active but callbacks are queued NoAnimationCallbacksPresent, } + +/// A sequence number generated by a script thread for its pipelines. The +/// constellation attaches the target pipeline's last seen `FocusSequenceNumber` +/// to every focus-related message it sends. +/// +/// This is used to resolve the inconsistency that occurs due to bidirectional +/// focus state synchronization and provide eventual consistency. Example: +/// +/// ```text +/// script constellation +/// ----------------------------------------------------------------------- +/// send ActivateDocument ----------> receive ActivateDocument +/// ,---- send FocusDocument +/// | +/// focus an iframe | +/// send Focus -----------------|---> receive Focus +/// | focus the iframe's content document +/// receive FocusDocument <-----' send FocusDocument to the content pipeline --> ... +/// unfocus the iframe +/// focus the document +/// +/// Final state: Final state: +/// the iframe is not focused the iframe is focused +/// ``` +/// +/// When the above sequence completes, from the script thread's point of view, +/// the iframe is unfocused, but from the constellation's point of view, the +/// iframe is still focused. +/// +/// This inconsistency can be resolved by associating a sequence number to each +/// message. Whenever a script thread initiates a focus operation, it generates +/// and sends a brand new sequence number. The constellation attaches the +/// last-received sequence number to each message it sends. This way, the script +/// thread can discard out-dated incoming focus messages, and eventually, all +/// actors converge to the consistent state which is determined based on the +/// last focus message received by the constellation. +/// +/// ```text +/// script constellation +/// ----------------------------------------------------------------------- +/// send ActivateDocument ----------> receive ActivateDocument +/// ,---- send FocusDocument (0) +/// | +/// seq_number += 1 | +/// focus an iframe | +/// send Focus (1) -------------|---> receive Focus (1) +/// | focus the iframe's content document +/// receive FocusDocument (0) <-' send FocusDocument to the content pipeline --> ... +/// ignore it because 0 < 1 +/// +/// Final state: Final state: +/// the iframe is focused the iframe is focused +/// ``` +#[derive( + Clone, + Copy, + Debug, + Default, + Deserialize, + Eq, + Hash, + MallocSizeOf, + PartialEq, + Serialize, + PartialOrd, +)] +pub struct FocusSequenceNumber(pub u64); + +impl Display for FocusSequenceNumber { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + Display::fmt(&self.0, f) + } +} diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 7323907cba3..748c42400a8 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -27,8 +27,8 @@ use crossbeam_channel::{RecvTimeoutError, Sender}; use devtools_traits::ScriptToDevtoolsControlMsg; use embedder_traits::user_content_manager::UserContentManager; use embedder_traits::{ - CompositorHitTestResult, InputEvent, MediaSessionActionType, Theme, ViewportDetails, - WebDriverScriptCommand, + CompositorHitTestResult, FocusSequenceNumber, InputEvent, MediaSessionActionType, Theme, + ViewportDetails, WebDriverScriptCommand, }; use euclid::{Rect, Scale, Size2D, UnknownUnit}; use ipc_channel::ipc::{IpcReceiver, IpcSender}; @@ -191,7 +191,15 @@ pub enum ScriptThreadMessage { RemoveHistoryStates(PipelineId, Vec<HistoryStateId>), /// Set an iframe to be focused. Used when an element in an iframe gains focus. /// PipelineId is for the parent, BrowsingContextId is for the nested browsing context - FocusIFrame(PipelineId, BrowsingContextId), + FocusIFrame(PipelineId, BrowsingContextId, FocusSequenceNumber), + /// Focus the document. Used when the container gains focus. + FocusDocument(PipelineId, FocusSequenceNumber), + /// Notifies that the document's container (e.g., an iframe) is not included + /// in the top-level browsing context's focus chain (not considering system + /// focus) anymore. + /// + /// Obviously, this message is invalid for a top-level document. + Unfocus(PipelineId, FocusSequenceNumber), /// Passes a webdriver command to the script thread for execution WebDriverScriptCommand(PipelineId, WebDriverScriptCommand), /// Notifies script thread that all animations are done diff --git a/components/webgl/Cargo.toml b/components/webgl/Cargo.toml new file mode 100644 index 00000000000..b0c1c0ceb29 --- /dev/null +++ b/components/webgl/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "webgl" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true +publish.workspace = true +rust-version.workspace = true + +[lib] +name = "webgl" +path = "lib.rs" + +[features] +webgl_backtrace = ["canvas_traits/webgl_backtrace"] +webxr = ["dep:webxr", "dep:webxr-api"] + +[dependencies] +bitflags = { workspace = true } +byteorder = { workspace = true } +canvas_traits = { workspace = true } +compositing_traits = { workspace = true } +crossbeam-channel = { workspace = true } +euclid = { workspace = true } +fnv = { workspace = true } +glow = { workspace = true } +half = "2" +ipc-channel = { workspace = true } +log = { workspace = true } +pixels = { path = "../pixels" } +snapshot = { workspace = true } +surfman = { workspace = true } +webrender = { workspace = true } +webrender_api = { workspace = true } +webxr = { path = "../webxr", features = ["ipc"], optional = true } +webxr-api = { workspace = true, features = ["ipc"], optional = true } diff --git a/components/webgl/lib.rs b/components/webgl/lib.rs new file mode 100644 index 00000000000..923e7faad24 --- /dev/null +++ b/components/webgl/lib.rs @@ -0,0 +1,13 @@ +/* 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/. */ + +#![deny(unsafe_code)] + +pub use webgl_mode::WebGLComm; + +mod webgl_limits; +mod webgl_mode; +pub mod webgl_thread; +#[cfg(feature = "webxr")] +mod webxr; diff --git a/components/canvas/webgl_limits.rs b/components/webgl/webgl_limits.rs index f683b6efff6..f683b6efff6 100644 --- a/components/canvas/webgl_limits.rs +++ b/components/webgl/webgl_limits.rs diff --git a/components/canvas/webgl_mode/inprocess.rs b/components/webgl/webgl_mode/inprocess.rs index 566da2c58c8..566da2c58c8 100644 --- a/components/canvas/webgl_mode/inprocess.rs +++ b/components/webgl/webgl_mode/inprocess.rs diff --git a/components/canvas/webgl_mode/mod.rs b/components/webgl/webgl_mode/mod.rs index 8bc74f6e244..8bc74f6e244 100644 --- a/components/canvas/webgl_mode/mod.rs +++ b/components/webgl/webgl_mode/mod.rs diff --git a/components/canvas/webgl_thread.rs b/components/webgl/webgl_thread.rs index b1ac2b2d3c4..b1ac2b2d3c4 100644 --- a/components/canvas/webgl_thread.rs +++ b/components/webgl/webgl_thread.rs diff --git a/components/canvas/webxr.rs b/components/webgl/webxr.rs index d43303e7393..d43303e7393 100644 --- a/components/canvas/webxr.rs +++ b/components/webgl/webxr.rs |