mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
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>
This commit is contained in:
parent
a0ff57cea1
commit
1b48bd18aa
14 changed files with 455 additions and 72 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
108
ports/servoshell/desktop/protocols/resource.rs
Normal file
108
ports/servoshell/desktop/protocols/resource.rs
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
43
ports/servoshell/desktop/protocols/servo.rs
Normal file
43
ports/servoshell/desktop/protocols/servo.rs
Normal file
|
@ -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",
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,17 +66,12 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> {
|
|||
gamepad: Option<Gilrs>,
|
||||
haptic_effects: HashMap<usize, HapticEffect>,
|
||||
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<String>,
|
||||
url: Option<ServoUrl>,
|
||||
}
|
||||
|
||||
#[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<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 {
|
||||
pub need_present: bool,
|
||||
pub need_update: bool,
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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.
|
||||
|
|
57
resources/resource_protocol/newtab.css
Normal file
57
resources/resource_protocol/newtab.css
Normal file
|
@ -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;
|
||||
}
|
19
resources/resource_protocol/newtab.html
Normal file
19
resources/resource_protocol/newtab.html
Normal file
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
Loading…
Add table
Add a link
Reference in a new issue