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 <i@yvt.jp>
This commit is contained in:
Fuguo 2025-04-30 12:37:53 +08:00 committed by GitHub
parent 27570987fd
commit 0c0ee04b8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1123 additions and 242 deletions

View file

@ -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,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<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));
// 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<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),
};
// Update the webviews 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::<Vec<_>>()
);
debug!(
"new_focus_chain_pipelines = {:?}",
new_focus_chain_pipelines
.iter()
.map(|p| p.id.to_string())
.collect::<Vec<_>>()
);
// 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);

View file

@ -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);

View file

@ -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"),