From 1b48bd18aa855cc966869dd81530aa0da3eea4f3 Mon Sep 17 00:00:00 2001 From: webbeef Date: Tue, 27 Aug 2024 13:17:33 -0700 Subject: [PATCH] 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 --- Cargo.lock | 2 + components/shared/net/response.rs | 4 + ports/servoshell/Cargo.toml | 3 +- ports/servoshell/desktop/app.rs | 4 +- ports/servoshell/desktop/embedder.rs | 4 +- ports/servoshell/desktop/minibrowser.rs | 68 +++++- ports/servoshell/desktop/protocols/mod.rs | 2 + .../servoshell/desktop/protocols/resource.rs | 108 +++++++++ ports/servoshell/desktop/protocols/servo.rs | 43 ++++ ports/servoshell/desktop/webview.rs | 211 +++++++++++++----- ports/servoshell/resources.rs | 2 +- resources/resource_protocol/newtab.css | 57 +++++ resources/resource_protocol/newtab.html | 19 ++ .../servo-color-negative-no-container.png | Bin 0 -> 30171 bytes 14 files changed, 455 insertions(+), 72 deletions(-) create mode 100644 ports/servoshell/desktop/protocols/resource.rs create mode 100644 ports/servoshell/desktop/protocols/servo.rs create mode 100644 resources/resource_protocol/newtab.css create mode 100644 resources/resource_protocol/newtab.html create mode 100644 resources/resource_protocol/servo-color-negative-no-container.png 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>(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, ) { 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 /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 + 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 + 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 + 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 { - current_url: Option, - current_url_string: Option, status_text: Option, /// List of top-level browsing contexts. @@ -56,7 +55,10 @@ pub struct WebViewManager { /// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred. focused_webview_id: Option, - title: Option, + /// 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, window: Rc, event_queue: Vec, @@ -64,17 +66,12 @@ pub struct WebViewManager { gamepad: Option, haptic_effects: HashMap, shutdown_requested: bool, - load_status: LoadStatus, } -#[derive(Debug)] -pub struct WebView { - pub rect: DeviceRect, -} - -pub struct ServoEventResponse { - pub need_present: bool, - pub need_update: bool, +#[derive(Clone, Default)] +struct WebViewPreloadData { + title: Option, + url: Option, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -84,6 +81,33 @@ pub enum LoadStatus { LoadComplete, } +// The state of each Tab/WebView +#[derive(Debug)] +pub struct WebView { + pub rect: DeviceRect, + pub title: Option, + pub url: Option, + 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 { + pub need_present: bool, + pub need_update: bool, +} + pub struct HapticEffect { pub effect: Effect, pub sender: IpcSender, @@ -95,13 +119,11 @@ where { pub fn new(window: Rc) -> WebViewManager { 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 { - 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 { 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 { + 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 { @@ -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) { 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, 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 @@ + + + + + + Servo - New Tab + + + + + + +
+ + +
+ Home + + diff --git a/resources/resource_protocol/servo-color-negative-no-container.png b/resources/resource_protocol/servo-color-negative-no-container.png new file mode 100644 index 0000000000000000000000000000000000000000..d467254537d2172c30138545c14dfa25d347b1d9 GIT binary patch literal 30171 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 Ys+sJs+~jJ3*IIF)YEM)vlx<)AAKaoGvj6}9 literal 0 HcmV?d00001