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 00000000000..d467254537d Binary files /dev/null and b/resources/resource_protocol/servo-color-negative-no-container.png differ