mirror of
https://github.com/servo/servo.git
synced 2025-08-06 14:10:11 +01:00
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:
parent
27570987fd
commit
0c0ee04b8e
33 changed files with 1123 additions and 242 deletions
|
@ -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 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::<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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue