constellation: focusing and closing webviews (#30842)

* constellation: focusing, closing, and native window visibility

* rename “browser” to “webview”, “unfocus” to “blur”

* remove native window visibility from constellation

* rename more “browser” to “webview”

* guard clauses

* don’t automatically focus when no webviews are focused

* comment spec steps for window.close()

* use format interpolation

Co-authored-by: Martin Robinson <mrobinson@igalia.com>

* fix formatting

* rename “Webview” to “WebView” in types and type parameters

* remove unused method

* fix libsimpleservo

---------

Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Delan Azabani 2024-01-24 19:45:54 +08:00 committed by GitHub
parent 6baaa82826
commit eb95703325
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 533 additions and 285 deletions

View file

@ -31,7 +31,7 @@
//! to a forest whose roots are top-level browsing context. The logical
//! relationship between these types is:
//!
//! ```
//! ```text
//! +------------+ +------------+ +---------+
//! | Browsing | ------parent?------> | Pipeline | --event_loop--> | Event |
//! | Context | ------current------> | | | Loop |
@ -172,6 +172,7 @@ use crate::session_history::{
JointSessionHistory, NeedsToReload, SessionHistoryChange, SessionHistoryDiff,
};
use crate::timer_scheduler::TimerScheduler;
use crate::webview::WebViewManager;
type PendingApprovalNavigations = HashMap<PipelineId, (LoadData, HistoryEntryReplacement)>;
@ -223,15 +224,15 @@ struct WebrenderWGPU {
wgpu_image_map: Arc<Mutex<HashMap<u64, webgpu::PresentationData>>>,
}
/// Servo supports tabs (referred to as browsers), so `Constellation` needs to
/// store browser specific data for bookkeeping.
struct Browser {
/// The currently focused browsing context in this browser for key events.
/// Servo supports multiple top-level browsing contexts or “webviews”, so `Constellation` needs to
/// store webview-specific data for bookkeeping.
struct WebView {
/// The currently focused browsing context in this webview for key events.
/// The focused pipeline is the current entry of the focused browsing
/// context.
focused_browsing_context_id: BrowsingContextId,
/// The joint session history for this browser.
/// The joint session history for this webview.
session_history: JointSessionHistory,
}
@ -325,13 +326,8 @@ pub struct Constellation<Message, LTF, STF, SWF> {
/// constellation to send messages to the compositor thread.
compositor_proxy: CompositorProxy,
/// The last frame tree sent to WebRender, denoting the browser (tab) user
/// has currently selected. This also serves as the key to retrieve data
/// about the current active browser from `browsers`.
active_browser_id: Option<TopLevelBrowsingContextId>,
/// Bookkeeping data for all browsers in constellation.
browsers: HashMap<TopLevelBrowsingContextId, Browser>,
/// Bookkeeping data for all webviews in the constellation.
webviews: WebViewManager<WebView>,
/// Channels for the constellation to send messages to the public
/// resource-related threads. There are two groups of resource threads: one
@ -758,8 +754,7 @@ where
network_listener_receiver: network_listener_receiver,
embedder_proxy: state.embedder_proxy,
compositor_proxy: state.compositor_proxy,
active_browser_id: None,
browsers: HashMap::new(),
webviews: WebViewManager::default(),
devtools_sender: state.devtools_sender,
bluetooth_ipc_sender: state.bluetooth_thread,
public_resource_threads: state.public_resource_threads,
@ -1352,9 +1347,7 @@ where
self.handle_get_pipeline(browsing_context_id, response_sender);
},
FromCompositorMsg::GetFocusTopLevelBrowsingContext(resp_chan) => {
// The focused browsing context's top-level browsing context is
// the active browser's id itself.
let _ = resp_chan.send(self.active_browser_id);
let _ = resp_chan.send(self.webviews.focused_webview().map(|(id, _)| id));
},
FromCompositorMsg::Keyboard(key_event) => {
self.handle_key_msg(key_event);
@ -1468,11 +1461,11 @@ where
},
// Create a new top level browsing context. Will use response_chan to return
// the browsing context id.
FromCompositorMsg::NewBrowser(url, top_level_browsing_context_id) => {
FromCompositorMsg::NewWebView(url, top_level_browsing_context_id) => {
self.handle_new_top_level_browsing_context(url, top_level_browsing_context_id);
},
// Close a top level browsing context.
FromCompositorMsg::CloseBrowser(top_level_browsing_context_id) => {
FromCompositorMsg::CloseWebView(top_level_browsing_context_id) => {
self.handle_close_top_level_browsing_context(top_level_browsing_context_id);
},
// Panic a top level browsing context.
@ -1483,9 +1476,23 @@ where
}
self.handle_panic(top_level_browsing_context_id, error, None);
},
// Send frame tree to WebRender. Make it visible.
FromCompositorMsg::SelectBrowser(top_level_browsing_context_id) => {
self.send_frame_tree(top_level_browsing_context_id);
FromCompositorMsg::FocusWebView(top_level_browsing_context_id) => {
if self.webviews.get(top_level_browsing_context_id).is_none() {
return warn!("{top_level_browsing_context_id}: FocusWebView on unknown top-level browsing context");
}
self.webviews.focus(top_level_browsing_context_id);
self.embedder_proxy.send((
Some(top_level_browsing_context_id),
EmbedderMsg::WebViewFocused(top_level_browsing_context_id),
));
if !cfg!(feature = "multiview") {
self.update_frame_tree_if_focused(top_level_browsing_context_id);
}
},
FromCompositorMsg::BlurWebView => {
self.webviews.unfocus();
self.embedder_proxy
.send((None, EmbedderMsg::WebViewBlurred));
},
// Handle a forward or back request
FromCompositorMsg::TraverseHistory(top_level_browsing_context_id, direction) => {
@ -1534,8 +1541,8 @@ where
FromCompositorMsg::MediaSessionAction(action) => {
self.handle_media_session_action_msg(action);
},
FromCompositorMsg::ChangeBrowserVisibility(top_level_browsing_context_id, visible) => {
self.handle_change_browser_visibility(top_level_browsing_context_id, visible);
FromCompositorMsg::WebViewVisibilityChanged(webview_id, visible) => {
self.notify_webview_visibility(webview_id, visible);
},
FromCompositorMsg::ReadyToPresent(top_level_browsing_context_id) => {
self.embedder_proxy.send((
@ -2934,7 +2941,7 @@ where
let pipeline_id = PipelineId::new();
let msg = (
Some(top_level_browsing_context_id),
EmbedderMsg::BrowserCreated(top_level_browsing_context_id),
EmbedderMsg::WebViewOpened(top_level_browsing_context_id),
);
self.embedder_proxy.send(msg);
let browsing_context_id = BrowsingContextId::from(top_level_browsing_context_id);
@ -2950,11 +2957,11 @@ where
let is_private = false;
let is_visible = true;
// Register this new top-level browsing context id as a browser and set
// Register this new top-level browsing context id as a webview and set
// its focused browsing context to be itself.
self.browsers.insert(
self.webviews.add(
top_level_browsing_context_id,
Browser {
WebView {
focused_browsing_context_id: browsing_context_id,
session_history: JointSessionHistory::new(),
},
@ -3000,22 +3007,45 @@ where
&mut self,
top_level_browsing_context_id: TopLevelBrowsingContextId,
) {
debug!("{top_level_browsing_context_id}: Closing");
let browsing_context_id = BrowsingContextId::from(top_level_browsing_context_id);
self.close_browsing_context(browsing_context_id, ExitPipelineMode::Normal);
self.browsers.remove(&top_level_browsing_context_id);
if self.active_browser_id == Some(top_level_browsing_context_id) {
self.active_browser_id = None;
let browsing_context =
self.close_browsing_context(browsing_context_id, ExitPipelineMode::Normal);
if self.webviews.focused_webview().map(|(id, _)| id) == Some(top_level_browsing_context_id)
{
self.embedder_proxy
.send((None, EmbedderMsg::WebViewBlurred));
}
let browsing_context = match self.browsing_contexts.get(&browsing_context_id) {
Some(bc) => bc,
None => {
warn!("Browsing context closed before it started");
return;
},
self.webviews.remove(top_level_browsing_context_id);
// TODO Send the compositor a RemoveWebView event.
self.embedder_proxy.send((
Some(top_level_browsing_context_id),
EmbedderMsg::WebViewClosed(top_level_browsing_context_id),
));
let Some(browsing_context) = browsing_context else {
return;
};
// https://html.spec.whatwg.org/multipage/#bcg-remove
self.browsing_context_group_set
.remove(&browsing_context.bc_group_id);
let bc_group_id = browsing_context.bc_group_id;
let Some(bc_group) = self.browsing_context_group_set.get_mut(&bc_group_id) else {
warn!("{}: Browsing context group not found!", bc_group_id);
return;
};
if !bc_group
.top_level_browsing_context_set
.remove(&top_level_browsing_context_id)
{
warn!(
"{top_level_browsing_context_id}: Top-level browsing context not found in {bc_group_id}",
);
}
if bc_group.top_level_browsing_context_set.is_empty() {
self.browsing_context_group_set
.remove(&browsing_context.bc_group_id);
}
debug!("{top_level_browsing_context_id}: Closed");
}
fn handle_iframe_size_msg(&mut self, iframe_sizes: Vec<IFrameSizeMsg>) {
@ -3312,9 +3342,9 @@ where
assert!(!self.pipelines.contains_key(&new_pipeline_id));
self.pipelines.insert(new_pipeline_id, pipeline);
self.browsers.insert(
self.webviews.add(
new_top_level_browsing_context_id,
Browser {
WebView {
focused_browsing_context_id: new_browsing_context_id,
session_history: JointSessionHistory::new(),
},
@ -3785,7 +3815,7 @@ where
self.notify_history_changed(top_level_browsing_context_id);
self.trim_history(top_level_browsing_context_id);
self.update_frame_tree_if_active(top_level_browsing_context_id);
self.update_frame_tree_if_focused(top_level_browsing_context_id);
}
fn update_browsing_context(
@ -3944,9 +3974,9 @@ where
response_sender: IpcSender<u32>,
) {
let length = self
.browsers
.get(&top_level_browsing_context_id)
.map(|browser| browser.session_history.history_length())
.webviews
.get(top_level_browsing_context_id)
.map(|webview| webview.session_history.history_length())
.unwrap_or(1);
let _ = response_sender.send(length as u32);
}
@ -4017,9 +4047,9 @@ where
fn handle_ime_dismissed(&mut self) {
// Send to the focused browsing contexts' current pipeline.
let focused_browsing_context_id = self
.active_browser_id
.and_then(|browser_id| self.browsers.get(&browser_id))
.map(|browser| browser.focused_browsing_context_id);
.webviews
.focused_webview()
.map(|(_, webview)| webview.focused_browsing_context_id);
if let Some(browsing_context_id) = focused_browsing_context_id {
let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
Some(ctx) => ctx.pipeline_id,
@ -4048,9 +4078,9 @@ where
// Send to the focused browsing contexts' current pipeline. If it
// doesn't exist, fall back to sending to the compositor.
let focused_browsing_context_id = self
.active_browser_id
.and_then(|browser_id| self.browsers.get(&browser_id))
.map(|browser| browser.focused_browsing_context_id);
.webviews
.focused_webview()
.map(|(_, webview)| webview.focused_browsing_context_id);
match focused_browsing_context_id {
Some(browsing_context_id) => {
let event = CompositorEvent::KeyboardEvent(event);
@ -4075,6 +4105,7 @@ where
}
},
None => {
warn!("No focused browsing context! Falling back to sending key to compositor");
let event = (None, EmbedderMsg::Keyboard(event));
self.embedder_proxy.send(event);
},
@ -4182,10 +4213,17 @@ where
None => return warn!("{}: Focus parent after closure", pipeline_id),
};
// Update the focused browsing context in its browser in `browsers`.
match self.browsers.get_mut(&top_level_browsing_context_id) {
Some(browser) => {
browser.focused_browsing_context_id = browsing_context_id;
// Focus the top-level browsing context.
self.webviews.focus(top_level_browsing_context_id);
self.embedder_proxy.send((
Some(top_level_browsing_context_id),
EmbedderMsg::WebViewFocused(top_level_browsing_context_id),
));
// Update the webviews focused browsing context.
match self.webviews.get_mut(top_level_browsing_context_id) {
Some(webview) => {
webview.focused_browsing_context_id = browsing_context_id;
},
None => {
return warn!(
@ -4428,7 +4466,7 @@ where
}
}
fn handle_change_browser_visibility(
fn notify_webview_visibility(
&mut self,
top_level_browsing_context_id: TopLevelBrowsingContextId,
visible: bool,
@ -4437,14 +4475,11 @@ where
let pipeline_id = match self.browsing_contexts.get(&browsing_context_id) {
Some(browsing_context) => browsing_context.pipeline_id,
None => {
return warn!(
"{}: Got visibility change event after closure",
browsing_context_id
);
return warn!("{browsing_context_id}: Tried to notify visibility after closure");
},
};
match self.pipelines.get(&pipeline_id) {
None => return warn!("{}: Got visibility change event after closure", pipeline_id),
None => return warn!("{pipeline_id}: Tried to notify visibility after closure"),
Some(pipeline) => pipeline.notify_visibility(visible),
};
}
@ -4456,8 +4491,8 @@ where
// LoadData of inner frames are ignored and replaced with the LoadData
// of the parent.
let session_history = match self.browsers.get(&top_level_browsing_context_id) {
Some(browser) => &browser.session_history,
let session_history = match self.webviews.get(top_level_browsing_context_id) {
Some(webview) => &webview.session_history,
None => {
return warn!(
"{}: Session history does not exist for browsing context",
@ -4602,11 +4637,9 @@ where
// context in which the page is being loaded, then update the focused
// browsing context to be the one where the page is being loaded.
if self.focused_browsing_context_is_descendant_of(change.browsing_context_id) {
self.browsers
.entry(change.top_level_browsing_context_id)
.and_modify(|browser| {
browser.focused_browsing_context_id = change.browsing_context_id
});
if let Some(webview) = self.webviews.get_mut(change.top_level_browsing_context_id) {
webview.focused_browsing_context_id = change.browsing_context_id;
}
}
let (old_pipeline_id, top_level_id) =
@ -4745,7 +4778,7 @@ where
}
self.notify_history_changed(change.top_level_browsing_context_id);
self.update_frame_tree_if_active(change.top_level_browsing_context_id);
self.update_frame_tree_if_focused(change.top_level_browsing_context_id);
}
fn focused_browsing_context_is_descendant_of(
@ -4753,9 +4786,9 @@ where
browsing_context_id: BrowsingContextId,
) -> bool {
let focused_browsing_context_id = self
.active_browser_id
.and_then(|browser_id| self.browsers.get(&browser_id))
.map(|browser| browser.focused_browsing_context_id);
.webviews
.focused_webview()
.map(|(_, webview)| webview.focused_browsing_context_id);
focused_browsing_context_id.map_or(false, |focus_ctx_id| {
focus_ctx_id == browsing_context_id ||
self.fully_active_descendant_browsing_contexts_iter(browsing_context_id)
@ -4912,10 +4945,8 @@ where
//
// If there is no focus browsing context yet, the initial page has
// not loaded, so there is nothing to save yet.
let top_level_browsing_context_id = match self.active_browser_id {
Some(id) => id,
None => return ReadyToSave::NoTopLevelBrowsingContext,
};
let Some(top_level_browsing_context_id) = self.webviews.focused_webview().map(|(id, _)| id)
else { return ReadyToSave::NoTopLevelBrowsingContext };
// If there are pending loads, wait for those to complete.
if !self.pending_changes.is_empty() {
@ -5141,12 +5172,12 @@ where
}
}
// Close a browsing context (and all children)
// Close and return the browsing context with the given id (and its children), if it exists.
fn close_browsing_context(
&mut self,
browsing_context_id: BrowsingContextId,
exit_mode: ExitPipelineMode,
) {
) -> Option<BrowsingContext> {
debug!("{}: Closing", browsing_context_id);
self.close_browsing_context_children(
@ -5157,7 +5188,10 @@ where
let browsing_context = match self.browsing_contexts.remove(&browsing_context_id) {
Some(ctx) => ctx,
None => return warn!("{}: Closing twice", browsing_context_id),
None => {
warn!("{browsing_context_id}: Closing twice");
return None;
},
};
{
@ -5168,12 +5202,13 @@ where
if let Some(parent_pipeline_id) = browsing_context.parent_pipeline_id {
match self.pipelines.get_mut(&parent_pipeline_id) {
None => {
return warn!("{}: Child closed after parent", parent_pipeline_id);
warn!("{parent_pipeline_id}: Child closed after parent");
},
Some(parent_pipeline) => parent_pipeline.remove_child(browsing_context_id),
};
}
debug!("{}: Closed", browsing_context_id);
Some(browsing_context)
}
// Close the children of a browsing context
@ -5212,13 +5247,13 @@ where
top_level_browsing_context_id: TopLevelBrowsingContextId,
pipeline_id: PipelineId,
) {
match self.browsers.get_mut(&top_level_browsing_context_id) {
Some(browser) => {
match self.webviews.get_mut(top_level_browsing_context_id) {
Some(webview) => {
let load_data = match self.pipelines.get(&pipeline_id) {
Some(pipeline) => pipeline.load_data.clone(),
None => return warn!("{}: Discarding closed pipeline", pipeline_id),
};
browser.session_history.replace_reloader(
webview.session_history.replace_reloader(
NeedsToReload::No(pipeline_id),
NeedsToReload::Yes(pipeline_id, load_data),
);
@ -5350,17 +5385,10 @@ where
&mut self,
top_level_id: TopLevelBrowsingContextId,
) -> &mut JointSessionHistory {
&mut self
.browsers
.entry(top_level_id)
// This shouldn't be necessary since `get_joint_session_history` is
// invoked for existing browsers but we need this to satisfy the
// type system.
.or_insert_with(|| Browser {
focused_browsing_context_id: BrowsingContextId::from(top_level_id),
session_history: JointSessionHistory::new(),
})
.session_history
self.webviews
.get_mut(top_level_id)
.map(|webview| &mut webview.session_history)
.expect("Unknown top-level browsing context")
}
// Convert a browsing context to a sendable form to pass to the compositor
@ -5392,29 +5420,23 @@ where
})
}
/// Re-send the frame tree to the compositor.
fn update_frame_tree_if_active(
/// Send the frame tree for the given webview to the compositor.
fn update_frame_tree_if_focused(
&mut self,
top_level_browsing_context_id: TopLevelBrowsingContextId,
) {
// Only send the frame tree if it's the active one or if no frame tree
// has been sent yet.
if self.active_browser_id.is_none() ||
Some(top_level_browsing_context_id) == self.active_browser_id
{
self.send_frame_tree(top_level_browsing_context_id);
// Only send the frame tree if the given webview is focused.
if let Some(focused_webview_id) = self.webviews.focused_webview().map(|(id, _)| id) {
if top_level_browsing_context_id != focused_webview_id {
return;
}
}
}
/// Send the current frame tree to compositor
fn send_frame_tree(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) {
// Note that this function can panic, due to ipc-channel creation failure.
// avoiding this panic would require a mechanism for dealing
// with low-resource scenarios.
let browsing_context_id = BrowsingContextId::from(top_level_browsing_context_id);
if let Some(frame_tree) = self.browsing_context_to_sendable(browsing_context_id) {
debug!("{}: Sending frame tree", browsing_context_id);
self.active_browser_id = Some(top_level_browsing_context_id);
self.compositor_proxy
.send(CompositorMsg::SetFrameTree(frame_tree));
}