diff options
author | Delan Azabani <dazabani@igalia.com> | 2024-01-24 19:45:54 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-24 11:45:54 +0000 |
commit | eb95703325aeb48d5f56a8da5b258bad608dd632 (patch) | |
tree | e3e636a74578ca431795511a3b30331422fb4a6e | |
parent | 6baaa828261af49e790938666ee89f156a736821 (diff) | |
download | servo-eb95703325aeb48d5f56a8da5b258bad608dd632.tar.gz servo-eb95703325aeb48d5f56a8da5b258bad608dd632.zip |
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>
-rw-r--r-- | components/compositing/windowing.rs | 18 | ||||
-rw-r--r-- | components/constellation/constellation.rs | 240 | ||||
-rw-r--r-- | components/constellation/lib.rs | 1 | ||||
-rw-r--r-- | components/constellation/webview.rs | 200 | ||||
-rw-r--r-- | components/script/dom/window.rs | 11 | ||||
-rw-r--r-- | components/script/dom/windowproxy.rs | 4 | ||||
-rw-r--r-- | components/servo/lib.rs | 23 | ||||
-rw-r--r-- | components/shared/compositing/constellation_msg.rs | 23 | ||||
-rw-r--r-- | components/shared/embedder/lib.rs | 22 | ||||
-rw-r--r-- | components/shared/msg/constellation_msg.rs | 8 | ||||
-rw-r--r-- | ports/libsimpleservo/api/src/lib.rs | 82 | ||||
-rw-r--r-- | ports/servoshell/app.rs | 45 | ||||
-rw-r--r-- | ports/servoshell/main.rs | 2 | ||||
-rw-r--r-- | ports/servoshell/minibrowser.rs | 16 | ||||
-rw-r--r-- | ports/servoshell/webview.rs (renamed from ports/servoshell/browser.rs) | 120 | ||||
-rw-r--r-- | python/servo/testing_commands.py | 1 |
16 files changed, 532 insertions, 284 deletions
diff --git a/components/compositing/windowing.rs b/components/compositing/windowing.rs index c78ddb6ba85..1508d6dac70 100644 --- a/components/compositing/windowing.rs +++ b/components/compositing/windowing.rs @@ -83,14 +83,14 @@ pub enum EmbedderEvent { /// Sent when Ctr+R/Apple+R is called to reload the current page. Reload(TopLevelBrowsingContextId), /// Create a new top level browsing context - NewBrowser(ServoUrl, TopLevelBrowsingContextId), + NewWebView(ServoUrl, TopLevelBrowsingContextId), /// Close a top level browsing context - CloseBrowser(TopLevelBrowsingContextId), + CloseWebView(TopLevelBrowsingContextId), /// Panic a top level browsing context. SendError(Option<TopLevelBrowsingContextId>, String), /// Make a top level browsing context visible, hiding the previous /// visible one. - SelectBrowser(TopLevelBrowsingContextId), + FocusWebView(TopLevelBrowsingContextId), /// Toggles a debug flag in WebRender ToggleWebRenderDebug(WebRenderDebugOption), /// Capture current WebRender @@ -102,8 +102,8 @@ pub enum EmbedderEvent { /// Sent when the user triggers a media action through the UA exposed media UI /// (play, pause, seek, etc.). MediaSessionAction(MediaSessionActionType), - /// Set browser visibility. A hidden browser will not tick the animations. - ChangeBrowserVisibility(TopLevelBrowsingContextId, bool), + /// The visibility of the webview has changed. + WebViewVisibilityChanged(TopLevelBrowsingContextId, bool), /// Virtual keyboard was dismissed IMEDismissed, /// Sent on platforms like Android where the native widget surface can be @@ -136,16 +136,16 @@ impl Debug for EmbedderEvent { EmbedderEvent::Navigation(..) => write!(f, "Navigation"), EmbedderEvent::Quit => write!(f, "Quit"), EmbedderEvent::Reload(..) => write!(f, "Reload"), - EmbedderEvent::NewBrowser(..) => write!(f, "NewBrowser"), + EmbedderEvent::NewWebView(..) => write!(f, "NewWebView"), EmbedderEvent::SendError(..) => write!(f, "SendError"), - EmbedderEvent::CloseBrowser(..) => write!(f, "CloseBrowser"), - EmbedderEvent::SelectBrowser(..) => write!(f, "SelectBrowser"), + EmbedderEvent::CloseWebView(..) => write!(f, "CloseWebView"), + EmbedderEvent::FocusWebView(..) => write!(f, "FocusWebView"), EmbedderEvent::ToggleWebRenderDebug(..) => write!(f, "ToggleWebRenderDebug"), EmbedderEvent::CaptureWebRender => write!(f, "CaptureWebRender"), EmbedderEvent::ToggleSamplingProfiler(..) => write!(f, "ToggleSamplingProfiler"), EmbedderEvent::ExitFullScreen(..) => write!(f, "ExitFullScreen"), EmbedderEvent::MediaSessionAction(..) => write!(f, "MediaSessionAction"), - EmbedderEvent::ChangeBrowserVisibility(..) => write!(f, "ChangeBrowserVisibility"), + EmbedderEvent::WebViewVisibilityChanged(..) => write!(f, "WebViewVisibilityChanged"), EmbedderEvent::IMEDismissed => write!(f, "IMEDismissed"), EmbedderEvent::ClearCache => write!(f, "ClearCache"), EmbedderEvent::InvalidateNativeSurface => write!(f, "InvalidateNativeSurface"), diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index b2cbcbb9a7b..cfc6ced6cb3 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -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 webview’s 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)); } diff --git a/components/constellation/lib.rs b/components/constellation/lib.rs index 17b83ba9a13..8cf7b30fed3 100644 --- a/components/constellation/lib.rs +++ b/components/constellation/lib.rs @@ -14,6 +14,7 @@ mod sandboxing; mod serviceworker; mod session_history; mod timer_scheduler; +mod webview; pub use crate::constellation::{Constellation, InitialConstellationState}; pub use crate::logging::{FromCompositorLogger, FromScriptLogger}; diff --git a/components/constellation/webview.rs b/components/constellation/webview.rs new file mode 100644 index 00000000000..cb2b85de468 --- /dev/null +++ b/components/constellation/webview.rs @@ -0,0 +1,200 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; + +use msg::constellation_msg::TopLevelBrowsingContextId; + +#[derive(Debug)] +pub struct WebViewManager<WebView> { + /// Our top-level browsing contexts. In the WebRender scene, their pipelines are the children of + /// a single root pipeline that also applies any pinch zoom transformation. + webviews: HashMap<TopLevelBrowsingContextId, WebView>, + + /// The order in which they were focused, latest last. + focus_order: Vec<TopLevelBrowsingContextId>, + + /// Whether the latest webview in focus order is currently focused. + is_focused: bool, +} + +impl<WebView> Default for WebViewManager<WebView> { + fn default() -> Self { + Self { + webviews: HashMap::default(), + focus_order: Vec::default(), + is_focused: false, + } + } +} + +impl<WebView> WebViewManager<WebView> { + pub fn add( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + webview: WebView, + ) { + self.webviews.insert(top_level_browsing_context_id, webview); + } + + pub fn remove( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + ) -> Option<WebView> { + if self.focus_order.last() == Some(&top_level_browsing_context_id) { + self.is_focused = false; + } + self.focus_order + .retain(|b| *b != top_level_browsing_context_id); + self.webviews.remove(&top_level_browsing_context_id) + } + + pub fn get( + &self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + ) -> Option<&WebView> { + self.webviews.get(&top_level_browsing_context_id) + } + + pub fn get_mut( + &mut self, + top_level_browsing_context_id: TopLevelBrowsingContextId, + ) -> Option<&mut WebView> { + self.webviews.get_mut(&top_level_browsing_context_id) + } + + pub fn focused_webview(&self) -> Option<(TopLevelBrowsingContextId, &WebView)> { + if !self.is_focused { + return None; + } + + if let Some(top_level_browsing_context_id) = self.focus_order.last().cloned() { + debug_assert!( + self.webviews.contains_key(&top_level_browsing_context_id), + "BUG: webview in .focus_order not in .webviews!", + ); + self.get(top_level_browsing_context_id) + .map(|webview| (top_level_browsing_context_id, webview)) + } else { + debug_assert!(false, "BUG: .is_focused but no webviews in .focus_order!"); + None + } + } + + pub fn focus(&mut self, top_level_browsing_context_id: TopLevelBrowsingContextId) { + debug_assert!(self.webviews.contains_key(&top_level_browsing_context_id)); + self.focus_order + .retain(|b| *b != top_level_browsing_context_id); + self.focus_order.push(top_level_browsing_context_id); + self.is_focused = true; + } + + pub fn unfocus(&mut self) { + self.is_focused = false; + } +} + +#[cfg(test)] +mod test { + use std::num::NonZeroU32; + + use msg::constellation_msg::{ + BrowsingContextId, BrowsingContextIndex, PipelineNamespace, PipelineNamespaceId, + TopLevelBrowsingContextId, + }; + + use crate::webview::WebViewManager; + + fn top_level_id(namespace_id: u32, index: u32) -> TopLevelBrowsingContextId { + TopLevelBrowsingContextId(BrowsingContextId { + namespace_id: PipelineNamespaceId(namespace_id), + index: BrowsingContextIndex(NonZeroU32::new(index).expect("Incorrect test case")), + }) + } + + fn webviews_sorted<WebView: Clone>( + webviews: &WebViewManager<WebView>, + ) -> Vec<(TopLevelBrowsingContextId, WebView)> { + let mut keys = webviews.webviews.keys().collect::<Vec<_>>(); + keys.sort(); + keys.iter() + .map(|&id| { + ( + *id, + webviews + .webviews + .get(id) + .cloned() + .expect("Incorrect test case"), + ) + }) + .collect() + } + + #[test] + fn test() { + PipelineNamespace::install(PipelineNamespaceId(0)); + let mut webviews = WebViewManager::default(); + + // add() adds the webview to the map, but does not focus it. + webviews.add(TopLevelBrowsingContextId::new(), 'a'); + webviews.add(TopLevelBrowsingContextId::new(), 'b'); + webviews.add(TopLevelBrowsingContextId::new(), 'c'); + assert_eq!( + webviews_sorted(&webviews), + vec![ + (top_level_id(0, 1), 'a'), + (top_level_id(0, 2), 'b'), + (top_level_id(0, 3), 'c'), + ] + ); + assert!(webviews.focus_order.is_empty()); + assert_eq!(webviews.is_focused, false); + + // focus() makes the given webview the latest in focus order. + webviews.focus(top_level_id(0, 2)); + assert_eq!(webviews.focus_order, vec![top_level_id(0, 2)]); + assert_eq!(webviews.is_focused, true); + webviews.focus(top_level_id(0, 1)); + assert_eq!( + webviews.focus_order, + vec![top_level_id(0, 2), top_level_id(0, 1)] + ); + assert_eq!(webviews.is_focused, true); + webviews.focus(top_level_id(0, 3)); + assert_eq!( + webviews.focus_order, + vec![top_level_id(0, 2), top_level_id(0, 1), top_level_id(0, 3)] + ); + assert_eq!(webviews.is_focused, true); + + // unfocus() clears the “is focused” flag, but does not touch the focus order. + webviews.unfocus(); + assert_eq!( + webviews.focus_order, + vec![top_level_id(0, 2), top_level_id(0, 1), top_level_id(0, 3)] + ); + assert_eq!(webviews.is_focused, false); + + // focus() avoids duplicates in focus order, when the given webview has been focused before. + webviews.focus(top_level_id(0, 1)); + assert_eq!( + webviews.focus_order, + vec![top_level_id(0, 2), top_level_id(0, 3), top_level_id(0, 1)] + ); + assert_eq!(webviews.is_focused, true); + + // remove() clears the “is focused” flag iff the given webview was focused. + webviews.remove(top_level_id(0, 2)); + assert_eq!(webviews.is_focused, true); + webviews.remove(top_level_id(0, 1)); + assert_eq!(webviews.is_focused, false); + webviews.remove(top_level_id(0, 3)); + assert_eq!(webviews.is_focused, false); + + // remove() removes the given webview from both the map and the focus order. + assert!(webviews_sorted(&webviews).is_empty()); + assert!(webviews.focus_order.is_empty()); + } +} diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index 10d6097b374..83bdc3363ed 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -790,13 +790,14 @@ impl WindowMethods for Window { let window = this.root(); let document = window.Document(); // https://html.spec.whatwg.org/multipage/#closing-browsing-contexts - // Step 1, prompt to unload. + // Step 1, check if traversable is closing, was already done above. + // Steps 2 and 3, prompt to unload for all inclusive descendant navigables. + // TODO: We should be prompting for all inclusive descendant navigables, + // but we pass false here, which suggests we are not doing that. Why? if document.prompt_to_unload(false) { - // Step 2, unload. + // Step 4, unload. document.unload(false); - // Step 3, remove from the user interface - let _ = window.send_to_embedder(EmbedderMsg::CloseBrowser); - // Step 4, discard browsing context. + // https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded // which calls into https://html.spec.whatwg.org/multipage/#discard-a-document. window.discard_browsing_context(); diff --git a/components/script/dom/windowproxy.rs b/components/script/dom/windowproxy.rs index 612476b76e2..886d63dc17c 100644 --- a/components/script/dom/windowproxy.rs +++ b/components/script/dom/windowproxy.rs @@ -293,7 +293,7 @@ impl WindowProxy { .and_then(|id| ScriptThread::find_document(id)) .and_then(|doc| Some(DomRoot::from_ref(doc.window()))) .unwrap(); - let msg = EmbedderMsg::AllowOpeningBrowser(chan); + let msg = EmbedderMsg::AllowOpeningWebView(chan); window.send_to_embedder(msg); if port.recv().unwrap() { let new_top_level_browsing_context_id = TopLevelBrowsingContextId::new(); @@ -337,7 +337,7 @@ impl WindowProxy { let constellation_msg = ScriptMsg::ScriptNewAuxiliary(load_info, pipeline_sender); window.send_to_constellation(constellation_msg); ScriptThread::process_attach_layout(new_layout_info, document.origin().clone()); - let msg = EmbedderMsg::BrowserCreated(new_top_level_browsing_context_id); + let msg = EmbedderMsg::WebViewOpened(new_top_level_browsing_context_id); window.send_to_embedder(msg); // TODO: if noopener is false, copy the sessionStorage storage area of the creator origin. // See step 14 of https://html.spec.whatwg.org/multipage/#creating-a-new-browsing-context diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 662a958460c..11af054132a 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -633,8 +633,8 @@ where self.compositor.capture_webrender(); }, - EmbedderEvent::NewBrowser(url, top_level_browsing_context_id) => { - let msg = ConstellationMsg::NewBrowser(url, top_level_browsing_context_id); + EmbedderEvent::NewWebView(url, top_level_browsing_context_id) => { + let msg = ConstellationMsg::NewWebView(url, top_level_browsing_context_id); if let Err(e) = self.constellation_chan.send(msg) { warn!( "Sending NewBrowser message to constellation failed ({:?}).", @@ -643,18 +643,18 @@ where } }, - EmbedderEvent::SelectBrowser(top_level_browsing_context_id) => { - let msg = ConstellationMsg::SelectBrowser(top_level_browsing_context_id); + EmbedderEvent::FocusWebView(top_level_browsing_context_id) => { + let msg = ConstellationMsg::FocusWebView(top_level_browsing_context_id); if let Err(e) = self.constellation_chan.send(msg) { warn!( - "Sending SelectBrowser message to constellation failed ({:?}).", + "Sending FocusBrowser message to constellation failed ({:?}).", e ); } }, - EmbedderEvent::CloseBrowser(top_level_browsing_context_id) => { - let msg = ConstellationMsg::CloseBrowser(top_level_browsing_context_id); + EmbedderEvent::CloseWebView(top_level_browsing_context_id) => { + let msg = ConstellationMsg::CloseWebView(top_level_browsing_context_id); if let Err(e) = self.constellation_chan.send(msg) { warn!( "Sending CloseBrowser message to constellation failed ({:?}).", @@ -683,14 +683,11 @@ where } }, - EmbedderEvent::ChangeBrowserVisibility(top_level_browsing_context_id, visible) => { - let msg = ConstellationMsg::ChangeBrowserVisibility( - top_level_browsing_context_id, - visible, - ); + EmbedderEvent::WebViewVisibilityChanged(webview_id, visible) => { + let msg = ConstellationMsg::WebViewVisibilityChanged(webview_id, visible); if let Err(e) = self.constellation_chan.send(msg) { warn!( - "Sending ChangeBrowserVisibility to constellation failed ({:?}).", + "Sending WebViewVisibilityChanged to constellation failed ({:?}).", e ); } diff --git a/components/shared/compositing/constellation_msg.rs b/components/shared/compositing/constellation_msg.rs index 49b7353adfb..12b74cb2f67 100644 --- a/components/shared/compositing/constellation_msg.rs +++ b/components/shared/compositing/constellation_msg.rs @@ -55,13 +55,15 @@ pub enum ConstellationMsg { /// A log entry, with the top-level browsing context id and thread name LogEntry(Option<TopLevelBrowsingContextId>, Option<String>, LogEntry), /// Create a new top level browsing context. - NewBrowser(ServoUrl, TopLevelBrowsingContextId), + NewWebView(ServoUrl, TopLevelBrowsingContextId), /// Close a top level browsing context. - CloseBrowser(TopLevelBrowsingContextId), + CloseWebView(TopLevelBrowsingContextId), /// Panic a top level browsing context. SendError(Option<TopLevelBrowsingContextId>, String), - /// Make browser visible. - SelectBrowser(TopLevelBrowsingContextId), + /// Make a top-level browsing context focused. + FocusWebView(TopLevelBrowsingContextId), + /// Make none of the top-level browsing contexts focused. + BlurWebView, /// Forward an event to the script task of the given pipeline. ForwardEvent(PipelineId, CompositorEvent), /// Requesting a change to the onscreen cursor. @@ -74,8 +76,8 @@ pub enum ConstellationMsg { ExitFullScreen(TopLevelBrowsingContextId), /// Media session action. MediaSessionAction(MediaSessionActionType), - /// Toggle browser visibility. - ChangeBrowserVisibility(TopLevelBrowsingContextId, bool), + /// The visibility of the webview has changed. + WebViewVisibilityChanged(TopLevelBrowsingContextId, bool), /// Virtual keyboard was dismissed IMEDismissed, /// Compositing done, but external code needs to present. @@ -100,17 +102,18 @@ impl fmt::Debug for ConstellationMsg { WebDriverCommand(..) => "WebDriverCommand", Reload(..) => "Reload", LogEntry(..) => "LogEntry", - NewBrowser(..) => "NewBrowser", - CloseBrowser(..) => "CloseBrowser", + NewWebView(..) => "NewWebView", + CloseWebView(..) => "CloseWebView", + FocusWebView(..) => "FocusWebView", + BlurWebView => "BlurWebView", SendError(..) => "SendError", - SelectBrowser(..) => "SelectBrowser", ForwardEvent(..) => "ForwardEvent", SetCursor(..) => "SetCursor", EnableProfiler(..) => "EnableProfiler", DisableProfiler => "DisableProfiler", ExitFullScreen(..) => "ExitFullScreen", MediaSessionAction(..) => "MediaSessionAction", - ChangeBrowserVisibility(..) => "ChangeBrowserVisibility", + WebViewVisibilityChanged(..) => "WebViewVisibilityChanged", IMEDismissed => "IMEDismissed", ClearCache => "ClearCache", ReadyToPresent(..) => "ReadyToPresent", diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 8c86f8d0a44..0337884ea1a 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -155,9 +155,15 @@ pub enum EmbedderMsg { /// Whether or not to allow a pipeline to load a url. AllowNavigationRequest(PipelineId, ServoUrl), /// Whether or not to allow script to open a new tab/browser - AllowOpeningBrowser(IpcSender<bool>), - /// A new browser was created by script - BrowserCreated(TopLevelBrowsingContextId), + AllowOpeningWebView(IpcSender<bool>), + /// A browser was created + WebViewOpened(TopLevelBrowsingContextId), + /// A browser was destroyed + WebViewClosed(TopLevelBrowsingContextId), + /// A browser gained focus for keyboard events + WebViewFocused(TopLevelBrowsingContextId), + /// All browsers lost focus for keyboard events + WebViewBlurred, /// Wether or not to unload a document AllowUnload(IpcSender<bool>), /// Sends an unconsumed key event back to the embedder. @@ -180,8 +186,6 @@ pub enum EmbedderMsg { LoadStart, /// The load of a page has completed LoadComplete, - /// A browser is to be closed - CloseBrowser, /// A pipeline panicked. First string is the reason, second one is the backtrace. Panic(String, Option<String>), /// Open dialog to select bluetooth device. @@ -241,7 +245,6 @@ impl Debug for EmbedderMsg { EmbedderMsg::SetCursor(..) => write!(f, "SetCursor"), EmbedderMsg::NewFavicon(..) => write!(f, "NewFavicon"), EmbedderMsg::HeadParsed => write!(f, "HeadParsed"), - EmbedderMsg::CloseBrowser => write!(f, "CloseBrowser"), EmbedderMsg::HistoryChanged(..) => write!(f, "HistoryChanged"), EmbedderMsg::SetFullscreenState(..) => write!(f, "SetFullscreenState"), EmbedderMsg::LoadStart => write!(f, "LoadStart"), @@ -253,8 +256,11 @@ impl Debug for EmbedderMsg { EmbedderMsg::ShowIME(..) => write!(f, "ShowIME"), EmbedderMsg::HideIME => write!(f, "HideIME"), EmbedderMsg::Shutdown => write!(f, "Shutdown"), - EmbedderMsg::AllowOpeningBrowser(..) => write!(f, "AllowOpeningBrowser"), - EmbedderMsg::BrowserCreated(..) => write!(f, "BrowserCreated"), + EmbedderMsg::AllowOpeningWebView(..) => write!(f, "AllowOpeningWebView"), + EmbedderMsg::WebViewOpened(..) => write!(f, "WebViewOpened"), + EmbedderMsg::WebViewClosed(..) => write!(f, "WebViewClosed"), + EmbedderMsg::WebViewFocused(..) => write!(f, "WebViewFocused"), + EmbedderMsg::WebViewBlurred => write!(f, "WebViewUnfocused"), EmbedderMsg::ReportProfile(..) => write!(f, "ReportProfile"), EmbedderMsg::MediaSessionEvent(..) => write!(f, "MediaSessionEvent"), EmbedderMsg::OnDevtoolsStarted(..) => write!(f, "OnDevtoolsStarted"), diff --git a/components/shared/msg/constellation_msg.rs b/components/shared/msg/constellation_msg.rs index 889f53e6d73..46c947de67b 100644 --- a/components/shared/msg/constellation_msg.rs +++ b/components/shared/msg/constellation_msg.rs @@ -257,8 +257,13 @@ impl BrowsingContextId { } } -#[derive(Clone, Default, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] pub struct BrowsingContextGroupId(pub u32); +impl fmt::Display for BrowsingContextGroupId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "BrowsingContextGroup{:?}", self) + } +} thread_local!(pub static TOP_LEVEL_BROWSING_CONTEXT_ID: Cell<Option<TopLevelBrowsingContextId>> = Cell::new(None)); @@ -266,6 +271,7 @@ thread_local!(pub static TOP_LEVEL_BROWSING_CONTEXT_ID: Cell<Option<TopLevelBrow Clone, Copy, Deserialize, Eq, Hash, MallocSizeOf, Ord, PartialEq, PartialOrd, Serialize, )] pub struct TopLevelBrowsingContextId(pub BrowsingContextId); +pub type WebViewId = TopLevelBrowsingContextId; size_of_test!(TopLevelBrowsingContextId, 8); size_of_test!(Option<TopLevelBrowsingContextId>, 8); diff --git a/ports/libsimpleservo/api/src/lib.rs b/ports/libsimpleservo/api/src/lib.rs index 5a3d1e4cd74..069e5745ae5 100644 --- a/ports/libsimpleservo/api/src/lib.rs +++ b/ports/libsimpleservo/api/src/lib.rs @@ -31,7 +31,7 @@ use servo::embedder_traits::{ use servo::euclid::{Point2D, Rect, Scale, Size2D, Vector2D}; use servo::keyboard_types::{Key, KeyState, KeyboardEvent}; pub use servo::msg::constellation_msg::InputMethodType; -use servo::msg::constellation_msg::TraversalDirection; +use servo::msg::constellation_msg::{TraversalDirection, WebViewId}; pub use servo::script_traits::{MediaSessionActionType, MouseButton}; use servo::script_traits::{TouchEventType, TouchId}; use servo::servo_config::{opts, pref}; @@ -168,19 +168,25 @@ pub struct ServoGlue { servo: Servo<ServoWindowCallbacks>, batch_mode: bool, callbacks: Rc<ServoWindowCallbacks>, - /// id of the top level browsing context. It is unique as tabs - /// are not supported yet. None until created. - browser_id: Option<BrowserId>, - // A rudimentary stack of "tabs". - // EmbedderMsg::BrowserCreated will push onto it. - // EmbedderMsg::CloseBrowser will pop from it, - // and exit if it is empty afterwards. - browsers: Vec<BrowserId>, events: Vec<EmbedderEvent>, - context_menu_sender: Option<IpcSender<ContextMenuResult>>, + + /// List of top-level browsing contexts. + /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, + /// and we exit if it ever becomes empty. + webviews: HashMap<WebViewId, WebView>, + + /// The order in which the webviews were created. + creation_order: Vec<WebViewId>, + + /// The webview that is currently focused. + /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. + focused_webview_id: Option<WebViewId>, } +#[derive(Debug)] +pub struct WebView {} + pub fn servo_version() -> String { format!( "Servo {}-{}", @@ -312,12 +318,13 @@ pub fn init( servo: servo.servo, batch_mode: false, callbacks: window_callbacks, - browser_id: None, - browsers: vec![], events: vec![], context_menu_sender: None, + webviews: HashMap::default(), + creation_order: vec![], + focused_webview_id: None, }; - let _ = servo_glue.process_event(EmbedderEvent::NewBrowser(url, servo.browser_id)); + let _ = servo_glue.process_event(EmbedderEvent::NewWebView(url, servo.browser_id)); *s.borrow_mut() = Some(servo_glue); }); @@ -330,11 +337,11 @@ pub fn deinit() { impl ServoGlue { fn get_browser_id(&self) -> Result<BrowserId, &'static str> { - let browser_id = match self.browser_id { + let webview_id = match self.focused_webview_id { Some(id) => id, - None => return Err("No BrowserId set yet."), + None => return Err("No focused WebViewId yet."), }; - Ok(browser_id) + Ok(webview_id) } /// Request shutdown. Will call on_shutdown_complete. @@ -604,7 +611,7 @@ impl ServoGlue { pub fn change_visibility(&mut self, visible: bool) -> Result<(), &'static str> { info!("change_visibility"); if let Ok(id) = self.get_browser_id() { - let event = EmbedderEvent::ChangeBrowserVisibility(id, visible); + let event = EmbedderEvent::WebViewVisibilityChanged(id, visible); self.process_event(event) } else { // Ignore visibility change if no browser has been created yet. @@ -714,21 +721,35 @@ impl ServoGlue { .push(EmbedderEvent::SendError(browser_id, reason)); } }, - EmbedderMsg::AllowOpeningBrowser(response_chan) => { + EmbedderMsg::AllowOpeningWebView(response_chan) => { // Note: would be a place to handle pop-ups config. // see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name if let Err(e) = response_chan.send(true) { warn!("Failed to send AllowOpeningBrowser response: {}", e); }; }, - EmbedderMsg::BrowserCreated(new_browser_id) => { - // TODO: properly handle a new "tab" - self.browsers.push(new_browser_id); - if self.browser_id.is_none() { - self.browser_id = Some(new_browser_id); - } + EmbedderMsg::WebViewOpened(new_webview_id) => { + self.webviews.insert(new_webview_id, WebView {}); + self.creation_order.push(new_webview_id); self.events - .push(EmbedderEvent::SelectBrowser(new_browser_id)); + .push(EmbedderEvent::FocusWebView(new_webview_id)); + }, + EmbedderMsg::WebViewClosed(webview_id) => { + self.webviews.retain(|&id, _| id != webview_id); + self.creation_order.retain(|&id| id != webview_id); + self.focused_webview_id = None; + if let Some(&newest_webview_id) = self.creation_order.last() { + self.events + .push(EmbedderEvent::FocusWebView(newest_webview_id)); + } else { + self.events.push(EmbedderEvent::Quit); + } + }, + EmbedderMsg::WebViewFocused(webview_id) => { + self.focused_webview_id = Some(webview_id); + }, + EmbedderMsg::WebViewBlurred => { + self.focused_webview_id = None; }, EmbedderMsg::GetClipboardContents(sender) => { let contents = self.callbacks.host_callbacks.get_clipboard_contents(); @@ -737,17 +758,6 @@ impl ServoGlue { EmbedderMsg::SetClipboardContents(text) => { self.callbacks.host_callbacks.set_clipboard_contents(text); }, - EmbedderMsg::CloseBrowser => { - // TODO: close the appropriate "tab". - let _ = self.browsers.pop(); - if let Some(prev_browser_id) = self.browsers.last() { - self.browser_id = Some(*prev_browser_id); - self.events - .push(EmbedderEvent::SelectBrowser(*prev_browser_id)); - } else { - self.events.push(EmbedderEvent::Quit); - } - }, EmbedderMsg::Shutdown => { self.callbacks.host_callbacks.on_shutdown_complete(); }, diff --git a/ports/servoshell/app.rs b/ports/servoshell/app.rs index b7e8b86b5b2..59b7709769f 100644 --- a/ports/servoshell/app.rs +++ b/ports/servoshell/app.rs @@ -23,17 +23,17 @@ use winit::event::WindowEvent; use winit::event_loop::EventLoopWindowTarget; use winit::window::WindowId; -use crate::browser::Browser; use crate::embedder::EmbedderCallbacks; use crate::events_loop::{EventsLoop, WakerEvent}; use crate::minibrowser::Minibrowser; use crate::parser::get_default_url; +use crate::webview::WebViewManager; use crate::window_trait::WindowPortsMethods; use crate::{headed_window, headless_window}; pub struct App { servo: Option<Servo<dyn WindowPortsMethods>>, - browser: RefCell<Browser<dyn WindowPortsMethods>>, + webviews: RefCell<WebViewManager<dyn WindowPortsMethods>>, event_queue: RefCell<Vec<EmbedderEvent>>, suspended: Cell<bool>, windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>, @@ -81,7 +81,7 @@ impl App { }; // Handle browser state. - let browser = Browser::new(window.clone()); + let webviews = WebViewManager::new(window.clone()); let initial_url = get_default_url( url.as_ref().map(String::as_str), env::current_dir().unwrap(), @@ -90,7 +90,7 @@ impl App { let mut app = App { event_queue: RefCell::new(vec![]), - browser: RefCell::new(browser), + webviews: RefCell::new(webviews), servo: None, suspended: Cell::new(false), windows: HashMap::new(), @@ -200,7 +200,7 @@ impl App { ); let mut servo = servo_data.servo; - servo.handle_events(vec![EmbedderEvent::NewBrowser( + servo.handle_events(vec![EmbedderEvent::NewWebView( initial_url.to_owned(), servo_data.browser_id, )]); @@ -297,9 +297,9 @@ impl App { // Consume and handle any events from the Minibrowser. if let Some(minibrowser) = app.minibrowser() { - let browser = &mut app.browser.borrow_mut(); + let webviews = &mut app.webviews.borrow_mut(); let app_event_queue = &mut app.event_queue.borrow_mut(); - minibrowser.queue_embedder_events_for_minibrowser_events(browser, app_event_queue); + minibrowser.queue_embedder_events_for_minibrowser_events(webviews, app_event_queue); } match app.handle_events() { @@ -316,8 +316,8 @@ impl App { } => { if history_changed { if let Some(mut minibrowser) = app.minibrowser() { - let browser = &mut app.browser.borrow_mut(); - if minibrowser.update_location_in_toolbar(browser) { + let webviews = &mut app.webviews.borrow_mut(); + if minibrowser.update_location_in_toolbar(webviews) { // Update the minibrowser immediately. While we could update by requesting a // redraw, doing so would delay the location update by two frames. minibrowser.update( @@ -422,18 +422,11 @@ impl App { /// towards Servo and embedder messages flow away from Servo, and also runs the compositor. /// /// As the embedder, we push embedder events through our event queues, from the App queue and - /// Window queues to the Browser queue, and from the Browser queue to Servo. We receive and - /// collect embedder messages from the various Servo components, then take them out of the - /// Servo interface so that the Browser can handle them. + /// Window queues to the WebViewManager queue, and from the WebViewManager queue to Servo. We + /// receive and collect embedder messages from the various Servo components, then take them out + /// of the Servo interface so that the WebViewManager can handle them. fn handle_events(&mut self) -> PumpResult { - let mut browser = self.browser.borrow_mut(); - - // FIXME: - // As of now, we support only one browser (self.browser) - // but have multiple windows (dom.webxr.glwindow). We forward - // the events of all the windows combined to that single - // browser instance. Pressing the "a" key on the glwindow - // will send a key event to the servo window. + let mut webviews = self.webviews.borrow_mut(); // Take any outstanding embedder events from the App and its Windows. let mut embedder_events = self.get_events(); @@ -441,8 +434,8 @@ impl App { embedder_events.extend(window.get_events()); } - // Catch some keyboard events, and push the rest onto the Browser event queue. - browser.handle_window_events(embedder_events); + // Catch some keyboard events, and push the rest onto the WebViewManager event queue. + webviews.handle_window_events(embedder_events); // Take any new embedder messages from Servo itself. let mut embedder_messages = self.servo.as_mut().unwrap().get_events(); @@ -451,19 +444,19 @@ impl App { let mut history_changed = false; loop { // Consume and handle those embedder messages. - let servo_event_response = browser.handle_servo_events(embedder_messages); + let servo_event_response = webviews.handle_servo_events(embedder_messages); need_present |= servo_event_response.need_present; history_changed |= servo_event_response.history_changed; - // Route embedder events from the Browser to the relevant Servo components, + // Route embedder events from the WebViewManager to the relevant Servo components, // receives and collects embedder messages from various Servo components, // and runs the compositor. need_resize |= self .servo .as_mut() .unwrap() - .handle_events(browser.get_events()); - if browser.shutdown_requested() { + .handle_events(webviews.get_events()); + if webviews.shutdown_requested() { return PumpResult::Shutdown; } diff --git a/ports/servoshell/main.rs b/ports/servoshell/main.rs index fd56fcdc61b..a215089bfea 100644 --- a/ports/servoshell/main.rs +++ b/ports/servoshell/main.rs @@ -31,7 +31,6 @@ cfg_if::cfg_if! { mod app; mod backtrace; - mod browser; mod crash_handler; mod egui_glue; mod embedder; @@ -45,6 +44,7 @@ cfg_if::cfg_if! { mod parser; mod prefs; mod resources; + mod webview; mod window_trait; pub mod platform { diff --git a/ports/servoshell/minibrowser.rs b/ports/servoshell/minibrowser.rs index 42659ec1964..b7bbabafdb5 100644 --- a/ports/servoshell/minibrowser.rs +++ b/ports/servoshell/minibrowser.rs @@ -20,11 +20,11 @@ use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_url::ServoUrl; use servo::webrender_surfman::WebrenderSurfman; -use crate::browser::Browser; use crate::egui_glue::EguiGlow; use crate::events_loop::EventsLoop; use crate::geometry::winit_position_to_euclid_point; use crate::parser::location_bar_input_to_url; +use crate::webview::WebViewManager; use crate::window_trait::WindowPortsMethods; pub struct Minibrowser { @@ -260,13 +260,13 @@ impl Minibrowser { /// routing those to the App event queue. pub fn queue_embedder_events_for_minibrowser_events( &self, - browser: &Browser<dyn WindowPortsMethods>, + browser: &WebViewManager<dyn WindowPortsMethods>, app_event_queue: &mut Vec<EmbedderEvent>, ) { for event in self.event_queue.borrow_mut().drain(..) { match event { MinibrowserEvent::Go => { - let browser_id = browser.browser_id().unwrap(); + let browser_id = browser.webview_id().unwrap(); let location = self.location.borrow(); if let Some(url) = location_bar_input_to_url(&location.clone()) { app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url)); @@ -276,14 +276,14 @@ impl Minibrowser { } }, MinibrowserEvent::Back => { - let browser_id = browser.browser_id().unwrap(); + let browser_id = browser.webview_id().unwrap(); app_event_queue.push(EmbedderEvent::Navigation( browser_id, TraversalDirection::Back(1), )); }, MinibrowserEvent::Forward => { - let browser_id = browser.browser_id().unwrap(); + let browser_id = browser.webview_id().unwrap(); app_event_queue.push(EmbedderEvent::Navigation( browser_id, TraversalDirection::Forward(1), @@ -293,11 +293,11 @@ impl Minibrowser { } } - /// Updates the location field from the given [Browser], unless the user has started editing it - /// without clicking Go, returning true iff the location has changed (needing an egui update). + /// Updates the location field from the given [BrowserManager], unless the user has started + /// editing it without clicking Go, returning true iff it has changed (needing an egui update). pub fn update_location_in_toolbar( &mut self, - browser: &mut Browser<dyn WindowPortsMethods>, + browser: &mut WebViewManager<dyn WindowPortsMethods>, ) -> bool { // User edited without clicking Go? if self.location_dirty.get() { diff --git a/ports/servoshell/browser.rs b/ports/servoshell/webview.rs index 92e31f18c4e..cf0400faff7 100644 --- a/ports/servoshell/browser.rs +++ b/ports/servoshell/webview.rs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::collections::HashMap; use std::fs::File; use std::io::Write; use std::rc::Rc; @@ -17,7 +18,7 @@ use servo::embedder_traits::{ CompositorEventVariant, ContextMenuResult, EmbedderMsg, FilterPattern, PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin, PromptResult, }; -use servo::msg::constellation_msg::{TopLevelBrowsingContextId as BrowserId, TraversalDirection}; +use servo::msg::constellation_msg::{TopLevelBrowsingContextId as WebViewId, TraversalDirection}; use servo::script_traits::TouchEventType; use servo::servo_config::opts; use servo::servo_url::ServoUrl; @@ -28,19 +29,21 @@ use crate::keyutils::{CMD_OR_ALT, CMD_OR_CONTROL}; use crate::parser::location_bar_input_to_url; use crate::window_trait::{WindowPortsMethods, LINE_HEIGHT}; -pub struct Browser<Window: WindowPortsMethods + ?Sized> { +pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> { current_url: Option<ServoUrl>, current_url_string: Option<String>, - /// id of the top level browsing context. It is unique as tabs - /// are not supported yet. None until created. - browser_id: Option<BrowserId>, + /// List of top-level browsing contexts. + /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, + /// and we exit if it ever becomes empty. + webviews: HashMap<WebViewId, WebView>, - // A rudimentary stack of "tabs". - // EmbedderMsg::BrowserCreated will push onto it. - // EmbedderMsg::CloseBrowser will pop from it, - // and exit if it is empty afterwards. - browsers: Vec<BrowserId>, + /// The order in which the webviews were created. + creation_order: Vec<WebViewId>, + + /// The webview that is currently focused. + /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. + focused_webview_id: Option<WebViewId>, title: Option<String>, @@ -50,22 +53,26 @@ pub struct Browser<Window: WindowPortsMethods + ?Sized> { shutdown_requested: bool, } +#[derive(Debug)] +pub struct WebView {} + pub struct ServoEventResponse { pub need_present: bool, pub history_changed: bool, } -impl<Window> Browser<Window> +impl<Window> WebViewManager<Window> where Window: WindowPortsMethods + ?Sized, { - pub fn new(window: Rc<Window>) -> Browser<Window> { - Browser { + pub fn new(window: Rc<Window>) -> WebViewManager<Window> { + WebViewManager { title: None, current_url: None, current_url_string: None, - browser_id: None, - browsers: Vec::new(), + webviews: HashMap::default(), + creation_order: vec![], + focused_webview_id: None, window, clipboard: match Clipboard::new() { Ok(c) => Some(c), @@ -79,8 +86,8 @@ where } } - pub fn browser_id(&self) -> Option<BrowserId> { - self.browser_id + pub fn webview_id(&self) -> Option<WebViewId> { + self.focused_webview_id } pub fn current_url_string(&self) -> Option<&str> { @@ -113,7 +120,7 @@ where fn handle_key_from_window(&mut self, key_event: KeyboardEvent) { ShortcutMatcher::from_event(key_event.clone()) .shortcut(CMD_OR_CONTROL, 'R', || { - if let Some(id) = self.browser_id { + if let Some(id) = self.focused_webview_id { self.event_queue.push(EmbedderEvent::Reload(id)); } }) @@ -128,7 +135,7 @@ where let input = tinyfiledialogs::input_box(title, title, &tiny_dialog_escape(&url)); if let Some(input) = input { if let Some(url) = location_bar_input_to_url(&input) { - if let Some(id) = self.browser_id { + if let Some(id) = self.focused_webview_id { self.event_queue.push(EmbedderEvent::LoadUrl(id, url)); } } @@ -171,13 +178,13 @@ where )); }) .shortcut(CMD_OR_ALT, Key::ArrowRight, || { - if let Some(id) = self.browser_id { + if let Some(id) = self.focused_webview_id { let event = EmbedderEvent::Navigation(id, TraversalDirection::Forward(1)); self.event_queue.push(event); } }) .shortcut(CMD_OR_ALT, Key::ArrowLeft, || { - if let Some(id) = self.browser_id { + if let Some(id) = self.focused_webview_id { let event = EmbedderEvent::Navigation(id, TraversalDirection::Back(1)); self.event_queue.push(event); } @@ -185,7 +192,7 @@ where .shortcut(Modifiers::empty(), Key::Escape, || { let state = self.window.get_fullscreen(); if state { - if let Some(id) = self.browser_id { + if let Some(id) = self.focused_webview_id { let event = EmbedderEvent::ExitFullScreen(id); self.event_queue.push(event); } @@ -198,7 +205,7 @@ where #[cfg(not(target_os = "win"))] fn platform_handle_key(&mut self, key_event: KeyboardEvent) { - if let Some(id) = self.browser_id { + if let Some(id) = self.focused_webview_id { if let Some(event) = ShortcutMatcher::from_event(key_event.clone()) .shortcut(CMD_OR_CONTROL, '[', || { EmbedderEvent::Navigation(id, TraversalDirection::Back(1)) @@ -217,7 +224,7 @@ where fn platform_handle_key(&mut self, _key_event: KeyboardEvent) {} /// Handle key events after they have been handled by Servo. - fn handle_key_from_servo(&mut self, _: Option<BrowserId>, event: KeyboardEvent) { + fn handle_key_from_servo(&mut self, _: Option<WebViewId>, event: KeyboardEvent) { ShortcutMatcher::from_event(event) .shortcut(CMD_OR_CONTROL, '=', || { self.event_queue.push(EmbedderEvent::Zoom(1.1)) @@ -285,14 +292,14 @@ where /// Returns true if the caller needs to manually present a new frame. pub fn handle_servo_events( &mut self, - events: Vec<(Option<BrowserId>, EmbedderMsg)>, + events: Vec<(Option<WebViewId>, EmbedderMsg)>, ) -> ServoEventResponse { let mut need_present = false; let mut history_changed = false; - for (browser_id, msg) in events { + for (webview_id, msg) in events { trace!( "embedder <- servo EmbedderMsg ({:?}, {:?})", - browser_id.map(|x| format!("{}", x)), + webview_id.map(|x| format!("{}", x)), msg ); match msg { @@ -395,7 +402,7 @@ where if let Err(e) = res { let reason = format!("Failed to send Prompt response: {}", e); self.event_queue - .push(EmbedderEvent::SendError(browser_id, reason)); + .push(EmbedderEvent::SendError(webview_id, reason)); } }, EmbedderMsg::AllowUnload(sender) => { @@ -403,35 +410,47 @@ where if let Err(e) = sender.send(true) { let reason = format!("Failed to send AllowUnload response: {}", e); self.event_queue - .push(EmbedderEvent::SendError(browser_id, reason)); + .push(EmbedderEvent::SendError(webview_id, reason)); } }, EmbedderMsg::AllowNavigationRequest(pipeline_id, _url) => { - if let Some(_browser_id) = browser_id { + if let Some(_webview_id) = webview_id { self.event_queue .push(EmbedderEvent::AllowNavigationResponse(pipeline_id, true)); } }, - EmbedderMsg::AllowOpeningBrowser(response_chan) => { + EmbedderMsg::AllowOpeningWebView(response_chan) => { // Note: would be a place to handle pop-ups config. // see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name if let Err(e) = response_chan.send(true) { - warn!("Failed to send AllowOpeningBrowser response: {}", e); + warn!("Failed to send AllowOpeningWebView response: {}", e); }; }, - EmbedderMsg::BrowserCreated(new_browser_id) => { - // TODO: properly handle a new "tab" - self.browsers.push(new_browser_id); - if self.browser_id.is_none() { - self.browser_id = Some(new_browser_id); + EmbedderMsg::WebViewOpened(new_webview_id) => { + self.webviews.insert(new_webview_id, WebView {}); + self.creation_order.push(new_webview_id); + self.event_queue + .push(EmbedderEvent::FocusWebView(new_webview_id)); + }, + EmbedderMsg::WebViewClosed(webview_id) => { + self.webviews.retain(|&id, _| id != webview_id); + self.creation_order.retain(|&id| id != webview_id); + self.focused_webview_id = None; + if let Some(&newest_webview_id) = self.creation_order.last() { + self.event_queue + .push(EmbedderEvent::FocusWebView(newest_webview_id)); } else { - error!("Multiple top level browsing contexts not supported yet."); + self.event_queue.push(EmbedderEvent::Quit); } - self.event_queue - .push(EmbedderEvent::SelectBrowser(new_browser_id)); + }, + EmbedderMsg::WebViewFocused(webview_id) => { + self.focused_webview_id = Some(webview_id); + }, + EmbedderMsg::WebViewBlurred => { + self.focused_webview_id = None; }, EmbedderMsg::Keyboard(key_event) => { - self.handle_key_from_servo(browser_id, key_event); + self.handle_key_from_servo(webview_id, key_event); }, EmbedderMsg::GetClipboardContents(sender) => { let contents = self @@ -476,17 +495,6 @@ where EmbedderMsg::LoadComplete => { // FIXME: surface the loading state in the UI somehow }, - EmbedderMsg::CloseBrowser => { - // TODO: close the appropriate "tab". - let _ = self.browsers.pop(); - if let Some(prev_browser_id) = self.browsers.last() { - self.browser_id = Some(*prev_browser_id); - self.event_queue - .push(EmbedderEvent::SelectBrowser(*prev_browser_id)); - } else { - self.event_queue.push(EmbedderEvent::Quit); - } - }, EmbedderMsg::Shutdown => { self.shutdown_requested = true; }, @@ -545,10 +553,10 @@ where EmbedderMsg::ReadyToPresent => { need_present = true; }, - EmbedderMsg::EventDelivered(event) => match (browser_id, event) { - (Some(browser_id), CompositorEventVariant::MouseButtonEvent) => { - // TODO Focus browser and/or raise to top if needed. - trace!("{}: Got a mouse button event", browser_id); + EmbedderMsg::EventDelivered(event) => match (webview_id, event) { + (Some(webview_id), CompositorEventVariant::MouseButtonEvent) => { + // TODO Focus webview and/or raise to top if needed. + trace!("{}: Got a mouse button event", webview_id); }, (_, _) => {}, }, diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index 13dd21e9405..2a2080b9f42 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -138,6 +138,7 @@ class MachCommands(CommandBase): "servo_config", "servo_remutex", "crown", + "constellation", ] if not packages: packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store']) |