diff options
author | webbeef <me@webbeef.org> | 2024-08-27 13:17:33 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-27 20:17:33 +0000 |
commit | 1b48bd18aa855cc966869dd81530aa0da3eea4f3 (patch) | |
tree | 8c44eead81b685f766d889586558475d30e74633 | |
parent | a0ff57cea1675e7ec9ee8657d80024a110a0092a (diff) | |
download | servo-1b48bd18aa855cc966869dd81530aa0da3eea4f3.tar.gz servo-1b48bd18aa855cc966869dd81530aa0da3eea4f3.zip |
Basic tab strip for the minibrowser (#33100)
This implements a simple tab system for servoshell:
- The egui part uses the built-in SelectableLabels components and
display the full tab title on hover.
- WebView structs now hold all the state for each WebView. When we
need "global" state, we return the focused WebView state, eg.
for the load status since it's still global in the UI.
- New keyboard shortcut: [Cmd-or-Ctrl]+[W] to close the current tab.
- New keyboard shortcut: [Cmd-or-Ctrl]+[T] to create a new tab.
- The new tab content is loaded from the 'servo:newtab' url using a
couple of custom protocol handlers.
Signed-off-by: webbeef <me@webbeef.org>
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | components/shared/net/response.rs | 4 | ||||
-rw-r--r-- | ports/servoshell/Cargo.toml | 3 | ||||
-rw-r--r-- | ports/servoshell/desktop/app.rs | 4 | ||||
-rw-r--r-- | ports/servoshell/desktop/embedder.rs | 4 | ||||
-rw-r--r-- | ports/servoshell/desktop/minibrowser.rs | 68 | ||||
-rw-r--r-- | ports/servoshell/desktop/protocols/mod.rs | 2 | ||||
-rw-r--r-- | ports/servoshell/desktop/protocols/resource.rs | 108 | ||||
-rw-r--r-- | ports/servoshell/desktop/protocols/servo.rs | 43 | ||||
-rw-r--r-- | ports/servoshell/desktop/webview.rs | 209 | ||||
-rw-r--r-- | ports/servoshell/resources.rs | 2 | ||||
-rw-r--r-- | resources/resource_protocol/newtab.css | 57 | ||||
-rw-r--r-- | resources/resource_protocol/newtab.html | 19 | ||||
-rw-r--r-- | resources/resource_protocol/servo-color-negative-no-container.png | bin | 0 -> 30171 bytes |
14 files changed, 454 insertions, 71 deletions
diff --git a/Cargo.lock b/Cargo.lock index 9f3db144fb9..c4d9ef910df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6194,6 +6194,7 @@ dependencies = [ "libloading", "libservo", "log", + "mime_guess", "napi-derive-ohos", "napi-ohos", "net", @@ -6208,6 +6209,7 @@ dependencies = [ "sig", "surfman", "tinyfiledialogs", + "tokio", "url", "vergen", "webxr", diff --git a/components/shared/net/response.rs b/components/shared/net/response.rs index 707066cd741..338cc002ac7 100644 --- a/components/shared/net/response.rs +++ b/components/shared/net/response.rs @@ -184,6 +184,10 @@ impl Response { } } + pub fn network_internal_error<T: Into<String>>(msg: T) -> Response { + Self::network_error(NetworkError::Internal(msg.into())) + } + pub fn url(&self) -> Option<&ServoUrl> { self.url.as_ref() } diff --git a/ports/servoshell/Cargo.toml b/ports/servoshell/Cargo.toml index dd156345869..a182a632ac6 100644 --- a/ports/servoshell/Cargo.toml +++ b/ports/servoshell/Cargo.toml @@ -57,9 +57,10 @@ libservo = { path = "../../components/servo" } cfg-if = { workspace = true } log = { workspace = true } getopts = { workspace = true } +mime_guess = { workspace = true } url = { workspace = true } servo-media = { workspace = true } - +tokio = { workspace = true } [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.14" diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index 87f5a6c1035..7420bd4035e 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -434,8 +434,8 @@ impl App { webviews.handle_window_events(embedder_events); // If the Gamepad API is enabled, handle gamepad events from GilRs. - // Checking for current_url_string should ensure we'll have a valid browsing context. - if pref!(dom.gamepad.enabled) && webviews.current_url_string().is_some() { + // Checking for focused_webview_id should ensure we'll have a valid browsing context. + if pref!(dom.gamepad.enabled) && webviews.focused_webview_id().is_some() { webviews.handle_gamepad_events(); } diff --git a/ports/servoshell/desktop/embedder.rs b/ports/servoshell/desktop/embedder.rs index d68bb350782..8c568f1f369 100644 --- a/ports/servoshell/desktop/embedder.rs +++ b/ports/servoshell/desktop/embedder.rs @@ -12,7 +12,7 @@ use webxr::glwindow::GlWindowDiscovery; #[cfg(target_os = "windows")] use webxr::openxr::OpenXrDiscovery; -use crate::desktop::protocols::urlinfo; +use crate::desktop::protocols::{resource, servo as servo_handler, urlinfo}; pub enum XrDiscovery { GlWindow(GlWindowDiscovery), @@ -61,6 +61,8 @@ impl EmbedderMethods for EmbedderCallbacks { fn get_protocol_handlers(&self) -> ProtocolRegistry { let mut registry = ProtocolRegistry::default(); registry.register("urlinfo", urlinfo::UrlInfoProtocolHander::default()); + registry.register("servo", servo_handler::ServoProtocolHander::default()); + registry.register("resource", resource::ResourceProtocolHander::default()); registry } } diff --git a/ports/servoshell/desktop/minibrowser.rs b/ports/servoshell/desktop/minibrowser.rs index d01efe886fa..af02a49538c 100644 --- a/ports/servoshell/desktop/minibrowser.rs +++ b/ports/servoshell/desktop/minibrowser.rs @@ -7,9 +7,11 @@ use std::num::NonZeroU32; use std::sync::Arc; use std::time::Instant; +use egui::text::{CCursor, CCursorRange}; +use egui::text_edit::TextEditState; use egui::{ - pos2, CentralPanel, Color32, Frame, Key, Label, Modifiers, PaintCallback, Pos2, TopBottomPanel, - Vec2, + pos2, CentralPanel, Color32, Frame, Key, Label, Modifiers, PaintCallback, Pos2, + SelectableLabel, TopBottomPanel, Vec2, }; use egui_glow::CallbackFn; use egui_winit::EventResponse; @@ -61,6 +63,15 @@ pub enum MinibrowserEvent { Reload, } +fn truncate_with_ellipsis(input: &str, max_length: usize) -> String { + if input.chars().count() > max_length { + let truncated: String = input.chars().take(max_length.saturating_sub(1)).collect(); + format!("{}…", truncated) + } else { + input.to_string() + } +} + impl Minibrowser { pub fn new( rendering_context: &RenderingContext, @@ -216,9 +227,11 @@ impl Minibrowser { ui.available_size(), egui::Layout::right_to_left(egui::Align::Center), |ui| { + let location_id = egui::Id::new("location_input"); let location_field = ui.add_sized( ui.available_size(), - egui::TextEdit::singleline(&mut *location.borrow_mut()), + egui::TextEdit::singleline(&mut *location.borrow_mut()) + .id(location_id), ); if location_field.changed() { @@ -228,6 +241,16 @@ impl Minibrowser { i.clone().consume_key(Modifiers::COMMAND, Key::L) }) { location_field.request_focus(); + if let Some(mut state) = + TextEditState::load(ui.ctx(), location_id) + { + // Select the whole input. + state.cursor.set_char_range(Some(CCursorRange::two( + CCursor::new(0), + CCursor::new(location.borrow().len()), + ))); + state.store(ui.ctx(), location_id); + } } if location_field.lost_focus() && ui.input(|i| i.clone().key_pressed(Key::Enter)) @@ -242,6 +265,36 @@ impl Minibrowser { }); }; + let mut embedder_events = vec![]; + + // A simple Tab header strip, using egui 'SelectableLabel' elements. + // TODO: Add a way to close a tab eg. with a [x] control. + TopBottomPanel::top("tabs").show(ctx, |ui| { + ui.allocate_ui_with_layout( + ui.available_size(), + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + for (webview_id, webview) in webviews.webviews().into_iter() { + let msg = match (webview.title.clone(), webview.url.clone()) { + (Some(title), _) => title, + (None, Some(url)) => url.to_string(), + _ => "".to_owned(), + }; + let tab = ui.add(SelectableLabel::new( + webview.focused, + truncate_with_ellipsis(&msg, 20), + )); + let tab = tab.on_hover_ui(|ui| { + ui.label(&msg); + }); + if !webview.focused && tab.clicked() { + embedder_events.push(EmbedderEvent::FocusWebView(webview_id)); + } + } + }, + ); + }); + // The toolbar height is where the Context’s available rect starts. // For reasons that are unclear, the TopBottomPanel’s ui cursor exceeds this by one egui // point, but the Context is correct and the TopBottomPanel is wrong. @@ -255,7 +308,6 @@ impl Minibrowser { let Some(webview) = webviews.get_mut(focused_webview_id) else { return; }; - let mut embedder_events = vec![]; CentralPanel::default() .frame(Frame::none()) @@ -362,9 +414,9 @@ impl Minibrowser { app_event_queue: &mut Vec<EmbedderEvent>, ) { for event in self.event_queue.borrow_mut().drain(..) { + let browser_id = browser.focused_webview_id().unwrap(); match event { MinibrowserEvent::Go => { - 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)); @@ -374,21 +426,19 @@ impl Minibrowser { } }, MinibrowserEvent::Back => { - let browser_id = browser.webview_id().unwrap(); app_event_queue.push(EmbedderEvent::Navigation( browser_id, TraversalDirection::Back(1), )); }, MinibrowserEvent::Forward => { - let browser_id = browser.webview_id().unwrap(); app_event_queue.push(EmbedderEvent::Navigation( browser_id, TraversalDirection::Forward(1), )); }, MinibrowserEvent::Reload => { - let browser_id = browser.webview_id().unwrap(); + let browser_id = browser.focused_webview_id().unwrap(); app_event_queue.push(EmbedderEvent::Reload(browser_id)); }, } @@ -407,7 +457,7 @@ impl Minibrowser { } match browser.current_url_string() { - Some(location) if location != self.location.get_mut() => { + Some(location) if location != *self.location.get_mut() => { self.location = RefCell::new(location.to_owned()); true }, diff --git a/ports/servoshell/desktop/protocols/mod.rs b/ports/servoshell/desktop/protocols/mod.rs index 409b6b1b5ba..434826f5bc7 100644 --- a/ports/servoshell/desktop/protocols/mod.rs +++ b/ports/servoshell/desktop/protocols/mod.rs @@ -2,4 +2,6 @@ * 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/. */ +pub(crate) mod resource; +pub(crate) mod servo; pub(crate) mod urlinfo; diff --git a/ports/servoshell/desktop/protocols/resource.rs b/ports/servoshell/desktop/protocols/resource.rs new file mode 100644 index 00000000000..3d1721fff6c --- /dev/null +++ b/ports/servoshell/desktop/protocols/resource.rs @@ -0,0 +1,108 @@ +/* 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/. */ + +//! This protocol handler loads files from the <resources_dir_path()>/protocol/resource directory, +//! sanitizing the path to prevent path escape attacks. +//! For security reasons, loads are only allowed if the referrer has a 'resource' or +//! 'servo' scheme. + +use std::fs::File; +use std::future::Future; +use std::io::BufReader; +use std::pin::Pin; + +use headers::{ContentType, HeaderMapExt}; +use net::fetch::methods::{DoneChannel, FetchContext}; +use net::filemanager_thread::FILE_CHUNK_SIZE; +use net::protocols::ProtocolHandler; +use net_traits::filemanager_thread::RelativePos; +use net_traits::request::Request; +use net_traits::response::{Response, ResponseBody}; +use net_traits::ResourceFetchTiming; +use tokio::sync::mpsc::unbounded_channel; + +#[derive(Default)] +pub struct ResourceProtocolHander {} + +impl ResourceProtocolHander { + pub fn response_for_path( + request: &mut Request, + done_chan: &mut DoneChannel, + context: &FetchContext, + path: &str, + ) -> Pin<Box<dyn Future<Output = Response> + Send>> { + if path.contains("..") || !path.starts_with("/") { + return Box::pin(std::future::ready(Response::network_internal_error( + "Invalid path", + ))); + } + + let path = if let Some(path) = path.strip_prefix("/") { + path + } else { + return Box::pin(std::future::ready(Response::network_internal_error( + "Invalid path", + ))); + }; + + let file_path = crate::resources::resources_dir_path() + .join("resource_protocol") + .join(path); + + if !file_path.exists() || file_path.is_dir() { + return Box::pin(std::future::ready(Response::network_internal_error( + "Invalid path", + ))); + } + + let response = if let Ok(file) = File::open(file_path.clone()) { + let mut response = Response::new( + request.current_url(), + ResourceFetchTiming::new(request.timing_type()), + ); + let reader = BufReader::with_capacity(FILE_CHUNK_SIZE, file); + + // Set Content-Type header. + let mime = mime_guess::from_path(file_path).first_or_octet_stream(); + response.headers.typed_insert(ContentType::from(mime)); + + // Setup channel to receive cross-thread messages about the file fetch + // operation. + let (mut done_sender, done_receiver) = unbounded_channel(); + *done_chan = Some((done_sender.clone(), done_receiver)); + + *response.body.lock().unwrap() = ResponseBody::Receiving(vec![]); + + context.filemanager.lock().unwrap().fetch_file_in_chunks( + &mut done_sender, + reader, + response.body.clone(), + context.cancellation_listener.clone(), + RelativePos::full_range(), + ); + + response + } else { + Response::network_internal_error("Opening file failed") + }; + + Box::pin(std::future::ready(response)) + } +} + +impl ProtocolHandler for ResourceProtocolHander { + fn load( + &self, + request: &mut Request, + done_chan: &mut DoneChannel, + context: &FetchContext, + ) -> Pin<Box<dyn Future<Output = Response> + Send>> { + let url = request.current_url(); + + // TODO: Check referrer. + // We unexpectedly get `NoReferrer` for all requests from the newtab page. + + Self::response_for_path(request, done_chan, context, url.path()) + } +} diff --git a/ports/servoshell/desktop/protocols/servo.rs b/ports/servoshell/desktop/protocols/servo.rs new file mode 100644 index 00000000000..f03d7715230 --- /dev/null +++ b/ports/servoshell/desktop/protocols/servo.rs @@ -0,0 +1,43 @@ +/* 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/. */ + +//! Loads resources using a mapping from well-known shortcuts to resource: urls. +//! Recognized shorcuts: +//! - servo:newtab + +use std::future::Future; +use std::pin::Pin; + +use net::fetch::methods::{DoneChannel, FetchContext}; +use net::protocols::ProtocolHandler; +use net_traits::request::Request; +use net_traits::response::Response; + +use crate::desktop::protocols::resource::ResourceProtocolHander; + +#[derive(Default)] +pub struct ServoProtocolHander {} + +impl ProtocolHandler for ServoProtocolHander { + fn load( + &self, + request: &mut Request, + done_chan: &mut DoneChannel, + context: &FetchContext, + ) -> Pin<Box<dyn Future<Output = Response> + Send>> { + let url = request.current_url(); + + match url.path() { + "newtab" => ResourceProtocolHander::response_for_path( + request, + done_chan, + context, + "/newtab.html", + ), + _ => Box::pin(std::future::ready(Response::network_internal_error( + "Invalid shortcut", + ))), + } + } +} diff --git a/ports/servoshell/desktop/webview.rs b/ports/servoshell/desktop/webview.rs index c480f392430..634a4366eec 100644 --- a/ports/servoshell/desktop/webview.rs +++ b/ports/servoshell/desktop/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::hash_map::Entry; use std::collections::HashMap; use std::fs::File; use std::io::Write; @@ -40,8 +41,6 @@ use crate::desktop::tracing::{trace_embedder_event, trace_embedder_msg}; use crate::parser::location_bar_input_to_url; pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> { - current_url: Option<ServoUrl>, - current_url_string: Option<String>, status_text: Option<String>, /// List of top-level browsing contexts. @@ -56,7 +55,10 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> { /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. focused_webview_id: Option<WebViewId>, - title: Option<String>, + /// Pre-creation state for WebViews. + /// This is needed because in some situations the WebViewOpened event is sent + /// after ChangePageTitle and HistoryChanged + webview_preload_data: HashMap<WebViewId, WebViewPreloadData>, window: Rc<Window>, event_queue: Vec<EmbedderEvent>, @@ -64,12 +66,41 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> { gamepad: Option<Gilrs>, haptic_effects: HashMap<usize, HapticEffect>, shutdown_requested: bool, - load_status: LoadStatus, } +#[derive(Clone, Default)] +struct WebViewPreloadData { + title: Option<String>, + url: Option<ServoUrl>, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LoadStatus { + HeadParsed, + LoadStart, + LoadComplete, +} + +// The state of each Tab/WebView #[derive(Debug)] pub struct WebView { pub rect: DeviceRect, + pub title: Option<String>, + pub url: Option<ServoUrl>, + pub focused: bool, + pub load_status: LoadStatus, +} + +impl WebView { + fn new(rect: DeviceRect, preload_data: WebViewPreloadData) -> Self { + Self { + rect, + title: preload_data.title, + url: preload_data.url, + focused: false, + load_status: LoadStatus::LoadComplete, + } + } } pub struct ServoEventResponse { @@ -77,13 +108,6 @@ pub struct ServoEventResponse { pub need_update: bool, } -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum LoadStatus { - HeadParsed, - LoadStart, - LoadComplete, -} - pub struct HapticEffect { pub effect: Effect, pub sender: IpcSender<bool>, @@ -95,13 +119,11 @@ where { pub fn new(window: Rc<Window>) -> WebViewManager<Window> { WebViewManager { - title: None, - current_url: None, - current_url_string: None, status_text: None, webviews: HashMap::default(), creation_order: vec![], focused_webview_id: None, + webview_preload_data: HashMap::default(), window, clipboard: match Clipboard::new() { Ok(c) => Some(c), @@ -121,28 +143,44 @@ where event_queue: Vec::new(), shutdown_requested: false, - load_status: LoadStatus::LoadComplete, } } - pub fn webview_id(&self) -> Option<WebViewId> { - self.focused_webview_id - } - pub fn get_mut(&mut self, webview_id: WebViewId) -> Option<&mut WebView> { self.webviews.get_mut(&webview_id) } + // Returns the existing preload data for the given WebView, or a new one. + fn ensure_preload_data_mut(&mut self, webview_id: &WebViewId) -> &mut WebViewPreloadData { + if let Entry::Vacant(entry) = self.webview_preload_data.entry(webview_id.clone()) { + entry.insert(WebViewPreloadData::default()); + } + self.webview_preload_data.get_mut(webview_id).unwrap() + } + pub fn focused_webview_id(&self) -> Option<WebViewId> { self.focused_webview_id } - pub fn current_url_string(&self) -> Option<&str> { - self.current_url_string.as_deref() + pub fn current_url_string(&self) -> Option<String> { + match self.focused_webview() { + Some(webview) => webview.url.as_ref().map(|url| url.to_string()), + None => None, + } + } + + pub fn focused_webview(&self) -> Option<&WebView> { + match self.focused_webview_id { + Some(id) => self.webviews.get(&id), + None => None, + } } pub fn load_status(&self) -> LoadStatus { - self.load_status + match self.focused_webview() { + Some(webview) => webview.load_status, + None => LoadStatus::LoadComplete, + } } pub fn status_text(&self) -> Option<String> { @@ -153,6 +191,15 @@ where std::mem::take(&mut self.event_queue) } + // Returns the webviews in the creation order. + pub fn webviews(&self) -> Vec<(WebViewId, &WebView)> { + let mut res = vec![]; + for id in &self.creation_order { + res.push((*id, self.webviews.get(id).unwrap())) + } + res + } + pub fn handle_window_events(&mut self, events: Vec<EmbedderEvent>) { for event in events { trace_embedder_event!(event, "{event:?}"); @@ -377,11 +424,15 @@ where }) .shortcut(CMD_OR_CONTROL, 'L', || { if !opts::get().minibrowser { - let url: String = if let Some(ref current_url) = self.current_url { - current_url.to_string() - } else { - String::from("") + let url = match self.focused_webview() { + Some(webview) => webview + .url + .as_ref() + .map(|url| url.to_string()) + .unwrap_or_else(String::default), + None => String::default(), }; + let title = "URL or search query"; let input = tinyfiledialogs::input_box(title, title, &tiny_dialog_escape(&url)); if let Some(input) = input { @@ -393,6 +444,16 @@ where } } }) + .shortcut(CMD_OR_CONTROL, 'W', || { + if let Some(id) = self.focused_webview_id { + self.event_queue.push(EmbedderEvent::CloseWebView(id)); + } + }) + .shortcut(CMD_OR_CONTROL, 'T', || { + let url = ServoUrl::parse("servo:newtab").unwrap(); + self.event_queue + .push(EmbedderEvent::NewWebView(url, WebViewId::new())); + }) .shortcut(CMD_OR_CONTROL, 'Q', || { self.event_queue.push(EmbedderEvent::Quit); }) @@ -545,7 +606,7 @@ where &mut self, events: Drain<'_, (Option<WebViewId>, EmbedderMsg)>, ) -> ServoEventResponse { - let mut need_present = self.load_status != LoadStatus::LoadComplete; + let mut need_present = self.load_status() != LoadStatus::LoadComplete; let mut need_update = false; for (webview_id, msg) in events { if let Some(webview_id) = webview_id { @@ -559,19 +620,23 @@ where need_update = true; }, EmbedderMsg::ChangePageTitle(title) => { - self.title = title; - - let fallback_title: String = if let Some(ref current_url) = self.current_url { - current_url.to_string() - } else { - String::from("Untitled") - }; - let title = match self.title { - Some(ref title) if !title.is_empty() => &**title, - _ => &fallback_title, - }; - let title = format!("{} - Servo", title); - self.window.set_title(&title); + // Set the title to the target webview, and update the OS window title + // if this is the currently focused one. + if let Some(webview_id) = webview_id { + if let Some(webview) = self.get_mut(webview_id) { + webview.title = title.clone(); + if webview.focused { + self.window.set_title(&format!( + "{} - Servo", + title.clone().unwrap_or_default() + )); + } + need_update = true; + } else { + let data = self.ensure_preload_data_mut(&webview_id); + data.title = title.clone(); + } + } }, EmbedderMsg::MoveTo(point) => { self.window.set_position(point); @@ -701,14 +766,19 @@ where let mut rect = self.window.get_coordinates().get_viewport().to_f32(); rect.min.y += toolbar * scale; - self.webviews.insert(new_webview_id, WebView { rect }); - self.creation_order.push(new_webview_id); - self.event_queue - .push(EmbedderEvent::FocusWebView(new_webview_id)); - self.event_queue - .push(EmbedderEvent::MoveResizeWebView(new_webview_id, rect)); - self.event_queue - .push(EmbedderEvent::RaiseWebViewToTop(new_webview_id, true)); + // Make sure to not add duplicates into the creation_order vector. + // This can happen as explained in https://github.com/servo/servo/issues/33075 + let preload_data = self.ensure_preload_data_mut(&new_webview_id).clone(); + if let Entry::Vacant(entry) = self.webviews.entry(new_webview_id) { + entry.insert(WebView::new(rect, preload_data)); + self.creation_order.push(new_webview_id); + self.event_queue + .push(EmbedderEvent::FocusWebView(new_webview_id)); + self.event_queue + .push(EmbedderEvent::MoveResizeWebView(new_webview_id, rect)); + self.event_queue + .push(EmbedderEvent::RaiseWebViewToTop(new_webview_id, true)); + } }, EmbedderMsg::WebViewClosed(webview_id) => { self.webviews.retain(|&id, _| id != webview_id); @@ -722,13 +792,20 @@ where } }, EmbedderMsg::WebViewFocused(webview_id) => { + for (id, webview) in &mut self.webviews { + webview.focused = *id == webview_id; + } self.focused_webview_id = Some(webview_id); + need_update = true; // Show the most recently created webview and hide all others. // TODO: Stop doing this once we have full multiple webviews support self.event_queue .push(EmbedderEvent::ShowWebView(webview_id, true)); }, EmbedderMsg::WebViewBlurred => { + for webview in self.webviews.values_mut() { + webview.focused = false; + } self.focused_webview_id = None; }, EmbedderMsg::Keyboard(key_event) => { @@ -761,24 +838,42 @@ where // FIXME: show favicons in the UI somehow }, EmbedderMsg::HeadParsed => { - self.load_status = LoadStatus::HeadParsed; - need_update = true; + if let Some(webview_id) = webview_id { + if let Some(webview) = self.get_mut(webview_id) { + webview.load_status = LoadStatus::HeadParsed; + need_update = true; + } + } }, EmbedderMsg::HistoryChanged(urls, current) => { - self.current_url = Some(urls[current].clone()); - self.current_url_string = Some(urls[current].clone().into_string()); - need_update = true; + if let Some(webview_id) = webview_id { + if let Some(webview) = self.get_mut(webview_id) { + webview.url = Some(urls[current].clone()); + need_update = true; + } else { + let data = self.ensure_preload_data_mut(&webview_id); + data.url = Some(urls[current].clone()); + } + } }, EmbedderMsg::SetFullscreenState(state) => { self.window.set_fullscreen(state); }, EmbedderMsg::LoadStart => { - self.load_status = LoadStatus::LoadStart; - need_update = true; + if let Some(webview_id) = webview_id { + if let Some(webview) = self.get_mut(webview_id) { + webview.load_status = LoadStatus::LoadStart; + need_update = true; + } + } }, EmbedderMsg::LoadComplete => { - self.load_status = LoadStatus::LoadComplete; - need_update = true; + if let Some(webview_id) = webview_id { + if let Some(webview) = self.get_mut(webview_id) { + webview.load_status = LoadStatus::LoadComplete; + need_update = true; + } + } }, EmbedderMsg::Shutdown => { self.shutdown_requested = true; diff --git a/ports/servoshell/resources.rs b/ports/servoshell/resources.rs index fe65dfafa5c..9801a8923ea 100644 --- a/ports/servoshell/resources.rs +++ b/ports/servoshell/resources.rs @@ -17,7 +17,7 @@ pub fn init() { resources::set(Box::new(ResourceReader)); } -fn resources_dir_path() -> PathBuf { +pub(crate) fn resources_dir_path() -> PathBuf { // This needs to be called before the process is sandboxed // as we only give permission to read inside the resources directory, // not the permissions the "search" for the resources directory. diff --git a/resources/resource_protocol/newtab.css b/resources/resource_protocol/newtab.css new file mode 100644 index 00000000000..4c1c0a19898 --- /dev/null +++ b/resources/resource_protocol/newtab.css @@ -0,0 +1,57 @@ +/* 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/. */ + +html, +body { + height: 100%; + padding: 0; + margin: 0; +} + +body { + background-color: #121619; + font-family: sans-serif; + color: hsl(0, 0%, 96%); + font-weight: 400; + line-height: 1.5; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +img { + width: 25vw; +} + +form { + margin: 1em; +} + +input { + width: 50vw; +} + +a { + color: #1191e8; + cursor: pointer; + text-decoration: none; +} + +a:hover { + color: #42bf64; +} + +/* This should not be needed but paper over missing default styles */ +button { + padding-block: 1px; + padding-inline: 8px; + box-sizing: border-box; +} + +form { + display: flex; + justify-items: center; + gap: 0.5em; +} diff --git a/resources/resource_protocol/newtab.html b/resources/resource_protocol/newtab.html new file mode 100644 index 00000000000..da5e76198ad --- /dev/null +++ b/resources/resource_protocol/newtab.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Servo - New Tab</title> + <link rel="stylesheet" href="resource:///newtab.css" /> + </head> + <html> + <a href="https://servo.org"> + <img src="resource:///servo-color-negative-no-container.png" /> + </a> + <form action="https://duckduckgo.com/html/"> + <input name="q" placeholder="Search the web…" autofocus /> + <button type="submit">Go!</button> + </form> + <a href="https://servo.org">Home</a> + </html> +</html> diff --git a/resources/resource_protocol/servo-color-negative-no-container.png b/resources/resource_protocol/servo-color-negative-no-container.png Binary files differnew file mode 100644 index 00000000000..d467254537d --- /dev/null +++ b/resources/resource_protocol/servo-color-negative-no-container.png |