From 0c0ee04b8e348ae43beaa786a8c6b0422fa71ad5 Mon Sep 17 00:00:00 2001 From: Fuguo <1782765876@qq.com> Date: Wed, 30 Apr 2025 12:37:53 +0800 Subject: [PATCH] Improve inter-document focus handling (#36649) *Describe the changes that this pull request makes here. This will be the commit message.* rewritten the PR #28571 Implement [Window#focus](https://html.spec.whatwg.org/multipage/#dom-window-focus), [Window#blur](https://html.spec.whatwg.org/multipage/#dom-window-blur) Testing: WPT Fixes: #8981 #9421 --------- Signed-off-by: kongbai1996 <1782765876@qq.com> Co-authored-by: yvt --- components/constellation/constellation.rs | 357 ++++++++++++-- components/constellation/pipeline.rs | 5 +- components/constellation/tracing.rs | 3 +- .../script/dom/dissimilaroriginwindow.rs | 7 +- components/script/dom/document.rs | 461 +++++++++++++----- components/script/dom/htmlelement.rs | 9 +- components/script/dom/window.rs | 26 + components/script/dom/windowproxy.rs | 17 + components/script/messaging.rs | 2 + components/script/script_thread.rs | 78 ++- .../script_bindings/webidls/Window.webidl | 4 +- .../constellation/from_script_message.rs | 20 +- components/shared/embedder/lib.rs | 75 ++- components/shared/script/lib.rs | 14 +- ...me-then-immediately-focusing-back.html.ini | 3 - ...er-focusing-different-site-iframe.html.ini | 3 - ...ng-same-site-iframe-contentwindow.html.ini | 3 +- ...t-after-focusing-same-site-iframe.html.ini | 3 - ...fferent-site-iframe-contentwindow.html.ini | 3 +- ...ng-same-site-iframe-contentwindow.html.ini | 3 +- ...-in-different-site-iframes-window.html.ini | 3 +- ...ation-in-same-site-iframes-window.html.ini | 3 +- .../iframe-focuses-parent-same-site.html.ini | 2 - ...s-origin-objects-function-caching.html.ini | 3 - .../the-window-object/focus.window.js.ini | 3 - .../window-security.https.html.ini | 6 - .../window-properties.https.html.ini | 5 - .../meta/html/dom/idlharness.https.html.ini | 12 - .../event-listeners.window.js.ini | 6 - ...t-implicit-this-value-cross-realm.html.ini | 3 - tests/wpt/mozilla/meta/MANIFEST.json | 9 +- .../wpt/mozilla/tests/mozilla/FocusEvent.html | 7 - .../tests/mozilla/focus_inter_documents.html | 207 ++++++++ 33 files changed, 1123 insertions(+), 242 deletions(-) delete mode 100644 tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html.ini delete mode 100644 tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe.html.ini delete mode 100644 tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe.html.ini delete mode 100644 tests/wpt/meta/focus/iframe-focuses-parent-same-site.html.ini delete mode 100644 tests/wpt/meta/html/browsers/the-window-object/focus.window.js.ini create mode 100644 tests/wpt/mozilla/tests/mozilla/focus_inter_documents.html 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 + '_ { + let mut state: Option = 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 + '_ { + 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,22 +4321,96 @@ 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, + 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)); + // 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), + }; + + // 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_browsing_context( + &mut self, + initiator_pipeline_id: Option, + 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), + }; + // Update the webview’s focused browsing context. - match self.webviews.get_mut(webview_id) { - Some(webview) => { - webview.focused_browsing_context_id = browsing_context_id; - }, + 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 warn!( "{}: Browsing context for focus msg does not exist", @@ -4299,42 +4419,133 @@ where }, }; - // Focus parent iframes recursively - self.focus_parent_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(); - #[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); - }, - }; - let parent_pipeline_id = match parent_pipeline_id { - Some(parent_id) => parent_id, - None => { - return debug!("{}: Focus has no parent", browsing_context_id); - }, - }; + debug!( + "old_focus_chain_pipelines = {:?}", + old_focus_chain_pipelines + .iter() + .map(|p| p.id.to_string()) + .collect::>() + ); + debug!( + "new_focus_chain_pipelines = {:?}", + new_focus_chain_pipelines + .iter() + .map(|p| p.id.to_string()) + .collect::>() + ); - // 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) + // 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/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 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 ec2ad98c464..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>), +struct FocusTransaction { + /// The focused element of this document. + element: Option>, + /// 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, /// The state of this document's focus transaction. - focus_transaction: DomRefCell, + focus_transaction: DomRefCell>, /// The element that currently has the document focus context. focused: MutNullableDom, + /// The last sequence number sent to the constellation. + #[no_trace] + focus_sequence: Cell, + /// 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, /// The script element that is currently executing. current_script: MutNullableDom, /// @@ -1120,124 +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(), + }); } /// 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, - ) - } - /// 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. - pub(crate) fn request_focus( - &self, - elem: Option<&Element>, - focus_type: FocusType, - can_gc: CanGc, - ) { - let implicit_transaction = matches!( - *self.focus_transaction.borrow(), - FocusTransaction::NotInTransaction - ); + 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)); + + // 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(focus_type, can_gc); + self.commit_focus_transaction(FocusInitiator::Local, 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)) - }, - }; - *self.focus_transaction.borrow_mut() = FocusTransaction::NotInTransaction; - if self.focused == possibly_focused.as_deref() { + /// 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_initiator: FocusInitiator, + can_gc: CanGc, + ) { + // If an element is specified, and it's non-focusable, ignore the + // request. + if elem.is_some_and(|e| !e.is_focusable_area()) { return; } - if let Some(ref elem) = self.focused.get() { - let node = elem.upcast::(); - elem.set_focus_state(false); - // FIXME: pass appropriate relatedTarget - if node.is_connected() { - 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 implicit_transaction = self.focus_transaction.borrow().is_none(); + + if implicit_transaction { + self.begin_focus_transaction(); + } + + { + 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_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 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() = 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" + ); } } - self.focused.set(possibly_focused.as_deref()); + let old_focused = self.focused.get(); + let old_focus_state = self.has_focus.get(); - if let Some(ref elem) = self.focused.get() { - elem.set_focus_state(true); - let node = elem.upcast::(); - // 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); + 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::(); + 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())); + } } + } - // Notify the embedder to display an input method. - if let Some(kind) = elem.input_method_type() { - let rect = elem.upcast::().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()), + if old_focus_state != new_focus_state && !new_focus_state { + self.fire_focus_event(FocusEventType::Blur, self.global().upcast(), None, can_gc); + } + + 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::(); + // 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::().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::() + { + ( + Some(( + (input.Value()).to_string(), + input.GetSelectionEnd().unwrap_or(0) as i32, + )), + false, + ) + } else if let Some(textarea) = elem.downcast::() { + ( + 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; + } + + // 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. + // + // + // + // 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::()) + .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::() { - ( - Some(( - input.Value().to_string(), - input.GetSelectionEnd().unwrap_or(0) as i32, - )), - false, - ) - } else if let Some(textarea) = elem.downcast::() { - ( - 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" + ); + }, } } @@ -1352,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::(MouseEvent::for_platform_mouse_event( @@ -1390,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, @@ -2217,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; @@ -3196,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, ) { @@ -3216,8 +3421,7 @@ impl Document { ); let event = event.upcast::(); event.set_trusted(true); - let target = node.upcast(); - event.fire(target, can_gc); + event.fire(event_target, can_gc); } /// @@ -3797,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 { @@ -3844,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), @@ -4991,12 +5199,34 @@ impl DocumentMethods 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; + // + // + // > 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 @@ -6399,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 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/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 for Window { doc.abort(can_gc); } + /// + 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/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/script_thread.rs b/components/script/script_thread.rs index 9c93bef22df..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; @@ -1803,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) @@ -2513,6 +2520,7 @@ impl ScriptThread { &self, parent_pipeline_id: PipelineId, browsing_context_id: BrowsingContextId, + sequence: FocusSequenceNumber, can_gc: CanGc, ) { let document = self @@ -2532,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/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/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, IpcSender<(IpcSender, 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, 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>), /// Get the browsing context id of the browsing context in which pipeline is 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), /// 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/tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html.ini b/tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html.ini deleted file mode 100644 index 8f3aec5177f..00000000000 --- a/tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html] - [Check focus event and active element after focusing different site iframe then immediately focusing back] - expected: FAIL diff --git a/tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe.html.ini b/tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe.html.ini deleted file mode 100644 index b489ab52a39..00000000000 --- a/tests/wpt/meta/focus/activeelement-after-focusing-different-site-iframe.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[activeelement-after-focusing-different-site-iframe.html] - [Check trailing events] - expected: FAIL diff --git a/tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe-contentwindow.html.ini b/tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe-contentwindow.html.ini index 5864035c9e1..612b845c7e9 100644 --- a/tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe-contentwindow.html.ini +++ b/tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe-contentwindow.html.ini @@ -1,2 +1,3 @@ [activeelement-after-focusing-same-site-iframe-contentwindow.html] - expected: TIMEOUT + [Check result] + expected: FAIL diff --git a/tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe.html.ini b/tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe.html.ini deleted file mode 100644 index 406e6a58324..00000000000 --- a/tests/wpt/meta/focus/activeelement-after-focusing-same-site-iframe.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[activeelement-after-focusing-same-site-iframe.html] - [Check trailing events] - expected: FAIL diff --git a/tests/wpt/meta/focus/activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html.ini b/tests/wpt/meta/focus/activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html.ini index 532cbcbabfe..a58db5d3146 100644 --- a/tests/wpt/meta/focus/activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html.ini +++ b/tests/wpt/meta/focus/activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html.ini @@ -1,2 +1,3 @@ [activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html] - expected: TIMEOUT + [Check result] + expected: FAIL diff --git a/tests/wpt/meta/focus/activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html.ini b/tests/wpt/meta/focus/activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html.ini index 8483775c0c1..d8b0d3212c9 100644 --- a/tests/wpt/meta/focus/activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html.ini +++ b/tests/wpt/meta/focus/activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html.ini @@ -1,2 +1,3 @@ [activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html] - expected: TIMEOUT + [Check result] + expected: FAIL diff --git a/tests/wpt/meta/focus/focus-restoration-in-different-site-iframes-window.html.ini b/tests/wpt/meta/focus/focus-restoration-in-different-site-iframes-window.html.ini index 8bdcea27053..1e3e377f307 100644 --- a/tests/wpt/meta/focus/focus-restoration-in-different-site-iframes-window.html.ini +++ b/tests/wpt/meta/focus/focus-restoration-in-different-site-iframes-window.html.ini @@ -1,2 +1,3 @@ [focus-restoration-in-different-site-iframes-window.html] - expected: TIMEOUT + [Check result] + expected: FAIL diff --git a/tests/wpt/meta/focus/focus-restoration-in-same-site-iframes-window.html.ini b/tests/wpt/meta/focus/focus-restoration-in-same-site-iframes-window.html.ini index 53f4db35f7e..f19949138fa 100644 --- a/tests/wpt/meta/focus/focus-restoration-in-same-site-iframes-window.html.ini +++ b/tests/wpt/meta/focus/focus-restoration-in-same-site-iframes-window.html.ini @@ -1,2 +1,3 @@ [focus-restoration-in-same-site-iframes-window.html] - expected: TIMEOUT + [Check result] + expected: FAIL diff --git a/tests/wpt/meta/focus/iframe-focuses-parent-same-site.html.ini b/tests/wpt/meta/focus/iframe-focuses-parent-same-site.html.ini deleted file mode 100644 index 8877baa8ac6..00000000000 --- a/tests/wpt/meta/focus/iframe-focuses-parent-same-site.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[iframe-focuses-parent-same-site.html] - expected: TIMEOUT diff --git a/tests/wpt/meta/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html.ini b/tests/wpt/meta/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html.ini index 6720c0f77da..3e682215a0e 100644 --- a/tests/wpt/meta/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html.ini +++ b/tests/wpt/meta/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html.ini @@ -1,7 +1,4 @@ [cross-origin-objects-function-caching.html] - [Cross-origin Window methods are cached] - expected: FAIL - [Cross-origin Location `replace` method is cached] expected: FAIL diff --git a/tests/wpt/meta/html/browsers/the-window-object/focus.window.js.ini b/tests/wpt/meta/html/browsers/the-window-object/focus.window.js.ini deleted file mode 100644 index 27a9640a02c..00000000000 --- a/tests/wpt/meta/html/browsers/the-window-object/focus.window.js.ini +++ /dev/null @@ -1,3 +0,0 @@ -[focus.window.html] - [focus] - expected: FAIL diff --git a/tests/wpt/meta/html/browsers/the-window-object/security-window/window-security.https.html.ini b/tests/wpt/meta/html/browsers/the-window-object/security-window/window-security.https.html.ini index 8afe0888e4e..32cd3f302d4 100644 --- a/tests/wpt/meta/html/browsers/the-window-object/security-window/window-security.https.html.ini +++ b/tests/wpt/meta/html/browsers/the-window-object/security-window/window-security.https.html.ini @@ -328,9 +328,3 @@ [A SecurityError exception must be thrown when window.stop is accessed from a different origin.] expected: FAIL - - [A SecurityError exception should not be thrown when window.blur is accessed from a different origin.] - expected: FAIL - - [A SecurityError exception should not be thrown when window.focus is accessed from a different origin.] - expected: FAIL diff --git a/tests/wpt/meta/html/browsers/the-window-object/window-properties.https.html.ini b/tests/wpt/meta/html/browsers/the-window-object/window-properties.https.html.ini index e9061d31d26..94ad9ce191c 100644 --- a/tests/wpt/meta/html/browsers/the-window-object/window-properties.https.html.ini +++ b/tests/wpt/meta/html/browsers/the-window-object/window-properties.https.html.ini @@ -1,9 +1,4 @@ [window-properties.https.html] - [Window method: focus] - expected: FAIL - - [Window method: blur] - expected: FAIL [Window method: print] expected: FAIL diff --git a/tests/wpt/meta/html/dom/idlharness.https.html.ini b/tests/wpt/meta/html/dom/idlharness.https.html.ini index 23b8e353fb0..64533ac1838 100644 --- a/tests/wpt/meta/html/dom/idlharness.https.html.ini +++ b/tests/wpt/meta/html/dom/idlharness.https.html.ini @@ -1738,9 +1738,6 @@ [Document interface: attribute all] expected: FAIL - [Window interface: operation focus()] - expected: FAIL - [Window interface: attribute scrollbars] expected: FAIL @@ -1870,9 +1867,6 @@ [Document interface: new Document() must inherit property "dir" with the proper type] expected: FAIL - [Window interface: window must inherit property "blur()" with the proper type] - expected: FAIL - [Document interface: operation execCommand(DOMString, optional boolean, optional DOMString)] expected: FAIL @@ -1897,9 +1891,6 @@ [Document interface: iframe.contentDocument must inherit property "queryCommandEnabled(DOMString)" with the proper type] expected: FAIL - [Window interface: operation blur()] - expected: FAIL - [Document interface: iframe.contentDocument must inherit property "onslotchange" with the proper type] expected: FAIL @@ -1924,9 +1915,6 @@ [Document interface: documentWithHandlers must inherit property "onauxclick" with the proper type] expected: FAIL - [Window interface: window must inherit property "focus()" with the proper type] - expected: FAIL - [Document interface: documentWithHandlers must inherit property "onwebkitanimationend" with the proper type] expected: FAIL diff --git a/tests/wpt/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js.ini b/tests/wpt/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js.ini index c00e2949bf5..b229be268ec 100644 --- a/tests/wpt/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js.ini +++ b/tests/wpt/meta/html/webappapis/dynamic-markup-insertion/opening-the-input-stream/event-listeners.window.js.ini @@ -1,12 +1,6 @@ [event-listeners.window.html] - [Standard event listeners are to be removed from Window] - expected: FAIL - [Standard event listeners are to be removed from Window for an active but not fully active document] expected: FAIL - [Standard event listeners are to be removed from Window for a non-active document that is the associated Document of a Window (frame is removed)] - expected: FAIL - [Custom event listeners are to be removed from Window for an active but not fully active document] expected: FAIL diff --git a/tests/wpt/meta/webidl/ecmascript-binding/global-object-implicit-this-value-cross-realm.html.ini b/tests/wpt/meta/webidl/ecmascript-binding/global-object-implicit-this-value-cross-realm.html.ini index 492ba730948..b63c174f353 100644 --- a/tests/wpt/meta/webidl/ecmascript-binding/global-object-implicit-this-value-cross-realm.html.ini +++ b/tests/wpt/meta/webidl/ecmascript-binding/global-object-implicit-this-value-cross-realm.html.ini @@ -1,6 +1,3 @@ [global-object-implicit-this-value-cross-realm.html] - [Cross-realm global object's operation throws when called on incompatible object] - expected: FAIL - [Cross-realm global object's operation called on null / undefined] expected: FAIL diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 4262fa5aca3..a3f77769a9d 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -12744,7 +12744,7 @@ ] }, "FocusEvent.html": [ - "9e002c1088de060b5e7f94c4152bf9fb779c04cc", + "7fb7aebf2afbac7f68a16308b9cc5d4588b7022f", [ null, {} @@ -13278,6 +13278,13 @@ {} ] ], + "focus_inter_documents.html": [ + "5c759772367e844066d1df0081917c9e129d09ec", + [ + null, + {} + ] + ], "follow-hyperlink.html": [ "6ac9eaeb5814a663988ed8c664c113072e329dc5", [ diff --git a/tests/wpt/mozilla/tests/mozilla/FocusEvent.html b/tests/wpt/mozilla/tests/mozilla/FocusEvent.html index 9e002c1088d..7fb7aebf2af 100644 --- a/tests/wpt/mozilla/tests/mozilla/FocusEvent.html +++ b/tests/wpt/mozilla/tests/mozilla/FocusEvent.html @@ -48,13 +48,6 @@ ] }, - { - element: document.body, - expected_events: [ - {element: input3, event_name: "blur"}, - ] - } - ]; var idx = 0; diff --git a/tests/wpt/mozilla/tests/mozilla/focus_inter_documents.html b/tests/wpt/mozilla/tests/mozilla/focus_inter_documents.html new file mode 100644 index 00000000000..5c759772367 --- /dev/null +++ b/tests/wpt/mozilla/tests/mozilla/focus_inter_documents.html @@ -0,0 +1,207 @@ + + + + + + + + + + + + + +