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:
Jonathan Schwender 2024-06-14 08:26:35 +02:00 committed by GitHub
parent bae9f6d844
commit ff4cd4af96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 233 additions and 179 deletions

View 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 eguis
// 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 doesnt 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 dont, so there wont 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())
}
}

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

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

View file

@ -0,0 +1,45 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Implements the global methods required by Servo (not window/gl/compositor related).
use servo::compositing::windowing::EmbedderMethods;
use servo::embedder_traits::{EmbedderProxy, EventLoopWaker};
use servo::servo_config::pref;
use webxr::glwindow::GlWindowDiscovery;
pub struct EmbedderCallbacks {
event_loop_waker: Box<dyn EventLoopWaker>,
xr_discovery: Option<GlWindowDiscovery>,
}
impl EmbedderCallbacks {
pub fn new(
event_loop_waker: Box<dyn EventLoopWaker>,
xr_discovery: Option<GlWindowDiscovery>,
) -> EmbedderCallbacks {
EmbedderCallbacks {
event_loop_waker,
xr_discovery,
}
}
}
impl EmbedderMethods for EmbedderCallbacks {
fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> {
self.event_loop_waker.clone()
}
fn register_webxr(
&mut self,
xr: &mut webxr::MainThreadRegistry,
_embedder_proxy: EmbedderProxy,
) {
if pref!(dom.webxr.test) {
xr.register_mock(webxr::headless::HeadlessMockDiscovery::new());
} else if let Some(xr_discovery) = self.xr_discovery.take() {
xr.register(xr_discovery);
}
}
}

View file

@ -0,0 +1,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()))
}
}

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

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

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

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

View 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 dont 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 Contexts available rect starts.
// For reasons that are unclear, the TopBottomPanels 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)
}
}

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

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

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

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