mirror of
https://github.com/servo/servo.git
synced 2025-08-12 17:05:33 +01:00
Rename ports/winit package to servoshell (#30163)
* rename winit package to servoshell * revert previous changes and rename only package
This commit is contained in:
parent
cc4fe4981f
commit
66567faeb2
26 changed files with 39 additions and 39 deletions
77
ports/servoshell/Cargo.toml
Normal file
77
ports/servoshell/Cargo.toml
Normal file
|
@ -0,0 +1,77 @@
|
|||
[package]
|
||||
|
||||
name = "servoshell"
|
||||
version = "0.0.1"
|
||||
authors = ["The Servo Project Developers"]
|
||||
license = "MPL-2.0"
|
||||
edition = "2018"
|
||||
build = "build.rs"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "servo"
|
||||
path = "main.rs"
|
||||
bench = false
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "8.0.0", features = ["git", "gitcl"] }
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winres = "0.1"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
cc = "1.0"
|
||||
|
||||
[package.metadata.winres]
|
||||
FileDescription = "Servo"
|
||||
LegalCopyright = "© The Servo Project Developers"
|
||||
OriginalFilename = "servo.exe"
|
||||
ProductName = "Servo"
|
||||
|
||||
[features]
|
||||
debugmozjs = ["libservo/debugmozjs"]
|
||||
default = ["max_log_level", "native-bluetooth", "webdriver"]
|
||||
jitspew = ["libservo/jitspew"]
|
||||
js_backtrace = ["libservo/js_backtrace"]
|
||||
max_log_level = ["log/release_max_level_info"]
|
||||
media-dummy = ["libservo/media-dummy"]
|
||||
media-gstreamer = ["libservo/media-gstreamer"]
|
||||
native-bluetooth = ["libservo/native-bluetooth"]
|
||||
no-wgl = ["libservo/no-wgl"]
|
||||
profilemozjs = ["libservo/profilemozjs"]
|
||||
refcell_backtrace = ["libservo/refcell_backtrace"]
|
||||
webdriver = ["libservo/webdriver"]
|
||||
webgl_backtrace = ["libservo/webgl_backtrace"]
|
||||
xr-profile = ["libservo/xr-profile"]
|
||||
|
||||
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||
backtrace = { workspace = true }
|
||||
clipboard = "0.5"
|
||||
egui = "0.22.0"
|
||||
egui_glow = { version = "0.22.0", features = ["winit"] }
|
||||
egui-winit = { version = "0.22.0", default-features = false, features = ["clipboard", "wayland"] }
|
||||
euclid = { workspace = true }
|
||||
getopts = { workspace = true }
|
||||
gleam = "0.12"
|
||||
glow = "0.12.2"
|
||||
keyboard-types = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
libservo = { path = "../../components/servo" }
|
||||
log = { workspace = true }
|
||||
raw-window-handle = "0.5"
|
||||
servo-media = { git = "https://github.com/servo/media" }
|
||||
shellwords = "1.0.0"
|
||||
surfman = { workspace = true, features = ["sm-x11", "sm-raw-window-handle"] }
|
||||
tinyfiledialogs = "3.0"
|
||||
webxr = { git = "https://github.com/servo/webxr", features = ["ipc", "glwindow", "headless"] }
|
||||
winit = "0.28.6"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "windows"))'.dependencies]
|
||||
image = { workspace = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
|
||||
sig = "1.0"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { workspace = true, features = ["wingdi", "winuser", "winnt", "winbase", "processenv", "namedpipeapi", "ntdef", "minwindef", "handleapi", "debugapi"] }
|
357
ports/servoshell/app.rs
Normal file
357
ports/servoshell/app.rs
Normal file
|
@ -0,0 +1,357 @@
|
|||
/* 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/. */
|
||||
|
||||
//! Application entry point, runs the event loop.
|
||||
|
||||
use crate::browser::Browser;
|
||||
use crate::embedder::EmbedderCallbacks;
|
||||
use crate::events_loop::{EventsLoop, WakerEvent};
|
||||
use crate::minibrowser::Minibrowser;
|
||||
use crate::window_trait::WindowPortsMethods;
|
||||
use crate::{headed_window, headless_window};
|
||||
use gleam::gl;
|
||||
use winit::window::WindowId;
|
||||
use winit::event_loop::EventLoopWindowTarget;
|
||||
use servo::compositing::windowing::EmbedderEvent;
|
||||
use servo::config::opts::{self, parse_url_or_filename};
|
||||
use servo::servo_config::pref;
|
||||
use servo::servo_url::ServoUrl;
|
||||
use servo::Servo;
|
||||
use std::cell::{Cell, RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
use std::rc::Rc;
|
||||
use surfman::GLApi;
|
||||
use webxr::glwindow::GlWindowDiscovery;
|
||||
|
||||
pub struct App {
|
||||
servo: Option<Servo<dyn WindowPortsMethods>>,
|
||||
browser: RefCell<Browser<dyn WindowPortsMethods>>,
|
||||
event_queue: RefCell<Vec<EmbedderEvent>>,
|
||||
suspended: Cell<bool>,
|
||||
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
|
||||
minibrowser: Option<RefCell<Minibrowser>>,
|
||||
}
|
||||
|
||||
/// Action to be taken by the caller of [`App::handle_events`].
|
||||
enum PumpResult {
|
||||
Shutdown,
|
||||
Present,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn run(
|
||||
no_native_titlebar: bool,
|
||||
device_pixels_per_px: Option<f32>,
|
||||
user_agent: Option<String>,
|
||||
) {
|
||||
let events_loop = EventsLoop::new(opts::get().headless, opts::get().output_file.is_some());
|
||||
|
||||
// Implements window methods, used by compositor.
|
||||
let window = if opts::get().headless {
|
||||
headless_window::Window::new(opts::get().initial_window_size, device_pixels_per_px)
|
||||
} else {
|
||||
Rc::new(headed_window::Window::new(
|
||||
opts::get().initial_window_size,
|
||||
&events_loop,
|
||||
no_native_titlebar,
|
||||
device_pixels_per_px,
|
||||
))
|
||||
};
|
||||
|
||||
// Handle browser state.
|
||||
let browser = Browser::new(window.clone());
|
||||
|
||||
let mut app = App {
|
||||
event_queue: RefCell::new(vec![]),
|
||||
browser: RefCell::new(browser),
|
||||
servo: None,
|
||||
suspended: Cell::new(false),
|
||||
windows: HashMap::new(),
|
||||
minibrowser: None,
|
||||
};
|
||||
|
||||
if opts::get().minibrowser && window.winit_window().is_some() {
|
||||
// Make sure the gl context is made current.
|
||||
let webrender_surfman = window.webrender_surfman();
|
||||
let webrender_gl = match webrender_surfman.connection().gl_api() {
|
||||
GLApi::GL => unsafe { gl::GlFns::load_with(|s| webrender_surfman.get_proc_address(s)) },
|
||||
GLApi::GLES => unsafe {
|
||||
gl::GlesFns::load_with(|s| webrender_surfman.get_proc_address(s))
|
||||
},
|
||||
};
|
||||
webrender_surfman.make_gl_context_current().unwrap();
|
||||
debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR);
|
||||
|
||||
// Set up egui context for minibrowser ui
|
||||
// Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs
|
||||
app.minibrowser = Some(Minibrowser::new(&webrender_surfman, &events_loop).into());
|
||||
}
|
||||
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
minibrowser.update(window.winit_window().unwrap());
|
||||
window.set_toolbar_height(minibrowser.toolbar_height.get());
|
||||
}
|
||||
|
||||
let ev_waker = events_loop.create_event_loop_waker();
|
||||
events_loop.run_forever(move |e, w, control_flow| {
|
||||
match e {
|
||||
winit::event::Event::NewEvents(winit::event::StartCause::Init) => {
|
||||
let surfman = window.webrender_surfman();
|
||||
|
||||
let xr_discovery = if pref!(dom.webxr.glwindow.enabled) && ! opts::get().headless {
|
||||
let window = window.clone();
|
||||
// This should be safe because run_forever does, in fact,
|
||||
// run forever. The event loop window target doesn't get
|
||||
// moved, and does outlast this closure, and we won't
|
||||
// ever try to make use of it once shutdown begins and
|
||||
// it stops being valid.
|
||||
let w = unsafe {
|
||||
std::mem::transmute::<
|
||||
&EventLoopWindowTarget<WakerEvent>,
|
||||
&'static EventLoopWindowTarget<WakerEvent>
|
||||
>(w.unwrap())
|
||||
};
|
||||
let factory = Box::new(move || Ok(window.new_glwindow(w)));
|
||||
Some(GlWindowDiscovery::new(
|
||||
surfman.connection(),
|
||||
surfman.adapter(),
|
||||
surfman.context_attributes(),
|
||||
factory,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let window = window.clone();
|
||||
// Implements embedder methods, used by libservo and constellation.
|
||||
let embedder = Box::new(EmbedderCallbacks::new(
|
||||
ev_waker.clone(),
|
||||
xr_discovery,
|
||||
));
|
||||
|
||||
let servo_data = Servo::new(embedder, window.clone(), user_agent.clone());
|
||||
let mut servo = servo_data.servo;
|
||||
|
||||
// If we have a minibrowser, ask the compositor to notify us when a new frame
|
||||
// is ready to present, so that we can paint the minibrowser then present.
|
||||
servo.set_external_present(app.minibrowser.is_some());
|
||||
|
||||
servo.handle_events(vec![EmbedderEvent::NewBrowser(get_default_url(), servo_data.browser_id)]);
|
||||
servo.setup_logging();
|
||||
|
||||
app.windows.insert(window.id(), window.clone());
|
||||
app.servo = Some(servo);
|
||||
}
|
||||
|
||||
// TODO does windows still need this workaround?
|
||||
// https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs#L203
|
||||
// winit::event::Event::RedrawEventsCleared => todo!(),
|
||||
winit::event::Event::RedrawRequested(_) => {
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
minibrowser.update(window.winit_window().unwrap());
|
||||
|
||||
// Tell Servo to repaint, which will in turn allow us to repaint the
|
||||
// minibrowser and present a complete frame without partial updates.
|
||||
app.event_queue.borrow_mut().push(EmbedderEvent::Refresh);
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// If self.servo is None here, it means that we're in the process of shutting down,
|
||||
// let's ignore events.
|
||||
if app.servo.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the event
|
||||
let mut consumed = false;
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
if let winit::event::Event::WindowEvent { ref event, .. } = e {
|
||||
let response = minibrowser.context.on_event(&event);
|
||||
if response.repaint {
|
||||
// Request a redraw event that will in turn trigger a minibrowser update.
|
||||
// This allows us to coalesce minibrowser updates across multiple events.
|
||||
window.winit_window().unwrap().request_redraw();
|
||||
}
|
||||
|
||||
// TODO how do we handle the tab key? (see doc for consumed)
|
||||
consumed = response.consumed;
|
||||
}
|
||||
}
|
||||
if !consumed {
|
||||
app.queue_embedder_events_for_winit_event(e);
|
||||
}
|
||||
|
||||
let animating = app.is_animating();
|
||||
|
||||
// Block until the window gets an event
|
||||
if !animating || app.suspended.get() {
|
||||
*control_flow = winit::event_loop::ControlFlow::Wait;
|
||||
} else {
|
||||
*control_flow = winit::event_loop::ControlFlow::Poll;
|
||||
}
|
||||
|
||||
// Consume and handle any events from the Minibrowser.
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
let browser = &mut app.browser.borrow_mut();
|
||||
let app_event_queue = &mut app.event_queue.borrow_mut();
|
||||
minibrowser.queue_embedder_events_for_minibrowser_events(browser, app_event_queue);
|
||||
if minibrowser.update_location_in_toolbar(browser) {
|
||||
// Update the minibrowser immediately. While we could update by requesting a
|
||||
// redraw, doing so would delay the location update by two frames.
|
||||
minibrowser.update(window.winit_window().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
match app.handle_events() {
|
||||
Some(PumpResult::Shutdown) => {
|
||||
*control_flow = winit::event_loop::ControlFlow::Exit;
|
||||
app.servo.take().unwrap().deinit();
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
minibrowser.context.destroy();
|
||||
}
|
||||
},
|
||||
Some(PumpResult::Present) => {
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
minibrowser.paint(window.winit_window().unwrap());
|
||||
}
|
||||
app.servo.as_mut().unwrap().present();
|
||||
},
|
||||
None => {},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn is_animating(&self) -> bool {
|
||||
self.windows.iter().any(|(_, window)| window.is_animating())
|
||||
}
|
||||
|
||||
fn get_events(&self) -> Vec<EmbedderEvent> {
|
||||
std::mem::take(&mut *self.event_queue.borrow_mut())
|
||||
}
|
||||
|
||||
/// Processes the given winit Event, possibly converting it to an [EmbedderEvent] and
|
||||
/// routing that to the App or relevant Window event queues.
|
||||
fn queue_embedder_events_for_winit_event(&self, event: winit::event::Event<'_, WakerEvent>) {
|
||||
match event {
|
||||
// App level events
|
||||
winit::event::Event::Suspended => {
|
||||
self.suspended.set(true);
|
||||
},
|
||||
winit::event::Event::Resumed => {
|
||||
self.suspended.set(false);
|
||||
self.event_queue.borrow_mut().push(EmbedderEvent::Idle);
|
||||
},
|
||||
winit::event::Event::UserEvent(_) => {
|
||||
self.event_queue.borrow_mut().push(EmbedderEvent::Idle);
|
||||
},
|
||||
winit::event::Event::DeviceEvent { .. } => {},
|
||||
|
||||
winit::event::Event::RedrawRequested(_) => {
|
||||
self.event_queue.borrow_mut().push(EmbedderEvent::Idle);
|
||||
},
|
||||
|
||||
// Window level events
|
||||
winit::event::Event::WindowEvent {
|
||||
window_id, event, ..
|
||||
} => {
|
||||
match self.windows.get(&window_id) {
|
||||
None => {
|
||||
warn!("Got an event from unknown window");
|
||||
},
|
||||
Some(window) => {
|
||||
window.queue_embedder_events_for_winit_event(event);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
winit::event::Event::LoopDestroyed |
|
||||
winit::event::Event::NewEvents(..) |
|
||||
winit::event::Event::MainEventsCleared |
|
||||
winit::event::Event::RedrawEventsCleared => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Pumps events and messages between the embedder and Servo, where embedder events flow
|
||||
/// towards Servo and embedder messages flow away from Servo, and also runs the compositor.
|
||||
///
|
||||
/// As the embedder, we push embedder events through our event queues, from the App queue and
|
||||
/// Window queues to the Browser queue, and from the Browser queue to Servo. We receive and
|
||||
/// collect embedder messages from the various Servo components, then take them out of the
|
||||
/// Servo interface so that the Browser can handle them.
|
||||
fn handle_events(&mut self) -> Option<PumpResult> {
|
||||
let mut browser = self.browser.borrow_mut();
|
||||
|
||||
// FIXME:
|
||||
// As of now, we support only one browser (self.browser)
|
||||
// but have multiple windows (dom.webxr.glwindow). We forward
|
||||
// the events of all the windows combined to that single
|
||||
// browser instance. Pressing the "a" key on the glwindow
|
||||
// will send a key event to the servo window.
|
||||
|
||||
// Take any outstanding embedder events from the App and its Windows.
|
||||
let mut embedder_events = self.get_events();
|
||||
for (_win_id, window) in &self.windows {
|
||||
embedder_events.extend(window.get_events());
|
||||
}
|
||||
|
||||
// Catch some keyboard events, and push the rest onto the Browser event queue.
|
||||
browser.handle_window_events(embedder_events);
|
||||
|
||||
// Take any new embedder messages from Servo itself.
|
||||
let mut embedder_messages = self.servo.as_mut().unwrap().get_events();
|
||||
let mut need_resize = false;
|
||||
let mut need_present = false;
|
||||
loop {
|
||||
// Consume and handle those embedder messages.
|
||||
need_present |= browser.handle_servo_events(embedder_messages);
|
||||
|
||||
// Route embedder events from the Browser to the relevant Servo components,
|
||||
// receives and collects embedder messages from various Servo components,
|
||||
// and runs the compositor.
|
||||
need_resize |= self.servo.as_mut().unwrap().handle_events(browser.get_events());
|
||||
if browser.shutdown_requested() {
|
||||
return Some(PumpResult::Shutdown);
|
||||
}
|
||||
|
||||
// Take any new embedder messages from Servo itself.
|
||||
embedder_messages = self.servo.as_mut().unwrap().get_events();
|
||||
if embedder_messages.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if need_resize {
|
||||
self.servo.as_mut().unwrap().repaint_synchronously();
|
||||
need_present = true;
|
||||
}
|
||||
if need_present {
|
||||
Some(PumpResult::Present)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn minibrowser(&self) -> Option<RefMut<Minibrowser>> {
|
||||
self.minibrowser.as_ref().map(|x| x.borrow_mut())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_default_url() -> ServoUrl {
|
||||
// If the url is not provided, we fallback to the homepage in prefs,
|
||||
// or a blank page in case the homepage is not set either.
|
||||
let cwd = env::current_dir().unwrap();
|
||||
let cmdline_url = opts::get().url.clone();
|
||||
let pref_url = {
|
||||
let homepage_url = pref!(shell.homepage);
|
||||
parse_url_or_filename(&cwd, &homepage_url).ok()
|
||||
};
|
||||
let blank_url = ServoUrl::parse("about:blank").ok();
|
||||
|
||||
cmdline_url.or(pref_url).or(blank_url).unwrap()
|
||||
}
|
105
ports/servoshell/backtrace.rs
Normal file
105
ports/servoshell/backtrace.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
/* 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/. */
|
||||
|
||||
//! Similar to `println!("{:?}", Backtrace::new())`, but doesn’t allocate.
|
||||
//!
|
||||
//! Seems to fix some deadlocks: https://github.com/servo/servo/issues/24881
|
||||
//!
|
||||
//! FIXME: if/when a future version of the `backtrace` crate has
|
||||
//! https://github.com/rust-lang/backtrace-rs/pull/265, use that instead.
|
||||
|
||||
use backtrace::{BytesOrWideString, PrintFmt};
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
#[inline(never)]
|
||||
pub(crate) fn print(w: &mut dyn std::io::Write) -> Result<(), std::io::Error> {
|
||||
write!(
|
||||
w,
|
||||
"{:?}",
|
||||
Print {
|
||||
print_fn_address: print as usize,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
struct Print {
|
||||
print_fn_address: usize,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Print {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// Safety: we’re in a signal handler that is about to call `libc::_exit`.
|
||||
// Potential data races from using `*_unsynchronized` functions are perhaps
|
||||
// less bad than potential deadlocks?
|
||||
unsafe {
|
||||
let mut print_fn_frame = 0;
|
||||
let mut frame_count = 0;
|
||||
backtrace::trace_unsynchronized(|frame| {
|
||||
let found = frame.symbol_address() as usize == self.print_fn_address;
|
||||
if found {
|
||||
print_fn_frame = frame_count;
|
||||
}
|
||||
frame_count += 1;
|
||||
!found
|
||||
});
|
||||
|
||||
let mode = PrintFmt::Short;
|
||||
let mut p = print_path;
|
||||
let mut f = backtrace::BacktraceFmt::new(fmt, mode, &mut p);
|
||||
f.add_context()?;
|
||||
let mut result = Ok(());
|
||||
let mut frame_count = 0;
|
||||
backtrace::trace_unsynchronized(|frame| {
|
||||
let skip = frame_count < print_fn_frame;
|
||||
frame_count += 1;
|
||||
if skip {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mut frame_fmt = f.frame();
|
||||
let mut any_symbol = false;
|
||||
backtrace::resolve_frame_unsynchronized(frame, |symbol| {
|
||||
any_symbol = true;
|
||||
if let Err(e) = frame_fmt.symbol(frame, symbol) {
|
||||
result = Err(e)
|
||||
}
|
||||
});
|
||||
if !any_symbol {
|
||||
if let Err(e) = frame_fmt.print_raw(frame.ip(), None, None, None) {
|
||||
result = Err(e)
|
||||
}
|
||||
}
|
||||
result.is_ok()
|
||||
});
|
||||
result?;
|
||||
f.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_path(fmt: &mut fmt::Formatter<'_>, path: BytesOrWideString<'_>) -> fmt::Result {
|
||||
match path {
|
||||
BytesOrWideString::Bytes(mut bytes) => loop {
|
||||
match std::str::from_utf8(bytes) {
|
||||
Ok(s) => {
|
||||
fmt.write_str(s)?;
|
||||
break;
|
||||
},
|
||||
Err(err) => {
|
||||
fmt.write_char(std::char::REPLACEMENT_CHARACTER)?;
|
||||
match err.error_len() {
|
||||
Some(len) => bytes = &bytes[err.valid_up_to() + len..],
|
||||
None => break,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
BytesOrWideString::Wide(wide) => {
|
||||
for c in std::char::decode_utf16(wide.iter().cloned()) {
|
||||
fmt.write_char(c.unwrap_or(std::char::REPLACEMENT_CHARACTER))?
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
680
ports/servoshell/browser.rs
Normal file
680
ports/servoshell/browser.rs
Normal file
|
@ -0,0 +1,680 @@
|
|||
/* 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 clipboard::{ClipboardContext, ClipboardProvider};
|
||||
use euclid::{Point2D, Vector2D};
|
||||
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
||||
use servo::compositing::windowing::{WebRenderDebugOption, EmbedderEvent};
|
||||
use servo::embedder_traits::{
|
||||
ContextMenuResult, EmbedderMsg, FilterPattern, PermissionPrompt, PermissionRequest,
|
||||
PromptDefinition, PromptOrigin, PromptResult,
|
||||
};
|
||||
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 std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
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>,
|
||||
current_url_string: Option<String>,
|
||||
|
||||
/// 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<EmbedderEvent>,
|
||||
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,
|
||||
current_url_string: None,
|
||||
browser_id: None,
|
||||
browsers: Vec::new(),
|
||||
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 browser_id(&self) -> Option<BrowserId> {
|
||||
self.browser_id
|
||||
}
|
||||
|
||||
pub fn current_url_string(&self) -> Option<&str> {
|
||||
self.current_url_string.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_events(&mut self) -> Vec<EmbedderEvent> {
|
||||
std::mem::take(&mut self.event_queue)
|
||||
}
|
||||
|
||||
pub fn handle_window_events(&mut self, events: Vec<EmbedderEvent>) {
|
||||
for event in events {
|
||||
match event {
|
||||
EmbedderEvent::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(EmbedderEvent::Reload(id));
|
||||
}
|
||||
})
|
||||
.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 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(EmbedderEvent::LoadUrl(id, url));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'Q', || {
|
||||
self.event_queue.push(EmbedderEvent::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(EmbedderEvent::ToggleSamplingProfiler(
|
||||
Duration::from_millis(rate),
|
||||
Duration::from_secs(duration),
|
||||
));
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F9, || {
|
||||
self.event_queue.push(EmbedderEvent::CaptureWebRender)
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F10, || {
|
||||
self.event_queue.push(EmbedderEvent::ToggleWebRenderDebug(
|
||||
WebRenderDebugOption::RenderTargetDebug,
|
||||
));
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F11, || {
|
||||
self.event_queue.push(EmbedderEvent::ToggleWebRenderDebug(
|
||||
WebRenderDebugOption::TextureCacheDebug,
|
||||
));
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::F12, || {
|
||||
self.event_queue.push(EmbedderEvent::ToggleWebRenderDebug(
|
||||
WebRenderDebugOption::Profiler,
|
||||
));
|
||||
})
|
||||
.shortcut(CMD_OR_ALT, Key::ArrowRight, || {
|
||||
if let Some(id) = self.browser_id {
|
||||
let event = EmbedderEvent::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 = EmbedderEvent::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 = EmbedderEvent::ExitFullScreen(id);
|
||||
self.event_queue.push(event);
|
||||
}
|
||||
} else {
|
||||
self.event_queue.push(EmbedderEvent::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, '[', || {
|
||||
EmbedderEvent::Navigation(id, TraversalDirection::Back(1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, ']', || {
|
||||
EmbedderEvent::Navigation(id, TraversalDirection::Forward(1))
|
||||
})
|
||||
.otherwise(|| EmbedderEvent::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(EmbedderEvent::Zoom(1.1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '+', || {
|
||||
self.event_queue.push(EmbedderEvent::Zoom(1.1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '-', || {
|
||||
self.event_queue.push(EmbedderEvent::Zoom(1.0 / 1.1))
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, '0', || {
|
||||
self.event_queue.push(EmbedderEvent::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 = EmbedderEvent::Scroll(scroll_location, Point2D::zero(), phase);
|
||||
self.event_queue.push(event);
|
||||
}
|
||||
|
||||
/// Returns true iff the caller needs to manually present a new frame.
|
||||
pub fn handle_servo_events(&mut self, events: Vec<(Option<BrowserId>, EmbedderMsg)>) -> bool {
|
||||
let mut need_present = false;
|
||||
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.is_empty() => &**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("AlertDialog".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(EmbedderEvent::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(EmbedderEvent::SendError(browser_id, reason));
|
||||
}
|
||||
},
|
||||
EmbedderMsg::AllowNavigationRequest(pipeline_id, _url) => {
|
||||
if let Some(_browser_id) = browser_id {
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::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(EmbedderEvent::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());
|
||||
self.current_url_string = Some(urls[current].clone().into_string());
|
||||
},
|
||||
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(EmbedderEvent::SelectBrowser(*prev_browser_id));
|
||||
} else {
|
||||
self.event_queue.push(EmbedderEvent::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(EmbedderEvent::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(EmbedderEvent::SendError(None, reason));
|
||||
};
|
||||
},
|
||||
EmbedderMsg::PromptPermission(prompt, sender) => {
|
||||
let permission_state = prompt_user(prompt);
|
||||
let _ = sender.send(permission_state);
|
||||
},
|
||||
EmbedderMsg::ShowIME(_kind, _text, _multiline, _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);
|
||||
},
|
||||
EmbedderMsg::ReadyToPresent => {
|
||||
need_present = true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
need_present
|
||||
}
|
||||
}
|
||||
|
||||
#[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> {
|
||||
thread::Builder::new()
|
||||
.name("DevicePicker".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("FilePicker".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.is_empty() {
|
||||
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();
|
||||
shellwords::escape(&s)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn tiny_dialog_escape(raw: &str) -> String {
|
||||
raw.to_string()
|
||||
}
|
45
ports/servoshell/build.rs
Normal file
45
ports/servoshell/build.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* 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/. */
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
extern crate cc;
|
||||
|
||||
#[cfg(windows)]
|
||||
extern crate winres;
|
||||
|
||||
use vergen::EmitBuilder;
|
||||
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("../../resources/servo.ico");
|
||||
res.set_manifest_file("platform/windows/servo.exe.manifest");
|
||||
res.compile().unwrap();
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
cc::Build::new()
|
||||
.file("platform/macos/count_threads.c")
|
||||
.compile("count_threads");
|
||||
}
|
||||
|
||||
if let Err(error) = EmitBuilder::builder()
|
||||
.fail_on_error()
|
||||
.git_sha(true /* short */)
|
||||
.emit()
|
||||
{
|
||||
println!(
|
||||
"cargo:warning=Could not generate git version information: {:?}",
|
||||
error
|
||||
);
|
||||
println!("cargo:rustc-env=VERGEN_GIT_SHA=nogit");
|
||||
}
|
||||
|
||||
// On MacOS, all dylib dependencies are shipped along with the binary
|
||||
// in the "/lib" directory. Setting the rpath here, allows the dynamic
|
||||
// linker to locate them. See `man dyld` for more info.
|
||||
#[cfg(target_os = "macos")]
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/lib/");
|
||||
}
|
58
ports/servoshell/crash_handler.rs
Normal file
58
ports/servoshell/crash_handler.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
/* 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/. */
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
pub fn install() {}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
pub fn install() {
|
||||
use crate::backtrace;
|
||||
use sig::ffi::Sig;
|
||||
use std::{io::Write, sync::atomic, thread};
|
||||
|
||||
extern "C" fn handler(sig: i32) {
|
||||
// Only print crash message and backtrace the first time, to avoid
|
||||
// infinite recursion if the printing causes another signal.
|
||||
static BEEN_HERE_BEFORE: atomic::AtomicBool = atomic::AtomicBool::new(false);
|
||||
if !BEEN_HERE_BEFORE.swap(true, atomic::Ordering::SeqCst) {
|
||||
// stderr is unbuffered, so we won’t lose output if we crash later
|
||||
// in this handler, and the std::io::stderr() call never allocates.
|
||||
// std::io::stdout() allocates the first time it’s called, which in
|
||||
// practice will often segfault (see below).
|
||||
let stderr = std::io::stderr();
|
||||
let mut stderr = stderr.lock();
|
||||
let _ = write!(&mut stderr, "Caught signal {sig}");
|
||||
if let Some(name) = thread::current().name() {
|
||||
let _ = write!(&mut stderr, " in thread \"{}\"", name);
|
||||
}
|
||||
let _ = writeln!(&mut stderr);
|
||||
|
||||
// This call always allocates, which in practice will segfault if
|
||||
// we’re handling a non-main-thread (e.g. layout) segfault. Strictly
|
||||
// speaking in POSIX terms, this is also undefined behaviour.
|
||||
let _ = backtrace::print(&mut stderr);
|
||||
}
|
||||
|
||||
// Outside the BEEN_HERE_BEFORE check, we must only call functions we
|
||||
// know to be “async-signal-safe”, which includes sigaction(), raise(),
|
||||
// and _exit(), but generally doesn’t include anything that allocates.
|
||||
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_04_03_03
|
||||
unsafe {
|
||||
// Reset the signal to the default action, and reraise the signal.
|
||||
// Unlike libc::_exit(sig), which terminates the process normally,
|
||||
// this terminates abnormally just like an uncaught signal, allowing
|
||||
// mach (or your shell) to distinguish it from an ordinary exit, and
|
||||
// allows your kernel to make a core dump if configured to do so.
|
||||
let mut action: libc::sigaction = std::mem::zeroed();
|
||||
action.sa_sigaction = libc::SIG_DFL;
|
||||
libc::sigaction(sig, &action, std::ptr::null_mut());
|
||||
libc::raise(sig);
|
||||
}
|
||||
}
|
||||
|
||||
signal!(Sig::SEGV, handler); // handle segfaults
|
||||
signal!(Sig::ILL, handler); // handle stack overflow and unsupported CPUs
|
||||
signal!(Sig::IOT, handler); // handle double panics
|
||||
signal!(Sig::BUS, handler); // handle invalid memory access
|
||||
}
|
129
ports/servoshell/egui_glue.rs
Normal file
129
ports/servoshell/egui_glue.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
/* 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/. */
|
||||
|
||||
//! A modified version of EguiGlow [from egui_glow 0.22.0][0] that retains its shapes,
|
||||
//! allowing [`EguiGlow::paint`] to be called multiple times.
|
||||
//!
|
||||
//! [0]: https://github.com/emilk/egui/blob/0.22.0/crates/egui_glow/src/winit.rs
|
||||
|
||||
// Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any
|
||||
// person obtaining a copy of this software and associated
|
||||
// documentation files (the "Software"), to deal in the
|
||||
// Software without restriction, including without
|
||||
// limitation the rights to use, copy, modify, merge,
|
||||
// publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software
|
||||
// is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice
|
||||
// shall be included in all copies or substantial portions
|
||||
// of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
// DEALINGS IN THE SOFTWARE.
|
||||
|
||||
use egui_glow::ShaderVersion;
|
||||
pub use egui_winit;
|
||||
use egui_winit::winit;
|
||||
pub use egui_winit::EventResponse;
|
||||
|
||||
/// Use [`egui`] from a [`glow`] app based on [`winit`].
|
||||
pub struct EguiGlow {
|
||||
pub egui_ctx: egui::Context,
|
||||
pub egui_winit: egui_winit::State,
|
||||
pub painter: egui_glow::Painter,
|
||||
|
||||
shapes: Vec<egui::epaint::ClippedShape>,
|
||||
textures_delta: egui::TexturesDelta,
|
||||
}
|
||||
|
||||
impl EguiGlow {
|
||||
/// For automatic shader version detection set `shader_version` to `None`.
|
||||
pub fn new<E>(
|
||||
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
|
||||
gl: std::sync::Arc<glow::Context>,
|
||||
shader_version: Option<ShaderVersion>,
|
||||
) -> Self {
|
||||
let painter = egui_glow::Painter::new(gl, "", shader_version)
|
||||
.map_err(|err| {
|
||||
log::error!("error occurred in initializing painter:\n{err}");
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Self {
|
||||
egui_ctx: Default::default(),
|
||||
egui_winit: egui_winit::State::new(event_loop),
|
||||
painter,
|
||||
shapes: Default::default(),
|
||||
textures_delta: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_event(&mut self, event: &winit::event::WindowEvent<'_>) -> EventResponse {
|
||||
self.egui_winit.on_event(&self.egui_ctx, event)
|
||||
}
|
||||
|
||||
/// Returns the `Duration` of the timeout after which egui should be repainted even if there's no new events.
|
||||
///
|
||||
/// Call [`Self::paint`] later to paint.
|
||||
pub fn run(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
run_ui: impl FnMut(&egui::Context),
|
||||
) -> std::time::Duration {
|
||||
let raw_input = self.egui_winit.take_egui_input(window);
|
||||
let egui::FullOutput {
|
||||
platform_output,
|
||||
repaint_after,
|
||||
textures_delta,
|
||||
shapes,
|
||||
} = self.egui_ctx.run(raw_input, run_ui);
|
||||
|
||||
self.egui_winit
|
||||
.handle_platform_output(window, &self.egui_ctx, platform_output);
|
||||
|
||||
self.shapes = shapes;
|
||||
self.textures_delta.append(textures_delta);
|
||||
repaint_after
|
||||
}
|
||||
|
||||
/// Paint the results of the last call to [`Self::run`].
|
||||
pub fn paint(&mut self, window: &winit::window::Window) {
|
||||
/////// let shapes = std::mem::take(&mut self.shapes);
|
||||
let shapes = &self.shapes;
|
||||
let mut textures_delta = std::mem::take(&mut self.textures_delta);
|
||||
|
||||
for (id, image_delta) in textures_delta.set {
|
||||
self.painter.set_texture(id, &image_delta);
|
||||
}
|
||||
|
||||
/////// let clipped_primitives = self.egui_ctx.tessellate(shapes);
|
||||
let clipped_primitives = self.egui_ctx.tessellate(shapes.clone());
|
||||
let dimensions: [u32; 2] = window.inner_size().into();
|
||||
self.painter.paint_primitives(
|
||||
dimensions,
|
||||
self.egui_ctx.pixels_per_point(),
|
||||
&clipped_primitives,
|
||||
);
|
||||
|
||||
for id in textures_delta.free.drain(..) {
|
||||
self.painter.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Call to release the allocated graphics resources.
|
||||
pub fn destroy(&mut self) {
|
||||
self.painter.destroy();
|
||||
}
|
||||
}
|
45
ports/servoshell/embedder.rs
Normal file
45
ports/servoshell/embedder.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* 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/. */
|
||||
|
||||
//! Implements the global methods required by Servo (not window/gl/compositor related).
|
||||
|
||||
use servo::compositing::windowing::EmbedderMethods;
|
||||
use servo::embedder_traits::{EmbedderProxy, EventLoopWaker};
|
||||
use servo::servo_config::pref;
|
||||
use webxr::glwindow::GlWindowDiscovery;
|
||||
|
||||
pub struct EmbedderCallbacks {
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
xr_discovery: Option<GlWindowDiscovery>,
|
||||
}
|
||||
|
||||
impl EmbedderCallbacks {
|
||||
pub fn new(
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
xr_discovery: Option<GlWindowDiscovery>,
|
||||
) -> EmbedderCallbacks {
|
||||
EmbedderCallbacks {
|
||||
event_loop_waker,
|
||||
xr_discovery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmbedderMethods for EmbedderCallbacks {
|
||||
fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> {
|
||||
self.event_loop_waker.clone()
|
||||
}
|
||||
|
||||
fn register_webxr(
|
||||
&mut self,
|
||||
xr: &mut webxr::MainThreadRegistry,
|
||||
_embedder_proxy: EmbedderProxy,
|
||||
) {
|
||||
if pref!(dom.webxr.test) {
|
||||
xr.register_mock(webxr::headless::HeadlessMockDiscovery::new());
|
||||
} else if let Some(xr_discovery) = self.xr_discovery.take() {
|
||||
xr.register(xr_discovery);
|
||||
}
|
||||
}
|
||||
}
|
175
ports/servoshell/events_loop.rs
Normal file
175
ports/servoshell/events_loop.rs
Normal file
|
@ -0,0 +1,175 @@
|
|||
/* 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/. */
|
||||
|
||||
//! An event loop implementation that works in headless mode.
|
||||
|
||||
use servo::embedder_traits::EventLoopWaker;
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS};
|
||||
|
||||
/// Another process or thread has kicked the OS event loop with EventLoopWaker.
|
||||
#[derive(Debug)]
|
||||
pub struct WakerEvent;
|
||||
|
||||
/// The real or fake OS event loop.
|
||||
#[allow(dead_code)]
|
||||
enum EventLoop {
|
||||
/// A real Winit windowing event loop.
|
||||
Winit(Option<winit::event_loop::EventLoop<WakerEvent>>),
|
||||
/// A fake event loop which contains a signalling flag used to ensure
|
||||
/// that pending events get processed in a timely fashion, and a condition
|
||||
/// variable to allow waiting on that flag changing state.
|
||||
Headless(Arc<(Mutex<bool>, Condvar)>),
|
||||
}
|
||||
|
||||
pub struct EventsLoop(EventLoop);
|
||||
|
||||
impl EventsLoop {
|
||||
// Ideally, we could use the winit event loop in both modes,
|
||||
// but on Linux, the event loop requires a X11 server.
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||
pub fn new(_headless: bool, _has_output_file: bool) -> EventsLoop {
|
||||
EventsLoop(EventLoop::Winit(Some(winit::event_loop::EventLoopBuilder::with_user_event().build())))
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn new(headless: bool, _has_output_file: bool) -> EventsLoop {
|
||||
EventsLoop(if headless {
|
||||
EventLoop::Headless(Arc::new((Mutex::new(false), Condvar::new())))
|
||||
} else {
|
||||
EventLoop::Winit(Some(winit::event_loop::EventLoopBuilder::with_user_event().build()))
|
||||
})
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn new(headless: bool, _has_output_file: bool) -> EventsLoop {
|
||||
EventsLoop(if headless {
|
||||
EventLoop::Headless(Arc::new((Mutex::new(false), Condvar::new())))
|
||||
} else {
|
||||
let mut event_loop_builder = winit::event_loop::EventLoopBuilder::with_user_event();
|
||||
if _has_output_file {
|
||||
// Prevent the window from showing in Dock.app, stealing focus,
|
||||
// when generating an output file.
|
||||
event_loop_builder.with_activation_policy(ActivationPolicy::Prohibited);
|
||||
}
|
||||
EventLoop::Winit(Some(event_loop_builder.build()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventsLoop {
|
||||
pub fn create_event_loop_waker(&self) -> Box<dyn EventLoopWaker> {
|
||||
match self.0 {
|
||||
EventLoop::Winit(ref events_loop) => {
|
||||
let events_loop = events_loop
|
||||
.as_ref()
|
||||
.expect("Can't create waker for unavailable event loop.");
|
||||
Box::new(HeadedEventLoopWaker::new(events_loop))
|
||||
},
|
||||
EventLoop::Headless(ref data) => Box::new(HeadlessEventLoopWaker(data.clone())),
|
||||
}
|
||||
}
|
||||
pub fn as_winit(&self) -> &winit::event_loop::EventLoop<WakerEvent> {
|
||||
match self.0 {
|
||||
EventLoop::Winit(Some(ref event_loop)) => event_loop,
|
||||
EventLoop::Winit(None) | EventLoop::Headless(..) => {
|
||||
panic!("Can't access winit event loop while using the fake headless event loop")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_forever<F: 'static>(self, mut callback: F)
|
||||
where F: FnMut(
|
||||
winit::event::Event<'_, WakerEvent>,
|
||||
Option<&winit::event_loop::EventLoopWindowTarget<WakerEvent>>,
|
||||
&mut winit::event_loop::ControlFlow
|
||||
) {
|
||||
match self.0 {
|
||||
EventLoop::Winit(events_loop) => {
|
||||
let events_loop = events_loop
|
||||
.expect("Can't run an unavailable event loop.");
|
||||
events_loop.run(move |e, window_target, ref mut control_flow| {
|
||||
callback(e, Some(window_target), control_flow)
|
||||
});
|
||||
}
|
||||
EventLoop::Headless(ref data) => {
|
||||
let (flag, condvar) = &**data;
|
||||
let mut event = winit::event::Event::NewEvents(winit::event::StartCause::Init);
|
||||
loop {
|
||||
self.sleep(flag, condvar);
|
||||
let mut control_flow = winit::event_loop::ControlFlow::Poll;
|
||||
callback(
|
||||
event,
|
||||
None,
|
||||
&mut control_flow
|
||||
);
|
||||
event = winit::event::Event::<WakerEvent>::UserEvent(WakerEvent);
|
||||
|
||||
if control_flow != winit::event_loop::ControlFlow::Poll {
|
||||
*flag.lock().unwrap() = false;
|
||||
}
|
||||
|
||||
if control_flow == winit::event_loop::ControlFlow::Exit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn sleep(&self, lock: &Mutex<bool>, condvar: &Condvar) {
|
||||
// To avoid sleeping when we should be processing events, do two things:
|
||||
// * before sleeping, check whether our signalling flag has been set
|
||||
// * wait on a condition variable with a maximum timeout, to allow
|
||||
// being woken up by any signals that occur while sleeping.
|
||||
let guard = lock.lock().unwrap();
|
||||
if *guard {
|
||||
return;
|
||||
}
|
||||
let _ = condvar
|
||||
.wait_timeout(guard, time::Duration::from_millis(5))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
struct HeadedEventLoopWaker {
|
||||
proxy: Arc<Mutex<winit::event_loop::EventLoopProxy<WakerEvent>>>,
|
||||
}
|
||||
impl HeadedEventLoopWaker {
|
||||
fn new(events_loop: &winit::event_loop::EventLoop<WakerEvent>) -> HeadedEventLoopWaker {
|
||||
let proxy = Arc::new(Mutex::new(events_loop.create_proxy()));
|
||||
HeadedEventLoopWaker { proxy }
|
||||
}
|
||||
}
|
||||
impl EventLoopWaker for HeadedEventLoopWaker {
|
||||
fn wake(&self) {
|
||||
// Kick the OS event loop awake.
|
||||
if let Err(err) = self.proxy.lock().unwrap().send_event(WakerEvent) {
|
||||
warn!("Failed to wake up event loop ({}).", err);
|
||||
}
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
|
||||
Box::new(HeadedEventLoopWaker {
|
||||
proxy: self.proxy.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct HeadlessEventLoopWaker(Arc<(Mutex<bool>, Condvar)>);
|
||||
impl EventLoopWaker for HeadlessEventLoopWaker {
|
||||
fn wake(&self) {
|
||||
// Set the signalling flag and notify the condition variable.
|
||||
// This ensures that any sleep operation is interrupted,
|
||||
// and any non-sleeping operation will have a change to check
|
||||
// the flag before going to sleep.
|
||||
let (ref flag, ref condvar) = *self.0;
|
||||
let mut flag = flag.lock().unwrap();
|
||||
*flag = true;
|
||||
condvar.notify_all();
|
||||
}
|
||||
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
|
||||
Box::new(HeadlessEventLoopWaker(self.0.clone()))
|
||||
}
|
||||
}
|
764
ports/servoshell/headed_window.rs
Normal file
764
ports/servoshell/headed_window.rs
Normal file
|
@ -0,0 +1,764 @@
|
|||
/* 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/. */
|
||||
|
||||
//! A winit window implementation.
|
||||
|
||||
use crate::events_loop::{EventsLoop, WakerEvent};
|
||||
use crate::keyutils::keyboard_event_from_winit;
|
||||
use crate::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||
use euclid::{
|
||||
Angle, Point2D, Rotation3D, Scale, Size2D, UnknownUnit,
|
||||
Vector2D, Vector3D,
|
||||
};
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use winit::window::Icon;
|
||||
use winit::event::{ElementState, KeyboardInput, MouseButton, MouseScrollDelta, TouchPhase, VirtualKeyCode};
|
||||
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
|
||||
use servo::keyboard_types::{Key, KeyState, KeyboardEvent};
|
||||
use servo::compositing::windowing::{AnimationState, MouseWindowEvent, EmbedderEvent};
|
||||
use servo::compositing::windowing::{EmbedderCoordinates, WindowMethods};
|
||||
use servo::embedder_traits::Cursor;
|
||||
use servo::script_traits::{TouchEventType, WheelDelta, WheelMode};
|
||||
use servo::servo_config::opts;
|
||||
use servo::servo_config::pref;
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::style_traits::DevicePixel;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize};
|
||||
use servo::webrender_api::ScrollLocation;
|
||||
use servo::webrender_surfman::WebrenderSurfman;
|
||||
use servo_media::player::context::{GlApi, GlContext as PlayerGLContext, NativeDisplay};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
#[cfg(target_os = "linux")]
|
||||
use surfman::platform::generic::multi::connection::NativeConnection;
|
||||
#[cfg(target_os = "linux")]
|
||||
use surfman::platform::generic::multi::context::NativeContext;
|
||||
use surfman::Connection;
|
||||
use surfman::Context;
|
||||
use surfman::Device;
|
||||
use surfman::GLApi;
|
||||
use surfman::GLVersion;
|
||||
use surfman::SurfaceType;
|
||||
#[cfg(target_os = "windows")]
|
||||
use winapi;
|
||||
use winit::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
|
||||
use winit::event::ModifiersState;
|
||||
|
||||
pub struct Window {
|
||||
winit_window: winit::window::Window,
|
||||
webrender_surfman: WebrenderSurfman,
|
||||
screen_size: Size2D<u32, DeviceIndependentPixel>,
|
||||
inner_size: Cell<Size2D<u32, DeviceIndependentPixel>>,
|
||||
toolbar_height: Cell<f32>,
|
||||
mouse_down_button: Cell<Option<winit::event::MouseButton>>,
|
||||
mouse_down_point: Cell<Point2D<i32, DevicePixel>>,
|
||||
primary_monitor: winit::monitor::MonitorHandle,
|
||||
event_queue: RefCell<Vec<EmbedderEvent>>,
|
||||
mouse_pos: Cell<Point2D<i32, DevicePixel>>,
|
||||
last_pressed: Cell<Option<(KeyboardEvent, Option<VirtualKeyCode>)>>,
|
||||
/// A map of winit's key codes to key values that are interpreted from
|
||||
/// winit's ReceivedChar events.
|
||||
keys_down: RefCell<HashMap<VirtualKeyCode, Key>>,
|
||||
animation_state: Cell<AnimationState>,
|
||||
fullscreen: Cell<bool>,
|
||||
device_pixels_per_px: Option<f32>,
|
||||
xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
|
||||
modifiers_state: Cell<ModifiersState>,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn window_creation_scale_factor() -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
Scale::new(1.0)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn window_creation_scale_factor() -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
let hdc = unsafe { winapi::um::winuser::GetDC(::std::ptr::null_mut()) };
|
||||
let ppi = unsafe { winapi::um::wingdi::GetDeviceCaps(hdc, winapi::um::wingdi::LOGPIXELSY) };
|
||||
Scale::new(ppi as f32 / 96.0)
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(
|
||||
win_size: Size2D<u32, DeviceIndependentPixel>,
|
||||
events_loop: &EventsLoop,
|
||||
no_native_titlebar: bool,
|
||||
device_pixels_per_px: Option<f32>,
|
||||
) -> Window {
|
||||
let opts = opts::get();
|
||||
|
||||
// If there's no chrome, start off with the window invisible. It will be set to visible in
|
||||
// `load_end()`. This avoids an ugly flash of unstyled content (especially important since
|
||||
// unstyled content is white and chrome often has a transparent background). See issue
|
||||
// #9996.
|
||||
let visible = opts.output_file.is_none() && !no_native_titlebar;
|
||||
|
||||
let win_size: DeviceIntSize = (win_size.to_f32() * window_creation_scale_factor()).to_i32();
|
||||
let width = win_size.to_untyped().width;
|
||||
let height = win_size.to_untyped().height;
|
||||
|
||||
let window_builder = winit::window::WindowBuilder::new()
|
||||
.with_title("Servo".to_string())
|
||||
.with_decorations(!no_native_titlebar)
|
||||
.with_transparent(no_native_titlebar)
|
||||
.with_inner_size(PhysicalSize::new(width as f64, height as f64))
|
||||
.with_visible(visible);
|
||||
|
||||
let winit_window = window_builder.build(events_loop.as_winit()).expect("Failed to create window.");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
{
|
||||
let icon_bytes = include_bytes!("../../resources/servo_64.png");
|
||||
winit_window.set_window_icon(Some(load_icon(icon_bytes)));
|
||||
}
|
||||
|
||||
let primary_monitor = events_loop.as_winit().available_monitors().nth(0).expect("No monitor detected");
|
||||
|
||||
let PhysicalSize {
|
||||
width: screen_width,
|
||||
height: screen_height,
|
||||
} = primary_monitor.size();
|
||||
let screen_size = Size2D::new(screen_width, screen_height);
|
||||
let PhysicalSize { width, height } = winit_window.inner_size();
|
||||
let inner_size = Size2D::new(width, height);
|
||||
|
||||
// Initialize surfman
|
||||
let display_handle = winit_window.raw_display_handle();
|
||||
let connection =
|
||||
Connection::from_raw_display_handle(display_handle).expect("Failed to create connection");
|
||||
let adapter = connection
|
||||
.create_adapter()
|
||||
.expect("Failed to create adapter");
|
||||
let window_handle = winit_window.raw_window_handle();
|
||||
let native_widget = connection
|
||||
.create_native_widget_from_rwh(window_handle)
|
||||
.expect("Failed to create native widget");
|
||||
let surface_type = SurfaceType::Widget { native_widget };
|
||||
let webrender_surfman = WebrenderSurfman::create(&connection, &adapter, surface_type)
|
||||
.expect("Failed to create WR surfman");
|
||||
|
||||
debug!("Created window {:?}", winit_window.id());
|
||||
Window {
|
||||
winit_window,
|
||||
webrender_surfman,
|
||||
event_queue: RefCell::new(vec![]),
|
||||
mouse_down_button: Cell::new(None),
|
||||
mouse_down_point: Cell::new(Point2D::new(0, 0)),
|
||||
mouse_pos: Cell::new(Point2D::new(0, 0)),
|
||||
last_pressed: Cell::new(None),
|
||||
keys_down: RefCell::new(HashMap::new()),
|
||||
animation_state: Cell::new(AnimationState::Idle),
|
||||
fullscreen: Cell::new(false),
|
||||
inner_size: Cell::new(inner_size),
|
||||
primary_monitor,
|
||||
screen_size,
|
||||
device_pixels_per_px,
|
||||
xr_window_poses: RefCell::new(vec![]),
|
||||
modifiers_state: Cell::new(ModifiersState::empty()),
|
||||
toolbar_height: Cell::new(0.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_received_character(&self, mut ch: char) {
|
||||
info!("winit received character: {:?}", ch);
|
||||
if ch.is_control() {
|
||||
if ch as u8 >= 32 {
|
||||
return;
|
||||
}
|
||||
// shift ASCII control characters to lowercase
|
||||
ch = (ch as u8 + 96) as char;
|
||||
}
|
||||
let (mut event, key_code) = if let Some((event, key_code)) = self.last_pressed.replace(None)
|
||||
{
|
||||
(event, key_code)
|
||||
} else if ch.is_ascii() {
|
||||
// Some keys like Backspace emit a control character in winit
|
||||
// but they are already dealt with in handle_keyboard_input
|
||||
// so just ignore the character.
|
||||
return;
|
||||
} else {
|
||||
// For combined characters like the letter e with an acute accent
|
||||
// no keyboard event is emitted. A dummy event is created in this case.
|
||||
(KeyboardEvent::default(), None)
|
||||
};
|
||||
event.key = Key::Character(ch.to_string());
|
||||
|
||||
if event.state == KeyState::Down {
|
||||
// Ensure that when we receive a keyup event from winit, we are able
|
||||
// to infer that it's related to this character and set the event
|
||||
// properties appropriately.
|
||||
if let Some(key_code) = key_code {
|
||||
self.keys_down
|
||||
.borrow_mut()
|
||||
.insert(key_code, event.key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let xr_poses = self.xr_window_poses.borrow();
|
||||
for xr_window_pose in &*xr_poses {
|
||||
xr_window_pose.handle_xr_translation(&event);
|
||||
}
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::Keyboard(event));
|
||||
}
|
||||
|
||||
fn handle_keyboard_input(&self, input: KeyboardInput) {
|
||||
let mut event = keyboard_event_from_winit(input, self.modifiers_state.get());
|
||||
trace!("handling {:?}", event);
|
||||
if event.state == KeyState::Down && event.key == Key::Unidentified {
|
||||
// If pressed and probably printable, we expect a ReceivedCharacter event.
|
||||
// Wait for that to be received and don't queue any event right now.
|
||||
self.last_pressed.set(Some((event, input.virtual_keycode)));
|
||||
return;
|
||||
} else if event.state == KeyState::Up && event.key == Key::Unidentified {
|
||||
// If release and probably printable, this is following a ReceiverCharacter event.
|
||||
if let Some(key_code) = input.virtual_keycode {
|
||||
if let Some(key) = self.keys_down.borrow_mut().remove(&key_code) {
|
||||
event.key = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if event.key != Key::Unidentified {
|
||||
self.last_pressed.set(None);
|
||||
let xr_poses = self.xr_window_poses.borrow();
|
||||
for xr_window_pose in &*xr_poses {
|
||||
xr_window_pose.handle_xr_rotation(&input, self.modifiers_state.get());
|
||||
}
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::Keyboard(event));
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to handle a click
|
||||
fn handle_mouse(
|
||||
&self,
|
||||
button: winit::event::MouseButton,
|
||||
action: winit::event::ElementState,
|
||||
coords: Point2D<i32, DevicePixel>,
|
||||
) {
|
||||
use servo::script_traits::MouseButton;
|
||||
|
||||
let max_pixel_dist = 10.0 * self.servo_hidpi_factor().get();
|
||||
let mouse_button = match &button {
|
||||
winit::event::MouseButton::Left => MouseButton::Left,
|
||||
winit::event::MouseButton::Right => MouseButton::Right,
|
||||
winit::event::MouseButton::Middle => MouseButton::Middle,
|
||||
_ => MouseButton::Left,
|
||||
};
|
||||
let event = match action {
|
||||
ElementState::Pressed => {
|
||||
self.mouse_down_point.set(coords);
|
||||
self.mouse_down_button.set(Some(button));
|
||||
MouseWindowEvent::MouseDown(mouse_button, coords.to_f32())
|
||||
},
|
||||
ElementState::Released => {
|
||||
let mouse_up_event = MouseWindowEvent::MouseUp(mouse_button, coords.to_f32());
|
||||
match self.mouse_down_button.get() {
|
||||
None => mouse_up_event,
|
||||
Some(but) if button == but => {
|
||||
let pixel_dist = self.mouse_down_point.get() - coords;
|
||||
let pixel_dist =
|
||||
((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32)
|
||||
.sqrt();
|
||||
if pixel_dist < max_pixel_dist {
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::MouseWindowEventClass(mouse_up_event));
|
||||
MouseWindowEvent::Click(mouse_button, coords.to_f32())
|
||||
} else {
|
||||
mouse_up_event
|
||||
}
|
||||
},
|
||||
Some(_) => mouse_up_event,
|
||||
}
|
||||
},
|
||||
};
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::MouseWindowEventClass(event));
|
||||
}
|
||||
|
||||
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
Scale::new(self.winit_window.scale_factor() as f32)
|
||||
}
|
||||
|
||||
fn servo_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
match self.device_pixels_per_px {
|
||||
Some(device_pixels_per_px) => Scale::new(device_pixels_per_px),
|
||||
_ => match opts::get().output_file {
|
||||
Some(_) => Scale::new(1.0),
|
||||
None => self.device_hidpi_factor(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPortsMethods for Window {
|
||||
fn get_events(&self) -> Vec<EmbedderEvent> {
|
||||
std::mem::take(&mut *self.event_queue.borrow_mut())
|
||||
}
|
||||
|
||||
fn has_events(&self) -> bool {
|
||||
!self.event_queue.borrow().is_empty()
|
||||
}
|
||||
|
||||
fn page_height(&self) -> f32 {
|
||||
let dpr = self.servo_hidpi_factor();
|
||||
let size = self
|
||||
.winit_window
|
||||
.inner_size();
|
||||
size.height as f32 * dpr.get()
|
||||
}
|
||||
|
||||
fn set_title(&self, title: &str) {
|
||||
self.winit_window.set_title(title);
|
||||
}
|
||||
|
||||
fn set_inner_size(&self, size: DeviceIntSize) {
|
||||
self.winit_window
|
||||
.set_inner_size::<PhysicalSize<i32>>(PhysicalSize::new(size.width, size.height))
|
||||
}
|
||||
|
||||
fn set_position(&self, point: DeviceIntPoint) {
|
||||
self.winit_window
|
||||
.set_outer_position::<PhysicalPosition<i32>>(PhysicalPosition::new(point.x, point.y))
|
||||
}
|
||||
|
||||
fn set_fullscreen(&self, state: bool) {
|
||||
if self.fullscreen.get() != state {
|
||||
self.winit_window
|
||||
.set_fullscreen(
|
||||
if state {
|
||||
Some(winit::window::Fullscreen::Borderless(Some(self.primary_monitor.clone())))
|
||||
} else { None }
|
||||
);
|
||||
}
|
||||
self.fullscreen.set(state);
|
||||
}
|
||||
|
||||
fn get_fullscreen(&self) -> bool {
|
||||
self.fullscreen.get()
|
||||
}
|
||||
|
||||
fn set_cursor(&self, cursor: Cursor) {
|
||||
use winit::window::CursorIcon;
|
||||
|
||||
let winit_cursor = match cursor {
|
||||
Cursor::Default => CursorIcon::Default,
|
||||
Cursor::Pointer => CursorIcon::Hand,
|
||||
Cursor::ContextMenu => CursorIcon::ContextMenu,
|
||||
Cursor::Help => CursorIcon::Help,
|
||||
Cursor::Progress => CursorIcon::Progress,
|
||||
Cursor::Wait => CursorIcon::Wait,
|
||||
Cursor::Cell => CursorIcon::Cell,
|
||||
Cursor::Crosshair => CursorIcon::Crosshair,
|
||||
Cursor::Text => CursorIcon::Text,
|
||||
Cursor::VerticalText => CursorIcon::VerticalText,
|
||||
Cursor::Alias => CursorIcon::Alias,
|
||||
Cursor::Copy => CursorIcon::Copy,
|
||||
Cursor::Move => CursorIcon::Move,
|
||||
Cursor::NoDrop => CursorIcon::NoDrop,
|
||||
Cursor::NotAllowed => CursorIcon::NotAllowed,
|
||||
Cursor::Grab => CursorIcon::Grab,
|
||||
Cursor::Grabbing => CursorIcon::Grabbing,
|
||||
Cursor::EResize => CursorIcon::EResize,
|
||||
Cursor::NResize => CursorIcon::NResize,
|
||||
Cursor::NeResize => CursorIcon::NeResize,
|
||||
Cursor::NwResize => CursorIcon::NwResize,
|
||||
Cursor::SResize => CursorIcon::SResize,
|
||||
Cursor::SeResize => CursorIcon::SeResize,
|
||||
Cursor::SwResize => CursorIcon::SwResize,
|
||||
Cursor::WResize => CursorIcon::WResize,
|
||||
Cursor::EwResize => CursorIcon::EwResize,
|
||||
Cursor::NsResize => CursorIcon::NsResize,
|
||||
Cursor::NeswResize => CursorIcon::NeswResize,
|
||||
Cursor::NwseResize => CursorIcon::NwseResize,
|
||||
Cursor::ColResize => CursorIcon::ColResize,
|
||||
Cursor::RowResize => CursorIcon::RowResize,
|
||||
Cursor::AllScroll => CursorIcon::AllScroll,
|
||||
Cursor::ZoomIn => CursorIcon::ZoomIn,
|
||||
Cursor::ZoomOut => CursorIcon::ZoomOut,
|
||||
_ => CursorIcon::Default,
|
||||
};
|
||||
self.winit_window.set_cursor_icon(winit_cursor);
|
||||
}
|
||||
|
||||
fn is_animating(&self) -> bool {
|
||||
self.animation_state.get() == AnimationState::Animating
|
||||
}
|
||||
|
||||
fn id(&self) -> winit::window::WindowId {
|
||||
self.winit_window.id()
|
||||
}
|
||||
|
||||
fn queue_embedder_events_for_winit_event(&self, event: winit::event::WindowEvent<'_>) {
|
||||
match event {
|
||||
winit::event::WindowEvent::ReceivedCharacter(ch) => self.handle_received_character(ch),
|
||||
winit::event::WindowEvent::KeyboardInput { input, .. } => self.handle_keyboard_input(input),
|
||||
winit::event::WindowEvent::ModifiersChanged(state) => self.modifiers_state.set(state),
|
||||
winit::event::WindowEvent::MouseInput { state, button, .. } => {
|
||||
if button == MouseButton::Left || button == MouseButton::Right {
|
||||
self.handle_mouse(button, state, self.mouse_pos.get());
|
||||
}
|
||||
},
|
||||
winit::event::WindowEvent::CursorMoved { position, .. } => {
|
||||
let (x, y): (f64, f64) = position.into();
|
||||
let y = y - f64::from(self.toolbar_height.get());
|
||||
self.mouse_pos.set(Point2D::new(x, y).to_i32());
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::MouseWindowMoveEventClass(Point2D::new(
|
||||
x as f32, y as f32,
|
||||
)));
|
||||
},
|
||||
winit::event::WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||
let (mut dx, mut dy, mode) = match delta {
|
||||
MouseScrollDelta::LineDelta(dx, dy) => {
|
||||
(dx as f64, (dy * LINE_HEIGHT) as f64, WheelMode::DeltaLine)
|
||||
},
|
||||
MouseScrollDelta::PixelDelta(position) => {
|
||||
let position: LogicalPosition<f64> =
|
||||
position.to_logical(self.device_hidpi_factor().get() as f64);
|
||||
(position.x, position.y, WheelMode::DeltaPixel)
|
||||
},
|
||||
};
|
||||
|
||||
// Create wheel event before snapping to the major axis of movement
|
||||
let wheel_delta = WheelDelta {
|
||||
x: dx,
|
||||
y: dy,
|
||||
z: 0.0,
|
||||
mode,
|
||||
};
|
||||
let pos = self.mouse_pos.get();
|
||||
let position = Point2D::new(pos.x as f32, pos.y as f32);
|
||||
let wheel_event = EmbedderEvent::Wheel(wheel_delta, position);
|
||||
|
||||
// Scroll events snap to the major axis of movement, with vertical
|
||||
// preferred over horizontal.
|
||||
if dy.abs() >= dx.abs() {
|
||||
dx = 0.0;
|
||||
} else {
|
||||
dy = 0.0;
|
||||
}
|
||||
|
||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(dx as f32, dy as f32));
|
||||
let phase = winit_phase_to_touch_event_type(phase);
|
||||
let scroll_event =
|
||||
EmbedderEvent::Scroll(scroll_location, self.mouse_pos.get(), phase);
|
||||
|
||||
// Send events
|
||||
self.event_queue.borrow_mut().push(wheel_event);
|
||||
self.event_queue.borrow_mut().push(scroll_event);
|
||||
},
|
||||
winit::event::WindowEvent::Touch(touch) => {
|
||||
use servo::script_traits::TouchId;
|
||||
|
||||
let phase = winit_phase_to_touch_event_type(touch.phase);
|
||||
let id = TouchId(touch.id as i32);
|
||||
let position = touch.location;
|
||||
let point = Point2D::new(position.x as f32, position.y as f32);
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::Touch(phase, id, point));
|
||||
},
|
||||
winit::event::WindowEvent::CloseRequested => {
|
||||
self.event_queue.borrow_mut().push(EmbedderEvent::Quit);
|
||||
},
|
||||
winit::event::WindowEvent::Resized(physical_size) => {
|
||||
let (width, height) = physical_size.into();
|
||||
let new_size = Size2D::new(width, height);
|
||||
if self.inner_size.get() != new_size {
|
||||
let physical_size = Size2D::new(physical_size.width, physical_size.height);
|
||||
self.webrender_surfman
|
||||
.resize(physical_size.to_i32())
|
||||
.expect("Failed to resize");
|
||||
self.inner_size.set(new_size);
|
||||
self.event_queue.borrow_mut().push(EmbedderEvent::Resize);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn new_glwindow(
|
||||
&self,
|
||||
event_loop: &winit::event_loop::EventLoopWindowTarget<WakerEvent>
|
||||
) -> Box<dyn webxr::glwindow::GlWindow> {
|
||||
let size = self.winit_window.outer_size();
|
||||
|
||||
let window_builder = winit::window::WindowBuilder::new()
|
||||
.with_title("Servo XR".to_string())
|
||||
.with_inner_size(size)
|
||||
.with_visible(true);
|
||||
|
||||
let winit_window = window_builder.build(event_loop)
|
||||
.expect("Failed to create window.");
|
||||
|
||||
let pose = Rc::new(XRWindowPose {
|
||||
xr_rotation: Cell::new(Rotation3D::identity()),
|
||||
xr_translation: Cell::new(Vector3D::zero()),
|
||||
});
|
||||
self.xr_window_poses.borrow_mut().push(pose.clone());
|
||||
Box::new(XRWindow { winit_window, pose })
|
||||
}
|
||||
|
||||
fn winit_window(&self) -> Option<&winit::window::Window> {
|
||||
Some(&self.winit_window)
|
||||
}
|
||||
|
||||
fn set_toolbar_height(&self, height: f32) {
|
||||
self.toolbar_height.set(height);
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowMethods for Window {
|
||||
fn get_coordinates(&self) -> EmbedderCoordinates {
|
||||
// Needed to convince the type system that winit's physical pixels
|
||||
// are actually device pixels.
|
||||
let dpr: Scale<f32, DeviceIndependentPixel, DevicePixel> = Scale::new(1.0);
|
||||
let PhysicalSize { width, height } = self
|
||||
.winit_window
|
||||
.outer_size();
|
||||
let PhysicalPosition { x, y } = self
|
||||
.winit_window
|
||||
.outer_position()
|
||||
.unwrap_or(PhysicalPosition::new(0, 0));
|
||||
let win_size = (Size2D::new(width as f32, height as f32) * dpr).to_i32();
|
||||
let win_origin = (Point2D::new(x as f32, y as f32) * dpr).to_i32();
|
||||
let screen = (self.screen_size.to_f32() * dpr).to_i32();
|
||||
|
||||
let PhysicalSize { width, height } = self
|
||||
.winit_window
|
||||
.inner_size();
|
||||
|
||||
// Subtract the minibrowser toolbar height if any
|
||||
let toolbar_height = self.toolbar_height.get();
|
||||
let inner_size = Size2D::new(width as f32, height as f32) * dpr;
|
||||
let viewport_size = inner_size - Size2D::new(0f32, toolbar_height);
|
||||
let viewport_origin = DeviceIntPoint::zero(); // bottom left
|
||||
let viewport = DeviceIntRect::new(viewport_origin, viewport_size.to_i32());
|
||||
|
||||
let framebuffer = DeviceIntSize::from_untyped(viewport.size.to_untyped());
|
||||
EmbedderCoordinates {
|
||||
viewport,
|
||||
framebuffer,
|
||||
window: (win_size, win_origin),
|
||||
screen,
|
||||
// FIXME: Winit doesn't have API for available size. Fallback to screen size
|
||||
screen_avail: screen,
|
||||
hidpi_factor: self.servo_hidpi_factor(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_animation_state(&self, state: AnimationState) {
|
||||
self.animation_state.set(state);
|
||||
}
|
||||
|
||||
fn webrender_surfman(&self) -> WebrenderSurfman {
|
||||
self.webrender_surfman.clone()
|
||||
}
|
||||
|
||||
fn get_gl_context(&self) -> PlayerGLContext {
|
||||
if !pref!(media.glvideo.enabled) {
|
||||
return PlayerGLContext::Unknown;
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let native_context = self.webrender_surfman.native_context();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return PlayerGLContext::Egl(native_context.egl_context as usize);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return match native_context {
|
||||
NativeContext::Default(NativeContext::Default(native_context)) => {
|
||||
PlayerGLContext::Egl(native_context.egl_context as usize)
|
||||
},
|
||||
NativeContext::Default(NativeContext::Alternate(native_context)) => {
|
||||
PlayerGLContext::Egl(native_context.egl_context as usize)
|
||||
},
|
||||
NativeContext::Alternate(_) => unimplemented!(),
|
||||
};
|
||||
|
||||
// @TODO(victor): https://github.com/servo/media/pull/315
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(unreachable_code)]
|
||||
return unimplemented!();
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||
return unimplemented!();
|
||||
}
|
||||
|
||||
fn get_native_display(&self) -> NativeDisplay {
|
||||
if !pref!(media.glvideo.enabled) {
|
||||
return NativeDisplay::Unknown;
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let native_connection = self.webrender_surfman.connection().native_connection();
|
||||
#[allow(unused_variables)]
|
||||
let native_device = self.webrender_surfman.native_device();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return NativeDisplay::Egl(native_device.egl_display as usize);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return match native_connection {
|
||||
NativeConnection::Default(NativeConnection::Default(conn)) => {
|
||||
NativeDisplay::Egl(conn.0 as usize)
|
||||
},
|
||||
NativeConnection::Default(NativeConnection::Alternate(conn)) => {
|
||||
NativeDisplay::X11(conn.x11_display as usize)
|
||||
},
|
||||
NativeConnection::Alternate(_) => unimplemented!(),
|
||||
};
|
||||
|
||||
// @TODO(victor): https://github.com/servo/media/pull/315
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(unreachable_code)]
|
||||
return unimplemented!();
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
|
||||
return unimplemented!();
|
||||
}
|
||||
|
||||
fn get_gl_api(&self) -> GlApi {
|
||||
let api = self.webrender_surfman.connection().gl_api();
|
||||
let attributes = self.webrender_surfman.context_attributes();
|
||||
let GLVersion { major, minor } = attributes.version;
|
||||
match api {
|
||||
GLApi::GL if major >= 3 && minor >= 2 => GlApi::OpenGL3,
|
||||
GLApi::GL => GlApi::OpenGL,
|
||||
GLApi::GLES if major > 1 => GlApi::Gles2,
|
||||
GLApi::GLES => GlApi::Gles1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
|
||||
match phase {
|
||||
TouchPhase::Started => TouchEventType::Down,
|
||||
TouchPhase::Moved => TouchEventType::Move,
|
||||
TouchPhase::Ended => TouchEventType::Up,
|
||||
TouchPhase::Cancelled => TouchEventType::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
fn load_icon(icon_bytes: &[u8]) -> Icon {
|
||||
let (icon_rgba, icon_width, icon_height) = {
|
||||
use image::{GenericImageView, Pixel};
|
||||
let image = image::load_from_memory(icon_bytes).expect("Failed to load icon");
|
||||
let (width, height) = image.dimensions();
|
||||
let mut rgba = Vec::with_capacity((width * height) as usize * 4);
|
||||
for (_, _, pixel) in image.pixels() {
|
||||
rgba.extend_from_slice(&pixel.to_rgba().0);
|
||||
}
|
||||
(rgba, width, height)
|
||||
};
|
||||
Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to load icon")
|
||||
}
|
||||
|
||||
struct XRWindow {
|
||||
winit_window: winit::window::Window,
|
||||
pose: Rc<XRWindowPose>,
|
||||
}
|
||||
|
||||
struct XRWindowPose {
|
||||
xr_rotation: Cell<Rotation3D<f32, UnknownUnit, UnknownUnit>>,
|
||||
xr_translation: Cell<Vector3D<f32, UnknownUnit>>,
|
||||
}
|
||||
|
||||
impl webxr::glwindow::GlWindow for XRWindow {
|
||||
fn get_render_target(
|
||||
&self,
|
||||
device: &mut Device,
|
||||
_context: &mut Context,
|
||||
) -> webxr::glwindow::GlWindowRenderTarget {
|
||||
let native_widget = device
|
||||
.connection()
|
||||
.create_native_widget_from_winit_window(&self.winit_window)
|
||||
.expect("Failed to create native widget");
|
||||
webxr::glwindow::GlWindowRenderTarget::NativeWidget(native_widget)
|
||||
}
|
||||
|
||||
fn get_rotation(&self) -> Rotation3D<f32, UnknownUnit, UnknownUnit> {
|
||||
self.pose.xr_rotation.get()
|
||||
}
|
||||
|
||||
fn get_translation(&self) -> Vector3D<f32, UnknownUnit> {
|
||||
self.pose.xr_translation.get()
|
||||
}
|
||||
|
||||
fn get_mode(&self) -> webxr::glwindow::GlWindowMode {
|
||||
if pref!(dom.webxr.glwindow.red_cyan) {
|
||||
webxr::glwindow::GlWindowMode::StereoRedCyan
|
||||
} else if pref!(dom.webxr.glwindow.left_right) {
|
||||
webxr::glwindow::GlWindowMode::StereoLeftRight
|
||||
} else if pref!(dom.webxr.glwindow.spherical) {
|
||||
webxr::glwindow::GlWindowMode::Spherical
|
||||
} else if pref!(dom.webxr.glwindow.cubemap) {
|
||||
webxr::glwindow::GlWindowMode::Cubemap
|
||||
} else {
|
||||
webxr::glwindow::GlWindowMode::Blit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl XRWindowPose {
|
||||
fn handle_xr_translation(&self, input: &KeyboardEvent) {
|
||||
if input.state != KeyState::Down {
|
||||
return;
|
||||
}
|
||||
const NORMAL_TRANSLATE: f32 = 0.1;
|
||||
const QUICK_TRANSLATE: f32 = 1.0;
|
||||
let mut x = 0.0;
|
||||
let mut z = 0.0;
|
||||
match input.key {
|
||||
Key::Character(ref k) => match &**k {
|
||||
"w" => z = -NORMAL_TRANSLATE,
|
||||
"W" => z = -QUICK_TRANSLATE,
|
||||
"s" => z = NORMAL_TRANSLATE,
|
||||
"S" => z = QUICK_TRANSLATE,
|
||||
"a" => x = -NORMAL_TRANSLATE,
|
||||
"A" => x = -QUICK_TRANSLATE,
|
||||
"d" => x = NORMAL_TRANSLATE,
|
||||
"D" => x = QUICK_TRANSLATE,
|
||||
_ => return,
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
let (old_x, old_y, old_z) = self.xr_translation.get().to_tuple();
|
||||
let vec = Vector3D::new(x + old_x, old_y, z + old_z);
|
||||
self.xr_translation.set(vec);
|
||||
}
|
||||
|
||||
fn handle_xr_rotation(&self, input: &KeyboardInput, modifiers: ModifiersState) {
|
||||
if input.state != winit::event::ElementState::Pressed {
|
||||
return;
|
||||
}
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
match input.virtual_keycode {
|
||||
Some(VirtualKeyCode::Up) => x = 1.0,
|
||||
Some(VirtualKeyCode::Down) => x = -1.0,
|
||||
Some(VirtualKeyCode::Left) => y = 1.0,
|
||||
Some(VirtualKeyCode::Right) => y = -1.0,
|
||||
_ => return,
|
||||
};
|
||||
if modifiers.shift() {
|
||||
x *= 10.0;
|
||||
y *= 10.0;
|
||||
}
|
||||
let x: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_x(Angle::degrees(x));
|
||||
let y: Rotation3D<_, UnknownUnit, UnknownUnit> = Rotation3D::around_y(Angle::degrees(y));
|
||||
let rotation = self.xr_rotation.get().then(&x).then(&y);
|
||||
self.xr_rotation.set(rotation);
|
||||
}
|
||||
}
|
178
ports/servoshell/headless_window.rs
Normal file
178
ports/servoshell/headless_window.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
/* 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/. */
|
||||
|
||||
//! A headless window implementation.
|
||||
|
||||
use crate::events_loop::WakerEvent;
|
||||
use crate::window_trait::WindowPortsMethods;
|
||||
use euclid::{Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector3D};
|
||||
use servo::compositing::windowing::{AnimationState, EmbedderEvent};
|
||||
use servo::compositing::windowing::{EmbedderCoordinates, WindowMethods};
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::style_traits::DevicePixel;
|
||||
use servo::webrender_api::units::DeviceIntRect;
|
||||
use servo::webrender_surfman::WebrenderSurfman;
|
||||
use servo_media::player::context as MediaPlayerCtxt;
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use surfman::Connection;
|
||||
use surfman::Context;
|
||||
use surfman::Device;
|
||||
use surfman::SurfaceType;
|
||||
|
||||
|
||||
pub struct Window {
|
||||
webrender_surfman: WebrenderSurfman,
|
||||
animation_state: Cell<AnimationState>,
|
||||
fullscreen: Cell<bool>,
|
||||
device_pixels_per_px: Option<f32>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(
|
||||
size: Size2D<u32, DeviceIndependentPixel>,
|
||||
device_pixels_per_px: Option<f32>,
|
||||
) -> Rc<dyn WindowPortsMethods> {
|
||||
// Initialize surfman
|
||||
let connection = Connection::new().expect("Failed to create connection");
|
||||
let adapter = connection
|
||||
.create_software_adapter()
|
||||
.expect("Failed to create adapter");
|
||||
let size = size.to_untyped().to_i32();
|
||||
let surface_type = SurfaceType::Generic { size };
|
||||
let webrender_surfman = WebrenderSurfman::create(&connection, &adapter, surface_type)
|
||||
.expect("Failed to create WR surfman");
|
||||
|
||||
let window = Window {
|
||||
webrender_surfman,
|
||||
animation_state: Cell::new(AnimationState::Idle),
|
||||
fullscreen: Cell::new(false),
|
||||
device_pixels_per_px,
|
||||
};
|
||||
|
||||
Rc::new(window)
|
||||
}
|
||||
|
||||
fn servo_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
match self.device_pixels_per_px {
|
||||
Some(device_pixels_per_px) => Scale::new(device_pixels_per_px),
|
||||
_ => Scale::new(1.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPortsMethods for Window {
|
||||
fn get_events(&self) -> Vec<EmbedderEvent> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn has_events(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn id(&self) -> winit::window::WindowId {
|
||||
unsafe { winit::window::WindowId::dummy() }
|
||||
}
|
||||
|
||||
fn page_height(&self) -> f32 {
|
||||
let height = self
|
||||
.webrender_surfman
|
||||
.context_surface_info()
|
||||
.unwrap_or(None)
|
||||
.map(|info| info.size.height)
|
||||
.unwrap_or(0);
|
||||
let dpr = self.servo_hidpi_factor();
|
||||
height as f32 * dpr.get()
|
||||
}
|
||||
|
||||
fn set_fullscreen(&self, state: bool) {
|
||||
self.fullscreen.set(state);
|
||||
}
|
||||
|
||||
fn get_fullscreen(&self) -> bool {
|
||||
self.fullscreen.get()
|
||||
}
|
||||
|
||||
fn is_animating(&self) -> bool {
|
||||
self.animation_state.get() == AnimationState::Animating
|
||||
}
|
||||
|
||||
fn queue_embedder_events_for_winit_event(&self, _event: winit::event::WindowEvent<'_>) {
|
||||
// Not expecting any winit events.
|
||||
}
|
||||
|
||||
fn new_glwindow(
|
||||
&self,
|
||||
_events_loop: &winit::event_loop::EventLoopWindowTarget<WakerEvent>
|
||||
) -> Box<dyn webxr::glwindow::GlWindow> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn winit_window(&self) -> Option<&winit::window::Window> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_toolbar_height(&self, _height: f32) {
|
||||
unimplemented!("headless Window only")
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowMethods for Window {
|
||||
fn get_coordinates(&self) -> EmbedderCoordinates {
|
||||
let dpr = self.servo_hidpi_factor();
|
||||
let size = self
|
||||
.webrender_surfman
|
||||
.context_surface_info()
|
||||
.unwrap_or(None)
|
||||
.map(|info| Size2D::from_untyped(info.size))
|
||||
.unwrap_or(Size2D::new(0, 0));
|
||||
let viewport = DeviceIntRect::new(Point2D::zero(), size);
|
||||
EmbedderCoordinates {
|
||||
viewport,
|
||||
framebuffer: size,
|
||||
window: (size, Point2D::zero()),
|
||||
screen: size,
|
||||
screen_avail: size,
|
||||
hidpi_factor: dpr,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_animation_state(&self, state: AnimationState) {
|
||||
self.animation_state.set(state);
|
||||
}
|
||||
|
||||
fn get_gl_context(&self) -> MediaPlayerCtxt::GlContext {
|
||||
MediaPlayerCtxt::GlContext::Unknown
|
||||
}
|
||||
|
||||
fn get_native_display(&self) -> MediaPlayerCtxt::NativeDisplay {
|
||||
MediaPlayerCtxt::NativeDisplay::Unknown
|
||||
}
|
||||
|
||||
fn get_gl_api(&self) -> MediaPlayerCtxt::GlApi {
|
||||
MediaPlayerCtxt::GlApi::None
|
||||
}
|
||||
|
||||
fn webrender_surfman(&self) -> WebrenderSurfman {
|
||||
self.webrender_surfman.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl webxr::glwindow::GlWindow for Window {
|
||||
fn get_render_target(
|
||||
&self,
|
||||
_device: &mut Device,
|
||||
_context: &mut Context,
|
||||
) -> webxr::glwindow::GlWindowRenderTarget {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_rotation(&self) -> Rotation3D<f32, UnknownUnit, UnknownUnit> {
|
||||
Rotation3D::identity()
|
||||
}
|
||||
|
||||
fn get_translation(&self) -> Vector3D<f32, UnknownUnit> {
|
||||
Vector3D::zero()
|
||||
}
|
||||
}
|
267
ports/servoshell/keyutils.rs
Normal file
267
ports/servoshell/keyutils.rs
Normal file
|
@ -0,0 +1,267 @@
|
|||
/* 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 winit::event::{ElementState, KeyboardInput, ModifiersState, VirtualKeyCode};
|
||||
use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers};
|
||||
|
||||
// Some shortcuts use Cmd on Mac and Control on other systems.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const CMD_OR_CONTROL: Modifiers = Modifiers::META;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const CMD_OR_CONTROL: Modifiers = Modifiers::CONTROL;
|
||||
|
||||
// Some shortcuts use Cmd on Mac and Alt on other systems.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const CMD_OR_ALT: Modifiers = Modifiers::META;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const CMD_OR_ALT: Modifiers = Modifiers::ALT;
|
||||
|
||||
fn get_servo_key_from_winit_key(key: Option<VirtualKeyCode>) -> Key {
|
||||
use winit::event::VirtualKeyCode::*;
|
||||
// TODO: figure out how to map NavigateForward, NavigateBackward
|
||||
// TODO: map the remaining keys if possible
|
||||
let key = if let Some(key) = key {
|
||||
key
|
||||
} else {
|
||||
return Key::Unidentified;
|
||||
};
|
||||
match key {
|
||||
// printable: Key1 to Key0
|
||||
// printable: A to Z
|
||||
Escape => Key::Escape,
|
||||
F1 => Key::F1,
|
||||
F2 => Key::F2,
|
||||
F3 => Key::F3,
|
||||
F4 => Key::F4,
|
||||
F5 => Key::F5,
|
||||
F6 => Key::F6,
|
||||
F7 => Key::F7,
|
||||
F8 => Key::F8,
|
||||
F9 => Key::F9,
|
||||
F10 => Key::F10,
|
||||
F11 => Key::F11,
|
||||
F12 => Key::F12,
|
||||
// F13 to F15 are not mapped
|
||||
Snapshot => Key::PrintScreen,
|
||||
// Scroll not mapped
|
||||
Pause => Key::Pause,
|
||||
Insert => Key::Insert,
|
||||
Home => Key::Home,
|
||||
Delete => Key::Delete,
|
||||
End => Key::End,
|
||||
PageDown => Key::PageDown,
|
||||
PageUp => Key::PageUp,
|
||||
Left => Key::ArrowLeft,
|
||||
Up => Key::ArrowUp,
|
||||
Right => Key::ArrowRight,
|
||||
Down => Key::ArrowDown,
|
||||
Back => Key::Backspace,
|
||||
Return => Key::Enter,
|
||||
// printable: Space
|
||||
Compose => Key::Compose,
|
||||
// Caret not mapped
|
||||
Numlock => Key::NumLock,
|
||||
// printable: Numpad0 to Numpad9
|
||||
// AbntC1 and AbntC2 not mapped
|
||||
// printable: Add, Apostrophe,
|
||||
// Apps, At, Ax not mapped
|
||||
// printable: Backslash,
|
||||
Calculator => Key::LaunchApplication2,
|
||||
Capital => Key::CapsLock,
|
||||
// printable: Colon, Comma,
|
||||
Convert => Key::Convert,
|
||||
// not mapped: Decimal,
|
||||
// printable: Divide, Equals, Grave,
|
||||
Kana => Key::KanaMode,
|
||||
Kanji => Key::KanjiMode,
|
||||
LAlt => Key::Alt,
|
||||
// printable: LBracket,
|
||||
LControl => Key::Control,
|
||||
LShift => Key::Shift,
|
||||
LWin => Key::Meta,
|
||||
Mail => Key::LaunchMail,
|
||||
// not mapped: MediaSelect,
|
||||
MediaStop => Key::MediaStop,
|
||||
// printable: Minus, Multiply,
|
||||
Mute => Key::AudioVolumeMute,
|
||||
MyComputer => Key::LaunchApplication1,
|
||||
// not mapped: NavigateForward, NavigateBackward
|
||||
NextTrack => Key::MediaTrackNext,
|
||||
NoConvert => Key::NonConvert,
|
||||
// printable: NumpadComma, NumpadEnter, NumpadEquals,
|
||||
// not mapped: OEM102,
|
||||
// printable: Period,
|
||||
PlayPause => Key::MediaPlayPause,
|
||||
Power => Key::Power,
|
||||
PrevTrack => Key::MediaTrackPrevious,
|
||||
RAlt => Key::Alt,
|
||||
// printable RBracket
|
||||
RControl => Key::Control,
|
||||
RShift => Key::Shift,
|
||||
RWin => Key::Meta,
|
||||
// printable Semicolon, Slash
|
||||
Sleep => Key::Standby,
|
||||
// not mapped: Stop,
|
||||
// printable Subtract,
|
||||
// not mapped: Sysrq,
|
||||
Tab => Key::Tab,
|
||||
// printable: Underline,
|
||||
// not mapped: Unlabeled,
|
||||
VolumeDown => Key::AudioVolumeDown,
|
||||
VolumeUp => Key::AudioVolumeUp,
|
||||
Wake => Key::WakeUp,
|
||||
WebBack => Key::BrowserBack,
|
||||
WebFavorites => Key::BrowserFavorites,
|
||||
WebForward => Key::BrowserForward,
|
||||
WebHome => Key::BrowserHome,
|
||||
WebRefresh => Key::BrowserRefresh,
|
||||
WebSearch => Key::BrowserSearch,
|
||||
WebStop => Key::BrowserStop,
|
||||
// printable Yen,
|
||||
Copy => Key::Copy,
|
||||
Paste => Key::Paste,
|
||||
Cut => Key::Cut,
|
||||
_ => Key::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_servo_location_from_winit_key(key: Option<VirtualKeyCode>) -> Location {
|
||||
use winit::event::VirtualKeyCode::*;
|
||||
// TODO: add more numpad keys
|
||||
let key = if let Some(key) = key {
|
||||
key
|
||||
} else {
|
||||
return Location::Standard;
|
||||
};
|
||||
match key {
|
||||
LShift | LControl | LAlt | LWin => Location::Left,
|
||||
RShift | RControl | RAlt | RWin => Location::Right,
|
||||
Numpad0 | Numpad1 | Numpad2 | Numpad3 | Numpad4 | Numpad5 | Numpad6 | Numpad7 |
|
||||
Numpad8 | Numpad9 => Location::Numpad,
|
||||
NumpadComma | NumpadEnter | NumpadEquals => Location::Numpad,
|
||||
_ => Location::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_servo_code_from_scancode(scancode: u32) -> Code {
|
||||
// TODO: Map more codes
|
||||
use keyboard_types::Code::*;
|
||||
match scancode {
|
||||
1 => Escape,
|
||||
2 => Digit1,
|
||||
3 => Digit2,
|
||||
4 => Digit3,
|
||||
5 => Digit4,
|
||||
6 => Digit5,
|
||||
7 => Digit6,
|
||||
8 => Digit7,
|
||||
9 => Digit8,
|
||||
10 => Digit9,
|
||||
11 => Digit0,
|
||||
|
||||
14 => Backspace,
|
||||
15 => Tab,
|
||||
16 => KeyQ,
|
||||
17 => KeyW,
|
||||
18 => KeyE,
|
||||
19 => KeyR,
|
||||
20 => KeyT,
|
||||
21 => KeyY,
|
||||
22 => KeyU,
|
||||
23 => KeyI,
|
||||
24 => KeyO,
|
||||
25 => KeyP,
|
||||
26 => BracketLeft,
|
||||
27 => BracketRight,
|
||||
28 => Enter,
|
||||
|
||||
30 => KeyA,
|
||||
31 => KeyS,
|
||||
32 => KeyD,
|
||||
33 => KeyF,
|
||||
34 => KeyG,
|
||||
35 => KeyH,
|
||||
36 => KeyJ,
|
||||
37 => KeyK,
|
||||
38 => KeyL,
|
||||
39 => Semicolon,
|
||||
40 => Quote,
|
||||
|
||||
42 => ShiftLeft,
|
||||
43 => Backslash,
|
||||
44 => KeyZ,
|
||||
45 => KeyX,
|
||||
46 => KeyC,
|
||||
47 => KeyV,
|
||||
48 => KeyB,
|
||||
49 => KeyN,
|
||||
50 => KeyM,
|
||||
51 => Comma,
|
||||
52 => Period,
|
||||
53 => Slash,
|
||||
54 => ShiftRight,
|
||||
|
||||
57 => Space,
|
||||
|
||||
59 => F1,
|
||||
60 => F2,
|
||||
61 => F3,
|
||||
62 => F4,
|
||||
63 => F5,
|
||||
64 => F6,
|
||||
65 => F7,
|
||||
66 => F8,
|
||||
67 => F9,
|
||||
68 => F10,
|
||||
|
||||
87 => F11,
|
||||
88 => F12,
|
||||
|
||||
103 => ArrowUp,
|
||||
104 => PageUp,
|
||||
105 => ArrowLeft,
|
||||
106 => ArrowRight,
|
||||
|
||||
102 => Home,
|
||||
107 => End,
|
||||
108 => ArrowDown,
|
||||
109 => PageDown,
|
||||
110 => Insert,
|
||||
111 => Delete,
|
||||
|
||||
_ => Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn get_servo_code_from_scancode(_scancode: u32) -> Code {
|
||||
// TODO: Implement for Windows and Mac OS
|
||||
Code::Unidentified
|
||||
}
|
||||
|
||||
fn get_modifiers(mods: ModifiersState) -> Modifiers {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
modifiers.set(Modifiers::CONTROL, mods.ctrl());
|
||||
modifiers.set(Modifiers::SHIFT, mods.shift());
|
||||
modifiers.set(Modifiers::ALT, mods.alt());
|
||||
modifiers.set(Modifiers::META, mods.logo());
|
||||
modifiers
|
||||
}
|
||||
|
||||
pub fn keyboard_event_from_winit(input: KeyboardInput, state: ModifiersState) -> KeyboardEvent {
|
||||
info!("winit keyboard input: {:?}", input);
|
||||
KeyboardEvent {
|
||||
state: match input.state {
|
||||
ElementState::Pressed => KeyState::Down,
|
||||
ElementState::Released => KeyState::Up,
|
||||
},
|
||||
key: get_servo_key_from_winit_key(input.virtual_keycode),
|
||||
code: get_servo_code_from_scancode(input.scancode),
|
||||
location: get_servo_location_from_winit_key(input.virtual_keycode),
|
||||
modifiers: get_modifiers(state),
|
||||
repeat: false,
|
||||
is_composing: false,
|
||||
}
|
||||
}
|
32
ports/servoshell/main.rs
Normal file
32
ports/servoshell/main.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
/* 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/. */
|
||||
|
||||
//! The `servo` test application.
|
||||
//!
|
||||
//! Creates a `Servo` instance with a simple implementation of
|
||||
//! the compositor's `WindowMethods` to create a working web browser.
|
||||
//!
|
||||
//! This browser's implementation of `WindowMethods` is built on top
|
||||
//! of [winit], the cross-platform windowing library.
|
||||
//!
|
||||
//! For the engine itself look next door in `components/servo/lib.rs`.
|
||||
//!
|
||||
//! [winit]: https://github.com/rust-windowing/winit
|
||||
|
||||
// Normally, rust uses the "Console" Windows subsystem, which pops up a console
|
||||
// when running an application. Switching to the "Windows" subsystem prevents
|
||||
// this, but also hides debugging output. Use the "Windows" console unless debug
|
||||
// mode is turned on.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
include!("main2.rs");
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn main() {
|
||||
println!(
|
||||
"Cannot start /ports/servo/ on Android. \
|
||||
Use /support/android/apk/ + /ports/libsimpleservo/ instead"
|
||||
);
|
||||
}
|
174
ports/servoshell/main2.rs
Normal file
174
ports/servoshell/main2.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
/* 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/. */
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
#[macro_use]
|
||||
extern crate sig;
|
||||
|
||||
mod app;
|
||||
mod backtrace;
|
||||
mod browser;
|
||||
mod crash_handler;
|
||||
mod egui_glue;
|
||||
mod embedder;
|
||||
mod events_loop;
|
||||
mod headed_window;
|
||||
mod headless_window;
|
||||
mod keyutils;
|
||||
mod minibrowser;
|
||||
mod prefs;
|
||||
mod resources;
|
||||
mod window_trait;
|
||||
|
||||
use app::App;
|
||||
use getopts::Options;
|
||||
use servo::config::opts::{self, ArgumentParsingResult};
|
||||
use servo::servo_config::pref;
|
||||
use std::env;
|
||||
use std::io::Write;
|
||||
use std::panic;
|
||||
use std::process;
|
||||
use std::thread;
|
||||
|
||||
pub mod platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use crate::platform::macos::deinit;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn deinit(_clean_shutdown: bool) {}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
crash_handler::install();
|
||||
|
||||
resources::init();
|
||||
|
||||
// Parse the command line options and store them globally
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut opts = Options::new();
|
||||
opts.optflag(
|
||||
"",
|
||||
"angle",
|
||||
"Use ANGLE to create a GL context (Windows-only)",
|
||||
);
|
||||
opts.optflag(
|
||||
"",
|
||||
"clean-shutdown",
|
||||
"Do not shutdown until all threads have finished (macos only)",
|
||||
);
|
||||
opts.optflag("b", "no-native-titlebar", "Do not use native titlebar");
|
||||
opts.optopt("", "device-pixel-ratio", "Device pixels per px", "");
|
||||
opts.optopt(
|
||||
"u",
|
||||
"user-agent",
|
||||
"Set custom user agent string (or ios / android / desktop for platform default)",
|
||||
"NCSA Mosaic/1.0 (X11;SunOS 4.1.4 sun4m)",
|
||||
);
|
||||
opts.optmulti(
|
||||
"",
|
||||
"pref",
|
||||
"A preference to set to enable",
|
||||
"dom.bluetooth.enabled",
|
||||
);
|
||||
opts.optmulti(
|
||||
"",
|
||||
"pref",
|
||||
"A preference to set to disable",
|
||||
"dom.webgpu.enabled=false",
|
||||
);
|
||||
|
||||
let opts_matches;
|
||||
let content_process_token;
|
||||
|
||||
match opts::from_cmdline_args(opts, &args) {
|
||||
ArgumentParsingResult::ContentProcess(matches, token) => {
|
||||
opts_matches = matches;
|
||||
content_process_token = Some(token);
|
||||
if opts::get().is_running_problem_test && env::var("RUST_LOG").is_err() {
|
||||
env::set_var("RUST_LOG", "compositing::constellation");
|
||||
}
|
||||
},
|
||||
ArgumentParsingResult::ChromeProcess(matches) => {
|
||||
opts_matches = matches;
|
||||
content_process_token = None;
|
||||
},
|
||||
};
|
||||
|
||||
prefs::register_user_prefs(&opts_matches);
|
||||
|
||||
// TODO: once log-panics is released, can this be replaced by
|
||||
// log_panics::init()?
|
||||
panic::set_hook(Box::new(|info| {
|
||||
warn!("Panic hook called.");
|
||||
let msg = match info.payload().downcast_ref::<&'static str>() {
|
||||
Some(s) => *s,
|
||||
None => match info.payload().downcast_ref::<String>() {
|
||||
Some(s) => &**s,
|
||||
None => "Box<Any>",
|
||||
},
|
||||
};
|
||||
let current_thread = thread::current();
|
||||
let name = current_thread.name().unwrap_or("<unnamed>");
|
||||
let stderr = std::io::stderr();
|
||||
let mut stderr = stderr.lock();
|
||||
if let Some(location) = info.location() {
|
||||
let _ = writeln!(
|
||||
&mut stderr,
|
||||
"{} (thread {}, at {}:{})",
|
||||
msg,
|
||||
name,
|
||||
location.file(),
|
||||
location.line()
|
||||
);
|
||||
} else {
|
||||
let _ = writeln!(&mut stderr, "{} (thread {})", msg, name);
|
||||
}
|
||||
if env::var("RUST_BACKTRACE").is_ok() {
|
||||
let _ = backtrace::print(&mut stderr);
|
||||
}
|
||||
drop(stderr);
|
||||
|
||||
if opts::get().hard_fail && !opts::get().multiprocess {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
error!("{}", msg);
|
||||
}));
|
||||
|
||||
if let Some(token) = content_process_token {
|
||||
return servo::run_content_process(token);
|
||||
}
|
||||
|
||||
if opts::get().is_printing_version {
|
||||
println!("{}", servo_version());
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
let clean_shutdown = opts_matches.opt_present("clean-shutdown");
|
||||
let do_not_use_native_titlebar =
|
||||
opts_matches.opt_present("no-native-titlebar") || !(pref!(shell.native_titlebar.enabled));
|
||||
let device_pixels_per_px = opts_matches.opt_str("device-pixel-ratio").map(|dppx_str| {
|
||||
dppx_str.parse().unwrap_or_else(|err| {
|
||||
error!("Error parsing option: --device-pixel-ratio ({})", err);
|
||||
process::exit(1);
|
||||
})
|
||||
});
|
||||
|
||||
let user_agent = opts_matches.opt_str("u");
|
||||
|
||||
App::run(do_not_use_native_titlebar, device_pixels_per_px, user_agent);
|
||||
|
||||
platform::deinit(clean_shutdown)
|
||||
}
|
||||
|
||||
pub fn servo_version() -> String {
|
||||
format!("Servo {}-{}", env!("CARGO_PKG_VERSION"), env!("VERGEN_GIT_SHA"))
|
||||
}
|
115
ports/servoshell/minibrowser.rs
Normal file
115
ports/servoshell/minibrowser.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
/* 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 std::{cell::{RefCell, Cell}, sync::Arc};
|
||||
|
||||
use egui::{TopBottomPanel, Modifiers, Key};
|
||||
use servo::{servo_url::ServoUrl, compositing::windowing::EmbedderEvent};
|
||||
use servo::webrender_surfman::WebrenderSurfman;
|
||||
|
||||
use crate::{egui_glue::EguiGlow, events_loop::EventsLoop, browser::Browser, window_trait::WindowPortsMethods};
|
||||
|
||||
pub struct Minibrowser {
|
||||
pub context: EguiGlow,
|
||||
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
|
||||
pub toolbar_height: Cell<f32>,
|
||||
location: RefCell<String>,
|
||||
|
||||
/// Whether the location has been edited by the user without clicking Go.
|
||||
location_dirty: Cell<bool>,
|
||||
}
|
||||
|
||||
pub enum MinibrowserEvent {
|
||||
/// Go button clicked.
|
||||
Go,
|
||||
}
|
||||
|
||||
impl Minibrowser {
|
||||
pub fn new(webrender_surfman: &WebrenderSurfman, events_loop: &EventsLoop) -> Self {
|
||||
let gl = unsafe {
|
||||
glow::Context::from_loader_function(|s| {
|
||||
webrender_surfman.get_proc_address(s)
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
context: EguiGlow::new(events_loop.as_winit(), Arc::new(gl), None),
|
||||
event_queue: RefCell::new(vec![]),
|
||||
toolbar_height: 0f32.into(),
|
||||
location: RefCell::new(String::default()),
|
||||
location_dirty: false.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the minibrowser, but don’t paint.
|
||||
pub fn update(&mut self, window: &winit::window::Window) {
|
||||
let Self { context, event_queue, location, location_dirty, toolbar_height } = self;
|
||||
let _duration = context.run(window, |ctx| {
|
||||
TopBottomPanel::top("toolbar").show(ctx, |ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
ui.available_size(),
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
if ui.button("go").clicked() {
|
||||
event_queue.borrow_mut().push(MinibrowserEvent::Go);
|
||||
location_dirty.set(false);
|
||||
}
|
||||
|
||||
let location_field = ui.add_sized(
|
||||
ui.available_size(),
|
||||
egui::TextEdit::singleline(&mut *location.borrow_mut()),
|
||||
);
|
||||
if location_field.changed() {
|
||||
location_dirty.set(true);
|
||||
}
|
||||
if ui.input(|i| i.clone().consume_key(Modifiers::COMMAND, Key::L)) {
|
||||
location_field.request_focus();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
toolbar_height.set(ctx.used_rect().height());
|
||||
});
|
||||
}
|
||||
|
||||
/// Paint the minibrowser, as of the last update.
|
||||
pub fn paint(&mut self, window: &winit::window::Window) {
|
||||
self.context.paint(window);
|
||||
}
|
||||
|
||||
/// Takes any outstanding events from the [Minibrowser], converting them to [EmbedderEvent] and
|
||||
/// routing those to the App event queue.
|
||||
pub fn queue_embedder_events_for_minibrowser_events(
|
||||
&self, browser: &Browser<dyn WindowPortsMethods>,
|
||||
app_event_queue: &mut Vec<EmbedderEvent>,
|
||||
) {
|
||||
for event in self.event_queue.borrow_mut().drain(..) {
|
||||
match event {
|
||||
MinibrowserEvent::Go => {
|
||||
let browser_id = browser.browser_id().unwrap();
|
||||
let location = self.location.borrow();
|
||||
let Ok(url) = ServoUrl::parse(&location) else {
|
||||
warn!("failed to parse location");
|
||||
break;
|
||||
};
|
||||
app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the location field when the [Browser] says it has changed, unless the user has
|
||||
/// started editing it without clicking Go.
|
||||
pub fn update_location_in_toolbar(&mut self, browser: &mut Browser<dyn WindowPortsMethods>) -> bool {
|
||||
if !self.location_dirty.get() {
|
||||
if let Some(location) = browser.current_url_string() {
|
||||
self.location = RefCell::new(location.to_owned());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
21
ports/servoshell/platform/macos/Info.plist
Normal file
21
ports/servoshell/platform/macos/Info.plist
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Servo</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Servo</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2016 The Servo Authors</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.0.1</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.servo.servo</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
13
ports/servoshell/platform/macos/count_threads.c
Normal file
13
ports/servoshell/platform/macos/count_threads.c
Normal file
|
@ -0,0 +1,13 @@
|
|||
/* 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/. */
|
||||
|
||||
#include <mach/mach.h>
|
||||
|
||||
int macos_count_running_threads() {
|
||||
task_t task = current_task();
|
||||
thread_act_array_t threads;
|
||||
mach_msg_type_number_t tcnt;
|
||||
task_threads(task, &threads, &tcnt);
|
||||
return tcnt;
|
||||
}
|
46
ports/servoshell/platform/macos/mod.rs
Normal file
46
ports/servoshell/platform/macos/mod.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
/* 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 std::ptr;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn deinit(clean_shutdown: bool) {
|
||||
// An unfortunate hack to make sure the linker's dead code stripping doesn't strip our
|
||||
// `Info.plist`.
|
||||
unsafe {
|
||||
ptr::read_volatile(&INFO_PLIST[0]);
|
||||
}
|
||||
|
||||
let thread_count = unsafe { macos_count_running_threads() };
|
||||
|
||||
if thread_count != 1 {
|
||||
println!(
|
||||
"{} threads are still running after shutdown (bad).",
|
||||
thread_count
|
||||
);
|
||||
if clean_shutdown {
|
||||
println!("Waiting until all threads have shutdown");
|
||||
loop {
|
||||
let thread_count = unsafe { macos_count_running_threads() };
|
||||
if thread_count == 1 {
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
println!("{} threads are still running.", thread_count);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("All threads have shutdown (good).");
|
||||
}
|
||||
}
|
||||
|
||||
#[link_section = "__TEXT,__info_plist"]
|
||||
#[no_mangle]
|
||||
pub static INFO_PLIST: [u8; 619] = *include_bytes!("Info.plist");
|
||||
|
||||
#[link(name = "count_threads")]
|
||||
extern "C" {
|
||||
fn macos_count_running_threads() -> i32;
|
||||
}
|
23
ports/servoshell/platform/windows/servo.exe.manifest
Normal file
23
ports/servoshell/platform/windows/servo.exe.manifest
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
|
||||
manifestVersion="1.0"
|
||||
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32"
|
||||
name="servo.Servo"
|
||||
version="0.1.0.0"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
|
||||
<dpiAware>true</dpiAware>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
127
ports/servoshell/prefs.rs
Normal file
127
ports/servoshell/prefs.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
/* 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 getopts::{Matches, Options};
|
||||
use servo::config::opts::{self, ArgumentParsingResult};
|
||||
use servo::config::prefs::{self, PrefValue};
|
||||
use servo::embedder_traits;
|
||||
use servo::servo_config::basedir;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
pub fn register_user_prefs(opts_matches: &Matches) {
|
||||
// Read user's prefs.json and then parse --pref command line args.
|
||||
|
||||
let user_prefs_path = opts::get()
|
||||
.config_dir
|
||||
.clone()
|
||||
.or_else(basedir::default_config_dir)
|
||||
.map(|path| path.join("prefs.json"))
|
||||
.filter(|path| path.exists());
|
||||
|
||||
let mut userprefs = if let Some(path) = user_prefs_path {
|
||||
let mut file = File::open(path).expect("Error opening user prefs");
|
||||
let mut txt = String::new();
|
||||
file.read_to_string(&mut txt)
|
||||
.expect("Can't read user prefs file");
|
||||
prefs::read_prefs_map(&txt).expect("Can't parse user prefs file")
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let argprefs: HashMap<String, PrefValue> = opts_matches
|
||||
.opt_strs("pref")
|
||||
.iter()
|
||||
.map(|pref| {
|
||||
let split: Vec<&str> = pref.splitn(2, '=').collect();
|
||||
let pref_name = split[0];
|
||||
let pref_value = match split.get(1).cloned() {
|
||||
Some("true") | None => PrefValue::Bool(true),
|
||||
Some("false") => PrefValue::Bool(false),
|
||||
Some(string) => {
|
||||
if let Ok(int) = string.parse::<i64>() {
|
||||
PrefValue::Int(int)
|
||||
} else if let Ok(float) = string.parse::<f64>() {
|
||||
PrefValue::Float(float)
|
||||
} else {
|
||||
PrefValue::from(string)
|
||||
}
|
||||
},
|
||||
};
|
||||
(pref_name.to_string(), pref_value)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// --pref overrides user prefs.json
|
||||
userprefs.extend(argprefs);
|
||||
|
||||
prefs::add_user_prefs(userprefs);
|
||||
}
|
||||
|
||||
// Use for test
|
||||
#[allow(dead_code)]
|
||||
fn test_parse_pref(arg: &str) {
|
||||
embedder_traits::resources::set_for_tests();
|
||||
let mut opts = Options::new();
|
||||
opts.optmulti("", "pref", "", "");
|
||||
let args = vec!["servo".to_string(), "--pref".to_string(), arg.to_string()];
|
||||
let matches = match opts::from_cmdline_args(opts, &args) {
|
||||
ArgumentParsingResult::ContentProcess(m, _) => m,
|
||||
ArgumentParsingResult::ChromeProcess(m) => m,
|
||||
};
|
||||
register_user_prefs(&matches);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_pref_from_command_line() {
|
||||
use servo::servo_config::pref;
|
||||
// Test with boolean values.
|
||||
test_parse_pref("dom.bluetooth.enabled=true");
|
||||
assert_eq!(
|
||||
prefs::pref_map().get("dom.bluetooth.enabled"),
|
||||
PrefValue::Bool(true)
|
||||
);
|
||||
assert!(pref!(dom.bluetooth.enabled));
|
||||
|
||||
test_parse_pref("dom.bluetooth.enabled=false");
|
||||
assert_eq!(
|
||||
prefs::pref_map().get("dom.bluetooth.enabled"),
|
||||
PrefValue::Bool(false)
|
||||
);
|
||||
assert_eq!(pref!(dom.bluetooth.enabled), false);
|
||||
|
||||
// Test with numbers
|
||||
test_parse_pref("layout.threads=42");
|
||||
assert_eq!(pref!(layout.threads), 42);
|
||||
|
||||
// Test string.
|
||||
test_parse_pref("shell.homepage=str");
|
||||
assert_eq!(pref!(shell.homepage), "str");
|
||||
|
||||
// Test with no value (defaults to true).
|
||||
prefs::pref_map()
|
||||
.set("dom.bluetooth.enabled", false)
|
||||
.unwrap();
|
||||
test_parse_pref("dom.bluetooth.enabled");
|
||||
assert!(pref!(dom.bluetooth.enabled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_prefs_from_command_line_panics() {
|
||||
let err_msg = std::panic::catch_unwind(|| {
|
||||
test_parse_pref("doesntexist=true");
|
||||
})
|
||||
.err()
|
||||
.and_then(|a| a.downcast_ref::<String>().cloned())
|
||||
.expect("Should panic");
|
||||
assert!(
|
||||
err_msg.starts_with("Error setting preference"),
|
||||
"Message should describe the problem"
|
||||
);
|
||||
assert!(
|
||||
err_msg.contains("doesntexist"),
|
||||
"Message should mention the name of the preference"
|
||||
);
|
||||
}
|
86
ports/servoshell/resources.rs
Normal file
86
ports/servoshell/resources.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
/* 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 servo::embedder_traits::resources::{self, Resource};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
lazy_static! {
|
||||
static ref CMD_RESOURCE_DIR: Mutex<Option<String>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
struct ResourceReader;
|
||||
|
||||
fn filename(file: Resource) -> &'static str {
|
||||
match file {
|
||||
Resource::Preferences => "prefs.json",
|
||||
Resource::BluetoothBlocklist => "gatt_blocklist.txt",
|
||||
Resource::DomainList => "public_domains.txt",
|
||||
Resource::HstsPreloadList => "hsts_preload.json",
|
||||
Resource::BadCertHTML => "badcert.html",
|
||||
Resource::NetErrorHTML => "neterror.html",
|
||||
Resource::UserAgentCSS => "user-agent.css",
|
||||
Resource::ServoCSS => "servo.css",
|
||||
Resource::PresentationalHintsCSS => "presentational-hints.css",
|
||||
Resource::QuirksModeCSS => "quirks-mode.css",
|
||||
Resource::RippyPNG => "rippy.png",
|
||||
Resource::MediaControlsCSS => "media-controls.css",
|
||||
Resource::MediaControlsJS => "media-controls.js",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
resources::set(Box::new(ResourceReader));
|
||||
}
|
||||
|
||||
fn resources_dir_path() -> io::Result<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.
|
||||
let mut dir = CMD_RESOURCE_DIR.lock().unwrap();
|
||||
if let Some(ref path) = *dir {
|
||||
return Ok(PathBuf::from(path));
|
||||
}
|
||||
|
||||
// FIXME: Find a way to not rely on the executable being
|
||||
// under `<servo source>[/$target_triple]/target/debug`
|
||||
// or `<servo source>[/$target_triple]/target/release`.
|
||||
let mut path = env::current_exe()?;
|
||||
// Follow symlink
|
||||
path = path.canonicalize()?;
|
||||
|
||||
while path.pop() {
|
||||
path.push("resources");
|
||||
if path.is_dir() {
|
||||
break;
|
||||
}
|
||||
path.pop();
|
||||
// Check for Resources on mac when using a case sensitive filesystem.
|
||||
path.push("Resources");
|
||||
if path.is_dir() {
|
||||
break;
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
*dir = Some(path.to_str().unwrap().to_owned());
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
impl resources::ResourceReaderMethods for ResourceReader {
|
||||
fn read(&self, file: Resource) -> Vec<u8> {
|
||||
let file = filename(file);
|
||||
let mut path = resources_dir_path().expect("Can't find resources directory");
|
||||
path.push(file);
|
||||
fs::read(path).expect("Can't read file")
|
||||
}
|
||||
fn sandbox_access_files_dirs(&self) -> Vec<PathBuf> {
|
||||
vec![resources_dir_path().expect("Can't find resources directory")]
|
||||
}
|
||||
fn sandbox_access_files(&self) -> Vec<PathBuf> {
|
||||
vec![]
|
||||
}
|
||||
}
|
36
ports/servoshell/window_trait.rs
Normal file
36
ports/servoshell/window_trait.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
/* 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/. */
|
||||
|
||||
//! Definition of Window.
|
||||
//! Implemented by headless and headed windows.
|
||||
|
||||
use crate::events_loop::WakerEvent;
|
||||
use servo::compositing::windowing::{EmbedderEvent, WindowMethods};
|
||||
use servo::embedder_traits::Cursor;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
|
||||
|
||||
|
||||
// This should vary by zoom level and maybe actual text size (focused or under cursor)
|
||||
pub const LINE_HEIGHT: f32 = 38.0;
|
||||
|
||||
pub trait WindowPortsMethods: WindowMethods {
|
||||
fn get_events(&self) -> Vec<EmbedderEvent>;
|
||||
fn id(&self) -> winit::window::WindowId;
|
||||
fn has_events(&self) -> bool;
|
||||
fn page_height(&self) -> f32;
|
||||
fn get_fullscreen(&self) -> bool;
|
||||
fn queue_embedder_events_for_winit_event(&self, event: winit::event::WindowEvent<'_>);
|
||||
fn is_animating(&self) -> bool;
|
||||
fn set_title(&self, _title: &str) {}
|
||||
fn set_inner_size(&self, _size: DeviceIntSize) {}
|
||||
fn set_position(&self, _point: DeviceIntPoint) {}
|
||||
fn set_fullscreen(&self, _state: bool) {}
|
||||
fn set_cursor(&self, _cursor: Cursor) {}
|
||||
fn new_glwindow(
|
||||
&self,
|
||||
events_loop: &winit::event_loop::EventLoopWindowTarget<WakerEvent>
|
||||
) -> Box<dyn webxr::glwindow::GlWindow>;
|
||||
fn winit_window(&self) -> Option<&winit::window::Window>;
|
||||
fn set_toolbar_height(&self, height: f32);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue