diff --git a/resources/components/panel.html b/resources/components/panel.html index 14377ebe..9cb1596c 100644 --- a/resources/components/panel.html +++ b/resources/components/panel.html @@ -4,10 +4,8 @@ - - + +
diff --git a/src/compositor.rs b/src/compositor.rs index e2498f85..7e029a19 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -1228,14 +1228,16 @@ impl IOCompositor { } let rect = DeviceIntRect::from_size(size); - let content_size = window.get_content_size(rect); - if let Some(w) = &mut window.webview { - w.set_size(content_size); - self.on_resize_webview_event(w.webview_id, w.rect); - } - if let Some(prompt) = &mut window.prompt { - prompt.resize(content_size); - self.on_resize_webview_event(prompt.webview().webview_id, rect); + let show_tab_bar = window.tab_manager.count() > 1; + let content_size = window.get_content_size(rect, show_tab_bar); + if let Some(tab_id) = window.tab_manager.current_tab_id() { + let (tab_id, prompt_id) = window.tab_manager.set_size(tab_id, content_size); + if let Some(tab_id) = tab_id { + self.on_resize_webview_event(tab_id, content_size); + } + if let Some(prompt_id) = prompt_id { + self.on_resize_webview_event(prompt_id, content_size); + } } self.send_root_pipeline_display_list(window); diff --git a/src/lib.rs b/src/lib.rs index 73c34cb0..aab96b83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,3 +29,5 @@ pub use errors::{Error, Result}; pub use verso::Verso; /// Re-exporting Winit for the sake of convenience. pub use winit; +/// Window tabs manager +pub mod tab; diff --git a/src/tab.rs b/src/tab.rs new file mode 100644 index 00000000..0e4cf1c8 --- /dev/null +++ b/src/tab.rs @@ -0,0 +1,295 @@ +use std::collections::HashMap; + +use crate::webview::{prompt::PromptDialog, WebView}; +use base::id::WebViewId; +use serde::{Deserialize, Serialize}; +use servo_url::ServoUrl; +use webrender_api::units::DeviceIntRect; + +/// Tab state +pub struct Tab { + /// Tab WebView id + id: WebViewId, + /// Tab WebView + webview: WebView, + /// History + history: TabHistory, + /// Prompt + prompt: Option, +} + +impl Tab { + /// Create a new tab state. + pub fn new(webview: WebView) -> Self { + Self { + id: webview.webview_id, + webview, + history: TabHistory { + list: Vec::new(), + current_idx: 0, + }, + prompt: None, + } + } + + /// Get tab WebView id. + pub fn id(&self) -> WebViewId { + self.id + } + + /// Get tab WebView. + pub fn webview(&self) -> &WebView { + &self.webview + } + + /// Set tab WebView size. + pub fn set_webview_size(&mut self, rect: DeviceIntRect) { + self.webview.set_size(rect); + } + + /// Get tab history. + pub fn history(&self) -> &TabHistory { + &self.history + } + + /// Set tab history. + pub fn set_history(&mut self, list: Vec, current_idx: usize) { + self.history = TabHistory { list, current_idx }; + } + + /// Get tab prompt dialog. + pub fn prompt(&self) -> Option<&PromptDialog> { + self.prompt.as_ref() + } + + /// Get tab prompt id. + pub fn prompt_id(&self) -> Option { + self.prompt.as_ref().map(|p| p.id()) + } + + /// Set tab prompt dialog. + pub fn set_prompt(&mut self, prompt: PromptDialog) { + self.prompt = Some(prompt); + } + + /// Remove tab prompt dialog. + pub fn remove_prompt(&mut self) -> Option { + self.prompt.take() + } + + /// Check if there is a prompt dialog. + pub fn has_prompt(&self) -> bool { + self.prompt.is_some() + } + + /// Set prompt webview size. + pub fn set_prompt_size(&mut self, rect: DeviceIntRect) { + if let Some(prompt) = self.prompt.as_mut() { + prompt.set_size(rect); + } + } +} + +/// Tab manager to handle multiple tab in a window. +pub struct TabManager { + /// Current active tab id + active_tab_id: Option, + /// Tab webview id -> Tab webview + tab_map: HashMap, + /// Prompt webview id -> Parent tab webview id + prompt_tab_map: HashMap, +} + +impl TabManager { + /// Create a new tab manager. + pub fn new() -> Self { + Self { + active_tab_id: None, + tab_map: HashMap::new(), + prompt_tab_map: HashMap::new(), + } + } + /// Get tab count. + pub fn count(&self) -> usize { + self.tab_map.len() + } + /// Get current actvie tab id. + pub fn current_tab_id(&self) -> Option { + self.active_tab_id + } + /// Get current active tab. + pub fn current_tab(&self) -> Option<&Tab> { + if let Some(tab_id) = self.active_tab_id { + self.tab_map.get(&tab_id) + } else { + None + } + } + /// Get all tab id. + pub fn tab_ids(&self) -> Vec { + self.tab_map.keys().cloned().collect() + } + /// Activate the tab by tab id. + pub fn activate_tab(&mut self, tab_id: WebViewId) -> Option<&Tab> { + if let Some(tab) = self.tab_map.get(&tab_id) { + self.active_tab_id = Some(tab_id); + Some(tab) + } else { + self.active_tab_id = None; + None + } + } + /// Get tab by tab id. + pub fn tab(&self, id: WebViewId) -> Option<&Tab> { + self.tab_map.get(&id) + } + /// Append a tab. + pub fn append_tab(&mut self, webview: WebView, active: bool) { + let id = webview.webview_id; + let tab = Tab::new(webview); + self.tab_map.insert(id, tab); + if active { + self.active_tab_id = Some(id); + } + } + /// Close a tab. + pub fn close_tab(&mut self, id: WebViewId) -> Result { + match self.tab_map.remove(&id) { + Some(tab) => Ok(tab), + None => Err(TabManagerErr::WebViewIdNotFound), + } + } + /// Set tab size. Will also set prompt dialog size if it exists. + /// - Returns the tab and prompt WebViewId if they exist. + pub fn set_size( + &mut self, + tab_id: WebViewId, + rect: DeviceIntRect, + ) -> (Option, Option) { + if let Some(tab) = self.tab_map.get_mut(&tab_id) { + tab.set_webview_size(rect); + + if let Some(prompt_id) = tab.prompt_id() { + tab.set_prompt_size(rect); + (Some(tab_id), Some(prompt_id)) + } else { + (Some(tab_id), None) + } + } else { + (None, None) + } + } + + /* History */ + + /// Get tab history. + pub fn history(&self, tab_id: WebViewId) -> Option<&TabHistory> { + self.tab_map.get(&tab_id).map(|tab| tab.history()) + } + /// Set tab history. + pub fn set_history(&mut self, tab_id: WebViewId, list: Vec, current_idx: usize) { + if let Some(tab) = self.tab_map.get_mut(&tab_id) { + tab.set_history(list, current_idx); + }; + } + + /* Prompt */ + + /// Get prompt dialog by tab id. + pub fn prompt_by_tab_id(&self, tab_id: WebViewId) -> Option<&PromptDialog> { + self.tab_map.get(&tab_id).and_then(|tab| tab.prompt()) + } + /// Get prompt dialog by tab id. + pub fn prompt_by_prompt_id(&self, prompt_id: WebViewId) -> Option<&PromptDialog> { + if let Some(tab_id) = self.prompt_tab_map.get(&prompt_id) { + self.prompt_by_tab_id(*tab_id) + } else { + None + } + } + /// Get current tabw prompt dialog. + pub fn current_prompt(&self) -> Option<&PromptDialog> { + if let Some(tab_id) = self.active_tab_id { + self.prompt_by_tab_id(tab_id) + } else { + None + } + } + /// Set tab prompt dialog. + pub fn set_prompt(&mut self, tab_id: WebViewId, prompt: PromptDialog) { + if let Some(tab) = self.tab_map.get_mut(&tab_id) { + self.prompt_tab_map.insert(prompt.id(), tab_id); + tab.set_prompt(prompt); + } + } + /// Remove prompt by tab webview ID. + pub fn remove_prompt_by_tab_id(&mut self, tab_id: WebViewId) -> Option { + if let Some(tab) = self.tab_map.get_mut(&tab_id) { + if let Some(prompt) = tab.remove_prompt() { + self.prompt_tab_map.remove(&prompt.id()); + return Some(prompt); + } + } + None + } + /// Remove prompt by prompt webview ID. + pub fn remove_prompt_by_prompt_id(&mut self, prompt_id: WebViewId) -> Option { + if let Some(tab_id) = self.prompt_tab_map.remove(&prompt_id) { + self.remove_prompt_by_tab_id(tab_id) + } else { + None + } + } + /// Check if there is a prompt dialog by prompt webview ID. + pub fn has_prompt(&self, prompt_id: WebViewId) -> bool { + self.prompt_tab_map.contains_key(&prompt_id) + } +} + +/// Tab history +pub struct TabHistory { + /// History list + pub list: Vec, + /// Current index + pub current_idx: usize, +} + +/// Tab manager errors. +pub enum TabManagerErr { + /// Index out of bounds. + IndexOutOfBounds, + /// WebView WebViewId not found. + WebViewIdNotFound, + /// Remove last WebView. + RemoveLastWebView, +} + +/// Response to UI that the tab was created. +#[derive(Debug, Clone, Serialize)] +pub struct TabCreateResponse { + /// Tab creation success + pub success: bool, + /// Tab WebView id + pub id: WebViewId, +} + +impl TabCreateResponse { + /// Create a new TabCreatedResult json string. + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +/// Activate the tab request from UI. +#[derive(Debug, Clone, Deserialize)] +pub struct TabActivateRequest { + /// Tab WebView id + pub id: WebViewId, +} + +/// Activate the tab request from UI. +#[derive(Debug, Clone, Deserialize)] +pub struct TabCloseRequest { + /// Tab WebView id + pub id: WebViewId, +} diff --git a/src/verso.rs b/src/verso.rs index 75e5cb95..50dea602 100644 --- a/src/verso.rs +++ b/src/verso.rs @@ -388,7 +388,7 @@ impl Verso { if with_panel { window.create_panel(&constellation_sender, initial_url); } else if let Some(initial_url) = initial_url { - window.create_webview(&constellation_sender, initial_url.into()); + window.create_tab(&constellation_sender, initial_url.into()); } let mut windows = HashMap::new(); @@ -587,9 +587,11 @@ impl Verso { pub fn handle_incoming_webview_message(&self, message: ControllerMessage) { match message { ControllerMessage::NavigateTo(to_url) => { - if let Some(webview_id) = self.windows.values().next().and_then(|(window, _)| { - window.webview.as_ref().map(|webview| webview.webview_id) - }) { + if let Some(webview_id) = + self.windows.values().next().and_then(|(window, _)| { + window.tab_manager.current_tab().map(|tab| tab.id()) + }) + { send_to_constellation( &self.constellation_sender, ConstellationMsg::LoadUrl(webview_id, ServoUrl::from_url(to_url)), diff --git a/src/webview/prompt.rs b/src/webview/prompt.rs index 80b150a0..60d76b99 100644 --- a/src/webview/prompt.rs +++ b/src/webview/prompt.rs @@ -79,6 +79,10 @@ impl PromptDialog { pub fn webview(&self) -> &WebView { &self.webview } + /// Get prompt webview ID + pub fn id(&self) -> WebViewId { + self.webview.webview_id + } /// Get prompt sender. Send user interaction result back to caller. pub fn sender(&self) -> Option { @@ -91,9 +95,9 @@ impl PromptDialog { /// ```rust /// let rect = window.webview.as_ref().unwrap().rect; /// let content_size = window.get_content_size(rect); - /// prompt.resize(content_size); + /// prompt.set_size(content_size); /// ``` - pub fn resize(&mut self, rect: DeviceIntRect) { + pub fn set_size(&mut self, rect: DeviceIntRect) { self.webview.set_size(rect); } diff --git a/src/webview/webview.rs b/src/webview/webview.rs index 37735ad4..2ed0ef8f 100644 --- a/src/webview/webview.rs +++ b/src/webview/webview.rs @@ -17,6 +17,7 @@ use webrender_api::units::DeviceIntRect; use crate::{ compositor::IOCompositor, + tab::{TabActivateRequest, TabCloseRequest, TabCreateResponse}, verso::send_to_constellation, webview::prompt::{PromptDialog, PromptInputResult, PromptSender}, window::Window, @@ -95,6 +96,31 @@ impl Window { self.window.request_redraw(); send_to_constellation(sender, ConstellationMsg::FocusWebView(webview_id)); } + EmbedderMsg::ChangePageTitle(title) => { + if let Some(panel) = self.panel.as_ref() { + let title = if let Some(title) = title { + format!("'{title}'") + } else { + "null".to_string() + }; + + let script = format!( + "window.navbar.setTabTitle('{}', {})", + serde_json::to_string(&webview_id).unwrap(), + title.as_str() + ); + + let (tx, rx) = ipc::channel::().unwrap(); + send_to_constellation( + sender, + ConstellationMsg::WebDriverCommand(WebDriverCommandMsg::ScriptCommand( + BrowsingContextId::from(panel.webview.webview_id), + WebDriverScriptCommand::ExecuteScript(script, tx), + )), + ); + let _ = rx.recv(); + } + } EmbedderMsg::AllowNavigationRequest(id, _url) => { // TODO should provide a API for users to check url send_to_constellation(sender, ConstellationMsg::AllowNavigationResponse(id, true)); @@ -129,8 +155,9 @@ impl Window { } } EmbedderMsg::HistoryChanged(list, index) => { - self.close_prompt_dialog(); - self.update_history(&list, index); + self.close_prompt_dialog(webview_id); + self.tab_manager + .set_history(webview_id, list.clone(), index); let url = list.get(index).unwrap(); if let Some(panel) = self.panel.as_ref() { let (tx, rx) = ipc::channel::().unwrap(); @@ -156,59 +183,64 @@ impl Window { // TODO: Implement context menu } EmbedderMsg::Prompt(prompt_type, _origin) => { - let mut prompt = PromptDialog::new(); - let rect = self.webview.as_ref().unwrap().rect; - - match prompt_type { - PromptDefinition::Alert(message, prompt_sender) => { - prompt.alert(sender, rect, message, prompt_sender); - } - PromptDefinition::OkCancel(message, prompt_sender) => { - prompt.ok_cancel(sender, rect, message, prompt_sender); - } - PromptDefinition::YesNo(message, prompt_sender) => { - prompt.yes_no( - sender, - rect, - message, - PromptSender::ConfirmSender(prompt_sender), - ); - } - PromptDefinition::Input(message, default_value, prompt_sender) => { - prompt.input(sender, rect, message, Some(default_value), prompt_sender); + if let Some(tab) = self.tab_manager.tab(webview_id) { + let mut prompt = PromptDialog::new(); + let rect = tab.webview().rect; + match prompt_type { + PromptDefinition::Alert(message, prompt_sender) => { + prompt.alert(sender, rect, message, prompt_sender); + } + PromptDefinition::OkCancel(message, prompt_sender) => { + prompt.ok_cancel(sender, rect, message, prompt_sender); + } + PromptDefinition::YesNo(message, prompt_sender) => { + prompt.yes_no( + sender, + rect, + message, + PromptSender::ConfirmSender(prompt_sender), + ); + } + PromptDefinition::Input(message, default_value, prompt_sender) => { + prompt.input(sender, rect, message, Some(default_value), prompt_sender); + } } - } - // save prompt in window to keep prompt_sender alive - // so that we can send the result back to the prompt after user clicked the button - self.prompt = Some(prompt); + // save prompt in window to keep prompt_sender alive + // so that we can send the result back to the prompt after user clicked the button + self.tab_manager.set_prompt(webview_id, prompt); + } else { + log::error!("Failed to get WebView {webview_id:?} in this window."); + } } EmbedderMsg::PromptPermission(prompt, prompt_sender) => { - let message = match prompt { - PermissionPrompt::Request(permission_name) => { - format!( - "This website would like to request permission for {:?}.", - permission_name - ) - } - PermissionPrompt::Insecure(permission_name) => { - format!( - "This website would like to request permission for {:?}. However current connection is not secure. Do you want to proceed?", - permission_name - ) - } - }; - - let mut prompt = PromptDialog::new(); - let rect = self.webview.as_ref().unwrap().rect; - prompt.yes_no( - sender, - rect, - message, - PromptSender::PermissionSender(prompt_sender), - ); + if let Some(tab) = self.tab_manager.tab(webview_id) { + let message = match prompt { + PermissionPrompt::Request(permission_name) => { + format!( + "This website would like to request permission for {:?}.", + permission_name + ) + } + PermissionPrompt::Insecure(permission_name) => { + format!( + "This website would like to request permission for {:?}. However current connection is not secure. Do you want to proceed?", + permission_name + ) + } + }; - self.prompt = Some(prompt); + let mut prompt = PromptDialog::new(); + prompt.yes_no( + sender, + tab.webview().rect, + message, + PromptSender::PermissionSender(prompt_sender), + ); + self.tab_manager.set_prompt(webview_id, prompt); + } else { + log::error!("Failed to get WebView {webview_id:?} in this window."); + } } e => { log::trace!("Verso WebView isn't supporting this message yet: {e:?}") @@ -223,7 +255,7 @@ impl Window { message: EmbedderMsg, sender: &Sender, clipboard: Option<&mut Clipboard>, - _compositor: &mut IOCompositor, + compositor: &mut IOCompositor, ) -> bool { log::trace!("Verso Panel {panel_id:?} is handling Embedder message: {message:?}",); match message { @@ -246,7 +278,7 @@ impl Window { self.window.request_redraw(); send_to_constellation(sender, ConstellationMsg::FocusWebView(panel_id)); - self.create_webview(sender, self.panel.as_ref().unwrap().initial_url.clone()); + self.create_tab(sender, self.panel.as_ref().unwrap().initial_url.clone()); } EmbedderMsg::AllowNavigationRequest(id, _url) => { // The panel shouldn't navigate to other pages. @@ -258,10 +290,86 @@ impl Window { EmbedderMsg::Prompt(definition, _origin) => { match definition { PromptDefinition::Input(msg, _, prompt_sender) => { + /* Tab */ + if msg.starts_with("CLOSE_TAB:") { + let request_str = msg.strip_prefix("CLOSE_TAB:").unwrap(); + let request: TabCloseRequest = serde_json::from_str(request_str) + .expect("Failed to parse TabCloseRequest"); + + // close the tab + if let Some(_) = self.tab_manager.tab(request.id) { + send_to_constellation( + sender, + ConstellationMsg::CloseWebView(request.id), + ); + } + + let _ = prompt_sender.send(None); + return false; + } else if msg.starts_with("ACTIVATE_TAB:") { + let request_str = msg.strip_prefix("ACTIVATE_TAB:").unwrap(); + let request: TabActivateRequest = serde_json::from_str(request_str) + .expect("Failed to parse TabActivateRequest"); + + let tab_id = request.id; + + // FIXME: set dirty flag, and only resize when flag is set + self.activate_tab(compositor, tab_id, self.tab_manager.count() > 1); + + let _ = prompt_sender.send(None); + return false; + } else if msg == "NEW_TAB" { + let webview_id = WebViewId::new(); + let size = self.size(); + let rect = DeviceIntRect::from_size(size); + let content_size = self.get_content_size(rect, true); + let mut webview = WebView::new(webview_id, rect); + webview.set_size(content_size); + + self.tab_manager.append_tab(webview, true); + + send_to_constellation( + sender, + ConstellationMsg::NewWebView( + ServoUrl::parse("https://example.com").unwrap(), + webview_id, + ), + ); + let result = TabCreateResponse { + success: true, + id: webview_id, + }; + let _ = prompt_sender.send(Some(result.to_json())); + return false; + } + let _ = prompt_sender.send(None); - if let Some(webview) = &self.webview { - let id = webview.webview_id; + /* Window */ + match msg.as_str() { + "NEW_WINDOW" => { + let _ = prompt_sender.send(None); + return true; + } + "MINIMIZE" => { + self.window.set_minimized(true); + return false; + } + "MAXIMIZE" | "DBCLICK_PANEL" => { + let is_maximized = self.window.is_maximized(); + self.window.set_maximized(!is_maximized); + return false; + } + "DRAG_WINDOW" => { + let _ = self.window.drag_window(); + return false; + } + _ => {} + } + + /* Main WebView */ + if let Some(tab) = self.tab_manager.current_tab() { + let id = tab.id(); if msg.starts_with("NAVIGATE_TO:") { let unparsed_url = msg.strip_prefix("NAVIGATE_TO:").unwrap(); let url = match Url::parse(unparsed_url) { @@ -305,19 +413,6 @@ impl Window { "REFRESH" => { send_to_constellation(sender, ConstellationMsg::Reload(id)); } - "NEW_WINDOW" => { - return true; - } - "MINIMIZE" => { - self.window.set_minimized(true); - } - "MAXIMIZE" | "DBCLICK_PANEL" => { - let is_maximized = self.window.is_maximized(); - self.window.set_maximized(!is_maximized); - } - "DRAG_WINDOW" => { - let _ = self.window.drag_window(); - } e => log::trace!( "Verso Panel isn't supporting this prompt message yet: {e}" ), @@ -405,7 +500,12 @@ impl Window { match message { EmbedderMsg::Prompt(prompt, _origin) => match prompt { PromptDefinition::Alert(msg, ignored_prompt_sender) => { - let prompt = self.prompt.as_ref().unwrap(); + let prompt = self.tab_manager.prompt_by_prompt_id(webview_id); + if prompt.is_none() { + log::error!("Prompt not found for WebView {webview_id:?}"); + return false; + } + let prompt = prompt.unwrap(); let prompt_sender = prompt.sender().unwrap(); match prompt_sender { diff --git a/src/window.rs b/src/window.rs index 0ae1fdeb..24a5245b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -10,11 +10,16 @@ use glutin::{ surface::{Surface, WindowSurface}, }; use glutin_winit::DisplayBuilder; +use ipc_channel::ipc; +use keyboard_types::{Code, KeyState, KeyboardEvent, Modifiers}; #[cfg(any(target_os = "macos", target_os = "windows"))] use muda::{Menu as MudaMenu, MenuEvent, MenuEventReceiver, MenuItem}; #[cfg(any(target_os = "macos", target_os = "windows"))] use raw_window_handle::HasWindowHandle; -use script_traits::TraversalDirection; +use script_traits::{ + webdriver_msg::{WebDriverJSResult, WebDriverJSValue, WebDriverScriptCommand}, + TraversalDirection, WebDriverCommandMsg, +}; use script_traits::{TouchEventType, WheelDelta, WheelMode}; use servo_url::ServoUrl; use webrender_api::{ @@ -35,16 +40,21 @@ use crate::{ compositor::{IOCompositor, MouseWindowEvent}, keyboard::keyboard_event_from_winit, rendering::{gl_config_picker, RenderingContext}, + tab::TabManager, verso::send_to_constellation, webview::{ context_menu::{ContextMenu, Menu}, - prompt::{PromptDialog, PromptSender}, + prompt::PromptSender, Panel, WebView, }, }; use arboard::Clipboard; +const PANEL_HEIGHT: f64 = 50.0; +const TAB_HEIGHT: f64 = 30.0; +const PANEL_PADDING: f64 = 4.0; + /// A Verso window is a Winit window containing several web views. pub struct Window { /// Access to Winit window @@ -54,15 +64,11 @@ pub struct Window { /// The main panel of this window. pub(crate) panel: Option, /// The WebView of this window. - pub(crate) webview: Option, + // pub(crate) webview: Option, /// The mouse physical position in the web view. mouse_position: Cell>>, /// Modifiers state of the keyboard. modifiers_state: Cell, - /// Browser history of the window. - history: Vec, - /// Current history index. - current_history_index: usize, /// State to indicate if the window is resizing. pub(crate) resizing: bool, // TODO: These two fields should unified once we figure out servo's menu events. @@ -72,9 +78,8 @@ pub struct Window { /// Global menu event receiver for muda crate #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEventReceiver, - - /// Current Prompt - pub(crate) prompt: Option, + /// Window tabs manager + pub(crate) tab_manager: TabManager, } impl Window { @@ -119,17 +124,14 @@ impl Window { window, surface, panel: None, - webview: None, mouse_position: Default::default(), modifiers_state: Cell::new(ModifiersState::default()), - history: vec![], - current_history_index: 0, resizing: false, #[cfg(linux)] context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), - prompt: None, + tab_manager: TabManager::new(), }, rendering_context, ) @@ -163,26 +165,29 @@ impl Window { window, surface, panel: None, - webview: None, + // webview: None, mouse_position: Default::default(), modifiers_state: Cell::new(ModifiersState::default()), - history: vec![], - current_history_index: 0, resizing: false, #[cfg(linux)] context_menu: None, #[cfg(any(target_os = "macos", target_os = "windows"))] menu_event_receiver: MenuEvent::receiver().clone(), - prompt: None, + tab_manager: TabManager::new(), }; compositor.swap_current_window(&mut window); window } /// Get the content area size for the webview to draw on - pub fn get_content_size(&self, mut size: DeviceIntRect) -> DeviceIntRect { + pub fn get_content_size(&self, mut size: DeviceIntRect, include_tab: bool) -> DeviceIntRect { if self.panel.is_some() { - size.min.y = size.max.y.min(100); + let height: f64 = if include_tab { + (PANEL_HEIGHT + TAB_HEIGHT + PANEL_PADDING) * self.scale_factor() + } else { + (PANEL_HEIGHT + PANEL_PADDING) * self.scale_factor() + }; + size.min.y = size.max.y.min(height as i32); size.min.x += 10; size.max.y -= 10; size.max.x -= 10; @@ -216,7 +221,7 @@ impl Window { } /// Create a new webview and send the constellation message to load the initial URL - pub fn create_webview( + pub fn create_tab( &mut self, constellation_sender: &Sender, initial_url: ServoUrl, @@ -224,9 +229,30 @@ impl Window { let webview_id = WebViewId::new(); let size = self.size(); let rect = DeviceIntRect::from_size(size); + + let show_tab = self.tab_manager.count() >= 1; + let content_size = self.get_content_size(rect, show_tab); + let mut webview = WebView::new(webview_id, rect); - webview.set_size(self.get_content_size(rect)); - self.webview.replace(webview); + webview.set_size(content_size); + + let (tx, rx) = ipc::channel::().unwrap(); + let cmd: String = format!( + "window.navbar.addTab('{}', {})", + serde_json::to_string(&webview.webview_id).unwrap(), + true, + ); + send_to_constellation( + constellation_sender, + ConstellationMsg::WebDriverCommand(WebDriverCommandMsg::ScriptCommand( + self.panel.as_ref().unwrap().webview.webview_id.into(), + WebDriverScriptCommand::ExecuteScript(cmd, tx), + )), + ); + let _ = rx.recv(); + + self.tab_manager.append_tab(webview, true); + send_to_constellation( constellation_sender, ConstellationMsg::NewWebView(initial_url, webview_id), @@ -234,6 +260,71 @@ impl Window { log::debug!("Verso Window {:?} adds webview {}", self.id(), webview_id); } + /// Close a tab + pub fn close_tab(&mut self, compositor: &mut IOCompositor, tab_id: WebViewId) { + let sender = compositor.constellation_chan.clone(); + // if there are more than 2 tabs, we need to ask for the new active tab after tab is closed + if self.tab_manager.count() > 1 { + let (tx, rx) = ipc::channel::().unwrap(); + let cmd: String = format!( + "window.navbar.closeTab('{}')", + serde_json::to_string(&tab_id).unwrap() + ); + send_to_constellation( + &sender, + ConstellationMsg::WebDriverCommand(WebDriverCommandMsg::ScriptCommand( + self.panel.as_ref().unwrap().webview.webview_id.into(), + WebDriverScriptCommand::ExecuteScript(cmd, tx), + )), + ); + + let active_tab_id = rx.recv().unwrap().unwrap(); + match active_tab_id { + WebDriverJSValue::String(resp) => { + let active_id: WebViewId = serde_json::from_str(&resp).unwrap(); + self.activate_tab(compositor, active_id, self.tab_manager.count() > 2); + } + _ => {} + } + } + send_to_constellation(&sender, ConstellationMsg::CloseWebView(tab_id)); + } + + /// Activate a tab + pub fn activate_tab( + &mut self, + compositor: &mut IOCompositor, + tab_id: WebViewId, + show_tab: bool, + ) { + let size = self.size(); + let rect = DeviceIntRect::from_size(size); + let content_size = self.get_content_size(rect, show_tab); + let (tab_id, prompt_id) = self.tab_manager.set_size(tab_id, content_size); + + if let Some(prompt_id) = prompt_id { + compositor.on_resize_webview_event(prompt_id, content_size); + } + if let Some(tab_id) = tab_id { + compositor.on_resize_webview_event(tab_id, content_size); + + if let Some(_) = self.tab_manager.activate_tab(tab_id) { + // throttle the old tab to avoid unnecessary animation caclulations + if let Some(old_tab_id) = self.tab_manager.current_tab_id() { + let _ = compositor + .constellation_chan + .send(ConstellationMsg::SetWebViewThrottled(old_tab_id, true)); + } + let _ = compositor + .constellation_chan + .send(ConstellationMsg::SetWebViewThrottled(tab_id, false)); + + // update painting order immediately to draw the active tab + compositor.send_root_pipeline_display_list(self); + } + } + } + /// Handle Winit window event and return a boolean to indicate if the compositor should repaint immediately. pub fn handle_winit_window_event( &mut self, @@ -297,11 +388,11 @@ impl Window { /* handle context menu */ // TODO(context-menu): should create on ShowContextMenu event - + let prompt = self.tab_manager.current_prompt(); match (state, button) { #[cfg(any(target_os = "macos", target_os = "windows"))] (ElementState::Pressed, winit::event::MouseButton::Right) => { - if self.prompt.is_some() { + if prompt.is_some() { return; } self.show_context_menu(); @@ -312,7 +403,7 @@ impl Window { } #[cfg(linux)] (ElementState::Pressed, winit::event::MouseButton::Right) => { - if self.prompt.is_some() { + if prompt.is_some() { return; } if self.context_menu.is_none() { @@ -426,6 +517,12 @@ impl Window { WindowEvent::KeyboardInput { event, .. } => { let event = keyboard_event_from_winit(event, self.modifiers_state.get()); log::trace!("Verso is handling {:?}", event); + + /* Window operation keyboard shortcut */ + if self.handle_keyboard_shortcut(compositor, &event) { + return; + } + let msg = ConstellationMsg::Keyboard(event); send_to_constellation(sender, msg); } @@ -433,6 +530,44 @@ impl Window { } } + /// Handle Window keyboard shortcut + /// + /// - Returns `true` if the event is handled, then we should skip sending it to constellation + fn handle_keyboard_shortcut( + &mut self, + compositor: &mut IOCompositor, + event: &KeyboardEvent, + ) -> bool { + let is_macos = cfg!(target_os = "macos"); + let control_or_meta = if is_macos { + Modifiers::META + } else { + Modifiers::CONTROL + }; + + if event.state == KeyState::Down { + // TODO: New Window, Close Browser + match (event.modifiers, event.code) { + (modifiers, Code::KeyT) if modifiers == control_or_meta => { + (*self).create_tab( + &compositor.constellation_chan, + ServoUrl::parse("https://example.com").unwrap(), + ); + return true; + } + (modifiers, Code::KeyW) if modifiers == control_or_meta => { + if let Some(tab_id) = self.tab_manager.current_tab_id() { + (*self).close_tab(compositor, tab_id); + } + return true; + } + _ => (), + } + } + + false + } + /// Handle servo messages. Return true if it requests a new window pub fn handle_servo_message( &mut self, @@ -459,13 +594,11 @@ impl Window { return false; } } - if let Some(prompt) = &self.prompt { - if prompt.webview().webview_id == webview_id { - self.handle_servo_messages_with_prompt( - webview_id, message, sender, clipboard, compositor, - ); - return false; - } + if self.tab_manager.has_prompt(webview_id) { + self.handle_servo_messages_with_prompt( + webview_id, message, sender, clipboard, compositor, + ); + return false; } // Handle message in Verso WebView @@ -505,22 +638,18 @@ impl Window { return true; } - if self - .prompt - .as_ref() - .map_or(false, |w| w.webview().webview_id == id) - { + if self.tab_manager.has_prompt(id) { return true; } self.panel .as_ref() .map_or(false, |w| w.webview.webview_id == id) - || self.webview.as_ref().map_or(false, |w| w.webview_id == id) + || self.tab_manager.tab(id).is_some() } - /// Remove the webview in this window by provided webview ID. If this is the panel, it will - /// shut down the compositor and then close whole application. + /// Remove the webview in this window by provided webview ID. + /// If provided ID is the panel, it will shut down the compositor and then close whole application. pub fn remove_webview( &mut self, id: WebViewId, @@ -537,13 +666,7 @@ impl Window { return (Some(context_menu.webview().clone()), false); } - if self - .prompt - .as_ref() - .filter(|menu| menu.webview().webview_id == id) - .is_some() - { - let prompt = self.prompt.take().expect("Prompt should exist"); + if let Some(prompt) = self.tab_manager.remove_prompt_by_prompt_id(id) { return (Some(prompt.webview().clone()), false); } @@ -553,20 +676,18 @@ impl Window { .filter(|w| w.webview.webview_id == id) .is_some() { - if let Some(w) = self.webview.as_ref() { + // Removing panel, remove all webviews and shut down the compositor + let tab_ids = self.tab_manager.tab_ids(); + for tab_id in tab_ids { send_to_constellation( &compositor.constellation_chan, - ConstellationMsg::CloseWebView(w.webview_id), - ) + ConstellationMsg::CloseWebView(tab_id), + ); } (self.panel.take().map(|panel| panel.webview), false) - } else if self - .webview - .as_ref() - .filter(|w| w.webview_id == id) - .is_some() - { - (self.webview.take(), self.panel.is_none()) + } else if let Ok(tab) = self.tab_manager.close_tab(id) { + let close_window = self.tab_manager.count() == 0 || self.panel.is_none(); + (Some(tab.webview().clone()), close_window) } else { (None, false) } @@ -578,8 +699,9 @@ impl Window { if let Some(panel) = &self.panel { order.push(&panel.webview); } - if let Some(webview) = &self.webview { - order.push(webview); + + if let Some(tab) = self.tab_manager.current_tab() { + order.push(tab.webview()); } #[cfg(linux)] @@ -587,7 +709,7 @@ impl Window { order.push(context_menu.webview()); } - if let Some(prompt) = &self.prompt { + if let Some(prompt) = self.tab_manager.current_prompt() { order.push(prompt.webview()); } @@ -635,26 +757,22 @@ impl Window { }; self.window.set_cursor(winit_cursor); } - - /// Update the history of the window. - pub fn update_history(&mut self, history: &Vec, current_index: usize) { - self.history = history.to_vec(); - self.current_history_index = current_index; - } } // Context Menu methods impl Window { #[cfg(any(target_os = "macos", target_os = "windows"))] pub(crate) fn show_context_menu(&self) { - let history_len = self.history.len(); + let tab = self.tab_manager.current_tab().unwrap(); + let history = tab.history(); + let history_len = history.list.len(); // items - let back = MenuItem::with_id("back", "Back", self.current_history_index > 0, None); + let back = MenuItem::with_id("back", "Back", history.current_idx > 0, None); let forward = MenuItem::with_id( "forward", "Forward", - self.current_history_index + 1 < history_len, + history.current_idx + 1 < history_len, None, ); let reload = MenuItem::with_id("reload", "Reload", true, None); @@ -670,14 +788,16 @@ impl Window { pub(crate) fn show_context_menu(&mut self, sender: &Sender) -> ContextMenu { use crate::webview::context_menu::MenuItem; - let history_len = self.history.len(); + let tab = self.tab_manager.current_tab().unwrap(); + let history = tab.history(); + let history_len = history.list.len(); // items - let back = MenuItem::new(Some("back"), "Back", self.current_history_index > 0); + let back = MenuItem::new(Some("back"), "Back", history.current_idx > 0); let forward = MenuItem::new( Some("forward"), "Forward", - self.current_history_index + 1 < history_len, + history.current_idx + 1 < history_len, ); let reload = MenuItem::new(Some("reload"), "Reload", true); @@ -703,30 +823,25 @@ impl Window { #[cfg(any(target_os = "macos", target_os = "windows"))] fn handle_context_menu_event(&self, sender: &Sender, event: MenuEvent) { // TODO: should be more flexible to handle different menu items + let active_tab = self.tab_manager.current_tab().unwrap(); match event.id().0.as_str() { "back" => { send_to_constellation( sender, - ConstellationMsg::TraverseHistory( - self.webview.as_ref().unwrap().webview_id, - TraversalDirection::Back(1), - ), + ConstellationMsg::TraverseHistory(active_tab.id(), TraversalDirection::Back(1)), ); } "forward" => { send_to_constellation( sender, ConstellationMsg::TraverseHistory( - self.webview.as_ref().unwrap().webview_id, + active_tab.id(), TraversalDirection::Forward(1), ), ); } "reload" => { - send_to_constellation( - sender, - ConstellationMsg::Reload(self.webview.as_ref().unwrap().webview_id), - ); + send_to_constellation(sender, ConstellationMsg::Reload(active_tab.id())); } _ => {} } @@ -742,32 +857,27 @@ impl Window { event: crate::webview::context_menu::ContextMenuResult, ) { self.close_context_menu(sender); - match event.id.as_str() { - "back" => { - send_to_constellation( - sender, - ConstellationMsg::TraverseHistory( - self.webview.as_ref().unwrap().webview_id, - TraversalDirection::Back(1), - ), - ); - } - "forward" => { - send_to_constellation( - sender, - ConstellationMsg::TraverseHistory( - self.webview.as_ref().unwrap().webview_id, - TraversalDirection::Forward(1), - ), - ); - } - "reload" => { - send_to_constellation( - sender, - ConstellationMsg::Reload(self.webview.as_ref().unwrap().webview_id), - ); + if let Some(tab_id) = self.tab_manager.current_tab_id() { + match event.id.as_str() { + "back" => { + send_to_constellation( + sender, + ConstellationMsg::TraverseHistory(tab_id, TraversalDirection::Back(1)), + ); + } + "forward" => { + send_to_constellation( + sender, + ConstellationMsg::TraverseHistory(tab_id, TraversalDirection::Forward(1)), + ); + } + "reload" => { + send_to_constellation(sender, ConstellationMsg::Reload(tab_id)); + } + _ => {} } - _ => {} + } else { + log::error!("No active webview to handle context menu event"); } } } @@ -775,8 +885,12 @@ impl Window { // Prompt methods impl Window { /// Close window's prompt dialog - pub(crate) fn close_prompt_dialog(&mut self) { - if let Some(sender) = self.prompt.take().and_then(|prompt| prompt.sender()) { + pub(crate) fn close_prompt_dialog(&mut self, tab_id: WebViewId) { + if let Some(sender) = self + .tab_manager + .remove_prompt_by_tab_id(tab_id) + .and_then(|prompt| prompt.sender()) + { match sender { PromptSender::AlertSender(sender) => { let _ = sender.send(());