mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
This does not (yet) upgrade ./rust-toolchain The warnings: * dead_code "field is never read" * redundant_semicolons "unnecessary trailing semicolon" * non_fmt_panic "panic message is not a string literal, this is no longer accepted in Rust 2021" * unstable_name_collisions "a method with this name may be added to the standard library in the future" * legacy_derive_helpers "derive helper attribute is used before it is introduced" https://github.com/rust-lang/rust/issues/79202
662 lines
28 KiB
Rust
662 lines
28 KiB
Rust
/* 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/. */
|
|
|
|
use crate::keyutils::{CMD_OR_ALT, CMD_OR_CONTROL};
|
|
use crate::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
|
use euclid::{Point2D, Vector2D};
|
|
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
|
use servo::compositing::windowing::{WebRenderDebugOption, WindowEvent};
|
|
use servo::embedder_traits::{
|
|
ContextMenuResult, EmbedderMsg, FilterPattern, PermissionRequest, PromptDefinition, PromptOrigin, PromptResult,
|
|
PermissionPrompt,
|
|
};
|
|
use servo::msg::constellation_msg::TopLevelBrowsingContextId as BrowserId;
|
|
use servo::msg::constellation_msg::TraversalDirection;
|
|
use servo::net_traits::pub_domains::is_reg_domain;
|
|
use servo::script_traits::TouchEventType;
|
|
use servo::servo_config::opts;
|
|
use servo::servo_config::pref;
|
|
use servo::servo_url::ServoUrl;
|
|
use servo::webrender_api::ScrollLocation;
|
|
use clipboard::{ClipboardContext, ClipboardProvider};
|
|
use std::env;
|
|
use std::fs::File;
|
|
use std::io::Write;
|
|
use std::mem;
|
|
use std::rc::Rc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo};
|
|
|
|
pub struct Browser<Window: WindowPortsMethods + ?Sized> {
|
|
current_url: Option<ServoUrl>,
|
|
/// id of the top level browsing context. It is unique as tabs
|
|
/// are not supported yet. None until created.
|
|
browser_id: Option<BrowserId>,
|
|
|
|
// A rudimentary stack of "tabs".
|
|
// EmbedderMsg::BrowserCreated will push onto it.
|
|
// EmbedderMsg::CloseBrowser will pop from it,
|
|
// and exit if it is empty afterwards.
|
|
browsers: Vec<BrowserId>,
|
|
|
|
title: Option<String>,
|
|
|
|
window: Rc<Window>,
|
|
event_queue: Vec<WindowEvent>,
|
|
clipboard_ctx: Option<ClipboardContext>,
|
|
shutdown_requested: bool,
|
|
}
|
|
|
|
impl<Window> Browser<Window>
|
|
where
|
|
Window: WindowPortsMethods + ?Sized,
|
|
{
|
|
pub fn new(window: Rc<Window>) -> Browser<Window> {
|
|
Browser {
|
|
title: None,
|
|
current_url: None,
|
|
browser_id: None,
|
|
browsers: Vec::new(),
|
|
window: window,
|
|
clipboard_ctx: match ClipboardContext::new() {
|
|
Ok(c) => Some(c),
|
|
Err(e) => {
|
|
warn!("Error creating clipboard context ({})", e);
|
|
None
|
|
},
|
|
},
|
|
event_queue: Vec::new(),
|
|
shutdown_requested: false,
|
|
}
|
|
}
|
|
|
|
pub fn get_events(&mut self) -> Vec<WindowEvent> {
|
|
mem::replace(&mut self.event_queue, Vec::new())
|
|
}
|
|
|
|
pub fn handle_window_events(&mut self, events: Vec<WindowEvent>) {
|
|
for event in events {
|
|
match event {
|
|
WindowEvent::Keyboard(key_event) => {
|
|
self.handle_key_from_window(key_event);
|
|
},
|
|
event => {
|
|
self.event_queue.push(event);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn shutdown_requested(&self) -> bool {
|
|
self.shutdown_requested
|
|
}
|
|
|
|
/// Handle key events before sending them to Servo.
|
|
fn handle_key_from_window(&mut self, key_event: KeyboardEvent) {
|
|
ShortcutMatcher::from_event(key_event.clone())
|
|
.shortcut(CMD_OR_CONTROL, 'R', || {
|
|
if let Some(id) = self.browser_id {
|
|
self.event_queue.push(WindowEvent::Reload(id));
|
|
}
|
|
})
|
|
.shortcut(CMD_OR_CONTROL, 'L', || {
|
|
let url: String = if let Some(ref current_url) = self.current_url {
|
|
current_url.to_string()
|
|
} else {
|
|
String::from("")
|
|
};
|
|
let title = "URL or search query";
|
|
let input = tinyfiledialogs::input_box(title, title, &tiny_dialog_escape(&url));
|
|
if let Some(input) = input {
|
|
if let Some(url) = sanitize_url(&input) {
|
|
if let Some(id) = self.browser_id {
|
|
self.event_queue.push(WindowEvent::LoadUrl(id, url));
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.shortcut(CMD_OR_CONTROL, 'Q', || {
|
|
self.event_queue.push(WindowEvent::Quit);
|
|
})
|
|
.shortcut(CMD_OR_CONTROL, 'P', || {
|
|
let rate = env::var("SAMPLING_RATE")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(10);
|
|
let duration = env::var("SAMPLING_DURATION")
|
|
.ok()
|
|
.and_then(|s| s.parse().ok())
|
|
.unwrap_or(10);
|
|
self.event_queue.push(WindowEvent::ToggleSamplingProfiler(
|
|
Duration::from_millis(rate),
|
|
Duration::from_secs(duration),
|
|
));
|
|
})
|
|
.shortcut(Modifiers::CONTROL, Key::F9, || {
|
|
self.event_queue.push(WindowEvent::CaptureWebRender)
|
|
})
|
|
.shortcut(Modifiers::CONTROL, Key::F10, || {
|
|
self.event_queue.push(WindowEvent::ToggleWebRenderDebug(
|
|
WebRenderDebugOption::RenderTargetDebug,
|
|
));
|
|
})
|
|
.shortcut(Modifiers::CONTROL, Key::F11, || {
|
|
self.event_queue.push(WindowEvent::ToggleWebRenderDebug(
|
|
WebRenderDebugOption::TextureCacheDebug,
|
|
));
|
|
})
|
|
.shortcut(Modifiers::CONTROL, Key::F12, || {
|
|
self.event_queue.push(WindowEvent::ToggleWebRenderDebug(
|
|
WebRenderDebugOption::Profiler,
|
|
));
|
|
})
|
|
.shortcut(CMD_OR_ALT, Key::ArrowRight, || {
|
|
if let Some(id) = self.browser_id {
|
|
let event = WindowEvent::Navigation(id, TraversalDirection::Forward(1));
|
|
self.event_queue.push(event);
|
|
}
|
|
})
|
|
.shortcut(CMD_OR_ALT, Key::ArrowLeft, || {
|
|
if let Some(id) = self.browser_id {
|
|
let event = WindowEvent::Navigation(id, TraversalDirection::Back(1));
|
|
self.event_queue.push(event);
|
|
}
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::Escape, || {
|
|
let state = self.window.get_fullscreen();
|
|
if state {
|
|
if let Some(id) = self.browser_id {
|
|
let event = WindowEvent::ExitFullScreen(id);
|
|
self.event_queue.push(event);
|
|
}
|
|
} else {
|
|
self.event_queue.push(WindowEvent::Quit);
|
|
}
|
|
})
|
|
.otherwise(|| self.platform_handle_key(key_event));
|
|
}
|
|
|
|
#[cfg(not(target_os = "win"))]
|
|
fn platform_handle_key(&mut self, key_event: KeyboardEvent) {
|
|
if let Some(id) = self.browser_id {
|
|
if let Some(event) = ShortcutMatcher::from_event(key_event.clone())
|
|
.shortcut(CMD_OR_CONTROL, '[', || {
|
|
WindowEvent::Navigation(id, TraversalDirection::Back(1))
|
|
})
|
|
.shortcut(CMD_OR_CONTROL, ']', || {
|
|
WindowEvent::Navigation(id, TraversalDirection::Forward(1))
|
|
})
|
|
.otherwise(|| WindowEvent::Keyboard(key_event))
|
|
{
|
|
self.event_queue.push(event)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "win")]
|
|
fn platform_handle_key(&mut self, _key_event: KeyboardEvent) {}
|
|
|
|
/// Handle key events after they have been handled by Servo.
|
|
fn handle_key_from_servo(&mut self, _: Option<BrowserId>, event: KeyboardEvent) {
|
|
ShortcutMatcher::from_event(event)
|
|
.shortcut(CMD_OR_CONTROL, '=', || {
|
|
self.event_queue.push(WindowEvent::Zoom(1.1))
|
|
})
|
|
.shortcut(CMD_OR_CONTROL, '+', || {
|
|
self.event_queue.push(WindowEvent::Zoom(1.1))
|
|
})
|
|
.shortcut(CMD_OR_CONTROL, '-', || {
|
|
self.event_queue.push(WindowEvent::Zoom(1.0 / 1.1))
|
|
})
|
|
.shortcut(CMD_OR_CONTROL, '0', || {
|
|
self.event_queue.push(WindowEvent::ResetZoom)
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::PageDown, || {
|
|
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
|
0.0,
|
|
-self.window.page_height() + 2.0 * LINE_HEIGHT,
|
|
));
|
|
self.scroll_window_from_key(scroll_location, TouchEventType::Move);
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::PageUp, || {
|
|
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
|
0.0,
|
|
self.window.page_height() - 2.0 * LINE_HEIGHT,
|
|
));
|
|
self.scroll_window_from_key(scroll_location, TouchEventType::Move);
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::Home, || {
|
|
self.scroll_window_from_key(ScrollLocation::Start, TouchEventType::Move);
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::End, || {
|
|
self.scroll_window_from_key(ScrollLocation::End, TouchEventType::Move);
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
|
|
self.scroll_window_from_key(
|
|
ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT)),
|
|
TouchEventType::Move,
|
|
);
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
|
|
self.scroll_window_from_key(
|
|
ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT)),
|
|
TouchEventType::Move,
|
|
);
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
|
|
self.scroll_window_from_key(
|
|
ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0)),
|
|
TouchEventType::Move,
|
|
);
|
|
})
|
|
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
|
|
self.scroll_window_from_key(
|
|
ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0)),
|
|
TouchEventType::Move,
|
|
);
|
|
});
|
|
}
|
|
|
|
fn scroll_window_from_key(&mut self, scroll_location: ScrollLocation, phase: TouchEventType) {
|
|
let event = WindowEvent::Scroll(scroll_location, Point2D::zero(), phase);
|
|
self.event_queue.push(event);
|
|
}
|
|
|
|
pub fn handle_servo_events(&mut self, events: Vec<(Option<BrowserId>, EmbedderMsg)>) {
|
|
for (browser_id, msg) in events {
|
|
match msg {
|
|
EmbedderMsg::Status(_status) => {
|
|
// FIXME: surface this status string in the UI somehow
|
|
},
|
|
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.len() > 0 => &**title,
|
|
_ => &fallback_title,
|
|
};
|
|
let title = format!("{} - Servo", title);
|
|
self.window.set_title(&title);
|
|
},
|
|
EmbedderMsg::MoveTo(point) => {
|
|
self.window.set_position(point);
|
|
},
|
|
EmbedderMsg::ResizeTo(size) => {
|
|
self.window.set_inner_size(size);
|
|
},
|
|
EmbedderMsg::Prompt(definition, origin) => {
|
|
let res = if opts::get().headless {
|
|
match definition {
|
|
PromptDefinition::Alert(_message, sender) => {
|
|
sender.send(())
|
|
}
|
|
PromptDefinition::YesNo(_message, sender) => {
|
|
sender.send(PromptResult::Primary)
|
|
}
|
|
PromptDefinition::OkCancel(_message, sender) => {
|
|
sender.send(PromptResult::Primary)
|
|
}
|
|
PromptDefinition::Input(_message, default, sender) => {
|
|
sender.send(Some(default.to_owned()))
|
|
}
|
|
}
|
|
} else {
|
|
thread::Builder::new()
|
|
.name("display alert dialog".to_owned())
|
|
.spawn(move || {
|
|
match definition {
|
|
PromptDefinition::Alert(mut message, sender) => {
|
|
if origin == PromptOrigin::Untrusted {
|
|
message = tiny_dialog_escape(&message);
|
|
}
|
|
tinyfiledialogs::message_box_ok(
|
|
"Alert!",
|
|
&message,
|
|
MessageBoxIcon::Warning,
|
|
);
|
|
sender.send(())
|
|
}
|
|
PromptDefinition::YesNo(mut message, sender) => {
|
|
if origin == PromptOrigin::Untrusted {
|
|
message = tiny_dialog_escape(&message);
|
|
}
|
|
let result = tinyfiledialogs::message_box_yes_no(
|
|
"", &message, MessageBoxIcon::Warning, YesNo::No,
|
|
);
|
|
sender.send(match result {
|
|
YesNo::Yes => PromptResult::Primary,
|
|
YesNo::No => PromptResult::Secondary,
|
|
})
|
|
}
|
|
PromptDefinition::OkCancel(mut message, sender) => {
|
|
if origin == PromptOrigin::Untrusted {
|
|
message = tiny_dialog_escape(&message);
|
|
}
|
|
let result = tinyfiledialogs::message_box_ok_cancel(
|
|
"", &message, MessageBoxIcon::Warning, OkCancel::Cancel,
|
|
);
|
|
sender.send(match result {
|
|
OkCancel::Ok => PromptResult::Primary,
|
|
OkCancel::Cancel => PromptResult::Secondary,
|
|
})
|
|
}
|
|
PromptDefinition::Input(mut message, mut default, sender) => {
|
|
if origin == PromptOrigin::Untrusted {
|
|
message = tiny_dialog_escape(&message);
|
|
default = tiny_dialog_escape(&default);
|
|
}
|
|
let result = tinyfiledialogs::input_box("", &message, &default);
|
|
sender.send(result)
|
|
}
|
|
}
|
|
})
|
|
.unwrap()
|
|
.join()
|
|
.expect("Thread spawning failed")
|
|
};
|
|
if let Err(e) = res {
|
|
let reason = format!("Failed to send Prompt response: {}", e);
|
|
self.event_queue
|
|
.push(WindowEvent::SendError(browser_id, reason));
|
|
}
|
|
},
|
|
EmbedderMsg::AllowUnload(sender) => {
|
|
// Always allow unload for now.
|
|
if let Err(e) = sender.send(true) {
|
|
let reason = format!("Failed to send AllowUnload response: {}", e);
|
|
self.event_queue
|
|
.push(WindowEvent::SendError(browser_id, reason));
|
|
}
|
|
},
|
|
EmbedderMsg::AllowNavigationRequest(pipeline_id, _url) => {
|
|
if let Some(_browser_id) = browser_id {
|
|
self.event_queue
|
|
.push(WindowEvent::AllowNavigationResponse(pipeline_id, true));
|
|
}
|
|
},
|
|
EmbedderMsg::AllowOpeningBrowser(response_chan) => {
|
|
// Note: would be a place to handle pop-ups config.
|
|
// see Step 7 of #the-rules-for-choosing-a-browsing-context-given-a-browsing-context-name
|
|
if let Err(e) = response_chan.send(true) {
|
|
warn!("Failed to send AllowOpeningBrowser response: {}", e);
|
|
};
|
|
},
|
|
EmbedderMsg::BrowserCreated(new_browser_id) => {
|
|
// TODO: properly handle a new "tab"
|
|
self.browsers.push(new_browser_id);
|
|
if self.browser_id.is_none() {
|
|
self.browser_id = Some(new_browser_id);
|
|
} else {
|
|
error!("Multiple top level browsing contexts not supported yet.");
|
|
}
|
|
self.event_queue
|
|
.push(WindowEvent::SelectBrowser(new_browser_id));
|
|
},
|
|
EmbedderMsg::Keyboard(key_event) => {
|
|
self.handle_key_from_servo(browser_id, key_event);
|
|
},
|
|
EmbedderMsg::GetClipboardContents(sender) => {
|
|
let contents = match self.clipboard_ctx {
|
|
Some(ref mut ctx) => {
|
|
match ctx.get_contents() {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
warn!("Error getting clipboard contents ({}), defaulting to empty string", e);
|
|
"".to_owned()
|
|
},
|
|
}
|
|
},
|
|
None => "".to_owned(),
|
|
};
|
|
if let Err(e) = sender.send(contents) {
|
|
warn!("Failed to send clipboard ({})", e);
|
|
}
|
|
}
|
|
EmbedderMsg::SetClipboardContents(text) => {
|
|
if let Some(ref mut ctx) = self.clipboard_ctx {
|
|
if let Err(e) = ctx.set_contents(text) {
|
|
warn!("Error setting clipboard contents ({})", e);
|
|
}
|
|
}
|
|
}
|
|
EmbedderMsg::SetCursor(cursor) => {
|
|
self.window.set_cursor(cursor);
|
|
},
|
|
EmbedderMsg::NewFavicon(_url) => {
|
|
// FIXME: show favicons in the UI somehow
|
|
},
|
|
EmbedderMsg::HeadParsed => {
|
|
// FIXME: surface the loading state in the UI somehow
|
|
},
|
|
EmbedderMsg::HistoryChanged(urls, current) => {
|
|
self.current_url = Some(urls[current].clone());
|
|
},
|
|
EmbedderMsg::SetFullscreenState(state) => {
|
|
self.window.set_fullscreen(state);
|
|
},
|
|
EmbedderMsg::LoadStart => {
|
|
// FIXME: surface the loading state in the UI somehow
|
|
},
|
|
EmbedderMsg::LoadComplete => {
|
|
// FIXME: surface the loading state in the UI somehow
|
|
},
|
|
EmbedderMsg::CloseBrowser => {
|
|
// TODO: close the appropriate "tab".
|
|
let _ = self.browsers.pop();
|
|
if let Some(prev_browser_id) = self.browsers.last() {
|
|
self.browser_id = Some(*prev_browser_id);
|
|
self.event_queue
|
|
.push(WindowEvent::SelectBrowser(*prev_browser_id));
|
|
} else {
|
|
self.event_queue.push(WindowEvent::Quit);
|
|
}
|
|
},
|
|
EmbedderMsg::Shutdown => {
|
|
self.shutdown_requested = true;
|
|
},
|
|
EmbedderMsg::Panic(_reason, _backtrace) => {},
|
|
EmbedderMsg::GetSelectedBluetoothDevice(devices, sender) => {
|
|
let selected = platform_get_selected_devices(devices);
|
|
if let Err(e) = sender.send(selected) {
|
|
let reason =
|
|
format!("Failed to send GetSelectedBluetoothDevice response: {}", e);
|
|
self.event_queue.push(WindowEvent::SendError(None, reason));
|
|
};
|
|
},
|
|
EmbedderMsg::SelectFiles(patterns, multiple_files, sender) => {
|
|
let res = match (
|
|
opts::get().headless,
|
|
get_selected_files(patterns, multiple_files),
|
|
) {
|
|
(true, _) | (false, None) => sender.send(None),
|
|
(false, Some(files)) => sender.send(Some(files)),
|
|
};
|
|
if let Err(e) = res {
|
|
let reason = format!("Failed to send SelectFiles response: {}", e);
|
|
self.event_queue.push(WindowEvent::SendError(None, reason));
|
|
};
|
|
},
|
|
EmbedderMsg::PromptPermission(prompt, sender) => {
|
|
let permission_state = prompt_user(prompt);
|
|
let _ = sender.send(permission_state);
|
|
}
|
|
EmbedderMsg::ShowIME(_kind, _text, _rect) => {
|
|
debug!("ShowIME received");
|
|
},
|
|
EmbedderMsg::HideIME => {
|
|
debug!("HideIME received");
|
|
},
|
|
EmbedderMsg::ReportProfile(bytes) => {
|
|
let filename = env::var("PROFILE_OUTPUT").unwrap_or("samples.json".to_string());
|
|
let result = File::create(&filename).and_then(|mut f| f.write_all(&bytes));
|
|
if let Err(e) = result {
|
|
error!("Failed to store profile: {}", e);
|
|
}
|
|
},
|
|
EmbedderMsg::MediaSessionEvent(_) => {
|
|
debug!("MediaSessionEvent received");
|
|
// TODO(ferjm): MediaSession support for winit based browsers.
|
|
},
|
|
EmbedderMsg::OnDevtoolsStarted(port, _token) => {
|
|
match port {
|
|
Ok(p) => info!("Devtools Server running on port {}", p),
|
|
Err(()) => error!("Error running devtools server"),
|
|
}
|
|
},
|
|
EmbedderMsg::ShowContextMenu(sender, ..) => {
|
|
let _ = sender.send(ContextMenuResult::Ignored);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn prompt_user(prompt: PermissionPrompt) -> PermissionRequest {
|
|
if opts::get().headless {
|
|
return PermissionRequest::Denied;
|
|
}
|
|
|
|
let message = match prompt {
|
|
PermissionPrompt::Request(permission_name) => {
|
|
format!("Do you want to grant permission for {:?}?", permission_name)
|
|
},
|
|
PermissionPrompt::Insecure(permission_name) => {
|
|
format!(
|
|
"The {:?} feature is only safe to use in secure context, but servo can't guarantee\n\
|
|
that the current context is secure. Do you want to proceed and grant permission?",
|
|
permission_name
|
|
)
|
|
},
|
|
};
|
|
|
|
match tinyfiledialogs::message_box_yes_no(
|
|
"Permission request dialog",
|
|
&message,
|
|
MessageBoxIcon::Question,
|
|
YesNo::No,
|
|
) {
|
|
YesNo::Yes => PermissionRequest::Granted,
|
|
YesNo::No => PermissionRequest::Denied,
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
fn prompt_user(_prompt: PermissionPrompt) -> PermissionRequest {
|
|
// TODO popup only supported on linux
|
|
PermissionRequest::Denied
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
|
|
let picker_name = "Choose a device";
|
|
|
|
thread::Builder::new()
|
|
.name(picker_name.to_owned())
|
|
.spawn(move || {
|
|
let dialog_rows: Vec<&str> = devices.iter().map(|s| s.as_ref()).collect();
|
|
let dialog_rows: Option<&[&str]> = Some(dialog_rows.as_slice());
|
|
|
|
match tinyfiledialogs::list_dialog("Choose a device", &["Id", "Name"], dialog_rows) {
|
|
Some(device) => {
|
|
// The device string format will be "Address|Name". We need the first part of it.
|
|
device.split("|").next().map(|s| s.to_string())
|
|
},
|
|
None => None,
|
|
}
|
|
})
|
|
.unwrap()
|
|
.join()
|
|
.expect("Thread spawning failed")
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
fn platform_get_selected_devices(devices: Vec<String>) -> Option<String> {
|
|
for device in devices {
|
|
if let Some(address) = device.split("|").next().map(|s| s.to_string()) {
|
|
return Some(address);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn get_selected_files(patterns: Vec<FilterPattern>, multiple_files: bool) -> Option<Vec<String>> {
|
|
let picker_name = if multiple_files {
|
|
"Pick files"
|
|
} else {
|
|
"Pick a file"
|
|
};
|
|
thread::Builder::new()
|
|
.name(picker_name.to_owned())
|
|
.spawn(move || {
|
|
let mut filters = vec![];
|
|
for p in patterns {
|
|
let s = "*.".to_string() + &p.0;
|
|
filters.push(tiny_dialog_escape(&s))
|
|
}
|
|
let filter_ref = &(filters.iter().map(|s| s.as_str()).collect::<Vec<&str>>()[..]);
|
|
let filter_opt = if filters.len() > 0 {
|
|
Some((filter_ref, ""))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if multiple_files {
|
|
tinyfiledialogs::open_file_dialog_multi(picker_name, "", filter_opt)
|
|
} else {
|
|
let file = tinyfiledialogs::open_file_dialog(picker_name, "", filter_opt);
|
|
file.map(|x| vec![x])
|
|
}
|
|
})
|
|
.unwrap()
|
|
.join()
|
|
.expect("Thread spawning failed")
|
|
}
|
|
|
|
fn sanitize_url(request: &str) -> Option<ServoUrl> {
|
|
let request = request.trim();
|
|
ServoUrl::parse(&request)
|
|
.ok()
|
|
.or_else(|| {
|
|
if request.contains('/') || is_reg_domain(request) {
|
|
ServoUrl::parse(&format!("https://{}", request)).ok()
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.or_else(|| {
|
|
let url = pref!(shell.searchpage).replace("%s", request);
|
|
ServoUrl::parse(&url).ok()
|
|
})
|
|
}
|
|
|
|
// This is a mitigation for #25498, not a verified solution.
|
|
// There may be codepaths in tinyfiledialog.c that this is
|
|
// inadquate against, as it passes the string via shell to
|
|
// different programs depending on what the user has installed.
|
|
#[cfg(target_os = "linux")]
|
|
fn tiny_dialog_escape(raw: &str) -> String {
|
|
let s:String = raw.chars()
|
|
.filter_map(|c| match c {
|
|
'\n' => Some('\n'),
|
|
'\0' ..= '\x1f' => None,
|
|
'<' => Some('\u{FF1C}'),
|
|
'>' => Some('\u{FF1E}'),
|
|
'&' => Some('\u{FF06}'),
|
|
_ => Some(c)
|
|
})
|
|
.collect();
|
|
return shellwords::escape(&s);
|
|
}
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
fn tiny_dialog_escape(raw: &str) -> String {
|
|
raw.to_string()
|
|
}
|