mirror of
https://github.com/servo/servo.git
synced 2025-09-30 00:29:14 +01:00
Split servoshell into Desktop and common part (#32457)
* servoshell: Move desktop files Move files related to winit into a desktop module. This is a preparation to merge the android and ohos apps into servoshell. * servoshell: Format imports * servoshell: Move panic hook into separate file * servoshell: Move desktop main * Consider ohos as not desktop * servoshell: Adjust dependencies for shared code * servoshell: Remove native-bluetooth from default features There currently is no good way to have target specific default features. * Rename desktop_main.rs to cli.rs * Remove todo
This commit is contained in:
parent
bae9f6d844
commit
ff4cd4af96
18 changed files with 233 additions and 179 deletions
477
ports/servoshell/desktop/app.rs
Normal file
477
ports/servoshell/desktop/app.rs
Normal file
|
@ -0,0 +1,477 @@
|
|||
/* 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 std::cell::{Cell, RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::time::Instant;
|
||||
use std::{env, fs};
|
||||
|
||||
use gleam::gl;
|
||||
use log::{info, trace, warn};
|
||||
use servo::compositing::windowing::EmbedderEvent;
|
||||
use servo::compositing::CompositeTarget;
|
||||
use servo::config::{opts, set_pref};
|
||||
use servo::servo_config::pref;
|
||||
use servo::Servo;
|
||||
use surfman::GLApi;
|
||||
use webxr::glwindow::GlWindowDiscovery;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::EventLoopWindowTarget;
|
||||
use winit::window::WindowId;
|
||||
|
||||
use super::events_loop::{EventsLoop, WakerEvent};
|
||||
use super::minibrowser::Minibrowser;
|
||||
use super::webview::WebViewManager;
|
||||
use super::{headed_window, headless_window};
|
||||
use crate::desktop::embedder::EmbedderCallbacks;
|
||||
use crate::desktop::tracing::trace_winit_event;
|
||||
use crate::desktop::window_trait::WindowPortsMethods;
|
||||
use crate::parser::get_default_url;
|
||||
|
||||
pub struct App {
|
||||
servo: Option<Servo<dyn WindowPortsMethods>>,
|
||||
webviews: RefCell<WebViewManager<dyn WindowPortsMethods>>,
|
||||
event_queue: RefCell<Vec<EmbedderEvent>>,
|
||||
suspended: Cell<bool>,
|
||||
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
|
||||
minibrowser: Option<RefCell<Minibrowser>>,
|
||||
}
|
||||
|
||||
enum Present {
|
||||
Immediate,
|
||||
Deferred,
|
||||
None,
|
||||
}
|
||||
|
||||
/// Action to be taken by the caller of [`App::handle_events`].
|
||||
enum PumpResult {
|
||||
/// The caller should shut down Servo and its related context.
|
||||
Shutdown,
|
||||
Continue {
|
||||
update: bool,
|
||||
present: Present,
|
||||
},
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn run(
|
||||
no_native_titlebar: bool,
|
||||
device_pixel_ratio_override: Option<f32>,
|
||||
user_agent: Option<String>,
|
||||
url: Option<String>,
|
||||
) {
|
||||
let events_loop = EventsLoop::new(opts::get().headless, opts::get().output_file.is_some())
|
||||
.expect("Failed to create events loop");
|
||||
|
||||
// Implements window methods, used by compositor.
|
||||
let window = if opts::get().headless {
|
||||
// GL video rendering is not supported on headless windows.
|
||||
set_pref!(media.glvideo.enabled, false);
|
||||
headless_window::Window::new(
|
||||
opts::get().initial_window_size,
|
||||
device_pixel_ratio_override,
|
||||
)
|
||||
} else {
|
||||
Rc::new(headed_window::Window::new(
|
||||
opts::get().initial_window_size,
|
||||
&events_loop,
|
||||
no_native_titlebar,
|
||||
device_pixel_ratio_override,
|
||||
))
|
||||
};
|
||||
|
||||
// Handle browser state.
|
||||
let webviews = WebViewManager::new(window.clone());
|
||||
let initial_url = get_default_url(url.as_deref(), env::current_dir().unwrap(), |path| {
|
||||
fs::metadata(path).is_ok()
|
||||
});
|
||||
|
||||
let mut app = App {
|
||||
event_queue: RefCell::new(vec![]),
|
||||
webviews: RefCell::new(webviews),
|
||||
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 rendering_context = window.rendering_context();
|
||||
let webrender_gl = match rendering_context.connection().gl_api() {
|
||||
GLApi::GL => unsafe {
|
||||
gl::GlFns::load_with(|s| rendering_context.get_proc_address(s))
|
||||
},
|
||||
GLApi::GLES => unsafe {
|
||||
gl::GlesFns::load_with(|s| rendering_context.get_proc_address(s))
|
||||
},
|
||||
};
|
||||
rendering_context.make_gl_context_current().unwrap();
|
||||
debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR);
|
||||
|
||||
app.minibrowser = Some(
|
||||
Minibrowser::new(&rendering_context, &events_loop, initial_url.clone()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
// Servo is not yet initialised, so there is no `servo_framebuffer_id`.
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut app.webviews.borrow_mut(),
|
||||
None,
|
||||
"init",
|
||||
);
|
||||
window.set_toolbar_height(minibrowser.toolbar_height);
|
||||
}
|
||||
|
||||
let t_start = Instant::now();
|
||||
let mut t = t_start;
|
||||
let ev_waker = events_loop.create_event_loop_waker();
|
||||
events_loop.run_forever(move |event, w, control_flow| {
|
||||
let now = Instant::now();
|
||||
trace_winit_event!(event, "@{:?} (+{:?}) {event:?}", now - t_start, now - t);
|
||||
t = now;
|
||||
if let winit::event::Event::NewEvents(winit::event::StartCause::Init) = event {
|
||||
let surfman = window.rendering_context();
|
||||
|
||||
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 composite_target = if app.minibrowser.is_some() {
|
||||
CompositeTarget::Fbo
|
||||
} else {
|
||||
CompositeTarget::Window
|
||||
};
|
||||
let servo_data = Servo::new(
|
||||
embedder,
|
||||
window.clone(),
|
||||
user_agent.clone(),
|
||||
composite_target,
|
||||
);
|
||||
let mut servo = servo_data.servo;
|
||||
|
||||
servo.handle_events(vec![EmbedderEvent::NewWebView(
|
||||
initial_url.to_owned(),
|
||||
servo_data.browser_id,
|
||||
)]);
|
||||
servo.setup_logging();
|
||||
|
||||
app.windows.insert(window.id(), window.clone());
|
||||
app.servo = Some(servo);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
if let winit::event::Event::WindowEvent {
|
||||
window_id: _,
|
||||
event: winit::event::WindowEvent::RedrawRequested,
|
||||
} = event
|
||||
{
|
||||
// We need to redraw the window for some reason.
|
||||
trace!("RedrawRequested");
|
||||
|
||||
// WARNING: do not defer painting or presenting to some later tick of the event
|
||||
// loop or servoshell may become unresponsive! (servo#30312)
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut app.webviews.borrow_mut(),
|
||||
app.servo.as_ref().unwrap().offscreen_framebuffer_id(),
|
||||
"RedrawRequested",
|
||||
);
|
||||
minibrowser.paint(window.winit_window().unwrap());
|
||||
}
|
||||
|
||||
app.servo.as_mut().unwrap().present();
|
||||
}
|
||||
|
||||
// Handle the event
|
||||
let mut consumed = false;
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
match event {
|
||||
winit::event::Event::WindowEvent {
|
||||
event: WindowEvent::ScaleFactorChanged { scale_factor, .. },
|
||||
..
|
||||
} => {
|
||||
// Intercept any ScaleFactorChanged events away from EguiGlow::on_window_event, so
|
||||
// we can use our own logic for calculating the scale factor and set egui’s
|
||||
// scale factor to that value manually.
|
||||
let effective_scale_factor = window.hidpi_factor().get();
|
||||
info!(
|
||||
"window scale factor changed to {}, setting scale factor to {}",
|
||||
scale_factor, effective_scale_factor
|
||||
);
|
||||
minibrowser
|
||||
.context
|
||||
.egui_ctx
|
||||
.set_pixels_per_point(effective_scale_factor);
|
||||
|
||||
// Request a winit redraw event, so we can recomposite, update and paint
|
||||
// the minibrowser, and present the new frame.
|
||||
window.winit_window().unwrap().request_redraw();
|
||||
},
|
||||
winit::event::Event::WindowEvent {
|
||||
ref event,
|
||||
window_id: _,
|
||||
} => {
|
||||
let response =
|
||||
minibrowser.on_window_event(window.winit_window().unwrap(), event);
|
||||
// Update minibrowser if there's resize event to sync up with window.
|
||||
if let WindowEvent::Resized(_) = event {
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut app.webviews.borrow_mut(),
|
||||
app.servo.as_ref().unwrap().offscreen_framebuffer_id(),
|
||||
"Sync WebView size with Window Resize event",
|
||||
);
|
||||
}
|
||||
if response.repaint {
|
||||
// Request a winit redraw event, so we can recomposite, update and paint
|
||||
// the minibrowser, and present the new frame.
|
||||
window.winit_window().unwrap().request_redraw();
|
||||
}
|
||||
|
||||
// TODO how do we handle the tab key? (see doc for consumed)
|
||||
// Note that servo doesn’t yet support tabbing through links and inputs
|
||||
consumed = response.consumed;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
if !consumed {
|
||||
app.queue_embedder_events_for_winit_event(event);
|
||||
}
|
||||
|
||||
let animating = app.is_animating();
|
||||
|
||||
// Block until the window gets an event
|
||||
if !animating || app.suspended.get() {
|
||||
control_flow.set_wait();
|
||||
} else {
|
||||
control_flow.set_poll();
|
||||
}
|
||||
|
||||
// Consume and handle any events from the Minibrowser.
|
||||
if let Some(minibrowser) = app.minibrowser() {
|
||||
let webviews = &mut app.webviews.borrow_mut();
|
||||
let app_event_queue = &mut app.event_queue.borrow_mut();
|
||||
minibrowser.queue_embedder_events_for_minibrowser_events(webviews, app_event_queue);
|
||||
}
|
||||
|
||||
match app.handle_events() {
|
||||
PumpResult::Shutdown => {
|
||||
control_flow.set_exit();
|
||||
app.servo.take().unwrap().deinit();
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
minibrowser.context.destroy();
|
||||
}
|
||||
},
|
||||
PumpResult::Continue { update, present } => {
|
||||
if update {
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
let webviews = &mut app.webviews.borrow_mut();
|
||||
if minibrowser.update_webview_data(webviews) {
|
||||
// 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(),
|
||||
webviews,
|
||||
app.servo.as_ref().unwrap().offscreen_framebuffer_id(),
|
||||
"update_location_in_toolbar",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
match present {
|
||||
Present::Immediate => {
|
||||
// The window was resized.
|
||||
trace!("PumpResult::Present::Immediate");
|
||||
|
||||
// If we had resized any of the viewports in response to this, we would need to
|
||||
// call Servo::repaint_synchronously. At the moment we don’t, so there won’t be
|
||||
// any paint scheduled, and calling it would hang the compositor forever.
|
||||
if let Some(mut minibrowser) = app.minibrowser() {
|
||||
minibrowser.update(
|
||||
window.winit_window().unwrap(),
|
||||
&mut app.webviews.borrow_mut(),
|
||||
app.servo.as_ref().unwrap().offscreen_framebuffer_id(),
|
||||
"PumpResult::Present::Immediate",
|
||||
);
|
||||
minibrowser.paint(window.winit_window().unwrap());
|
||||
}
|
||||
app.servo.as_mut().unwrap().present();
|
||||
},
|
||||
Present::Deferred => {
|
||||
// The compositor has painted to this frame.
|
||||
trace!("PumpResult::Present::Deferred");
|
||||
|
||||
// Request a winit redraw event, so we can paint the minibrowser and present.
|
||||
// Otherwise, it's in headless mode and we present directly.
|
||||
if let Some(window) = window.winit_window() {
|
||||
window.request_redraw();
|
||||
} else {
|
||||
app.servo.as_mut().unwrap().present();
|
||||
}
|
||||
},
|
||||
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 { .. } => {},
|
||||
|
||||
// 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) => {
|
||||
if event == winit::event::WindowEvent::RedrawRequested {
|
||||
self.event_queue.borrow_mut().push(EmbedderEvent::Idle);
|
||||
}
|
||||
|
||||
window.queue_embedder_events_for_winit_event(event);
|
||||
},
|
||||
},
|
||||
|
||||
winit::event::Event::LoopExiting |
|
||||
winit::event::Event::AboutToWait |
|
||||
winit::event::Event::MemoryWarning |
|
||||
winit::event::Event::NewEvents(..) => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 WebViewManager queue, and from the WebViewManager 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 WebViewManager can handle them.
|
||||
fn handle_events(&mut self) -> PumpResult {
|
||||
let mut webviews = self.webviews.borrow_mut();
|
||||
|
||||
// Take any outstanding embedder events from the App and its Windows.
|
||||
let mut embedder_events = self.get_events();
|
||||
for window in self.windows.values() {
|
||||
embedder_events.extend(window.get_events());
|
||||
}
|
||||
|
||||
// Catch some keyboard events, and push the rest onto the WebViewManager event queue.
|
||||
webviews.handle_window_events(embedder_events);
|
||||
|
||||
// If the Gamepad API is enabled, handle gamepad events from GilRs.
|
||||
// Checking for current_url_string should ensure we'll have a valid browsing context.
|
||||
if pref!(dom.gamepad.enabled) && webviews.current_url_string().is_some() {
|
||||
webviews.handle_gamepad_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;
|
||||
let mut need_update = false;
|
||||
loop {
|
||||
// Consume and handle those embedder messages.
|
||||
let servo_event_response = webviews.handle_servo_events(embedder_messages);
|
||||
need_present |= servo_event_response.need_present;
|
||||
need_update |= servo_event_response.need_update;
|
||||
|
||||
// Route embedder events from the WebViewManager 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(webviews.get_events());
|
||||
if webviews.shutdown_requested() {
|
||||
return PumpResult::Shutdown;
|
||||
}
|
||||
|
||||
// Take any new embedder messages from Servo itself.
|
||||
embedder_messages = self.servo.as_mut().unwrap().get_events();
|
||||
if embedder_messages.len() == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let present = if need_resize {
|
||||
Present::Immediate
|
||||
} else if need_present {
|
||||
Present::Deferred
|
||||
} else {
|
||||
Present::None
|
||||
};
|
||||
|
||||
PumpResult::Continue {
|
||||
update: need_update,
|
||||
present,
|
||||
}
|
||||
}
|
||||
|
||||
fn minibrowser(&self) -> Option<RefMut<Minibrowser>> {
|
||||
self.minibrowser.as_ref().map(|x| x.borrow_mut())
|
||||
}
|
||||
}
|
104
ports/servoshell/desktop/cli.rs
Normal file
104
ports/servoshell/desktop/cli.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
/* 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::{env, panic, process};
|
||||
|
||||
use getopts::Options;
|
||||
use log::error;
|
||||
use servo::config::opts::{self, ArgumentParsingResult};
|
||||
use servo::servo_config::pref;
|
||||
|
||||
use crate::desktop::app::App;
|
||||
use crate::panic_hook;
|
||||
|
||||
pub fn main() {
|
||||
crate::crash_handler::install();
|
||||
|
||||
crate::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(
|
||||
"",
|
||||
"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);
|
||||
},
|
||||
ArgumentParsingResult::ChromeProcess(matches) => {
|
||||
opts_matches = matches;
|
||||
content_process_token = None;
|
||||
},
|
||||
};
|
||||
|
||||
crate::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(panic_hook::panic_hook));
|
||||
|
||||
if let Some(token) = content_process_token {
|
||||
return servo::run_content_process(token);
|
||||
}
|
||||
|
||||
if opts::get().is_printing_version {
|
||||
println!("{}", crate::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_pixel_ratio_override = 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");
|
||||
|
||||
let url_opt = if !opts_matches.free.is_empty() {
|
||||
Some(&opts_matches.free[0][..])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
App::run(
|
||||
do_not_use_native_titlebar,
|
||||
device_pixel_ratio_override,
|
||||
user_agent,
|
||||
url_opt.map(|s| s.to_string()),
|
||||
);
|
||||
|
||||
crate::platform::deinit(clean_shutdown)
|
||||
}
|
143
ports/servoshell/desktop/egui_glue.rs
Normal file
143
ports/servoshell/desktop/egui_glue.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
/* 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::{ViewportId, ViewportOutput};
|
||||
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();
|
||||
|
||||
let egui_ctx = egui::Context::default();
|
||||
Self {
|
||||
egui_winit: egui_winit::State::new(
|
||||
egui_ctx.clone(),
|
||||
ViewportId::ROOT,
|
||||
event_loop,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
egui_ctx,
|
||||
painter,
|
||||
shapes: Default::default(),
|
||||
textures_delta: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_window_event(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event: &winit::event::WindowEvent,
|
||||
) -> EventResponse {
|
||||
self.egui_winit.on_window_event(window, 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,
|
||||
viewport_output,
|
||||
textures_delta,
|
||||
shapes,
|
||||
pixels_per_point: _pixels_per_point,
|
||||
} = self.egui_ctx.run(raw_input, run_ui);
|
||||
|
||||
self.egui_winit
|
||||
.handle_platform_output(window, platform_output);
|
||||
|
||||
self.shapes = shapes;
|
||||
self.textures_delta.append(textures_delta);
|
||||
match viewport_output.get(&ViewportId::ROOT) {
|
||||
Some(&ViewportOutput { repaint_delay, .. }) => repaint_delay,
|
||||
None => std::time::Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 pixels_per_point = self.egui_ctx.pixels_per_point();
|
||||
/////// let clipped_primitives = self.egui_ctx.tessellate(shapes);
|
||||
let clipped_primitives = self.egui_ctx.tessellate(shapes.clone(), pixels_per_point);
|
||||
let dimensions: [u32; 2] = window.inner_size().into();
|
||||
self.painter
|
||||
.paint_primitives(dimensions, pixels_per_point, &clipped_primitives);
|
||||
|
||||
for id in textures_delta.free.drain(..) {
|
||||
self.painter.free_texture(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Call to release the allocated graphics resources.
|
||||
pub fn destroy(&mut self) {
|
||||
self.painter.destroy();
|
||||
}
|
||||
}
|
45
ports/servoshell/desktop/embedder.rs
Normal file
45
ports/servoshell/desktop/embedder.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
//! Implements the global methods required by Servo (not window/gl/compositor related).
|
||||
|
||||
use servo::compositing::windowing::EmbedderMethods;
|
||||
use servo::embedder_traits::{EmbedderProxy, EventLoopWaker};
|
||||
use servo::servo_config::pref;
|
||||
use webxr::glwindow::GlWindowDiscovery;
|
||||
|
||||
pub struct EmbedderCallbacks {
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
xr_discovery: Option<GlWindowDiscovery>,
|
||||
}
|
||||
|
||||
impl EmbedderCallbacks {
|
||||
pub fn new(
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
xr_discovery: Option<GlWindowDiscovery>,
|
||||
) -> EmbedderCallbacks {
|
||||
EmbedderCallbacks {
|
||||
event_loop_waker,
|
||||
xr_discovery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmbedderMethods for EmbedderCallbacks {
|
||||
fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> {
|
||||
self.event_loop_waker.clone()
|
||||
}
|
||||
|
||||
fn register_webxr(
|
||||
&mut self,
|
||||
xr: &mut webxr::MainThreadRegistry,
|
||||
_embedder_proxy: EmbedderProxy,
|
||||
) {
|
||||
if pref!(dom.webxr.test) {
|
||||
xr.register_mock(webxr::headless::HeadlessMockDiscovery::new());
|
||||
} else if let Some(xr_discovery) = self.xr_discovery.take() {
|
||||
xr.register(xr_discovery);
|
||||
}
|
||||
}
|
||||
}
|
247
ports/servoshell/desktop/events_loop.rs
Normal file
247
ports/servoshell/desktop/events_loop.rs
Normal file
|
@ -0,0 +1,247 @@
|
|||
/* 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 std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time;
|
||||
|
||||
use log::warn;
|
||||
use servo::embedder_traits::EventLoopWaker;
|
||||
#[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,
|
||||
) -> Result<EventsLoop, winit::error::EventLoopError> {
|
||||
Ok(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,
|
||||
) -> Result<EventsLoop, winit::error::EventLoopError> {
|
||||
Ok(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,
|
||||
) -> Result<EventsLoop, winit::error::EventLoopError> {
|
||||
Ok(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 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| {
|
||||
let mut control_flow = ControlFlow::default();
|
||||
callback(e, Some(window_target), &mut control_flow);
|
||||
control_flow.apply_to(window_target);
|
||||
})
|
||||
.expect("Failed while running events loop");
|
||||
},
|
||||
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 = ControlFlow::Poll;
|
||||
callback(event, None, &mut control_flow);
|
||||
event = winit::event::Event::<WakerEvent>::UserEvent(WakerEvent);
|
||||
|
||||
if control_flow != ControlFlow::Poll {
|
||||
*flag.lock().unwrap() = false;
|
||||
}
|
||||
|
||||
if control_flow == 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();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum ControlFlow {
|
||||
Poll,
|
||||
|
||||
#[default]
|
||||
Wait,
|
||||
|
||||
WaitUntil(std::time::Instant),
|
||||
|
||||
/// winit removed their ControlFlow::Exit variant in 0.29.2
|
||||
Exit,
|
||||
}
|
||||
|
||||
impl ControlFlow {
|
||||
fn apply_to(self, window_target: &winit::event_loop::EventLoopWindowTarget<WakerEvent>) {
|
||||
match self {
|
||||
ControlFlow::Poll => {
|
||||
window_target.set_control_flow(winit::event_loop::ControlFlow::Poll)
|
||||
},
|
||||
ControlFlow::Wait => {
|
||||
window_target.set_control_flow(winit::event_loop::ControlFlow::Wait)
|
||||
},
|
||||
ControlFlow::WaitUntil(instant) => {
|
||||
window_target.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(instant))
|
||||
},
|
||||
ControlFlow::Exit => window_target.exit(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_poll(&mut self) {
|
||||
*self = ControlFlow::Poll;
|
||||
}
|
||||
|
||||
pub fn set_wait(&mut self) {
|
||||
*self = ControlFlow::Wait;
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn set_wait_until(&mut self, instant: std::time::Instant) {
|
||||
*self = ControlFlow::WaitUntil(instant);
|
||||
}
|
||||
|
||||
pub fn set_exit(&mut self) {
|
||||
*self = ControlFlow::Exit;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<winit::event_loop::ControlFlow> for ControlFlow {
|
||||
fn from(cf: winit::event_loop::ControlFlow) -> Self {
|
||||
match cf {
|
||||
winit::event_loop::ControlFlow::Poll => Self::Poll,
|
||||
winit::event_loop::ControlFlow::Wait => Self::Wait,
|
||||
winit::event_loop::ControlFlow::WaitUntil(instant) => Self::WaitUntil(instant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
15
ports/servoshell/desktop/geometry.rs
Normal file
15
ports/servoshell/desktop/geometry.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* 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 euclid::{Point2D, Size2D};
|
||||
use servo::style_traits::DevicePixel;
|
||||
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||
|
||||
pub fn winit_size_to_euclid_size<T>(size: PhysicalSize<T>) -> Size2D<T, DevicePixel> {
|
||||
Size2D::new(size.width, size.height)
|
||||
}
|
||||
|
||||
pub fn winit_position_to_euclid_point<T>(position: PhysicalPosition<T>) -> Point2D<T, DevicePixel> {
|
||||
Point2D::new(position.x, position.y)
|
||||
}
|
697
ports/servoshell/desktop/headed_window.rs
Normal file
697
ports/servoshell/desktop/headed_window.rs
Normal file
|
@ -0,0 +1,697 @@
|
|||
/* 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 std::cell::{Cell, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
|
||||
use log::{debug, info, trace};
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||
use servo::compositing::windowing::{
|
||||
AnimationState, EmbedderCoordinates, EmbedderEvent, MouseWindowEvent, WindowMethods,
|
||||
};
|
||||
use servo::embedder_traits::Cursor;
|
||||
use servo::keyboard_types::{Key, KeyState, KeyboardEvent};
|
||||
use servo::script_traits::{TouchEventType, WheelDelta, WheelMode};
|
||||
use servo::servo_config::{opts, 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_traits::RenderingContext;
|
||||
use surfman::{Connection, Context, Device, SurfaceType};
|
||||
#[cfg(target_os = "windows")]
|
||||
use winapi;
|
||||
use winit::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
|
||||
use winit::event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase};
|
||||
use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey};
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
use winit::window::Icon;
|
||||
|
||||
use super::events_loop::{EventsLoop, WakerEvent};
|
||||
use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
|
||||
use super::keyutils::keyboard_event_from_winit;
|
||||
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||
|
||||
pub struct Window {
|
||||
winit_window: winit::window::Window,
|
||||
rendering_context: RenderingContext,
|
||||
screen_size: Size2D<u32, DevicePixel>,
|
||||
inner_size: Cell<Size2D<u32, DevicePixel>>,
|
||||
toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>,
|
||||
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<LogicalKey>)>>,
|
||||
/// A map of winit's key codes to key values that are interpreted from
|
||||
/// winit's ReceivedChar events.
|
||||
keys_down: RefCell<HashMap<LogicalKey, Key>>,
|
||||
animation_state: Cell<AnimationState>,
|
||||
fullscreen: Cell<bool>,
|
||||
device_pixel_ratio_override: 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_pixel_ratio_override: 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 screen_size = winit_size_to_euclid_size(primary_monitor.size());
|
||||
let inner_size = winit_size_to_euclid_size(winit_window.inner_size());
|
||||
|
||||
// Initialize surfman
|
||||
let display_handle = winit_window
|
||||
.display_handle()
|
||||
.expect("could not get display handle from window");
|
||||
let connection =
|
||||
Connection::from_display_handle(display_handle).expect("Failed to create connection");
|
||||
let adapter = connection
|
||||
.create_adapter()
|
||||
.expect("Failed to create adapter");
|
||||
let window_handle = winit_window
|
||||
.window_handle()
|
||||
.expect("could not get window handle from window");
|
||||
let native_widget = connection
|
||||
.create_native_widget_from_window_handle(window_handle, Size2D::new(width, height))
|
||||
.expect("Failed to create native widget");
|
||||
let surface_type = SurfaceType::Widget { native_widget };
|
||||
let rendering_context = RenderingContext::create(&connection, &adapter, surface_type)
|
||||
.expect("Failed to create WR surfman");
|
||||
|
||||
debug!("Created window {:?}", winit_window.id());
|
||||
Window {
|
||||
winit_window,
|
||||
rendering_context,
|
||||
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_pixel_ratio_override,
|
||||
xr_window_poses: RefCell::new(vec![]),
|
||||
modifiers_state: Cell::new(ModifiersState::empty()),
|
||||
toolbar_height: Cell::new(Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
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: KeyEvent) {
|
||||
if let Some(input_text) = &input.text {
|
||||
for ch in input_text.chars() {
|
||||
self.handle_received_character(ch);
|
||||
}
|
||||
}
|
||||
|
||||
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, Some(input.logical_key))));
|
||||
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) = self.keys_down.borrow_mut().remove(&input.logical_key) {
|
||||
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.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));
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPortsMethods for Window {
|
||||
fn get_events(&self) -> Vec<EmbedderEvent> {
|
||||
std::mem::take(&mut *self.event_queue.borrow_mut())
|
||||
}
|
||||
|
||||
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
Scale::new(self.winit_window.scale_factor() as f32)
|
||||
}
|
||||
|
||||
fn device_pixel_ratio_override(
|
||||
&self,
|
||||
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> {
|
||||
self.device_pixel_ratio_override.map(Scale::new)
|
||||
}
|
||||
|
||||
fn page_height(&self) -> f32 {
|
||||
let dpr = self.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 request_inner_size(&self, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
let toolbar_height = self.toolbar_height() * self.hidpi_factor();
|
||||
let toolbar_height = toolbar_height.get().ceil() as i32;
|
||||
let total_size = PhysicalSize::new(size.width, size.height + toolbar_height);
|
||||
self.winit_window
|
||||
.request_inner_size::<PhysicalSize<i32>>(PhysicalSize::new(
|
||||
total_size.width,
|
||||
total_size.height,
|
||||
))
|
||||
.and_then(|size| {
|
||||
Some(DeviceIntSize::new(
|
||||
size.width.try_into().ok()?,
|
||||
size.height.try_into().ok()?,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
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::Pointer,
|
||||
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,
|
||||
Cursor::None => {
|
||||
self.winit_window.set_cursor_visible(false);
|
||||
return;
|
||||
},
|
||||
};
|
||||
self.winit_window.set_cursor_icon(winit_cursor);
|
||||
self.winit_window.set_cursor_visible(true);
|
||||
}
|
||||
|
||||
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::KeyboardInput { event, .. } => {
|
||||
self.handle_keyboard_input(event)
|
||||
},
|
||||
winit::event::WindowEvent::ModifiersChanged(modifiers) => {
|
||||
self.modifiers_state.set(modifiers.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 position = winit_position_to_euclid_point(position);
|
||||
self.mouse_pos.set(position.to_i32());
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::MouseWindowMoveEventClass(position.to_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::TouchpadMagnify { delta, .. } => {
|
||||
let magnification = delta as f32 + 1.0;
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::PinchZoom(magnification));
|
||||
},
|
||||
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.rendering_context
|
||||
.resize(physical_size.to_i32())
|
||||
.expect("Failed to resize");
|
||||
self.inner_size.set(new_size);
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(EmbedderEvent::WindowResize);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
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 toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
|
||||
self.toolbar_height.get()
|
||||
}
|
||||
|
||||
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) {
|
||||
self.toolbar_height.set(height);
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowMethods for Window {
|
||||
fn get_coordinates(&self) -> EmbedderCoordinates {
|
||||
let window_size = winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32();
|
||||
let window_origin = self.winit_window.outer_position().unwrap_or_default();
|
||||
let window_origin = winit_position_to_euclid_point(window_origin).to_i32();
|
||||
let viewport_origin = DeviceIntPoint::zero(); // bottom left
|
||||
let viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32();
|
||||
let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32());
|
||||
let screen = self.screen_size.to_i32();
|
||||
|
||||
EmbedderCoordinates {
|
||||
viewport,
|
||||
framebuffer: viewport.size(),
|
||||
window: (window_size, window_origin),
|
||||
screen,
|
||||
// FIXME: Winit doesn't have API for available size. Fallback to screen size
|
||||
screen_avail: screen,
|
||||
hidpi_factor: self.hidpi_factor(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_animation_state(&self, state: AnimationState) {
|
||||
self.animation_state.set(state);
|
||||
}
|
||||
|
||||
fn rendering_context(&self) -> RenderingContext {
|
||||
self.rendering_context.clone()
|
||||
}
|
||||
}
|
||||
|
||||
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 window_handle = self
|
||||
.winit_window
|
||||
.window_handle()
|
||||
.expect("could not get window handle from window");
|
||||
let size = self.winit_window.inner_size();
|
||||
let size = Size2D::new(size.width as i32, size.height as i32);
|
||||
let native_widget = device
|
||||
.connection()
|
||||
.create_native_widget_from_window_handle(window_handle, size)
|
||||
.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: &KeyEvent, modifiers: ModifiersState) {
|
||||
if input.state != winit::event::ElementState::Pressed {
|
||||
return;
|
||||
}
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
match input.logical_key {
|
||||
LogicalKey::Named(NamedKey::ArrowUp) => x = 1.0,
|
||||
LogicalKey::Named(NamedKey::ArrowDown) => x = -1.0,
|
||||
LogicalKey::Named(NamedKey::ArrowLeft) => y = 1.0,
|
||||
LogicalKey::Named(NamedKey::ArrowRight) => y = -1.0,
|
||||
_ => return,
|
||||
};
|
||||
if modifiers.shift_key() {
|
||||
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);
|
||||
}
|
||||
}
|
202
ports/servoshell/desktop/headless_window.rs
Normal file
202
ports/servoshell/desktop/headless_window.rs
Normal file
|
@ -0,0 +1,202 @@
|
|||
/* 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 std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use euclid::num::Zero;
|
||||
use euclid::{Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector3D};
|
||||
use log::warn;
|
||||
use servo::compositing::windowing::{
|
||||
AnimationState, EmbedderCoordinates, EmbedderEvent, WindowMethods,
|
||||
};
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::style_traits::DevicePixel;
|
||||
use servo::webrender_api::units::{DeviceIntRect, DeviceIntSize};
|
||||
use servo::webrender_traits::RenderingContext;
|
||||
use surfman::{Connection, Context, Device, SurfaceType};
|
||||
|
||||
use super::events_loop::WakerEvent;
|
||||
use crate::desktop::window_trait::WindowPortsMethods;
|
||||
|
||||
pub struct Window {
|
||||
rendering_context: RenderingContext,
|
||||
animation_state: Cell<AnimationState>,
|
||||
fullscreen: Cell<bool>,
|
||||
device_pixel_ratio_override: Option<f32>,
|
||||
inner_size: Cell<Size2D<i32, UnknownUnit>>,
|
||||
event_queue: RwLock<Vec<EmbedderEvent>>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
size: Size2D<u32, DeviceIndependentPixel>,
|
||||
device_pixel_ratio_override: 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 rendering_context = RenderingContext::create(&connection, &adapter, surface_type)
|
||||
.expect("Failed to create WR surfman");
|
||||
|
||||
let window = Window {
|
||||
rendering_context,
|
||||
animation_state: Cell::new(AnimationState::Idle),
|
||||
fullscreen: Cell::new(false),
|
||||
device_pixel_ratio_override,
|
||||
inner_size: Cell::new(size.to_i32()),
|
||||
event_queue: RwLock::new(Vec::new()),
|
||||
};
|
||||
|
||||
Rc::new(window)
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPortsMethods for Window {
|
||||
fn get_events(&self) -> Vec<EmbedderEvent> {
|
||||
match self.event_queue.write() {
|
||||
Ok(ref mut event_queue) => std::mem::take(event_queue),
|
||||
Err(_) => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> winit::window::WindowId {
|
||||
unsafe { winit::window::WindowId::dummy() }
|
||||
}
|
||||
|
||||
fn request_inner_size(&self, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
let (width, height) = size.into();
|
||||
|
||||
// Surfman doesn't support zero-sized surfaces.
|
||||
let new_size = DeviceIntSize::new(width.max(1), height.max(1));
|
||||
if self.inner_size.get() == new_size.to_untyped() {
|
||||
return Some(new_size);
|
||||
}
|
||||
|
||||
match self.rendering_context.resize(new_size.to_untyped()) {
|
||||
Ok(()) => {
|
||||
self.inner_size.set(new_size.to_untyped());
|
||||
if let Ok(ref mut queue) = self.event_queue.write() {
|
||||
queue.push(EmbedderEvent::WindowResize);
|
||||
}
|
||||
Some(new_size)
|
||||
},
|
||||
Err(error) => {
|
||||
warn!("Could not resize window: {error:?}");
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
Scale::new(1.0)
|
||||
}
|
||||
|
||||
fn device_pixel_ratio_override(
|
||||
&self,
|
||||
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> {
|
||||
self.device_pixel_ratio_override.map(Scale::new)
|
||||
}
|
||||
|
||||
fn page_height(&self) -> f32 {
|
||||
let height = self
|
||||
.rendering_context
|
||||
.context_surface_info()
|
||||
.unwrap_or(None)
|
||||
.map(|info| info.size.height)
|
||||
.unwrap_or(0);
|
||||
let dpr = self.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 toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
|
||||
Length::zero()
|
||||
}
|
||||
|
||||
fn set_toolbar_height(&self, _height: Length<f32, DeviceIndependentPixel>) {
|
||||
unimplemented!("headless Window only")
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowMethods for Window {
|
||||
fn get_coordinates(&self) -> EmbedderCoordinates {
|
||||
let dpr = self.hidpi_factor();
|
||||
let size = self
|
||||
.rendering_context
|
||||
.context_surface_info()
|
||||
.unwrap_or(None)
|
||||
.map(|info| Size2D::from_untyped(info.size))
|
||||
.unwrap_or(Size2D::new(0, 0));
|
||||
let viewport = DeviceIntRect::from_origin_and_size(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 rendering_context(&self) -> RenderingContext {
|
||||
self.rendering_context.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()
|
||||
}
|
||||
}
|
644
ports/servoshell/desktop/keyutils.rs
Normal file
644
ports/servoshell/desktop/keyutils.rs
Normal file
|
@ -0,0 +1,644 @@
|
|||
/* 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 keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers};
|
||||
use log::info;
|
||||
use winit::event::{ElementState, KeyEvent};
|
||||
use winit::keyboard::{Key as LogicalKey, KeyCode, ModifiersState, NamedKey, PhysicalKey};
|
||||
|
||||
// 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: &LogicalKey) -> Key {
|
||||
let named_key = match key {
|
||||
LogicalKey::Named(named_key) => named_key,
|
||||
LogicalKey::Character(string) => return Key::Character(string.to_string()),
|
||||
LogicalKey::Unidentified(_) => return Key::Unidentified,
|
||||
LogicalKey::Dead(_) => return Key::Unidentified,
|
||||
};
|
||||
match named_key {
|
||||
// printable: Key1 to Key0
|
||||
// printable: A to Z
|
||||
NamedKey::Escape => Key::Escape,
|
||||
NamedKey::F1 => Key::F1,
|
||||
NamedKey::F2 => Key::F2,
|
||||
NamedKey::F3 => Key::F3,
|
||||
NamedKey::F4 => Key::F4,
|
||||
NamedKey::F5 => Key::F5,
|
||||
NamedKey::F6 => Key::F6,
|
||||
NamedKey::F7 => Key::F7,
|
||||
NamedKey::F8 => Key::F8,
|
||||
NamedKey::F9 => Key::F9,
|
||||
NamedKey::F10 => Key::F10,
|
||||
NamedKey::F11 => Key::F11,
|
||||
NamedKey::F12 => Key::F12,
|
||||
NamedKey::F13 => Key::F13,
|
||||
NamedKey::F14 => Key::F14,
|
||||
NamedKey::F15 => Key::F15,
|
||||
NamedKey::F16 => Key::F16,
|
||||
NamedKey::F17 => Key::F17,
|
||||
NamedKey::F18 => Key::F18,
|
||||
NamedKey::F19 => Key::F19,
|
||||
NamedKey::F20 => Key::F20,
|
||||
NamedKey::F21 => Key::F21,
|
||||
NamedKey::F22 => Key::F22,
|
||||
NamedKey::F23 => Key::F23,
|
||||
NamedKey::F24 => Key::F24,
|
||||
NamedKey::F25 => Key::F25,
|
||||
NamedKey::F26 => Key::F26,
|
||||
NamedKey::F27 => Key::F27,
|
||||
NamedKey::F28 => Key::F28,
|
||||
NamedKey::F29 => Key::F29,
|
||||
NamedKey::F30 => Key::F30,
|
||||
NamedKey::F31 => Key::F31,
|
||||
NamedKey::F32 => Key::F32,
|
||||
NamedKey::F33 => Key::F33,
|
||||
NamedKey::F34 => Key::F34,
|
||||
NamedKey::F35 => Key::F35,
|
||||
NamedKey::PrintScreen => Key::PrintScreen,
|
||||
NamedKey::ScrollLock => Key::ScrollLock,
|
||||
NamedKey::Pause => Key::Pause,
|
||||
NamedKey::Insert => Key::Insert,
|
||||
NamedKey::Home => Key::Home,
|
||||
NamedKey::Delete => Key::Delete,
|
||||
NamedKey::End => Key::End,
|
||||
NamedKey::PageDown => Key::PageDown,
|
||||
NamedKey::PageUp => Key::PageUp,
|
||||
NamedKey::ArrowLeft => Key::ArrowLeft,
|
||||
NamedKey::ArrowUp => Key::ArrowUp,
|
||||
NamedKey::ArrowRight => Key::ArrowRight,
|
||||
NamedKey::ArrowDown => Key::ArrowDown,
|
||||
NamedKey::Backspace => Key::Backspace,
|
||||
NamedKey::Enter => Key::Enter,
|
||||
// printable: Space
|
||||
NamedKey::Compose => Key::Compose,
|
||||
NamedKey::NumLock => Key::NumLock,
|
||||
// printable: Numpad0 to Numpad9
|
||||
// printable: Add, Apostrophe,
|
||||
// printable: Backslash,
|
||||
NamedKey::LaunchApplication2 => Key::LaunchApplication2,
|
||||
NamedKey::CapsLock => Key::CapsLock,
|
||||
// printable: Colon, Comma,
|
||||
NamedKey::Convert => Key::Convert,
|
||||
// printable: Divide, Equals, Grave,
|
||||
NamedKey::KanaMode => Key::KanaMode,
|
||||
NamedKey::KanjiMode => Key::KanjiMode,
|
||||
NamedKey::Alt => Key::Alt,
|
||||
// printable: LBracket,
|
||||
NamedKey::Control => Key::Control,
|
||||
NamedKey::Shift => Key::Shift,
|
||||
NamedKey::Meta => Key::Meta,
|
||||
NamedKey::LaunchMail => Key::LaunchMail,
|
||||
NamedKey::MediaStop => Key::MediaStop,
|
||||
// printable: Minus, Multiply,
|
||||
NamedKey::AudioVolumeMute => Key::AudioVolumeMute,
|
||||
NamedKey::LaunchApplication1 => Key::LaunchApplication1,
|
||||
NamedKey::MediaTrackNext => Key::MediaTrackNext,
|
||||
NamedKey::NonConvert => Key::NonConvert,
|
||||
// printable: NumpadComma, NumpadEnter, NumpadEquals,
|
||||
// printable: Period,
|
||||
NamedKey::MediaPlayPause => Key::MediaPlayPause,
|
||||
NamedKey::Power => Key::Power,
|
||||
NamedKey::MediaTrackPrevious => Key::MediaTrackPrevious,
|
||||
// printable RBracket
|
||||
// printable Semicolon, Slash
|
||||
NamedKey::Standby => Key::Standby,
|
||||
// printable Subtract,
|
||||
NamedKey::Tab => Key::Tab,
|
||||
// printable: Underline,
|
||||
NamedKey::AudioVolumeDown => Key::AudioVolumeDown,
|
||||
NamedKey::AudioVolumeUp => Key::AudioVolumeUp,
|
||||
NamedKey::WakeUp => Key::WakeUp,
|
||||
NamedKey::BrowserBack => Key::BrowserBack,
|
||||
NamedKey::BrowserFavorites => Key::BrowserFavorites,
|
||||
NamedKey::BrowserForward => Key::BrowserForward,
|
||||
NamedKey::BrowserHome => Key::BrowserHome,
|
||||
NamedKey::BrowserRefresh => Key::BrowserRefresh,
|
||||
NamedKey::BrowserSearch => Key::BrowserSearch,
|
||||
NamedKey::BrowserStop => Key::BrowserStop,
|
||||
// printable Yen,
|
||||
NamedKey::Copy => Key::Copy,
|
||||
NamedKey::Paste => Key::Paste,
|
||||
NamedKey::Cut => Key::Cut,
|
||||
NamedKey::AltGraph => Key::AltGraph,
|
||||
NamedKey::Fn => Key::Fn,
|
||||
NamedKey::FnLock => Key::FnLock,
|
||||
NamedKey::Symbol => Key::Symbol,
|
||||
NamedKey::SymbolLock => Key::SymbolLock,
|
||||
NamedKey::Hyper => Key::Hyper,
|
||||
NamedKey::Super => Key::Super,
|
||||
NamedKey::Clear => Key::Clear,
|
||||
NamedKey::CrSel => Key::CrSel,
|
||||
NamedKey::EraseEof => Key::EraseEof,
|
||||
NamedKey::ExSel => Key::ExSel,
|
||||
NamedKey::Redo => Key::Redo,
|
||||
NamedKey::Undo => Key::Undo,
|
||||
NamedKey::Accept => Key::Accept,
|
||||
NamedKey::Again => Key::Again,
|
||||
NamedKey::Attn => Key::Attn,
|
||||
NamedKey::Cancel => Key::Cancel,
|
||||
NamedKey::ContextMenu => Key::ContextMenu,
|
||||
NamedKey::Execute => Key::Execute,
|
||||
NamedKey::Find => Key::Find,
|
||||
NamedKey::Help => Key::Help,
|
||||
NamedKey::Play => Key::Play,
|
||||
NamedKey::Props => Key::Props,
|
||||
NamedKey::Select => Key::Select,
|
||||
NamedKey::ZoomIn => Key::ZoomIn,
|
||||
NamedKey::ZoomOut => Key::ZoomOut,
|
||||
NamedKey::BrightnessDown => Key::BrightnessDown,
|
||||
NamedKey::BrightnessUp => Key::BrightnessUp,
|
||||
NamedKey::Eject => Key::Eject,
|
||||
NamedKey::LogOff => Key::LogOff,
|
||||
NamedKey::PowerOff => Key::PowerOff,
|
||||
NamedKey::Hibernate => Key::Hibernate,
|
||||
NamedKey::AllCandidates => Key::AllCandidates,
|
||||
NamedKey::Alphanumeric => Key::Alphanumeric,
|
||||
NamedKey::CodeInput => Key::CodeInput,
|
||||
NamedKey::FinalMode => Key::FinalMode,
|
||||
NamedKey::GroupFirst => Key::GroupFirst,
|
||||
NamedKey::GroupLast => Key::GroupLast,
|
||||
NamedKey::GroupNext => Key::GroupNext,
|
||||
NamedKey::GroupPrevious => Key::GroupPrevious,
|
||||
NamedKey::ModeChange => Key::ModeChange,
|
||||
NamedKey::NextCandidate => Key::NextCandidate,
|
||||
NamedKey::PreviousCandidate => Key::PreviousCandidate,
|
||||
NamedKey::Process => Key::Process,
|
||||
NamedKey::SingleCandidate => Key::SingleCandidate,
|
||||
NamedKey::HangulMode => Key::HangulMode,
|
||||
NamedKey::HanjaMode => Key::HanjaMode,
|
||||
NamedKey::JunjaMode => Key::JunjaMode,
|
||||
NamedKey::Eisu => Key::Eisu,
|
||||
NamedKey::Hankaku => Key::Hankaku,
|
||||
NamedKey::Hiragana => Key::Hiragana,
|
||||
NamedKey::HiraganaKatakana => Key::HiraganaKatakana,
|
||||
NamedKey::Katakana => Key::Katakana,
|
||||
NamedKey::Romaji => Key::Romaji,
|
||||
NamedKey::Zenkaku => Key::Zenkaku,
|
||||
NamedKey::ZenkakuHankaku => Key::ZenkakuHankaku,
|
||||
NamedKey::Soft1 => Key::Soft1,
|
||||
NamedKey::Soft2 => Key::Soft2,
|
||||
NamedKey::Soft3 => Key::Soft3,
|
||||
NamedKey::Soft4 => Key::Soft4,
|
||||
NamedKey::ChannelDown => Key::ChannelDown,
|
||||
NamedKey::ChannelUp => Key::ChannelUp,
|
||||
NamedKey::Close => Key::Close,
|
||||
NamedKey::MailForward => Key::MailForward,
|
||||
NamedKey::MailReply => Key::MailReply,
|
||||
NamedKey::MailSend => Key::MailSend,
|
||||
NamedKey::MediaClose => Key::MediaClose,
|
||||
NamedKey::MediaFastForward => Key::MediaFastForward,
|
||||
NamedKey::MediaPause => Key::MediaPause,
|
||||
NamedKey::MediaPlay => Key::MediaPlay,
|
||||
NamedKey::MediaRecord => Key::MediaRecord,
|
||||
NamedKey::MediaRewind => Key::MediaRewind,
|
||||
NamedKey::New => Key::New,
|
||||
NamedKey::Open => Key::Open,
|
||||
NamedKey::Print => Key::Print,
|
||||
NamedKey::Save => Key::Save,
|
||||
NamedKey::SpellCheck => Key::SpellCheck,
|
||||
NamedKey::Key11 => Key::Key11,
|
||||
NamedKey::Key12 => Key::Key12,
|
||||
NamedKey::AudioBalanceLeft => Key::AudioBalanceLeft,
|
||||
NamedKey::AudioBalanceRight => Key::AudioBalanceRight,
|
||||
NamedKey::AudioBassBoostDown => Key::AudioBassBoostDown,
|
||||
NamedKey::AudioBassBoostToggle => Key::AudioBassBoostToggle,
|
||||
NamedKey::AudioBassBoostUp => Key::AudioBassBoostUp,
|
||||
NamedKey::AudioFaderFront => Key::AudioFaderFront,
|
||||
NamedKey::AudioFaderRear => Key::AudioFaderRear,
|
||||
NamedKey::AudioSurroundModeNext => Key::AudioSurroundModeNext,
|
||||
NamedKey::AudioTrebleDown => Key::AudioTrebleDown,
|
||||
NamedKey::AudioTrebleUp => Key::AudioTrebleUp,
|
||||
NamedKey::MicrophoneToggle => Key::MicrophoneToggle,
|
||||
NamedKey::MicrophoneVolumeDown => Key::MicrophoneVolumeDown,
|
||||
NamedKey::MicrophoneVolumeUp => Key::MicrophoneVolumeUp,
|
||||
NamedKey::MicrophoneVolumeMute => Key::MicrophoneVolumeMute,
|
||||
NamedKey::SpeechCorrectionList => Key::SpeechCorrectionList,
|
||||
NamedKey::SpeechInputToggle => Key::SpeechInputToggle,
|
||||
NamedKey::LaunchCalendar => Key::LaunchCalendar,
|
||||
NamedKey::LaunchContacts => Key::LaunchContacts,
|
||||
NamedKey::LaunchMediaPlayer => Key::LaunchMediaPlayer,
|
||||
NamedKey::LaunchMusicPlayer => Key::LaunchMusicPlayer,
|
||||
NamedKey::LaunchPhone => Key::LaunchPhone,
|
||||
NamedKey::LaunchScreenSaver => Key::LaunchScreenSaver,
|
||||
NamedKey::LaunchSpreadsheet => Key::LaunchSpreadsheet,
|
||||
NamedKey::LaunchWebBrowser => Key::LaunchWebBrowser,
|
||||
NamedKey::LaunchWebCam => Key::LaunchWebCam,
|
||||
NamedKey::LaunchWordProcessor => Key::LaunchWordProcessor,
|
||||
NamedKey::AppSwitch => Key::AppSwitch,
|
||||
NamedKey::Call => Key::Call,
|
||||
NamedKey::Camera => Key::Camera,
|
||||
NamedKey::CameraFocus => Key::CameraFocus,
|
||||
NamedKey::EndCall => Key::EndCall,
|
||||
NamedKey::GoBack => Key::GoBack,
|
||||
NamedKey::GoHome => Key::GoHome,
|
||||
NamedKey::HeadsetHook => Key::HeadsetHook,
|
||||
NamedKey::LastNumberRedial => Key::LastNumberRedial,
|
||||
NamedKey::Notification => Key::Notification,
|
||||
NamedKey::MannerMode => Key::MannerMode,
|
||||
NamedKey::VoiceDial => Key::VoiceDial,
|
||||
NamedKey::TV => Key::TV,
|
||||
NamedKey::TV3DMode => Key::TV3DMode,
|
||||
NamedKey::TVAntennaCable => Key::TVAntennaCable,
|
||||
NamedKey::TVAudioDescription => Key::TVAudioDescription,
|
||||
NamedKey::TVAudioDescriptionMixDown => Key::TVAudioDescriptionMixDown,
|
||||
NamedKey::TVAudioDescriptionMixUp => Key::TVAudioDescriptionMixUp,
|
||||
NamedKey::TVContentsMenu => Key::TVContentsMenu,
|
||||
NamedKey::TVDataService => Key::TVDataService,
|
||||
NamedKey::TVInput => Key::TVInput,
|
||||
NamedKey::TVInputComponent1 => Key::TVInputComponent1,
|
||||
NamedKey::TVInputComponent2 => Key::TVInputComponent2,
|
||||
NamedKey::TVInputComposite1 => Key::TVInputComposite1,
|
||||
NamedKey::TVInputComposite2 => Key::TVInputComposite2,
|
||||
NamedKey::TVInputHDMI1 => Key::TVInputHDMI1,
|
||||
NamedKey::TVInputHDMI2 => Key::TVInputHDMI2,
|
||||
NamedKey::TVInputHDMI3 => Key::TVInputHDMI3,
|
||||
NamedKey::TVInputHDMI4 => Key::TVInputHDMI4,
|
||||
NamedKey::TVInputVGA1 => Key::TVInputVGA1,
|
||||
NamedKey::TVMediaContext => Key::TVMediaContext,
|
||||
NamedKey::TVNetwork => Key::TVNetwork,
|
||||
NamedKey::TVNumberEntry => Key::TVNumberEntry,
|
||||
NamedKey::TVPower => Key::TVPower,
|
||||
NamedKey::TVRadioService => Key::TVRadioService,
|
||||
NamedKey::TVSatellite => Key::TVSatellite,
|
||||
NamedKey::TVSatelliteBS => Key::TVSatelliteBS,
|
||||
NamedKey::TVSatelliteCS => Key::TVSatelliteCS,
|
||||
NamedKey::TVSatelliteToggle => Key::TVSatelliteToggle,
|
||||
NamedKey::TVTerrestrialAnalog => Key::TVTerrestrialAnalog,
|
||||
NamedKey::TVTerrestrialDigital => Key::TVTerrestrialDigital,
|
||||
NamedKey::TVTimer => Key::TVTimer,
|
||||
NamedKey::AVRInput => Key::AVRInput,
|
||||
NamedKey::AVRPower => Key::AVRPower,
|
||||
NamedKey::ColorF0Red => Key::ColorF0Red,
|
||||
NamedKey::ColorF1Green => Key::ColorF1Green,
|
||||
NamedKey::ColorF2Yellow => Key::ColorF2Yellow,
|
||||
NamedKey::ColorF3Blue => Key::ColorF3Blue,
|
||||
NamedKey::ColorF4Grey => Key::ColorF4Grey,
|
||||
NamedKey::ColorF5Brown => Key::ColorF5Brown,
|
||||
NamedKey::ClosedCaptionToggle => Key::ClosedCaptionToggle,
|
||||
NamedKey::Dimmer => Key::Dimmer,
|
||||
NamedKey::DisplaySwap => Key::DisplaySwap,
|
||||
NamedKey::DVR => Key::DVR,
|
||||
NamedKey::Exit => Key::Exit,
|
||||
NamedKey::FavoriteClear0 => Key::FavoriteClear0,
|
||||
NamedKey::FavoriteClear1 => Key::FavoriteClear1,
|
||||
NamedKey::FavoriteClear2 => Key::FavoriteClear2,
|
||||
NamedKey::FavoriteClear3 => Key::FavoriteClear3,
|
||||
NamedKey::FavoriteRecall0 => Key::FavoriteRecall0,
|
||||
NamedKey::FavoriteRecall1 => Key::FavoriteRecall1,
|
||||
NamedKey::FavoriteRecall2 => Key::FavoriteRecall2,
|
||||
NamedKey::FavoriteRecall3 => Key::FavoriteRecall3,
|
||||
NamedKey::FavoriteStore0 => Key::FavoriteStore0,
|
||||
NamedKey::FavoriteStore1 => Key::FavoriteStore1,
|
||||
NamedKey::FavoriteStore2 => Key::FavoriteStore2,
|
||||
NamedKey::FavoriteStore3 => Key::FavoriteStore3,
|
||||
NamedKey::Guide => Key::Guide,
|
||||
NamedKey::GuideNextDay => Key::GuideNextDay,
|
||||
NamedKey::GuidePreviousDay => Key::GuidePreviousDay,
|
||||
NamedKey::Info => Key::Info,
|
||||
NamedKey::InstantReplay => Key::InstantReplay,
|
||||
NamedKey::Link => Key::Link,
|
||||
NamedKey::ListProgram => Key::ListProgram,
|
||||
NamedKey::LiveContent => Key::LiveContent,
|
||||
NamedKey::Lock => Key::Lock,
|
||||
NamedKey::MediaApps => Key::MediaApps,
|
||||
NamedKey::MediaAudioTrack => Key::MediaAudioTrack,
|
||||
NamedKey::MediaLast => Key::MediaLast,
|
||||
NamedKey::MediaSkipBackward => Key::MediaSkipBackward,
|
||||
NamedKey::MediaSkipForward => Key::MediaSkipForward,
|
||||
NamedKey::MediaStepBackward => Key::MediaStepBackward,
|
||||
NamedKey::MediaStepForward => Key::MediaStepForward,
|
||||
NamedKey::MediaTopMenu => Key::MediaTopMenu,
|
||||
NamedKey::NavigateIn => Key::NavigateIn,
|
||||
NamedKey::NavigateNext => Key::NavigateNext,
|
||||
NamedKey::NavigateOut => Key::NavigateOut,
|
||||
NamedKey::NavigatePrevious => Key::NavigatePrevious,
|
||||
NamedKey::NextFavoriteChannel => Key::NextFavoriteChannel,
|
||||
NamedKey::NextUserProfile => Key::NextUserProfile,
|
||||
NamedKey::OnDemand => Key::OnDemand,
|
||||
NamedKey::Pairing => Key::Pairing,
|
||||
NamedKey::PinPDown => Key::PinPDown,
|
||||
NamedKey::PinPMove => Key::PinPMove,
|
||||
NamedKey::PinPToggle => Key::PinPToggle,
|
||||
NamedKey::PinPUp => Key::PinPUp,
|
||||
NamedKey::PlaySpeedDown => Key::PlaySpeedDown,
|
||||
NamedKey::PlaySpeedReset => Key::PlaySpeedReset,
|
||||
NamedKey::PlaySpeedUp => Key::PlaySpeedUp,
|
||||
NamedKey::RandomToggle => Key::RandomToggle,
|
||||
NamedKey::RcLowBattery => Key::RcLowBattery,
|
||||
NamedKey::RecordSpeedNext => Key::RecordSpeedNext,
|
||||
NamedKey::RfBypass => Key::RfBypass,
|
||||
NamedKey::ScanChannelsToggle => Key::ScanChannelsToggle,
|
||||
NamedKey::ScreenModeNext => Key::ScreenModeNext,
|
||||
NamedKey::Settings => Key::Settings,
|
||||
NamedKey::SplitScreenToggle => Key::SplitScreenToggle,
|
||||
NamedKey::STBInput => Key::STBInput,
|
||||
NamedKey::STBPower => Key::STBPower,
|
||||
NamedKey::Subtitle => Key::Subtitle,
|
||||
NamedKey::Teletext => Key::Teletext,
|
||||
NamedKey::VideoModeNext => Key::VideoModeNext,
|
||||
NamedKey::Wink => Key::Wink,
|
||||
NamedKey::ZoomToggle => Key::ZoomToggle,
|
||||
|
||||
NamedKey::Space => Key::Character(" ".to_string()),
|
||||
_ => Key::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_servo_location_from_physical_key(physical_key: PhysicalKey) -> Location {
|
||||
let key_code = if let PhysicalKey::Code(key_code) = physical_key {
|
||||
key_code
|
||||
} else {
|
||||
return Location::Standard;
|
||||
};
|
||||
|
||||
match key_code {
|
||||
KeyCode::ShiftLeft | KeyCode::ControlLeft | KeyCode::AltLeft | KeyCode::SuperLeft => {
|
||||
Location::Left
|
||||
},
|
||||
KeyCode::ShiftRight | KeyCode::ControlRight | KeyCode::AltRight | KeyCode::SuperRight => {
|
||||
Location::Right
|
||||
},
|
||||
KeyCode::Numpad0 |
|
||||
KeyCode::Numpad1 |
|
||||
KeyCode::Numpad2 |
|
||||
KeyCode::Numpad3 |
|
||||
KeyCode::Numpad4 |
|
||||
KeyCode::Numpad5 |
|
||||
KeyCode::Numpad6 |
|
||||
KeyCode::Numpad7 |
|
||||
KeyCode::Numpad8 |
|
||||
KeyCode::Numpad9 |
|
||||
KeyCode::NumpadAdd |
|
||||
KeyCode::NumpadBackspace |
|
||||
KeyCode::NumpadClear |
|
||||
KeyCode::NumpadClearEntry |
|
||||
KeyCode::NumpadComma |
|
||||
KeyCode::NumpadDecimal |
|
||||
KeyCode::NumpadDivide |
|
||||
KeyCode::NumpadEnter |
|
||||
KeyCode::NumpadEqual |
|
||||
KeyCode::NumpadHash |
|
||||
KeyCode::NumpadMemoryAdd |
|
||||
KeyCode::NumpadMemoryClear |
|
||||
KeyCode::NumpadMemoryRecall |
|
||||
KeyCode::NumpadMemoryStore |
|
||||
KeyCode::NumpadMemorySubtract |
|
||||
KeyCode::NumpadMultiply |
|
||||
KeyCode::NumpadParenLeft |
|
||||
KeyCode::NumpadParenRight |
|
||||
KeyCode::NumpadStar |
|
||||
KeyCode::NumpadSubtract => Location::Numpad,
|
||||
_ => Location::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_servo_code_from_physical_key(physical_key: PhysicalKey) -> Code {
|
||||
let key_code = match physical_key {
|
||||
PhysicalKey::Code(key_code) => key_code,
|
||||
PhysicalKey::Unidentified(_os_code) => return Code::Unidentified,
|
||||
};
|
||||
|
||||
match key_code {
|
||||
KeyCode::Escape => Code::Escape,
|
||||
KeyCode::Digit1 => Code::Digit1,
|
||||
KeyCode::Digit2 => Code::Digit2,
|
||||
KeyCode::Digit3 => Code::Digit3,
|
||||
KeyCode::Digit4 => Code::Digit4,
|
||||
KeyCode::Digit5 => Code::Digit5,
|
||||
KeyCode::Digit6 => Code::Digit6,
|
||||
KeyCode::Digit7 => Code::Digit7,
|
||||
KeyCode::Digit8 => Code::Digit8,
|
||||
KeyCode::Digit9 => Code::Digit9,
|
||||
KeyCode::Digit0 => Code::Digit0,
|
||||
|
||||
KeyCode::Backspace => Code::Backspace,
|
||||
KeyCode::Tab => Code::Tab,
|
||||
KeyCode::KeyQ => Code::KeyQ,
|
||||
KeyCode::KeyW => Code::KeyW,
|
||||
KeyCode::KeyE => Code::KeyE,
|
||||
KeyCode::KeyR => Code::KeyR,
|
||||
KeyCode::KeyT => Code::KeyT,
|
||||
KeyCode::KeyY => Code::KeyY,
|
||||
KeyCode::KeyU => Code::KeyU,
|
||||
KeyCode::KeyI => Code::KeyI,
|
||||
KeyCode::KeyO => Code::KeyO,
|
||||
KeyCode::KeyP => Code::KeyP,
|
||||
KeyCode::BracketLeft => Code::BracketLeft,
|
||||
KeyCode::BracketRight => Code::BracketRight,
|
||||
KeyCode::Enter => Code::Enter,
|
||||
|
||||
KeyCode::KeyA => Code::KeyA,
|
||||
KeyCode::KeyS => Code::KeyS,
|
||||
KeyCode::KeyD => Code::KeyD,
|
||||
KeyCode::KeyF => Code::KeyF,
|
||||
KeyCode::KeyG => Code::KeyG,
|
||||
KeyCode::KeyH => Code::KeyH,
|
||||
KeyCode::KeyJ => Code::KeyJ,
|
||||
KeyCode::KeyK => Code::KeyK,
|
||||
KeyCode::KeyL => Code::KeyL,
|
||||
KeyCode::Semicolon => Code::Semicolon,
|
||||
KeyCode::Quote => Code::Quote,
|
||||
|
||||
KeyCode::ShiftLeft => Code::ShiftLeft,
|
||||
KeyCode::Backslash => Code::Backslash,
|
||||
KeyCode::KeyZ => Code::KeyZ,
|
||||
KeyCode::KeyX => Code::KeyX,
|
||||
KeyCode::KeyC => Code::KeyC,
|
||||
KeyCode::KeyV => Code::KeyV,
|
||||
KeyCode::KeyB => Code::KeyB,
|
||||
KeyCode::KeyN => Code::KeyN,
|
||||
KeyCode::KeyM => Code::KeyM,
|
||||
KeyCode::Comma => Code::Comma,
|
||||
KeyCode::Period => Code::Period,
|
||||
KeyCode::Slash => Code::Slash,
|
||||
KeyCode::ShiftRight => Code::ShiftRight,
|
||||
|
||||
KeyCode::Space => Code::Space,
|
||||
|
||||
KeyCode::F1 => Code::F1,
|
||||
KeyCode::F2 => Code::F2,
|
||||
KeyCode::F3 => Code::F3,
|
||||
KeyCode::F4 => Code::F4,
|
||||
KeyCode::F5 => Code::F5,
|
||||
KeyCode::F6 => Code::F6,
|
||||
KeyCode::F7 => Code::F7,
|
||||
KeyCode::F8 => Code::F8,
|
||||
KeyCode::F9 => Code::F9,
|
||||
KeyCode::F10 => Code::F10,
|
||||
|
||||
KeyCode::F11 => Code::F11,
|
||||
KeyCode::F12 => Code::F12,
|
||||
|
||||
KeyCode::ArrowUp => Code::ArrowUp,
|
||||
KeyCode::PageUp => Code::PageUp,
|
||||
KeyCode::ArrowLeft => Code::ArrowLeft,
|
||||
KeyCode::ArrowRight => Code::ArrowRight,
|
||||
|
||||
KeyCode::SuperLeft => Code::MetaLeft,
|
||||
KeyCode::SuperRight => Code::MetaRight,
|
||||
|
||||
KeyCode::Home => Code::Home,
|
||||
KeyCode::End => Code::End,
|
||||
KeyCode::ArrowDown => Code::ArrowDown,
|
||||
KeyCode::PageDown => Code::PageDown,
|
||||
KeyCode::Insert => Code::Insert,
|
||||
KeyCode::Delete => Code::Delete,
|
||||
KeyCode::Backquote => Code::Backquote,
|
||||
KeyCode::Equal => Code::Equal,
|
||||
KeyCode::IntlBackslash => Code::IntlBackslash,
|
||||
KeyCode::IntlRo => Code::IntlRo,
|
||||
KeyCode::IntlYen => Code::IntlYen,
|
||||
KeyCode::Minus => Code::Minus,
|
||||
KeyCode::AltLeft => Code::AltLeft,
|
||||
KeyCode::AltRight => Code::AltRight,
|
||||
KeyCode::CapsLock => Code::CapsLock,
|
||||
KeyCode::ContextMenu => Code::ContextMenu,
|
||||
KeyCode::ControlLeft => Code::ControlLeft,
|
||||
KeyCode::ControlRight => Code::ControlRight,
|
||||
KeyCode::Convert => Code::Convert,
|
||||
KeyCode::KanaMode => Code::KanaMode,
|
||||
KeyCode::Lang1 => Code::Lang1,
|
||||
KeyCode::Lang2 => Code::Lang2,
|
||||
KeyCode::Lang3 => Code::Lang3,
|
||||
KeyCode::Lang4 => Code::Lang4,
|
||||
KeyCode::Lang5 => Code::Lang5,
|
||||
KeyCode::NonConvert => Code::NonConvert,
|
||||
KeyCode::Help => Code::Help,
|
||||
KeyCode::NumLock => Code::NumLock,
|
||||
KeyCode::Numpad0 => Code::Numpad0,
|
||||
KeyCode::Numpad1 => Code::Numpad1,
|
||||
KeyCode::Numpad2 => Code::Numpad2,
|
||||
KeyCode::Numpad3 => Code::Numpad3,
|
||||
KeyCode::Numpad4 => Code::Numpad4,
|
||||
KeyCode::Numpad5 => Code::Numpad5,
|
||||
KeyCode::Numpad6 => Code::Numpad6,
|
||||
KeyCode::Numpad7 => Code::Numpad7,
|
||||
KeyCode::Numpad8 => Code::Numpad8,
|
||||
KeyCode::Numpad9 => Code::Numpad9,
|
||||
KeyCode::NumpadAdd => Code::NumpadAdd,
|
||||
KeyCode::NumpadBackspace => Code::NumpadBackspace,
|
||||
KeyCode::NumpadClear => Code::NumpadClear,
|
||||
KeyCode::NumpadClearEntry => Code::NumpadClearEntry,
|
||||
KeyCode::NumpadComma => Code::NumpadComma,
|
||||
KeyCode::NumpadDecimal => Code::NumpadDecimal,
|
||||
KeyCode::NumpadDivide => Code::NumpadDivide,
|
||||
KeyCode::NumpadEnter => Code::NumpadEnter,
|
||||
KeyCode::NumpadEqual => Code::NumpadEqual,
|
||||
KeyCode::NumpadHash => Code::NumpadHash,
|
||||
KeyCode::NumpadMemoryAdd => Code::NumpadMemoryAdd,
|
||||
KeyCode::NumpadMemoryClear => Code::NumpadMemoryClear,
|
||||
KeyCode::NumpadMemoryRecall => Code::NumpadMemoryRecall,
|
||||
KeyCode::NumpadMemoryStore => Code::NumpadMemoryStore,
|
||||
KeyCode::NumpadMemorySubtract => Code::NumpadMemorySubtract,
|
||||
KeyCode::NumpadMultiply => Code::NumpadMultiply,
|
||||
KeyCode::NumpadParenLeft => Code::NumpadParenLeft,
|
||||
KeyCode::NumpadParenRight => Code::NumpadParenRight,
|
||||
KeyCode::NumpadStar => Code::NumpadStar,
|
||||
KeyCode::NumpadSubtract => Code::NumpadSubtract,
|
||||
KeyCode::Fn => Code::Fn,
|
||||
KeyCode::FnLock => Code::FnLock,
|
||||
KeyCode::PrintScreen => Code::PrintScreen,
|
||||
KeyCode::ScrollLock => Code::ScrollLock,
|
||||
KeyCode::Pause => Code::Pause,
|
||||
KeyCode::BrowserBack => Code::BrowserBack,
|
||||
KeyCode::BrowserFavorites => Code::BrowserFavorites,
|
||||
KeyCode::BrowserForward => Code::BrowserForward,
|
||||
KeyCode::BrowserHome => Code::BrowserHome,
|
||||
KeyCode::BrowserRefresh => Code::BrowserRefresh,
|
||||
KeyCode::BrowserSearch => Code::BrowserSearch,
|
||||
KeyCode::BrowserStop => Code::BrowserStop,
|
||||
KeyCode::Eject => Code::Eject,
|
||||
KeyCode::LaunchApp1 => Code::LaunchApp1,
|
||||
KeyCode::LaunchApp2 => Code::LaunchApp2,
|
||||
KeyCode::LaunchMail => Code::LaunchMail,
|
||||
KeyCode::MediaPlayPause => Code::MediaPlayPause,
|
||||
KeyCode::MediaSelect => Code::MediaSelect,
|
||||
KeyCode::MediaStop => Code::MediaStop,
|
||||
KeyCode::MediaTrackNext => Code::MediaTrackNext,
|
||||
KeyCode::MediaTrackPrevious => Code::MediaTrackPrevious,
|
||||
KeyCode::Power => Code::Power,
|
||||
KeyCode::Sleep => Code::Sleep,
|
||||
KeyCode::AudioVolumeDown => Code::AudioVolumeDown,
|
||||
KeyCode::AudioVolumeMute => Code::AudioVolumeMute,
|
||||
KeyCode::AudioVolumeUp => Code::AudioVolumeUp,
|
||||
KeyCode::WakeUp => Code::WakeUp,
|
||||
KeyCode::Meta => Code::Super,
|
||||
KeyCode::Hyper => Code::Hyper,
|
||||
KeyCode::Turbo => Code::Turbo,
|
||||
KeyCode::Abort => Code::Abort,
|
||||
KeyCode::Resume => Code::Resume,
|
||||
KeyCode::Suspend => Code::Suspend,
|
||||
KeyCode::Again => Code::Again,
|
||||
KeyCode::Copy => Code::Copy,
|
||||
KeyCode::Cut => Code::Cut,
|
||||
KeyCode::Find => Code::Find,
|
||||
KeyCode::Open => Code::Open,
|
||||
KeyCode::Paste => Code::Paste,
|
||||
KeyCode::Props => Code::Props,
|
||||
KeyCode::Select => Code::Select,
|
||||
KeyCode::Undo => Code::Undo,
|
||||
KeyCode::Hiragana => Code::Hiragana,
|
||||
KeyCode::Katakana => Code::Katakana,
|
||||
KeyCode::F13 => Code::F13,
|
||||
KeyCode::F14 => Code::F14,
|
||||
KeyCode::F15 => Code::F15,
|
||||
KeyCode::F16 => Code::F16,
|
||||
KeyCode::F17 => Code::F17,
|
||||
KeyCode::F18 => Code::F18,
|
||||
KeyCode::F19 => Code::F19,
|
||||
KeyCode::F20 => Code::F20,
|
||||
KeyCode::F21 => Code::F21,
|
||||
KeyCode::F22 => Code::F22,
|
||||
KeyCode::F23 => Code::F23,
|
||||
KeyCode::F24 => Code::F24,
|
||||
KeyCode::F25 => Code::F25,
|
||||
KeyCode::F26 => Code::F26,
|
||||
KeyCode::F27 => Code::F27,
|
||||
KeyCode::F28 => Code::F28,
|
||||
KeyCode::F29 => Code::F29,
|
||||
KeyCode::F30 => Code::F30,
|
||||
KeyCode::F31 => Code::F31,
|
||||
KeyCode::F32 => Code::F32,
|
||||
KeyCode::F33 => Code::F33,
|
||||
KeyCode::F34 => Code::F34,
|
||||
KeyCode::F35 => Code::F35,
|
||||
|
||||
_ => Code::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_modifiers(mods: ModifiersState) -> Modifiers {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
modifiers.set(Modifiers::CONTROL, mods.control_key());
|
||||
modifiers.set(Modifiers::SHIFT, mods.shift_key());
|
||||
modifiers.set(Modifiers::ALT, mods.alt_key());
|
||||
modifiers.set(Modifiers::META, mods.super_key());
|
||||
modifiers
|
||||
}
|
||||
|
||||
pub fn keyboard_event_from_winit(input: &KeyEvent, 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.logical_key),
|
||||
code: get_servo_code_from_physical_key(input.physical_key),
|
||||
location: get_servo_location_from_physical_key(input.physical_key),
|
||||
modifiers: get_modifiers(state),
|
||||
repeat: false,
|
||||
is_composing: false,
|
||||
}
|
||||
}
|
437
ports/servoshell/desktop/minibrowser.rs
Normal file
437
ports/servoshell/desktop/minibrowser.rs
Normal file
|
@ -0,0 +1,437 @@
|
|||
/* 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::{Cell, RefCell};
|
||||
use std::num::NonZeroU32;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use egui::{
|
||||
pos2, CentralPanel, Color32, Frame, Key, Modifiers, PaintCallback, Pos2, Spinner,
|
||||
TopBottomPanel, Vec2,
|
||||
};
|
||||
use egui_glow::CallbackFn;
|
||||
use egui_winit::EventResponse;
|
||||
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
|
||||
use gleam::gl;
|
||||
use glow::NativeFramebuffer;
|
||||
use log::{trace, warn};
|
||||
use servo::compositing::windowing::EmbedderEvent;
|
||||
use servo::script_traits::TraversalDirection;
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::servo_url::ServoUrl;
|
||||
use servo::style_traits::DevicePixel;
|
||||
use servo::webrender_traits::RenderingContext;
|
||||
use winit::event::{ElementState, MouseButton};
|
||||
|
||||
use super::egui_glue::EguiGlow;
|
||||
use super::events_loop::EventsLoop;
|
||||
use super::geometry::winit_position_to_euclid_point;
|
||||
use super::webview::{LoadStatus, WebViewManager};
|
||||
use super::window_trait::WindowPortsMethods;
|
||||
use crate::parser::location_bar_input_to_url;
|
||||
|
||||
pub struct Minibrowser {
|
||||
pub context: EguiGlow,
|
||||
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
|
||||
pub toolbar_height: Length<f32, DeviceIndependentPixel>,
|
||||
|
||||
/// The framebuffer object name for the widget surface we should draw to, or None if our widget
|
||||
/// surface does not use a framebuffer object.
|
||||
widget_surface_fbo: Option<NativeFramebuffer>,
|
||||
|
||||
last_update: Instant,
|
||||
last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
|
||||
location: RefCell<String>,
|
||||
|
||||
/// Whether the location has been edited by the user without clicking Go.
|
||||
location_dirty: Cell<bool>,
|
||||
|
||||
load_status: LoadStatus,
|
||||
|
||||
status_text: Option<String>,
|
||||
}
|
||||
|
||||
pub enum MinibrowserEvent {
|
||||
/// Go button clicked.
|
||||
Go,
|
||||
Back,
|
||||
Forward,
|
||||
}
|
||||
|
||||
impl Minibrowser {
|
||||
pub fn new(
|
||||
rendering_context: &RenderingContext,
|
||||
events_loop: &EventsLoop,
|
||||
initial_url: ServoUrl,
|
||||
) -> Self {
|
||||
let gl = unsafe {
|
||||
glow::Context::from_loader_function(|s| rendering_context.get_proc_address(s))
|
||||
};
|
||||
|
||||
// Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs
|
||||
let context = EguiGlow::new(events_loop.as_winit(), Arc::new(gl), None);
|
||||
let widget_surface_fbo = match rendering_context.context_surface_info() {
|
||||
Ok(Some(info)) => NonZeroU32::new(info.framebuffer_object).map(NativeFramebuffer),
|
||||
Ok(None) => panic!("Failed to get widget surface info from surfman!"),
|
||||
Err(error) => panic!("Failed to get widget surface info from surfman! {error:?}"),
|
||||
};
|
||||
|
||||
Self {
|
||||
context,
|
||||
event_queue: RefCell::new(vec![]),
|
||||
toolbar_height: Default::default(),
|
||||
widget_surface_fbo,
|
||||
last_update: Instant::now(),
|
||||
last_mouse_position: None,
|
||||
location: RefCell::new(initial_url.to_string()),
|
||||
location_dirty: false.into(),
|
||||
load_status: LoadStatus::LoadComplete,
|
||||
status_text: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Preprocess the given [winit::event::WindowEvent], returning unconsumed for mouse events in
|
||||
/// the Servo browser rect. This is needed because the CentralPanel we create for our webview
|
||||
/// would otherwise make egui report events in that area as consumed.
|
||||
pub fn on_window_event(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
event: &winit::event::WindowEvent,
|
||||
) -> EventResponse {
|
||||
let mut result = self.context.on_window_event(window, event);
|
||||
result.consumed &= match event {
|
||||
winit::event::WindowEvent::CursorMoved { position, .. } => {
|
||||
let scale = Scale::<_, DeviceIndependentPixel, _>::new(
|
||||
self.context.egui_ctx.pixels_per_point(),
|
||||
);
|
||||
self.last_mouse_position =
|
||||
Some(winit_position_to_euclid_point(*position).to_f32() / scale);
|
||||
self.last_mouse_position
|
||||
.map_or(false, |p| self.is_in_browser_rect(p))
|
||||
},
|
||||
winit::event::WindowEvent::MouseInput {
|
||||
state: ElementState::Pressed,
|
||||
button: MouseButton::Forward,
|
||||
..
|
||||
} => {
|
||||
self.event_queue
|
||||
.borrow_mut()
|
||||
.push(MinibrowserEvent::Forward);
|
||||
true
|
||||
},
|
||||
winit::event::WindowEvent::MouseInput {
|
||||
state: ElementState::Pressed,
|
||||
button: MouseButton::Back,
|
||||
..
|
||||
} => {
|
||||
self.event_queue.borrow_mut().push(MinibrowserEvent::Back);
|
||||
true
|
||||
},
|
||||
winit::event::WindowEvent::MouseWheel { .. } |
|
||||
winit::event::WindowEvent::MouseInput { .. } => self
|
||||
.last_mouse_position
|
||||
.map_or(false, |p| self.is_in_browser_rect(p)),
|
||||
_ => true,
|
||||
};
|
||||
result
|
||||
}
|
||||
|
||||
/// Return true iff the given position is in the Servo browser rect.
|
||||
fn is_in_browser_rect(&self, position: Point2D<f32, DeviceIndependentPixel>) -> bool {
|
||||
position.y < self.toolbar_height.get()
|
||||
}
|
||||
|
||||
/// Update the minibrowser, but don’t paint.
|
||||
/// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our
|
||||
/// CentralPanel when [`Minibrowser::paint`] is called.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
window: &winit::window::Window,
|
||||
webviews: &mut WebViewManager<dyn WindowPortsMethods>,
|
||||
servo_framebuffer_id: Option<gl::GLuint>,
|
||||
reason: &'static str,
|
||||
) {
|
||||
let now = Instant::now();
|
||||
trace!(
|
||||
"{:?} since last update ({})",
|
||||
now - self.last_update,
|
||||
reason
|
||||
);
|
||||
let Self {
|
||||
context,
|
||||
event_queue,
|
||||
toolbar_height,
|
||||
widget_surface_fbo,
|
||||
last_update,
|
||||
location,
|
||||
location_dirty,
|
||||
..
|
||||
} = self;
|
||||
let widget_fbo = *widget_surface_fbo;
|
||||
let _duration = context.run(window, |ctx| {
|
||||
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
|
||||
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
|
||||
if !window.fullscreen().is_some() {
|
||||
TopBottomPanel::top("toolbar").show(ctx, |ui| {
|
||||
ui.allocate_ui_with_layout(
|
||||
ui.available_size(),
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
if ui.button("back").clicked() {
|
||||
event_queue.borrow_mut().push(MinibrowserEvent::Back);
|
||||
}
|
||||
if ui.button("forward").clicked() {
|
||||
event_queue.borrow_mut().push(MinibrowserEvent::Forward);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
match self.load_status {
|
||||
LoadStatus::LoadStart => {
|
||||
ui.add(Spinner::new().color(Color32::GRAY));
|
||||
},
|
||||
LoadStatus::HeadParsed => {
|
||||
ui.add(Spinner::new().color(Color32::WHITE));
|
||||
},
|
||||
LoadStatus::LoadComplete => { /* No Spinner */ },
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
if location_field.lost_focus() &&
|
||||
ui.input(|i| i.clone().key_pressed(Key::Enter))
|
||||
{
|
||||
event_queue.borrow_mut().push(MinibrowserEvent::Go);
|
||||
location_dirty.set(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// The toolbar height is where the Context’s available rect starts.
|
||||
// For reasons that are unclear, the TopBottomPanel’s ui cursor exceeds this by one egui
|
||||
// point, but the Context is correct and the TopBottomPanel is wrong.
|
||||
*toolbar_height = Length::new(ctx.available_rect().min.y);
|
||||
|
||||
let scale =
|
||||
Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
|
||||
let Some(focused_webview_id) = webviews.focused_webview_id() else {
|
||||
return;
|
||||
};
|
||||
let Some(webview) = webviews.get_mut(focused_webview_id) else {
|
||||
return;
|
||||
};
|
||||
let mut embedder_events = vec![];
|
||||
|
||||
if let Some(status_text) = &self.status_text {
|
||||
let position = Some(pos2(0.0, ctx.available_rect().max.y));
|
||||
egui::containers::popup::show_tooltip_at(
|
||||
ctx,
|
||||
"tooltip_for_status_text".into(),
|
||||
position,
|
||||
|ui| {
|
||||
ui.label(status_text.clone());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
CentralPanel::default()
|
||||
.frame(Frame::none())
|
||||
.show(ctx, |ui| {
|
||||
let Pos2 { x, y } = ui.cursor().min;
|
||||
let Vec2 {
|
||||
x: width,
|
||||
y: height,
|
||||
} = ui.available_size();
|
||||
let rect = Box2D::from_origin_and_size(
|
||||
Point2D::new(x, y),
|
||||
Size2D::new(width, height),
|
||||
) * scale;
|
||||
if rect != webview.rect {
|
||||
webview.rect = rect;
|
||||
embedder_events
|
||||
.push(EmbedderEvent::MoveResizeWebView(focused_webview_id, rect));
|
||||
}
|
||||
let min = ui.cursor().min;
|
||||
let size = ui.available_size();
|
||||
let rect = egui::Rect::from_min_size(min, size);
|
||||
ui.allocate_space(size);
|
||||
|
||||
let Some(servo_fbo) = servo_framebuffer_id else {
|
||||
return;
|
||||
};
|
||||
ui.painter().add(PaintCallback {
|
||||
rect,
|
||||
callback: Arc::new(CallbackFn::new(move |info, painter| {
|
||||
use glow::HasContext as _;
|
||||
let clip = info.viewport_in_pixels();
|
||||
let x = clip.left_px as gl::GLint;
|
||||
let y = clip.from_bottom_px as gl::GLint;
|
||||
let width = clip.width_px as gl::GLsizei;
|
||||
let height = clip.height_px as gl::GLsizei;
|
||||
unsafe {
|
||||
painter.gl().clear_color(0.0, 0.0, 0.0, 0.0);
|
||||
painter.gl().scissor(x, y, width, height);
|
||||
painter.gl().enable(gl::SCISSOR_TEST);
|
||||
painter.gl().clear(gl::COLOR_BUFFER_BIT);
|
||||
painter.gl().disable(gl::SCISSOR_TEST);
|
||||
|
||||
let servo_fbo = NonZeroU32::new(servo_fbo).map(NativeFramebuffer);
|
||||
painter
|
||||
.gl()
|
||||
.bind_framebuffer(gl::READ_FRAMEBUFFER, servo_fbo);
|
||||
painter
|
||||
.gl()
|
||||
.bind_framebuffer(gl::DRAW_FRAMEBUFFER, widget_fbo);
|
||||
painter.gl().blit_framebuffer(
|
||||
x,
|
||||
y,
|
||||
x + width,
|
||||
y + height,
|
||||
x,
|
||||
y,
|
||||
x + width,
|
||||
y + height,
|
||||
gl::COLOR_BUFFER_BIT,
|
||||
gl::NEAREST,
|
||||
);
|
||||
painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo);
|
||||
}
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
if !embedder_events.is_empty() {
|
||||
webviews.handle_window_events(embedder_events);
|
||||
}
|
||||
|
||||
*last_update = now;
|
||||
});
|
||||
}
|
||||
|
||||
/// Paint the minibrowser, as of the last update.
|
||||
pub fn paint(&mut self, window: &winit::window::Window) {
|
||||
unsafe {
|
||||
use glow::HasContext as _;
|
||||
self.context
|
||||
.painter
|
||||
.gl()
|
||||
.bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo);
|
||||
}
|
||||
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: &WebViewManager<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.webview_id().unwrap();
|
||||
let location = self.location.borrow();
|
||||
if let Some(url) = location_bar_input_to_url(&location.clone()) {
|
||||
app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url));
|
||||
} else {
|
||||
warn!("failed to parse location");
|
||||
break;
|
||||
}
|
||||
},
|
||||
MinibrowserEvent::Back => {
|
||||
let browser_id = browser.webview_id().unwrap();
|
||||
app_event_queue.push(EmbedderEvent::Navigation(
|
||||
browser_id,
|
||||
TraversalDirection::Back(1),
|
||||
));
|
||||
},
|
||||
MinibrowserEvent::Forward => {
|
||||
let browser_id = browser.webview_id().unwrap();
|
||||
app_event_queue.push(EmbedderEvent::Navigation(
|
||||
browser_id,
|
||||
TraversalDirection::Forward(1),
|
||||
));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the location field from the given [WebViewManager], unless the user has started
|
||||
/// editing it without clicking Go, returning true iff it has changed (needing an egui update).
|
||||
pub fn update_location_in_toolbar(
|
||||
&mut self,
|
||||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||||
) -> bool {
|
||||
// User edited without clicking Go?
|
||||
if self.location_dirty.get() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match browser.current_url_string() {
|
||||
Some(location) if location != self.location.get_mut() => {
|
||||
self.location = RefCell::new(location.to_owned());
|
||||
true
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the spinner from the given [WebViewManager], returning true iff it has changed
|
||||
/// (needing an egui update).
|
||||
pub fn update_spinner_in_toolbar(
|
||||
&mut self,
|
||||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||||
) -> bool {
|
||||
let need_update = browser.load_status() != self.load_status;
|
||||
self.load_status = browser.load_status();
|
||||
need_update
|
||||
}
|
||||
|
||||
pub fn update_status_text(
|
||||
&mut self,
|
||||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||||
) -> bool {
|
||||
let need_update = browser.status_text() != self.status_text;
|
||||
self.status_text = browser.status_text();
|
||||
need_update
|
||||
}
|
||||
|
||||
/// Updates all fields taken from the given [WebViewManager], such as the location field.
|
||||
/// Returns true iff the egui needs an update.
|
||||
pub fn update_webview_data(
|
||||
&mut self,
|
||||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||||
) -> bool {
|
||||
// Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
|
||||
// because logical OR would short-circuit if any of the functions return true.
|
||||
// We want to ensure that all functions are called. The "bitwise OR" operator
|
||||
// does not short-circuit.
|
||||
self.update_location_in_toolbar(browser) |
|
||||
self.update_spinner_in_toolbar(browser) |
|
||||
self.update_status_text(browser)
|
||||
}
|
||||
}
|
19
ports/servoshell/desktop/mod.rs
Normal file
19
ports/servoshell/desktop/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
/* 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/. */
|
||||
|
||||
//! Contains files specific to the servoshell app for Desktop systems.
|
||||
|
||||
pub(crate) mod app;
|
||||
pub(crate) mod cli;
|
||||
mod egui_glue;
|
||||
mod embedder;
|
||||
pub(crate) mod events_loop;
|
||||
pub mod geometry;
|
||||
mod headed_window;
|
||||
mod headless_window;
|
||||
mod keyutils;
|
||||
mod minibrowser;
|
||||
mod tracing;
|
||||
mod webview;
|
||||
mod window_trait;
|
236
ports/servoshell/desktop/tracing.rs
Normal file
236
ports/servoshell/desktop/tracing.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
/* 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/. */
|
||||
|
||||
/// Log an event from winit ([winit::event::Event]) at trace level.
|
||||
/// - To disable tracing: RUST_LOG='servoshell<winit@=off'
|
||||
/// - To enable tracing: RUST_LOG='servoshell<winit@'
|
||||
/// - Recommended filters when tracing is enabled:
|
||||
/// - servoshell<winit@DeviceEvent=off
|
||||
/// - servoshell<winit@MainEventsCleared=off
|
||||
/// - servoshell<winit@NewEvents(WaitCancelled)=off
|
||||
/// - servoshell<winit@RedrawEventsCleared=off
|
||||
/// - servoshell<winit@RedrawRequested=off
|
||||
/// - servoshell<winit@UserEvent(WakerEvent)=off
|
||||
/// - servoshell<winit@WindowEvent(AxisMotion)=off
|
||||
/// - servoshell<winit@WindowEvent(CursorMoved)=off
|
||||
macro_rules! trace_winit_event {
|
||||
// This macro only exists to put the docs in the same file as the target prefix,
|
||||
// so the macro definition is always the same.
|
||||
($event:expr, $($rest:tt)+) => {
|
||||
::log::trace!(target: $crate::desktop::tracing::LogTarget::log_target(&$event), $($rest)+)
|
||||
};
|
||||
}
|
||||
|
||||
/// Log an event from servo ([servo::embedder_traits::EmbedderMsg]) at trace level.
|
||||
/// - To disable tracing: RUST_LOG='servoshell<servo@=off'
|
||||
/// - To enable tracing: RUST_LOG='servoshell<servo@'
|
||||
/// - Recommended filters when tracing is enabled:
|
||||
/// - servoshell<servo@EventDelivered=off
|
||||
/// - servoshell<servo@ReadyToPresent=off
|
||||
macro_rules! trace_embedder_msg {
|
||||
// This macro only exists to put the docs in the same file as the target prefix,
|
||||
// so the macro definition is always the same.
|
||||
($event:expr, $($rest:tt)+) => {
|
||||
::log::trace!(target: $crate::desktop::tracing::LogTarget::log_target(&$event), $($rest)+)
|
||||
};
|
||||
}
|
||||
|
||||
/// Log an event to servo ([servo::compositing::windowing::EmbedderEvent]) at trace level.
|
||||
/// - To disable tracing: RUST_LOG='servoshell>servo@=off'
|
||||
/// - To enable tracing: RUST_LOG='servoshell>servo@'
|
||||
/// - Recommended filters when tracing is enabled:
|
||||
/// - servoshell>servo@Idle=off
|
||||
/// - servoshell>servo@MouseWindowMoveEventClass=off
|
||||
macro_rules! trace_embedder_event {
|
||||
// This macro only exists to put the docs in the same file as the target prefix,
|
||||
// so the macro definition is always the same.
|
||||
($event:expr, $($rest:tt)+) => {
|
||||
::log::trace!(target: $crate::desktop::tracing::LogTarget::log_target(&$event), $($rest)+)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use {trace_embedder_event, trace_embedder_msg, trace_winit_event};
|
||||
|
||||
/// Get the log target for an event, as a static string.
|
||||
pub(crate) trait LogTarget {
|
||||
fn log_target(&self) -> &'static str;
|
||||
}
|
||||
|
||||
mod from_winit {
|
||||
use super::LogTarget;
|
||||
use crate::desktop::events_loop::WakerEvent;
|
||||
|
||||
macro_rules! target {
|
||||
($($name:literal)+) => {
|
||||
concat!("servoshell<winit@", $($name),+)
|
||||
};
|
||||
}
|
||||
|
||||
impl LogTarget for winit::event::Event<WakerEvent> {
|
||||
fn log_target(&self) -> &'static str {
|
||||
use winit::event::StartCause;
|
||||
match self {
|
||||
Self::NewEvents(start_cause) => match start_cause {
|
||||
StartCause::ResumeTimeReached { .. } => target!("NewEvents(ResumeTimeReached)"),
|
||||
StartCause::WaitCancelled { .. } => target!("NewEvents(WaitCancelled)"),
|
||||
StartCause::Poll => target!("NewEvents(Poll)"),
|
||||
StartCause::Init => target!("NewEvents(Init)"),
|
||||
},
|
||||
Self::WindowEvent { event, .. } => event.log_target(),
|
||||
Self::DeviceEvent { .. } => target!("DeviceEvent"),
|
||||
Self::UserEvent(WakerEvent) => target!("UserEvent(WakerEvent)"),
|
||||
Self::Suspended => target!("Suspended"),
|
||||
Self::Resumed => target!("Resumed"),
|
||||
Self::AboutToWait => target!("AboutToWait"),
|
||||
Self::LoopExiting => target!("LoopExiting"),
|
||||
Self::MemoryWarning => target!("MemoryWarning"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogTarget for winit::event::WindowEvent {
|
||||
fn log_target(&self) -> &'static str {
|
||||
macro_rules! target_variant {
|
||||
($name:literal) => {
|
||||
target!("WindowEvent(" $name ")")
|
||||
};
|
||||
}
|
||||
match self {
|
||||
Self::ActivationTokenDone { .. } => target!("ActivationTokenDone"),
|
||||
Self::Resized(..) => target_variant!("Resized"),
|
||||
Self::Moved(..) => target_variant!("Moved"),
|
||||
Self::CloseRequested => target_variant!("CloseRequested"),
|
||||
Self::Destroyed => target_variant!("Destroyed"),
|
||||
Self::DroppedFile(..) => target_variant!("DroppedFile"),
|
||||
Self::HoveredFile(..) => target_variant!("HoveredFile"),
|
||||
Self::HoveredFileCancelled => target_variant!("HoveredFileCancelled"),
|
||||
Self::Focused(..) => target_variant!("Focused"),
|
||||
Self::KeyboardInput { .. } => target_variant!("KeyboardInput"),
|
||||
Self::ModifiersChanged(..) => target_variant!("ModifiersChanged"),
|
||||
Self::Ime(..) => target_variant!("Ime"),
|
||||
Self::CursorMoved { .. } => target_variant!("CursorMoved"),
|
||||
Self::CursorEntered { .. } => target_variant!("CursorEntered"),
|
||||
Self::CursorLeft { .. } => target_variant!("CursorLeft"),
|
||||
Self::MouseWheel { .. } => target_variant!("MouseWheel"),
|
||||
Self::MouseInput { .. } => target_variant!("MouseInput"),
|
||||
Self::TouchpadMagnify { .. } => target_variant!("TouchpadMagnify"),
|
||||
Self::SmartMagnify { .. } => target_variant!("SmartMagnify"),
|
||||
Self::TouchpadRotate { .. } => target_variant!("TouchpadRotate"),
|
||||
Self::TouchpadPressure { .. } => target_variant!("TouchpadPressure"),
|
||||
Self::AxisMotion { .. } => target_variant!("AxisMotion"),
|
||||
Self::Touch(..) => target_variant!("Touch"),
|
||||
Self::ScaleFactorChanged { .. } => target_variant!("ScaleFactorChanged"),
|
||||
Self::ThemeChanged(..) => target_variant!("ThemeChanged"),
|
||||
Self::Occluded(..) => target_variant!("Occluded"),
|
||||
Self::RedrawRequested => target!("RedrawRequested"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod from_servo {
|
||||
use super::LogTarget;
|
||||
|
||||
macro_rules! target {
|
||||
($($name:literal)+) => {
|
||||
concat!("servoshell<servo@", $($name),+)
|
||||
};
|
||||
}
|
||||
|
||||
impl LogTarget for servo::embedder_traits::EmbedderMsg {
|
||||
fn log_target(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Status(..) => target!("Status"),
|
||||
Self::ChangePageTitle(..) => target!("ChangePageTitle"),
|
||||
Self::MoveTo(..) => target!("MoveTo"),
|
||||
Self::ResizeTo(..) => target!("ResizeTo"),
|
||||
Self::Prompt(..) => target!("Prompt"),
|
||||
Self::ShowContextMenu(..) => target!("ShowContextMenu"),
|
||||
Self::AllowNavigationRequest(..) => target!("AllowNavigationRequest"),
|
||||
Self::AllowOpeningWebView(..) => target!("AllowOpeningWebView"),
|
||||
Self::WebViewOpened(..) => target!("WebViewOpened"),
|
||||
Self::WebViewClosed(..) => target!("WebViewClosed"),
|
||||
Self::WebViewFocused(..) => target!("WebViewFocused"),
|
||||
Self::WebViewBlurred => target!("WebViewBlurred"),
|
||||
Self::AllowUnload(..) => target!("AllowUnload"),
|
||||
Self::Keyboard(..) => target!("Keyboard"),
|
||||
Self::GetClipboardContents(..) => target!("GetClipboardContents"),
|
||||
Self::SetClipboardContents(..) => target!("SetClipboardContents"),
|
||||
Self::SetCursor(..) => target!("SetCursor"),
|
||||
Self::NewFavicon(..) => target!("NewFavicon"),
|
||||
Self::HeadParsed => target!("HeadParsed"),
|
||||
Self::HistoryChanged(..) => target!("HistoryChanged"),
|
||||
Self::SetFullscreenState(..) => target!("SetFullscreenState"),
|
||||
Self::LoadStart => target!("LoadStart"),
|
||||
Self::LoadComplete => target!("LoadComplete"),
|
||||
Self::Panic(..) => target!("Panic"),
|
||||
Self::GetSelectedBluetoothDevice(..) => target!("GetSelectedBluetoothDevice"),
|
||||
Self::SelectFiles(..) => target!("SelectFiles"),
|
||||
Self::PromptPermission(..) => target!("PromptPermission"),
|
||||
Self::ShowIME(..) => target!("ShowIME"),
|
||||
Self::HideIME => target!("HideIME"),
|
||||
Self::Shutdown => target!("Shutdown"),
|
||||
Self::ReportProfile(..) => target!("ReportProfile"),
|
||||
Self::MediaSessionEvent(..) => target!("MediaSessionEvent"),
|
||||
Self::OnDevtoolsStarted(..) => target!("OnDevtoolsStarted"),
|
||||
Self::ReadyToPresent(..) => target!("ReadyToPresent"),
|
||||
Self::EventDelivered(..) => target!("EventDelivered"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod to_servo {
|
||||
use super::LogTarget;
|
||||
|
||||
macro_rules! target {
|
||||
($($name:literal)+) => {
|
||||
concat!("servoshell>servo@", $($name),+)
|
||||
};
|
||||
}
|
||||
|
||||
impl LogTarget for servo::compositing::windowing::EmbedderEvent {
|
||||
fn log_target(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Idle => target!("Idle"),
|
||||
Self::Refresh => target!("Refresh"),
|
||||
Self::WindowResize => target!("WindowResize"),
|
||||
Self::AllowNavigationResponse(..) => target!("AllowNavigationResponse"),
|
||||
Self::LoadUrl(..) => target!("LoadUrl"),
|
||||
Self::MouseWindowEventClass(..) => target!("MouseWindowEventClass"),
|
||||
Self::MouseWindowMoveEventClass(..) => target!("MouseWindowMoveEventClass"),
|
||||
Self::Touch(..) => target!("Touch"),
|
||||
Self::Wheel(..) => target!("Wheel"),
|
||||
Self::Scroll(..) => target!("Scroll"),
|
||||
Self::Zoom(..) => target!("Zoom"),
|
||||
Self::PinchZoom(..) => target!("PinchZoom"),
|
||||
Self::ResetZoom => target!("ResetZoom"),
|
||||
Self::Navigation(..) => target!("Navigation"),
|
||||
Self::Quit => target!("Quit"),
|
||||
Self::ExitFullScreen(..) => target!("ExitFullScreen"),
|
||||
Self::Keyboard(..) => target!("Keyboard"),
|
||||
Self::Reload(..) => target!("Reload"),
|
||||
Self::NewWebView(..) => target!("NewWebView"),
|
||||
Self::CloseWebView(..) => target!("CloseWebView"),
|
||||
Self::SendError(..) => target!("SendError"),
|
||||
Self::MoveResizeWebView(..) => target!("MoveResizeWebView"),
|
||||
Self::ShowWebView(..) => target!("ShowWebView"),
|
||||
Self::HideWebView(..) => target!("HideWebView"),
|
||||
Self::RaiseWebViewToTop(..) => target!("RaiseWebViewToTop"),
|
||||
Self::FocusWebView(..) => target!("FocusWebView"),
|
||||
Self::BlurWebView => target!("BlurWebView"),
|
||||
Self::ToggleWebRenderDebug(..) => target!("ToggleWebRenderDebug"),
|
||||
Self::CaptureWebRender => target!("CaptureWebRender"),
|
||||
Self::ClearCache => target!("ClearCache"),
|
||||
Self::ToggleSamplingProfiler(..) => target!("ToggleSamplingProfiler"),
|
||||
Self::MediaSessionAction(..) => target!("MediaSessionAction"),
|
||||
Self::SetWebViewThrottled(..) => target!("SetWebViewThrottled"),
|
||||
Self::IMEDismissed => target!("IMEDismissed"),
|
||||
Self::InvalidateNativeSurface => target!("InvalidateNativeSurface"),
|
||||
Self::ReplaceNativeSurface(..) => target!("ReplaceNativeSurface"),
|
||||
Self::Gamepad(..) => target!("Gamepad"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
880
ports/servoshell/desktop/webview.rs
Normal file
880
ports/servoshell/desktop/webview.rs
Normal file
|
@ -0,0 +1,880 @@
|
|||
/* 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::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::vec::Drain;
|
||||
use std::{env, thread};
|
||||
|
||||
use arboard::Clipboard;
|
||||
use euclid::{Point2D, Vector2D};
|
||||
use gilrs::{EventType, Gilrs};
|
||||
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use servo::base::id::TopLevelBrowsingContextId as WebViewId;
|
||||
use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption};
|
||||
use servo::embedder_traits::{
|
||||
CompositorEventVariant, ContextMenuResult, EmbedderMsg, FilterPattern, PermissionPrompt,
|
||||
PermissionRequest, PromptDefinition, PromptOrigin, PromptResult,
|
||||
};
|
||||
use servo::script_traits::{
|
||||
GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadUpdateType, TouchEventType,
|
||||
TraversalDirection,
|
||||
};
|
||||
use servo::servo_config::opts;
|
||||
use servo::servo_url::ServoUrl;
|
||||
use servo::webrender_api::units::DeviceRect;
|
||||
use servo::webrender_api::ScrollLocation;
|
||||
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel, YesNo};
|
||||
|
||||
use super::keyutils::{CMD_OR_ALT, CMD_OR_CONTROL};
|
||||
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||
use crate::desktop::tracing::{trace_embedder_event, trace_embedder_msg};
|
||||
use crate::parser::location_bar_input_to_url;
|
||||
|
||||
pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> {
|
||||
current_url: Option<ServoUrl>,
|
||||
current_url_string: Option<String>,
|
||||
status_text: Option<String>,
|
||||
|
||||
/// List of top-level browsing contexts.
|
||||
/// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed,
|
||||
/// and we exit if it ever becomes empty.
|
||||
webviews: HashMap<WebViewId, WebView>,
|
||||
|
||||
/// The order in which the webviews were created.
|
||||
creation_order: Vec<WebViewId>,
|
||||
|
||||
/// The webview that is currently focused.
|
||||
/// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred.
|
||||
focused_webview_id: Option<WebViewId>,
|
||||
|
||||
title: Option<String>,
|
||||
|
||||
window: Rc<Window>,
|
||||
event_queue: Vec<EmbedderEvent>,
|
||||
clipboard: Option<Clipboard>,
|
||||
gamepad: Option<Gilrs>,
|
||||
shutdown_requested: bool,
|
||||
load_status: LoadStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WebView {
|
||||
pub rect: DeviceRect,
|
||||
}
|
||||
|
||||
pub struct ServoEventResponse {
|
||||
pub need_present: bool,
|
||||
pub need_update: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum LoadStatus {
|
||||
HeadParsed,
|
||||
LoadStart,
|
||||
LoadComplete,
|
||||
}
|
||||
|
||||
impl<Window> WebViewManager<Window>
|
||||
where
|
||||
Window: WindowPortsMethods + ?Sized,
|
||||
{
|
||||
pub fn new(window: Rc<Window>) -> WebViewManager<Window> {
|
||||
WebViewManager {
|
||||
title: None,
|
||||
current_url: None,
|
||||
current_url_string: None,
|
||||
status_text: None,
|
||||
webviews: HashMap::default(),
|
||||
creation_order: vec![],
|
||||
focused_webview_id: None,
|
||||
window,
|
||||
clipboard: match Clipboard::new() {
|
||||
Ok(c) => Some(c),
|
||||
Err(e) => {
|
||||
warn!("Error creating clipboard context ({})", e);
|
||||
None
|
||||
},
|
||||
},
|
||||
gamepad: match Gilrs::new() {
|
||||
Ok(g) => Some(g),
|
||||
Err(e) => {
|
||||
warn!("Error creating gamepad input connection ({})", e);
|
||||
None
|
||||
},
|
||||
},
|
||||
event_queue: Vec::new(),
|
||||
shutdown_requested: false,
|
||||
load_status: LoadStatus::LoadComplete,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn webview_id(&self) -> Option<WebViewId> {
|
||||
self.focused_webview_id
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, webview_id: WebViewId) -> Option<&mut WebView> {
|
||||
self.webviews.get_mut(&webview_id)
|
||||
}
|
||||
|
||||
pub fn focused_webview_id(&self) -> Option<WebViewId> {
|
||||
self.focused_webview_id
|
||||
}
|
||||
|
||||
pub fn current_url_string(&self) -> Option<&str> {
|
||||
self.current_url_string.as_deref()
|
||||
}
|
||||
|
||||
pub fn load_status(&self) -> LoadStatus {
|
||||
self.load_status
|
||||
}
|
||||
|
||||
pub fn status_text(&self) -> Option<String> {
|
||||
self.status_text.clone()
|
||||
}
|
||||
|
||||
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 {
|
||||
trace_embedder_event!(event, "{event:?}");
|
||||
match event {
|
||||
EmbedderEvent::Keyboard(key_event) => {
|
||||
self.handle_key_from_window(key_event);
|
||||
},
|
||||
event => {
|
||||
self.event_queue.push(event);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle updates to connected gamepads from GilRs
|
||||
pub fn handle_gamepad_events(&mut self) {
|
||||
if let Some(ref mut gilrs) = self.gamepad {
|
||||
while let Some(event) = gilrs.next_event() {
|
||||
let gamepad = gilrs.gamepad(event.id);
|
||||
let name = gamepad.name();
|
||||
let index = GamepadIndex(event.id.into());
|
||||
let mut gamepad_event: Option<GamepadEvent> = None;
|
||||
match event.event {
|
||||
EventType::ButtonPressed(button, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid digital button, aka on/off only
|
||||
if !matches!(mapped_index, 6 | 7 | 17) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, 1.0);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::ButtonReleased(button, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid digital button, aka on/off only
|
||||
if !matches!(mapped_index, 6 | 7 | 17) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, 0.0);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::ButtonChanged(button, value, _) => {
|
||||
let mapped_index = Self::map_gamepad_button(button);
|
||||
// We only want to send this for a valid non-digital button, aka the triggers
|
||||
if matches!(mapped_index, 6 | 7) {
|
||||
let update_type = GamepadUpdateType::Button(mapped_index, value as f64);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::AxisChanged(axis, value, _) => {
|
||||
// Map axis index and value to represent Standard Gamepad axis
|
||||
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-axis>
|
||||
let mapped_axis: usize = match axis {
|
||||
gilrs::Axis::LeftStickX => 0,
|
||||
gilrs::Axis::LeftStickY => 1,
|
||||
gilrs::Axis::RightStickX => 2,
|
||||
gilrs::Axis::RightStickY => 3,
|
||||
_ => 4, // Other axes do not map to "standard" gamepad mapping and are ignored
|
||||
};
|
||||
if mapped_axis < 4 {
|
||||
// The Gamepad spec designates down as positive and up as negative.
|
||||
// GilRs does the inverse of this, so correct for it here.
|
||||
let axis_value = match mapped_axis {
|
||||
0 | 2 => value,
|
||||
1 | 3 => -value,
|
||||
_ => 0., // Should not reach here
|
||||
};
|
||||
let update_type =
|
||||
GamepadUpdateType::Axis(mapped_axis, axis_value as f64);
|
||||
gamepad_event = Some(GamepadEvent::Updated(index, update_type));
|
||||
}
|
||||
},
|
||||
EventType::Connected => {
|
||||
let name = String::from(name);
|
||||
let bounds = GamepadInputBounds {
|
||||
axis_bounds: (-1.0, 1.0),
|
||||
button_bounds: (0.0, 1.0),
|
||||
};
|
||||
gamepad_event = Some(GamepadEvent::Connected(index, name, bounds));
|
||||
},
|
||||
EventType::Disconnected => {
|
||||
gamepad_event = Some(GamepadEvent::Disconnected(index));
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(event) = gamepad_event {
|
||||
self.event_queue.push(EmbedderEvent::Gamepad(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Map button index and value to represent Standard Gamepad button
|
||||
// <https://www.w3.org/TR/gamepad/#dfn-represents-a-standard-gamepad-button>
|
||||
fn map_gamepad_button(button: gilrs::Button) -> usize {
|
||||
match button {
|
||||
gilrs::Button::South => 0,
|
||||
gilrs::Button::East => 1,
|
||||
gilrs::Button::West => 2,
|
||||
gilrs::Button::North => 3,
|
||||
gilrs::Button::LeftTrigger => 4,
|
||||
gilrs::Button::RightTrigger => 5,
|
||||
gilrs::Button::LeftTrigger2 => 6,
|
||||
gilrs::Button::RightTrigger2 => 7,
|
||||
gilrs::Button::Select => 8,
|
||||
gilrs::Button::Start => 9,
|
||||
gilrs::Button::LeftThumb => 10,
|
||||
gilrs::Button::RightThumb => 11,
|
||||
gilrs::Button::DPadUp => 12,
|
||||
gilrs::Button::DPadDown => 13,
|
||||
gilrs::Button::DPadLeft => 14,
|
||||
gilrs::Button::DPadRight => 15,
|
||||
gilrs::Button::Mode => 16,
|
||||
_ => 17, // Other buttons do not map to "standard" gamepad mapping and are ignored
|
||||
}
|
||||
}
|
||||
|
||||
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.focused_webview_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) = location_bar_input_to_url(&input) {
|
||||
if let Some(id) = self.focused_webview_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.focused_webview_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.focused_webview_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.focused_webview_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.focused_webview_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<WebViewId>, 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 if the caller needs to manually present a new frame.
|
||||
pub fn handle_servo_events(
|
||||
&mut self,
|
||||
events: Drain<'_, (Option<WebViewId>, EmbedderMsg)>,
|
||||
) -> ServoEventResponse {
|
||||
let mut need_present = self.load_status != LoadStatus::LoadComplete;
|
||||
let mut need_update = false;
|
||||
for (webview_id, msg) in events {
|
||||
if let Some(webview_id) = webview_id {
|
||||
trace_embedder_msg!(msg, "{webview_id} {msg:?}");
|
||||
} else {
|
||||
trace_embedder_msg!(msg, "{msg:?}");
|
||||
}
|
||||
match msg {
|
||||
EmbedderMsg::Status(status) => {
|
||||
self.status_text = status;
|
||||
need_update = true;
|
||||
},
|
||||
EmbedderMsg::ChangePageTitle(title) => {
|
||||
self.title = title;
|
||||
|
||||
let fallback_title: String = if let Some(ref current_url) = self.current_url {
|
||||
current_url.to_string()
|
||||
} else {
|
||||
String::from("Untitled")
|
||||
};
|
||||
let title = match self.title {
|
||||
Some(ref title) if !title.is_empty() => &**title,
|
||||
_ => &fallback_title,
|
||||
};
|
||||
let title = format!("{} - Servo", title);
|
||||
self.window.set_title(&title);
|
||||
},
|
||||
EmbedderMsg::MoveTo(point) => {
|
||||
self.window.set_position(point);
|
||||
},
|
||||
EmbedderMsg::ResizeTo(size) => {
|
||||
if let Some(webview_id) = webview_id {
|
||||
let new_rect = self.get_mut(webview_id).and_then(|webview| {
|
||||
if webview.rect.size() != size.to_f32() {
|
||||
webview.rect.set_size(size.to_f32());
|
||||
Some(webview.rect)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(new_rect) = new_rect {
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::MoveResizeWebView(webview_id, new_rect));
|
||||
}
|
||||
}
|
||||
self.window.request_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(webview_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(webview_id, reason));
|
||||
}
|
||||
},
|
||||
EmbedderMsg::AllowNavigationRequest(pipeline_id, _url) => {
|
||||
if let Some(_webview_id) = webview_id {
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::AllowNavigationResponse(pipeline_id, true));
|
||||
}
|
||||
},
|
||||
EmbedderMsg::AllowOpeningWebView(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 AllowOpeningWebView response: {}", e);
|
||||
};
|
||||
},
|
||||
EmbedderMsg::WebViewOpened(new_webview_id) => {
|
||||
let scale = self.window.hidpi_factor().get();
|
||||
let toolbar = self.window.toolbar_height().get();
|
||||
|
||||
// Adjust for our toolbar height.
|
||||
// TODO: Adjust for egui window decorations if we end up using those
|
||||
let mut rect = self.window.get_coordinates().get_viewport().to_f32();
|
||||
rect.min.y += toolbar * scale;
|
||||
|
||||
self.webviews.insert(new_webview_id, WebView { rect });
|
||||
self.creation_order.push(new_webview_id);
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::FocusWebView(new_webview_id));
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::MoveResizeWebView(new_webview_id, rect));
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::RaiseWebViewToTop(new_webview_id, true));
|
||||
},
|
||||
EmbedderMsg::WebViewClosed(webview_id) => {
|
||||
self.webviews.retain(|&id, _| id != webview_id);
|
||||
self.creation_order.retain(|&id| id != webview_id);
|
||||
self.focused_webview_id = None;
|
||||
if let Some(&newest_webview_id) = self.creation_order.last() {
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::FocusWebView(newest_webview_id));
|
||||
} else {
|
||||
self.event_queue.push(EmbedderEvent::Quit);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::WebViewFocused(webview_id) => {
|
||||
self.focused_webview_id = Some(webview_id);
|
||||
// Show the most recently created webview and hide all others.
|
||||
// TODO: Stop doing this once we have full multiple webviews support
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::ShowWebView(webview_id, true));
|
||||
},
|
||||
EmbedderMsg::WebViewBlurred => {
|
||||
self.focused_webview_id = None;
|
||||
},
|
||||
EmbedderMsg::Keyboard(key_event) => {
|
||||
self.handle_key_from_servo(webview_id, key_event);
|
||||
},
|
||||
EmbedderMsg::GetClipboardContents(sender) => {
|
||||
let contents = self
|
||||
.clipboard
|
||||
.as_mut()
|
||||
.and_then(|clipboard| clipboard.get_text().ok())
|
||||
.unwrap_or_else(|| {
|
||||
warn!("Error getting clipboard text. Returning empty string.");
|
||||
String::new()
|
||||
});
|
||||
if let Err(e) = sender.send(contents) {
|
||||
warn!("Failed to send clipboard ({})", e);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::SetClipboardContents(text) => {
|
||||
if let Some(ref mut clipboard) = self.clipboard {
|
||||
if let Err(e) = clipboard.set_text(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 => {
|
||||
self.load_status = LoadStatus::HeadParsed;
|
||||
need_update = true;
|
||||
},
|
||||
EmbedderMsg::HistoryChanged(urls, current) => {
|
||||
self.current_url = Some(urls[current].clone());
|
||||
self.current_url_string = Some(urls[current].clone().into_string());
|
||||
need_update = true;
|
||||
},
|
||||
EmbedderMsg::SetFullscreenState(state) => {
|
||||
self.window.set_fullscreen(state);
|
||||
},
|
||||
EmbedderMsg::LoadStart => {
|
||||
self.load_status = LoadStatus::LoadStart;
|
||||
need_update = true;
|
||||
},
|
||||
EmbedderMsg::LoadComplete => {
|
||||
self.load_status = LoadStatus::LoadComplete;
|
||||
need_update = true;
|
||||
},
|
||||
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(_webview_ids) => {
|
||||
need_present = true;
|
||||
},
|
||||
EmbedderMsg::EventDelivered(event) => {
|
||||
if let (Some(webview_id), CompositorEventVariant::MouseButtonEvent) =
|
||||
(webview_id, event)
|
||||
{
|
||||
trace!("{}: Got a mouse button event", webview_id);
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::RaiseWebViewToTop(webview_id, true));
|
||||
self.event_queue
|
||||
.push(EmbedderEvent::FocusWebView(webview_id));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ServoEventResponse {
|
||||
need_present,
|
||||
need_update,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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")
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
51
ports/servoshell/desktop/window_trait.rs
Normal file
51
ports/servoshell/desktop/window_trait.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
/* 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 euclid::{Length, Scale};
|
||||
use servo::compositing::windowing::{EmbedderEvent, WindowMethods};
|
||||
use servo::config::opts;
|
||||
use servo::embedder_traits::Cursor;
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::style_traits::DevicePixel;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
|
||||
|
||||
use super::events_loop::WakerEvent;
|
||||
|
||||
// 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 hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
self.device_pixel_ratio_override()
|
||||
.unwrap_or_else(|| match opts::get().output_file {
|
||||
Some(_) => Scale::new(1.0),
|
||||
None => self.device_hidpi_factor(),
|
||||
})
|
||||
}
|
||||
fn device_hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
|
||||
fn device_pixel_ratio_override(
|
||||
&self,
|
||||
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>;
|
||||
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 request_inner_size(&self, size: DeviceIntSize) -> Option<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 toolbar_height(&self) -> Length<f32, DeviceIndependentPixel>;
|
||||
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue