Rename ports/winit package to servoshell (#30163)

* rename winit package to servoshell

* revert previous changes and rename only package
This commit is contained in:
Atbrakhi 2023-08-28 16:36:57 +02:00 committed by GitHub
parent cc4fe4981f
commit 66567faeb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 39 additions and 39 deletions

View 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
View 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()
}

View 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 doesnt 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: were 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
View 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
View 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/");
}

View 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 wont lose output if we crash later
// in this handler, and the std::io::stderr() call never allocates.
// std::io::stdout() allocates the first time its 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
// were 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 doesnt 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
}

View 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();
}
}

View 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);
}
}
}

View 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()))
}
}

View 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);
}
}

View 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()
}
}

View 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
View 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
View 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"))
}

View 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 dont 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
}
}

View 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 &#169; 2016 The Servo Authors</string>
<key>CFBundleVersion</key>
<string>0.0.1</string>
<key>CFBundleIdentifier</key>
<string>org.servo.servo</string>
</dict>
</plist>

View 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;
}

View 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;
}

View 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
View 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"
);
}

View 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![]
}
}

View 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);
}