mirror of
https://github.com/servo/servo.git
synced 2025-08-06 22:15:33 +01:00
servoshell: Port desktop servoshell to use delegate API (#35284)
Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Delan Azabani <dazabani@igalia.com> Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
parent
6b12499077
commit
5f08e4fa76
15 changed files with 1109 additions and 1258 deletions
|
@ -1089,7 +1089,11 @@ impl Servo {
|
||||||
webview.delegate().notify_ready_to_show(webview);
|
webview.delegate().notify_ready_to_show(webview);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EmbedderMsg::WebViewClosed(_) => {},
|
EmbedderMsg::WebViewClosed(webview_id) => {
|
||||||
|
if let Some(webview) = self.get_webview_handle(webview_id) {
|
||||||
|
webview.delegate().notify_closed(webview);
|
||||||
|
}
|
||||||
|
},
|
||||||
EmbedderMsg::WebViewFocused(webview_id) => {
|
EmbedderMsg::WebViewFocused(webview_id) => {
|
||||||
for id in self.webviews.borrow().keys() {
|
for id in self.webviews.borrow().keys() {
|
||||||
if let Some(webview) = self.get_webview_handle(*id) {
|
if let Some(webview) = self.get_webview_handle(*id) {
|
||||||
|
|
|
@ -117,6 +117,10 @@ pub trait WebViewDelegate {
|
||||||
/// The history state has changed.
|
/// The history state has changed.
|
||||||
// changed pattern; maybe wasteful if embedder doesn’t care?
|
// changed pattern; maybe wasteful if embedder doesn’t care?
|
||||||
fn notify_history_changed(&self, _webview: WebView, _: Vec<Url>, _: usize) {}
|
fn notify_history_changed(&self, _webview: WebView, _: Vec<Url>, _: usize) {}
|
||||||
|
/// Page content has closed this [`WebView`] via `window.close()`. It's the embedder's
|
||||||
|
/// responsibility to remove the [`WebView`] from the interface when this notification
|
||||||
|
/// occurs.
|
||||||
|
fn notify_closed(&self, _webview: WebView) {}
|
||||||
|
|
||||||
/// A keyboard event has been sent to Servo, but remains unprocessed. This allows the
|
/// A keyboard event has been sent to Servo, but remains unprocessed. This allows the
|
||||||
/// embedding application to handle key events while first letting the [`WebView`]
|
/// embedding application to handle key events while first letting the [`WebView`]
|
||||||
|
@ -144,10 +148,6 @@ pub trait WebViewDelegate {
|
||||||
fn request_open_auxiliary_webview(&self, _parent_webview: WebView) -> Option<WebView> {
|
fn request_open_auxiliary_webview(&self, _parent_webview: WebView) -> Option<WebView> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
/// Page content has requested that this [`WebView`] be closed. It's the embedder's
|
|
||||||
/// responsibility to either ignore this request or to remove the [`WebView`] from the
|
|
||||||
/// interface.
|
|
||||||
fn request_close(&self, _webview: WebView) {}
|
|
||||||
/// Open interface to request permission specified by prompt.
|
/// Open interface to request permission specified by prompt.
|
||||||
fn request_permission(
|
fn request_permission(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -10,7 +10,6 @@ use std::rc::Rc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
|
|
||||||
use arboard::Clipboard;
|
|
||||||
use log::{info, trace, warn};
|
use log::{info, trace, warn};
|
||||||
use raw_window_handle::HasDisplayHandle;
|
use raw_window_handle::HasDisplayHandle;
|
||||||
use servo::compositing::windowing::{AnimationState, WindowMethods};
|
use servo::compositing::windowing::{AnimationState, WindowMethods};
|
||||||
|
@ -31,10 +30,11 @@ use winit::event::WindowEvent;
|
||||||
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
use winit::event_loop::{ActiveEventLoop, ControlFlow};
|
||||||
use winit::window::WindowId;
|
use winit::window::WindowId;
|
||||||
|
|
||||||
|
use super::app_state::AppState;
|
||||||
use super::events_loop::{EventsLoop, WakerEvent};
|
use super::events_loop::{EventsLoop, WakerEvent};
|
||||||
use super::minibrowser::{Minibrowser, MinibrowserEvent};
|
use super::minibrowser::{Minibrowser, MinibrowserEvent};
|
||||||
use super::webview::WebViewManager;
|
|
||||||
use super::{headed_window, headless_window};
|
use super::{headed_window, headless_window};
|
||||||
|
use crate::desktop::app_state::RunningAppState;
|
||||||
use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery};
|
use crate::desktop::embedder::{EmbedderCallbacks, XrDiscovery};
|
||||||
use crate::desktop::tracing::trace_winit_event;
|
use crate::desktop::tracing::trace_winit_event;
|
||||||
use crate::desktop::window_trait::WindowPortsMethods;
|
use crate::desktop::window_trait::WindowPortsMethods;
|
||||||
|
@ -45,9 +45,6 @@ pub struct App {
|
||||||
opts: Opts,
|
opts: Opts,
|
||||||
preferences: Preferences,
|
preferences: Preferences,
|
||||||
servo_shell_preferences: ServoShellPreferences,
|
servo_shell_preferences: ServoShellPreferences,
|
||||||
clipboard: Option<Clipboard>,
|
|
||||||
servo: Option<Servo>,
|
|
||||||
webviews: WebViewManager,
|
|
||||||
suspended: Cell<bool>,
|
suspended: Cell<bool>,
|
||||||
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
|
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
|
||||||
minibrowser: Option<Minibrowser>,
|
minibrowser: Option<Minibrowser>,
|
||||||
|
@ -55,15 +52,16 @@ pub struct App {
|
||||||
initial_url: ServoUrl,
|
initial_url: ServoUrl,
|
||||||
t_start: Instant,
|
t_start: Instant,
|
||||||
t: Instant,
|
t: Instant,
|
||||||
|
state: AppState,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Present {
|
pub(crate) enum Present {
|
||||||
Deferred,
|
Deferred,
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Action to be taken by the caller of [`App::handle_events`].
|
/// Action to be taken by the caller of [`App::handle_events`].
|
||||||
enum PumpResult {
|
pub(crate) enum PumpResult {
|
||||||
/// The caller should shut down Servo and its related context.
|
/// The caller should shut down Servo and its related context.
|
||||||
Shutdown,
|
Shutdown,
|
||||||
Continue {
|
Continue {
|
||||||
|
@ -91,9 +89,6 @@ impl App {
|
||||||
opts,
|
opts,
|
||||||
preferences,
|
preferences,
|
||||||
servo_shell_preferences,
|
servo_shell_preferences,
|
||||||
clipboard: Clipboard::new().ok(),
|
|
||||||
webviews: WebViewManager::new(),
|
|
||||||
servo: None,
|
|
||||||
suspended: Cell::new(false),
|
suspended: Cell::new(false),
|
||||||
windows: HashMap::new(),
|
windows: HashMap::new(),
|
||||||
minibrowser: None,
|
minibrowser: None,
|
||||||
|
@ -101,6 +96,7 @@ impl App {
|
||||||
initial_url: initial_url.clone(),
|
initial_url: initial_url.clone(),
|
||||||
t_start: t,
|
t_start: t,
|
||||||
t,
|
t,
|
||||||
|
state: AppState::Initializing,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,8 +145,6 @@ impl App {
|
||||||
))
|
))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create window's context
|
|
||||||
self.webviews.set_window(window.clone());
|
|
||||||
if window.winit_window().is_some() {
|
if window.winit_window().is_some() {
|
||||||
self.minibrowser = Some(Minibrowser::new(
|
self.minibrowser = Some(Minibrowser::new(
|
||||||
&rendering_context,
|
&rendering_context,
|
||||||
|
@ -159,17 +153,6 @@ impl App {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
|
||||||
// Servo is not yet initialised, so there is no `servo_framebuffer_id`.
|
|
||||||
minibrowser.update(
|
|
||||||
window.winit_window().unwrap(),
|
|
||||||
&mut self.webviews,
|
|
||||||
self.servo.as_ref(),
|
|
||||||
"init",
|
|
||||||
);
|
|
||||||
window.set_toolbar_height(minibrowser.toolbar_height);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.windows.insert(window.id(), window);
|
self.windows.insert(window.id(), window);
|
||||||
|
|
||||||
self.suspended.set(false);
|
self.suspended.set(false);
|
||||||
|
@ -192,6 +175,15 @@ impl App {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Implements embedder methods, used by libservo and constellation.
|
||||||
|
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
|
||||||
|
|
||||||
|
let composite_target = if self.minibrowser.is_some() {
|
||||||
|
CompositeTarget::OffscreenFbo
|
||||||
|
} else {
|
||||||
|
CompositeTarget::ContextFbo
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: Remove this once dyn upcasting coercion stabilises
|
// TODO: Remove this once dyn upcasting coercion stabilises
|
||||||
// <https://github.com/rust-lang/rust/issues/65991>
|
// <https://github.com/rust-lang/rust/issues/65991>
|
||||||
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
|
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
|
||||||
|
@ -203,116 +195,61 @@ impl App {
|
||||||
self.0.set_animation_state(state);
|
self.0.set_animation_state(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let window = UpcastedWindow(window.clone());
|
|
||||||
// Implements embedder methods, used by libservo and constellation.
|
|
||||||
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
|
|
||||||
|
|
||||||
let composite_target = if self.minibrowser.is_some() {
|
|
||||||
CompositeTarget::OffscreenFbo
|
|
||||||
} else {
|
|
||||||
CompositeTarget::ContextFbo
|
|
||||||
};
|
|
||||||
let servo = Servo::new(
|
let servo = Servo::new(
|
||||||
self.opts.clone(),
|
self.opts.clone(),
|
||||||
self.preferences.clone(),
|
self.preferences.clone(),
|
||||||
Rc::new(rendering_context),
|
Rc::new(rendering_context),
|
||||||
embedder,
|
embedder,
|
||||||
Rc::new(window),
|
Rc::new(UpcastedWindow(window.clone())),
|
||||||
self.servo_shell_preferences.user_agent.clone(),
|
self.servo_shell_preferences.user_agent.clone(),
|
||||||
composite_target,
|
composite_target,
|
||||||
);
|
);
|
||||||
|
|
||||||
servo.setup_logging();
|
servo.setup_logging();
|
||||||
|
|
||||||
let webview = servo.new_webview(self.initial_url.clone().into_url());
|
let running_state = Rc::new(RunningAppState::new(
|
||||||
self.webviews.add(webview);
|
servo,
|
||||||
|
window.clone(),
|
||||||
|
self.opts.headless,
|
||||||
|
));
|
||||||
|
running_state.new_toplevel_webview(self.initial_url.clone().into_url());
|
||||||
|
|
||||||
self.servo = Some(servo);
|
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||||
|
minibrowser.update(window.winit_window().unwrap(), &running_state, "init");
|
||||||
|
window.set_toolbar_height(minibrowser.toolbar_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = AppState::Running(running_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_animating(&self) -> bool {
|
pub fn is_animating(&self) -> bool {
|
||||||
self.windows.iter().any(|(_, window)| window.is_animating())
|
self.windows.iter().any(|(_, window)| window.is_animating())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spins the Servo event loop, and (for now) handles a few other tasks:
|
|
||||||
/// - Notifying Servo about incoming gamepad events
|
|
||||||
/// - Receiving updates from Servo
|
|
||||||
/// - Performing updates in the compositor, such as queued pinch zoom events
|
|
||||||
///
|
|
||||||
/// In the future, these tasks may be decoupled.
|
|
||||||
fn handle_events(&mut self) -> PumpResult {
|
|
||||||
// If the Gamepad API is enabled, handle gamepad events from GilRs.
|
|
||||||
// Checking for focused_webview_id should ensure we'll have a valid browsing context.
|
|
||||||
if pref!(dom_gamepad_enabled) && self.webviews.focused_webview_id().is_some() {
|
|
||||||
self.webviews.handle_gamepad_events();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take any new embedder messages from Servo.
|
|
||||||
let servo = self.servo.as_mut().expect("Servo should be running.");
|
|
||||||
let mut embedder_messages = servo.get_events();
|
|
||||||
let mut need_present = false;
|
|
||||||
let mut need_update = false;
|
|
||||||
loop {
|
|
||||||
// Consume and handle those embedder messages.
|
|
||||||
let servo_event_response = self.webviews.handle_servo_events(
|
|
||||||
servo,
|
|
||||||
&mut self.clipboard,
|
|
||||||
&self.opts,
|
|
||||||
embedder_messages,
|
|
||||||
);
|
|
||||||
need_present |= servo_event_response.need_present;
|
|
||||||
need_update |= servo_event_response.need_update;
|
|
||||||
|
|
||||||
// Runs the compositor, and receives and collects embedder messages from various Servo components.
|
|
||||||
servo.handle_events(vec![]);
|
|
||||||
|
|
||||||
if self.webviews.shutdown_requested() {
|
|
||||||
return PumpResult::Shutdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take any new embedder messages from Servo itself.
|
|
||||||
embedder_messages = servo.get_events();
|
|
||||||
if embedder_messages.is_empty() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let present = if need_present {
|
|
||||||
Present::Deferred
|
|
||||||
} else {
|
|
||||||
Present::None
|
|
||||||
};
|
|
||||||
|
|
||||||
PumpResult::Continue {
|
|
||||||
update: need_update,
|
|
||||||
present,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle events with winit contexts
|
/// Handle events with winit contexts
|
||||||
pub fn handle_events_with_winit(
|
pub fn handle_events_with_winit(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &ActiveEventLoop,
|
event_loop: &ActiveEventLoop,
|
||||||
window: Rc<dyn WindowPortsMethods>,
|
window: Rc<dyn WindowPortsMethods>,
|
||||||
) {
|
) {
|
||||||
match self.handle_events() {
|
let AppState::Running(state) = &self.state else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.pump_event_loop() {
|
||||||
PumpResult::Shutdown => {
|
PumpResult::Shutdown => {
|
||||||
event_loop.exit();
|
state.shutdown();
|
||||||
self.servo.take().unwrap().deinit();
|
self.state = AppState::ShuttingDown;
|
||||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
|
||||||
minibrowser.context.destroy();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
PumpResult::Continue { update, present } => {
|
PumpResult::Continue { update, present } => {
|
||||||
if update {
|
if update {
|
||||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||||
if minibrowser.update_webview_data(&mut self.webviews) {
|
if minibrowser.update_webview_data(state) {
|
||||||
// Update the minibrowser immediately. While we could update by requesting a
|
// Update the minibrowser immediately. While we could update by requesting a
|
||||||
// redraw, doing so would delay the location update by two frames.
|
// redraw, doing so would delay the location update by two frames.
|
||||||
minibrowser.update(
|
minibrowser.update(
|
||||||
window.winit_window().unwrap(),
|
window.winit_window().unwrap(),
|
||||||
&mut self.webviews,
|
state,
|
||||||
self.servo.as_ref(),
|
|
||||||
"update_location_in_toolbar",
|
"update_location_in_toolbar",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -327,16 +264,21 @@ impl App {
|
||||||
if let Some(window) = window.winit_window() {
|
if let Some(window) = window.winit_window() {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
} else {
|
} else {
|
||||||
self.servo.as_mut().unwrap().present();
|
state.servo().present();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Present::None => {},
|
Present::None => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matches!(self.state, AppState::ShuttingDown) {
|
||||||
|
event_loop.exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle all servo events with headless mode. Return true if servo request to shutdown.
|
/// Handle all servo events with headless mode. Return true if the application should
|
||||||
|
/// continue.
|
||||||
pub fn handle_events_with_headless(&mut self) -> bool {
|
pub fn handle_events_with_headless(&mut self) -> bool {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let event = winit::event::Event::UserEvent(WakerEvent);
|
let event = winit::event::Event::UserEvent(WakerEvent);
|
||||||
|
@ -347,20 +289,16 @@ impl App {
|
||||||
now - self.t
|
now - self.t
|
||||||
);
|
);
|
||||||
self.t = now;
|
self.t = now;
|
||||||
// If self.servo is None here, it means that we're in the process of shutting down,
|
|
||||||
// let's ignore events.
|
|
||||||
if self.servo.is_none() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut exit = false;
|
// We should always be in the running state.
|
||||||
match self.handle_events() {
|
let AppState::Running(state) = &self.state else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.pump_event_loop() {
|
||||||
PumpResult::Shutdown => {
|
PumpResult::Shutdown => {
|
||||||
exit = true;
|
state.shutdown();
|
||||||
self.servo.take().unwrap().deinit();
|
self.state = AppState::ShuttingDown;
|
||||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
|
||||||
minibrowser.context.destroy();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
PumpResult::Continue { present, .. } => {
|
PumpResult::Continue { present, .. } => {
|
||||||
match present {
|
match present {
|
||||||
|
@ -368,13 +306,14 @@ impl App {
|
||||||
// The compositor has painted to this frame.
|
// The compositor has painted to this frame.
|
||||||
trace!("PumpResult::Present::Deferred");
|
trace!("PumpResult::Present::Deferred");
|
||||||
// In headless mode, we present directly.
|
// In headless mode, we present directly.
|
||||||
self.servo.as_mut().unwrap().present();
|
state.servo().present();
|
||||||
},
|
},
|
||||||
Present::None => {},
|
Present::None => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
exit
|
|
||||||
|
!matches!(self.state, AppState::ShuttingDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes any events generated during `egui` updates and performs their actions.
|
/// Takes any events generated during `egui` updates and performs their actions.
|
||||||
|
@ -382,7 +321,8 @@ impl App {
|
||||||
let Some(minibrowser) = self.minibrowser.as_ref() else {
|
let Some(minibrowser) = self.minibrowser.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(servo) = self.servo.as_ref() else {
|
// We should always be in the running state.
|
||||||
|
let AppState::Running(state) = &self.state else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -397,34 +337,33 @@ impl App {
|
||||||
warn!("failed to parse location");
|
warn!("failed to parse location");
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
if let Some(focused_webview) = state.focused_webview() {
|
||||||
focused_webview.servo_webview.load(url.into_url());
|
focused_webview.load(url.into_url());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MinibrowserEvent::Back => {
|
MinibrowserEvent::Back => {
|
||||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
if let Some(focused_webview) = state.focused_webview() {
|
||||||
focused_webview.servo_webview.go_back(1);
|
focused_webview.go_back(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MinibrowserEvent::Forward => {
|
MinibrowserEvent::Forward => {
|
||||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
if let Some(focused_webview) = state.focused_webview() {
|
||||||
focused_webview.servo_webview.go_forward(1);
|
focused_webview.go_forward(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MinibrowserEvent::Reload => {
|
MinibrowserEvent::Reload => {
|
||||||
minibrowser.update_location_dirty(false);
|
minibrowser.update_location_dirty(false);
|
||||||
if let Some(focused_webview) = self.webviews.focused_webview() {
|
if let Some(focused_webview) = state.focused_webview() {
|
||||||
focused_webview.servo_webview.reload();
|
focused_webview.reload();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MinibrowserEvent::NewWebView => {
|
MinibrowserEvent::NewWebView => {
|
||||||
minibrowser.update_location_dirty(false);
|
minibrowser.update_location_dirty(false);
|
||||||
let webview = servo.new_webview(Url::parse("servo:newtab").unwrap());
|
state.new_toplevel_webview(Url::parse("servo:newtab").unwrap());
|
||||||
self.webviews.add(webview);
|
|
||||||
},
|
},
|
||||||
MinibrowserEvent::CloseWebView(id) => {
|
MinibrowserEvent::CloseWebView(id) => {
|
||||||
minibrowser.update_location_dirty(false);
|
minibrowser.update_location_dirty(false);
|
||||||
self.webviews.close_webview(servo, id);
|
state.close_webview(id);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -450,17 +389,16 @@ impl ApplicationHandler<WakerEvent> for App {
|
||||||
now - self.t
|
now - self.t
|
||||||
);
|
);
|
||||||
self.t = now;
|
self.t = now;
|
||||||
// If self.servo is None here, it means that we're in the process of shutting down,
|
|
||||||
// let's ignore events.
|
let AppState::Running(state) = &self.state else {
|
||||||
let Some(ref mut servo) = self.servo else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(window) = self.windows.get(&window_id) else {
|
let Some(window) = self.windows.get(&window_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let window = window.clone();
|
|
||||||
|
|
||||||
|
let window = window.clone();
|
||||||
if event == winit::event::WindowEvent::RedrawRequested {
|
if event == winit::event::WindowEvent::RedrawRequested {
|
||||||
// We need to redraw the window for some reason.
|
// We need to redraw the window for some reason.
|
||||||
trace!("RedrawRequested");
|
trace!("RedrawRequested");
|
||||||
|
@ -468,16 +406,11 @@ impl ApplicationHandler<WakerEvent> for App {
|
||||||
// WARNING: do not defer painting or presenting to some later tick of the event
|
// WARNING: do not defer painting or presenting to some later tick of the event
|
||||||
// loop or servoshell may become unresponsive! (servo#30312)
|
// loop or servoshell may become unresponsive! (servo#30312)
|
||||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
if let Some(ref mut minibrowser) = self.minibrowser {
|
||||||
minibrowser.update(
|
minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
|
||||||
window.winit_window().unwrap(),
|
|
||||||
&mut self.webviews,
|
|
||||||
Some(servo),
|
|
||||||
"RedrawRequested",
|
|
||||||
);
|
|
||||||
minibrowser.paint(window.winit_window().unwrap());
|
minibrowser.paint(window.winit_window().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
servo.present();
|
state.servo().present();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the event
|
// Handle the event
|
||||||
|
@ -512,8 +445,7 @@ impl ApplicationHandler<WakerEvent> for App {
|
||||||
if let WindowEvent::Resized(_) = event {
|
if let WindowEvent::Resized(_) = event {
|
||||||
minibrowser.update(
|
minibrowser.update(
|
||||||
window.winit_window().unwrap(),
|
window.winit_window().unwrap(),
|
||||||
&mut self.webviews,
|
state,
|
||||||
Some(servo),
|
|
||||||
"Sync WebView size with Window Resize event",
|
"Sync WebView size with Window Resize event",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -530,7 +462,7 @@ impl ApplicationHandler<WakerEvent> for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !consumed {
|
if !consumed {
|
||||||
window.handle_winit_event(servo, &mut self.clipboard, &mut self.webviews, event);
|
window.handle_winit_event(state.clone(), event);
|
||||||
}
|
}
|
||||||
|
|
||||||
let animating = self.is_animating();
|
let animating = self.is_animating();
|
||||||
|
@ -558,12 +490,10 @@ impl ApplicationHandler<WakerEvent> for App {
|
||||||
now - self.t
|
now - self.t
|
||||||
);
|
);
|
||||||
self.t = now;
|
self.t = now;
|
||||||
// If self.servo is None here, it means that we're in the process of shutting down,
|
|
||||||
// let's ignore events.
|
|
||||||
if self.servo.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if !matches!(self.state, AppState::Running(_)) {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let Some(window) = self.windows.values().next() else {
|
let Some(window) = self.windows.values().next() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
686
ports/servoshell/desktop/app_state.rs
Normal file
686
ports/servoshell/desktop/app_state.rs
Normal file
|
@ -0,0 +1,686 @@
|
||||||
|
/* 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::{Ref, RefCell, RefMut};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
use arboard::Clipboard;
|
||||||
|
use euclid::Vector2D;
|
||||||
|
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use servo::base::id::WebViewId;
|
||||||
|
use servo::config::pref;
|
||||||
|
use servo::ipc_channel::ipc::IpcSender;
|
||||||
|
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
|
||||||
|
use servo::webrender_api::ScrollLocation;
|
||||||
|
use servo::{
|
||||||
|
AllowOrDenyRequest, CompositorEventVariant, FilterPattern, GamepadHapticEffectType, LoadStatus,
|
||||||
|
PermissionPrompt, PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin,
|
||||||
|
PromptResult, Servo, ServoDelegate, ServoError, TouchEventType, WebView, WebViewDelegate,
|
||||||
|
};
|
||||||
|
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::app::{Present, PumpResult};
|
||||||
|
use super::dialog::Dialog;
|
||||||
|
use super::gamepad::GamepadSupport;
|
||||||
|
use super::keyutils::CMD_OR_CONTROL;
|
||||||
|
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||||
|
|
||||||
|
pub(crate) enum AppState {
|
||||||
|
Initializing,
|
||||||
|
Running(Rc<RunningAppState>),
|
||||||
|
ShuttingDown,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct RunningAppState {
|
||||||
|
/// A handle to the Servo instance of the [`RunningAppState`]. This is not stored inside
|
||||||
|
/// `inner` so that we can keep a reference to Servo in order to spin the event loop,
|
||||||
|
/// which will in turn call delegates doing a mutable borrow on `inner`.
|
||||||
|
servo: Servo,
|
||||||
|
inner: RefCell<RunningAppStateInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RunningAppStateInner {
|
||||||
|
/// Whether or not this is a headless servoshell window.
|
||||||
|
headless: bool,
|
||||||
|
|
||||||
|
/// The clipboard to use for this collection of [`WebView`]s.
|
||||||
|
pub(crate) clipboard: Option<Clipboard>,
|
||||||
|
|
||||||
|
/// 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>,
|
||||||
|
|
||||||
|
/// The current set of open dialogs.
|
||||||
|
dialogs: HashMap<WebViewId, Vec<Dialog>>,
|
||||||
|
|
||||||
|
/// A handle to the Window that Servo is rendering in -- either headed or headless.
|
||||||
|
window: Rc<dyn WindowPortsMethods>,
|
||||||
|
|
||||||
|
/// Gamepad support, which may be `None` if it failed to initialize.
|
||||||
|
gamepad_support: Option<GamepadSupport>,
|
||||||
|
|
||||||
|
/// Whether or not the application interface needs to be updated.
|
||||||
|
need_update: bool,
|
||||||
|
|
||||||
|
/// Whether or not the application needs to be redrawn.
|
||||||
|
need_present: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for RunningAppState {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.servo.deinit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunningAppState {
|
||||||
|
pub fn new(
|
||||||
|
servo: Servo,
|
||||||
|
window: Rc<dyn WindowPortsMethods>,
|
||||||
|
headless: bool,
|
||||||
|
) -> RunningAppState {
|
||||||
|
RunningAppState {
|
||||||
|
servo,
|
||||||
|
inner: RefCell::new(RunningAppStateInner {
|
||||||
|
headless,
|
||||||
|
clipboard: Clipboard::new().ok(),
|
||||||
|
webviews: HashMap::default(),
|
||||||
|
creation_order: Default::default(),
|
||||||
|
focused_webview_id: None,
|
||||||
|
dialogs: Default::default(),
|
||||||
|
window,
|
||||||
|
gamepad_support: GamepadSupport::maybe_new(),
|
||||||
|
need_update: false,
|
||||||
|
need_present: false,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_toplevel_webview(self: &Rc<Self>, url: Url) {
|
||||||
|
let webview = self.servo().new_webview(url);
|
||||||
|
webview.set_delegate(self.clone());
|
||||||
|
self.add(webview);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inner(&self) -> Ref<RunningAppStateInner> {
|
||||||
|
self.inner.borrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inner_mut(&self) -> RefMut<RunningAppStateInner> {
|
||||||
|
self.inner.borrow_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn servo(&self) -> &Servo {
|
||||||
|
&self.servo
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spins the internal application event loop.
|
||||||
|
///
|
||||||
|
/// - Notifies Servo about incoming gamepad events
|
||||||
|
/// - Spin the Servo event loop, which will run the compositor and trigger delegate methods.
|
||||||
|
pub(crate) fn pump_event_loop(&self) -> PumpResult {
|
||||||
|
if pref!(dom_gamepad_enabled) {
|
||||||
|
self.handle_gamepad_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_continue = self.servo().spin_event_loop();
|
||||||
|
|
||||||
|
// Delegate handlers may have asked us to present or update compositor contents.
|
||||||
|
let need_present = std::mem::replace(&mut self.inner_mut().need_present, false);
|
||||||
|
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
|
||||||
|
|
||||||
|
if !should_continue {
|
||||||
|
return PumpResult::Shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Currently, egui-file-dialog dialogs need to be constantly presented or animations aren't fluid.
|
||||||
|
let need_present = need_present || self.has_active_file_dialog();
|
||||||
|
|
||||||
|
let present = if need_present {
|
||||||
|
Present::Deferred
|
||||||
|
} else {
|
||||||
|
Present::None
|
||||||
|
};
|
||||||
|
|
||||||
|
PumpResult::Continue {
|
||||||
|
update: need_update,
|
||||||
|
present,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add(&self, webview: WebView) {
|
||||||
|
self.inner_mut().creation_order.push(webview.id());
|
||||||
|
self.inner_mut().webviews.insert(webview.id(), webview);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn shutdown(&self) {
|
||||||
|
self.inner_mut().webviews.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn for_each_active_dialog(&self, callback: impl Fn(&mut Dialog) -> bool) {
|
||||||
|
let Some(webview_id) = self.focused_webview().as_ref().map(WebView::id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) {
|
||||||
|
dialogs.retain_mut(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_webview(&self, webview_id: WebViewId) {
|
||||||
|
// This can happen because we can trigger a close with a UI action and then get the
|
||||||
|
// close event from Servo later.
|
||||||
|
let mut inner = self.inner_mut();
|
||||||
|
if !inner.webviews.contains_key(&webview_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.webviews.retain(|&id, _| id != webview_id);
|
||||||
|
inner.creation_order.retain(|&id| id != webview_id);
|
||||||
|
inner.focused_webview_id = None;
|
||||||
|
inner.dialogs.remove(&webview_id);
|
||||||
|
|
||||||
|
let last_created = inner
|
||||||
|
.creation_order
|
||||||
|
.last()
|
||||||
|
.and_then(|id| inner.webviews.get(id));
|
||||||
|
|
||||||
|
match last_created {
|
||||||
|
Some(last_created_webview) => last_created_webview.focus(),
|
||||||
|
None => self.servo.start_shutting_down(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused_webview(&self) -> Option<WebView> {
|
||||||
|
self.inner()
|
||||||
|
.focused_webview_id
|
||||||
|
.and_then(|id| self.inner().webviews.get(&id).cloned())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the webviews in the creation order.
|
||||||
|
pub fn webviews(&self) -> Vec<(WebViewId, WebView)> {
|
||||||
|
let inner = self.inner();
|
||||||
|
inner
|
||||||
|
.creation_order
|
||||||
|
.iter()
|
||||||
|
.map(|id| (*id, inner.webviews.get(id).unwrap().clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_gamepad_events(&self) {
|
||||||
|
let Some(active_webview) = self.focused_webview() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(gamepad_support) = self.inner_mut().gamepad_support.as_mut() {
|
||||||
|
gamepad_support.handle_gamepad_events(active_webview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focus_webview_by_index(&self, index: usize) {
|
||||||
|
if let Some((_, webview)) = self.webviews().get(index) {
|
||||||
|
webview.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_active_file_dialog(&self) -> bool {
|
||||||
|
let Some(webview) = self.focused_webview() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let inner = self.inner();
|
||||||
|
let Some(dialogs) = inner.dialogs.get(&webview.id()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
dialogs.iter().any(Dialog::is_file_dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
|
||||||
|
let focused_id = self.inner().focused_webview_id?;
|
||||||
|
self.webviews()
|
||||||
|
.iter()
|
||||||
|
.position(|webview| webview.0 == focused_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle servoshell key bindings that may have been prevented by the page in the focused webview.
|
||||||
|
fn handle_overridable_key_bindings(&self, webview: ::servo::WebView, event: KeyboardEvent) {
|
||||||
|
let origin = webview.rect().min.ceil().to_i32();
|
||||||
|
ShortcutMatcher::from_event(event)
|
||||||
|
.shortcut(CMD_OR_CONTROL, '=', || {
|
||||||
|
webview.set_zoom(1.1);
|
||||||
|
})
|
||||||
|
.shortcut(CMD_OR_CONTROL, '+', || {
|
||||||
|
webview.set_zoom(1.1);
|
||||||
|
})
|
||||||
|
.shortcut(CMD_OR_CONTROL, '-', || {
|
||||||
|
webview.set_zoom(1.0 / 1.1);
|
||||||
|
})
|
||||||
|
.shortcut(CMD_OR_CONTROL, '0', || {
|
||||||
|
webview.reset_zoom();
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::PageDown, || {
|
||||||
|
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||||
|
0.0,
|
||||||
|
-self.inner().window.page_height() + 2.0 * LINE_HEIGHT,
|
||||||
|
));
|
||||||
|
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::PageUp, || {
|
||||||
|
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
||||||
|
0.0,
|
||||||
|
self.inner().window.page_height() - 2.0 * LINE_HEIGHT,
|
||||||
|
));
|
||||||
|
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::Home, || {
|
||||||
|
webview.notify_scroll_event(ScrollLocation::Start, origin, TouchEventType::Move);
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::End, || {
|
||||||
|
webview.notify_scroll_event(ScrollLocation::End, origin, TouchEventType::Move);
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
|
||||||
|
let location = ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT));
|
||||||
|
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
|
||||||
|
let location = ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT));
|
||||||
|
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
|
||||||
|
let location = ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0));
|
||||||
|
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||||
|
})
|
||||||
|
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
|
||||||
|
let location = ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0));
|
||||||
|
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServoDelegate for RunningAppState {
|
||||||
|
fn notify_devtools_server_started(&self, _servo: &Servo, port: u16, _token: String) {
|
||||||
|
info!("Devtools Server running on port {port}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_devtools_connection(&self, _servo: &Servo, request: AllowOrDenyRequest) {
|
||||||
|
request.allow();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_error(&self, _servo: &Servo, error: ServoError) {
|
||||||
|
error!("Saw Servo error: {error:?}!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebViewDelegate for RunningAppState {
|
||||||
|
fn notify_status_text_changed(&self, _webview: servo::WebView, _status: Option<String>) {
|
||||||
|
self.inner_mut().need_update = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_page_title_changed(&self, webview: servo::WebView, title: Option<String>) {
|
||||||
|
if webview.focused() {
|
||||||
|
let window_title = format!("{} - Servo", title.clone().unwrap_or_default());
|
||||||
|
self.inner().window.set_title(&window_title);
|
||||||
|
self.inner_mut().need_update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_move_to(&self, _: servo::WebView, new_position: DeviceIntPoint) {
|
||||||
|
self.inner().window.set_position(new_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_resize_to(&self, webview: servo::WebView, new_size: DeviceIntSize) {
|
||||||
|
let mut rect = webview.rect();
|
||||||
|
rect.set_size(new_size.to_f32());
|
||||||
|
webview.move_resize(rect);
|
||||||
|
self.inner().window.request_resize(&webview, new_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_prompt(
|
||||||
|
&self,
|
||||||
|
webview: servo::WebView,
|
||||||
|
definition: PromptDefinition,
|
||||||
|
origin: PromptOrigin,
|
||||||
|
) {
|
||||||
|
let res = if self.inner().headless {
|
||||||
|
match definition {
|
||||||
|
PromptDefinition::Alert(_message, sender) => sender.send(()),
|
||||||
|
PromptDefinition::OkCancel(_message, sender) => sender.send(PromptResult::Primary),
|
||||||
|
PromptDefinition::Input(_message, default, sender) => {
|
||||||
|
sender.send(Some(default.to_owned()))
|
||||||
|
},
|
||||||
|
PromptDefinition::Credentials(sender) => sender.send(PromptCredentialsInput {
|
||||||
|
username: None,
|
||||||
|
password: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
} 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::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)
|
||||||
|
},
|
||||||
|
PromptDefinition::Credentials(sender) => {
|
||||||
|
// TODO: figure out how to make the message a localized string
|
||||||
|
let username = tinyfiledialogs::input_box("", "username", "");
|
||||||
|
let password = tinyfiledialogs::input_box("", "password", "");
|
||||||
|
sender.send(PromptCredentialsInput { username, password })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.join()
|
||||||
|
.expect("Thread spawning failed")
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
webview.send_error(format!("Failed to send Prompt response: {e}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_open_auxiliary_webview(
|
||||||
|
&self,
|
||||||
|
parent_webview: servo::WebView,
|
||||||
|
) -> Option<servo::WebView> {
|
||||||
|
let webview = self.servo.new_auxiliary_webview();
|
||||||
|
webview.set_delegate(parent_webview.delegate());
|
||||||
|
self.add(webview.clone());
|
||||||
|
Some(webview)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_ready_to_show(&self, webview: servo::WebView) {
|
||||||
|
let scale = self.inner().window.hidpi_factor().get();
|
||||||
|
let toolbar = self.inner().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
|
||||||
|
.inner()
|
||||||
|
.window
|
||||||
|
.get_coordinates()
|
||||||
|
.get_viewport()
|
||||||
|
.to_f32();
|
||||||
|
rect.min.y += toolbar * scale;
|
||||||
|
|
||||||
|
webview.focus();
|
||||||
|
webview.move_resize(rect);
|
||||||
|
webview.raise_to_top(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_closed(&self, webview: servo::WebView) {
|
||||||
|
self.close_webview(webview.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_focus_changed(&self, webview: servo::WebView, focused: bool) {
|
||||||
|
let mut inner_mut = self.inner_mut();
|
||||||
|
if focused {
|
||||||
|
webview.show(true);
|
||||||
|
inner_mut.need_update = true;
|
||||||
|
inner_mut.focused_webview_id = Some(webview.id());
|
||||||
|
} else if inner_mut.focused_webview_id == Some(webview.id()) {
|
||||||
|
inner_mut.focused_webview_id = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_keyboard_event(&self, webview: servo::WebView, keyboard_event: KeyboardEvent) {
|
||||||
|
self.handle_overridable_key_bindings(webview, keyboard_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_clipboard_contents(&self, _webview: servo::WebView) {
|
||||||
|
self.inner_mut()
|
||||||
|
.clipboard
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|clipboard| clipboard.clear().ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_clipboard_contents(&self, _webview: servo::WebView, result_sender: IpcSender<String>) {
|
||||||
|
let contents = self
|
||||||
|
.inner_mut()
|
||||||
|
.clipboard
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|clipboard| clipboard.get_text().ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if let Err(e) = result_sender.send(contents) {
|
||||||
|
warn!("Failed to send clipboard ({})", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_clipboard_contents(&self, _webview: servo::WebView, text: String) {
|
||||||
|
if let Some(clipboard) = self.inner_mut().clipboard.as_mut() {
|
||||||
|
if let Err(e) = clipboard.set_text(text) {
|
||||||
|
warn!("Error setting clipboard contents ({})", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_cursor_changed(&self, _webview: servo::WebView, cursor: servo::Cursor) {
|
||||||
|
self.inner().window.set_cursor(cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_load_status_changed(&self, _webview: servo::WebView, _status: LoadStatus) {
|
||||||
|
self.inner_mut().need_update = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_fullscreen_state_change(&self, _webview: servo::WebView, fullscreen_state: bool) {
|
||||||
|
self.inner().window.set_fullscreen(fullscreen_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_bluetooth_device_dialog(
|
||||||
|
&self,
|
||||||
|
webview: servo::WebView,
|
||||||
|
devices: Vec<String>,
|
||||||
|
response_sender: IpcSender<Option<String>>,
|
||||||
|
) {
|
||||||
|
let selected = platform_get_selected_devices(devices);
|
||||||
|
if let Err(e) = response_sender.send(selected) {
|
||||||
|
webview.send_error(format!(
|
||||||
|
"Failed to send GetSelectedBluetoothDevice response: {e}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_file_selection_dialog(
|
||||||
|
&self,
|
||||||
|
webview: servo::WebView,
|
||||||
|
filter_pattern: Vec<FilterPattern>,
|
||||||
|
allow_select_mutiple: bool,
|
||||||
|
response_sender: IpcSender<Option<Vec<PathBuf>>>,
|
||||||
|
) {
|
||||||
|
let mut inner_mut = self.inner_mut();
|
||||||
|
inner_mut
|
||||||
|
.dialogs
|
||||||
|
.entry(webview.id())
|
||||||
|
.or_default()
|
||||||
|
.push(Dialog::new_file_dialog(
|
||||||
|
allow_select_mutiple,
|
||||||
|
response_sender,
|
||||||
|
filter_pattern,
|
||||||
|
));
|
||||||
|
inner_mut.need_update = true;
|
||||||
|
inner_mut.need_present = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_permission(
|
||||||
|
&self,
|
||||||
|
_webview: servo::WebView,
|
||||||
|
prompt: PermissionPrompt,
|
||||||
|
result_sender: IpcSender<PermissionRequest>,
|
||||||
|
) {
|
||||||
|
let _ = result_sender.send(match self.inner().headless {
|
||||||
|
true => PermissionRequest::Denied,
|
||||||
|
false => prompt_user(prompt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_new_frame_ready(&self, _webview: servo::WebView) {
|
||||||
|
self.inner_mut().need_present = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_event_delivered(&self, webview: servo::WebView, event: CompositorEventVariant) {
|
||||||
|
if let CompositorEventVariant::MouseButtonEvent = event {
|
||||||
|
webview.raise_to_top(true);
|
||||||
|
webview.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_gamepad_haptic_effect(
|
||||||
|
&self,
|
||||||
|
_webview: servo::WebView,
|
||||||
|
index: usize,
|
||||||
|
effect_type: GamepadHapticEffectType,
|
||||||
|
effect_complete_sender: IpcSender<bool>,
|
||||||
|
) {
|
||||||
|
match self.inner_mut().gamepad_support.as_mut() {
|
||||||
|
Some(gamepad_support) => {
|
||||||
|
gamepad_support.play_haptic_effect(index, effect_type, effect_complete_sender);
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let _ = effect_complete_sender.send(false);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_gamepad_haptic_effect(
|
||||||
|
&self,
|
||||||
|
_webview: servo::WebView,
|
||||||
|
index: usize,
|
||||||
|
haptic_stop_sender: IpcSender<bool>,
|
||||||
|
) {
|
||||||
|
let stopped = match self.inner_mut().gamepad_support.as_mut() {
|
||||||
|
Some(gamepad_support) => gamepad_support.stop_haptic_effect(index),
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
let _ = haptic_stop_sender.send(stopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn prompt_user(prompt: PermissionPrompt) -> PermissionRequest {
|
||||||
|
use tinyfiledialogs::YesNo;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
|
@ -31,9 +31,10 @@ pub fn main() {
|
||||||
let event_loop = EventsLoop::new(opts.headless, opts.output_file.is_some())
|
let event_loop = EventsLoop::new(opts.headless, opts.output_file.is_some())
|
||||||
.expect("Failed to create events loop");
|
.expect("Failed to create events loop");
|
||||||
|
|
||||||
let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop);
|
{
|
||||||
|
let mut app = App::new(opts, preferences, servoshell_preferences, &event_loop);
|
||||||
event_loop.run_app(&mut app);
|
event_loop.run_app(&mut app);
|
||||||
|
}
|
||||||
|
|
||||||
crate::platform::deinit(clean_shutdown)
|
crate::platform::deinit(clean_shutdown)
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,4 +92,8 @@ impl Dialog {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_file_dialog(&self) -> bool {
|
||||||
|
matches!(self, Dialog::File(..))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,7 @@ impl EventsLoop {
|
||||||
app.init(None);
|
app.init(None);
|
||||||
loop {
|
loop {
|
||||||
self.sleep(flag, condvar);
|
self.sleep(flag, condvar);
|
||||||
if app.handle_events_with_headless() {
|
if !app.handle_events_with_headless() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if !app.is_animating() {
|
if !app.is_animating() {
|
||||||
|
|
230
ports/servoshell/desktop/gamepad.rs
Normal file
230
ports/servoshell/desktop/gamepad.rs
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
/* 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 gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
|
||||||
|
use gilrs::{EventType, Gilrs};
|
||||||
|
use log::{debug, warn};
|
||||||
|
use servo::ipc_channel::ipc::IpcSender;
|
||||||
|
use servo::{
|
||||||
|
GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds,
|
||||||
|
GamepadSupportedHapticEffects, GamepadUpdateType, WebView,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct HapticEffect {
|
||||||
|
pub effect: Effect,
|
||||||
|
pub sender: IpcSender<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct GamepadSupport {
|
||||||
|
handle: Gilrs,
|
||||||
|
haptic_effects: HashMap<usize, HapticEffect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GamepadSupport {
|
||||||
|
pub(crate) fn maybe_new() -> Option<Self> {
|
||||||
|
let handle = match Gilrs::new() {
|
||||||
|
Ok(handle) => handle,
|
||||||
|
Err(error) => {
|
||||||
|
warn!("Error creating gamepad input connection ({error})");
|
||||||
|
return None;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Some(Self {
|
||||||
|
handle,
|
||||||
|
haptic_effects: Default::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle updates to connected gamepads from GilRs
|
||||||
|
pub(crate) fn handle_gamepad_events(&mut self, active_webview: WebView) {
|
||||||
|
while let Some(event) = self.handle.next_event() {
|
||||||
|
let gamepad = self.handle.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),
|
||||||
|
};
|
||||||
|
// GilRs does not yet support trigger rumble
|
||||||
|
let supported_haptic_effects = GamepadSupportedHapticEffects {
|
||||||
|
supports_dual_rumble: true,
|
||||||
|
supports_trigger_rumble: false,
|
||||||
|
};
|
||||||
|
gamepad_event = Some(GamepadEvent::Connected(
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
bounds,
|
||||||
|
supported_haptic_effects,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
EventType::Disconnected => {
|
||||||
|
gamepad_event = Some(GamepadEvent::Disconnected(index));
|
||||||
|
},
|
||||||
|
EventType::ForceFeedbackEffectCompleted => {
|
||||||
|
let Some(effect) = self.haptic_effects.get(&event.id.into()) else {
|
||||||
|
warn!("Failed to find haptic effect for id {}", event.id);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
effect
|
||||||
|
.sender
|
||||||
|
.send(true)
|
||||||
|
.expect("Failed to send haptic effect completion.");
|
||||||
|
self.haptic_effects.remove(&event.id.into());
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(event) = gamepad_event {
|
||||||
|
active_webview.notify_gamepad_event(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(crate) fn play_haptic_effect(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
effect_type: GamepadHapticEffectType,
|
||||||
|
effect_complete_sender: IpcSender<bool>,
|
||||||
|
) {
|
||||||
|
let GamepadHapticEffectType::DualRumble(params) = effect_type;
|
||||||
|
if let Some(connected_gamepad) = self
|
||||||
|
.handle
|
||||||
|
.gamepads()
|
||||||
|
.find(|gamepad| usize::from(gamepad.0) == index)
|
||||||
|
{
|
||||||
|
let start_delay = Ticks::from_ms(params.start_delay as u32);
|
||||||
|
let duration = Ticks::from_ms(params.duration as u32);
|
||||||
|
let strong_magnitude = (params.strong_magnitude * u16::MAX as f64).round() as u16;
|
||||||
|
let weak_magnitude = (params.weak_magnitude * u16::MAX as f64).round() as u16;
|
||||||
|
|
||||||
|
let scheduling = Replay {
|
||||||
|
after: start_delay,
|
||||||
|
play_for: duration,
|
||||||
|
with_delay: Ticks::from_ms(0),
|
||||||
|
};
|
||||||
|
let effect = EffectBuilder::new()
|
||||||
|
.add_effect(BaseEffect {
|
||||||
|
kind: BaseEffectType::Strong { magnitude: strong_magnitude },
|
||||||
|
scheduling,
|
||||||
|
envelope: Default::default(),
|
||||||
|
})
|
||||||
|
.add_effect(BaseEffect {
|
||||||
|
kind: BaseEffectType::Weak { magnitude: weak_magnitude },
|
||||||
|
scheduling,
|
||||||
|
envelope: Default::default(),
|
||||||
|
})
|
||||||
|
.repeat(Repeat::For(start_delay + duration))
|
||||||
|
.add_gamepad(&connected_gamepad.1)
|
||||||
|
.finish(&mut self.handle)
|
||||||
|
.expect("Failed to create haptic effect, ensure connected gamepad supports force feedback.");
|
||||||
|
self.haptic_effects.insert(
|
||||||
|
index,
|
||||||
|
HapticEffect {
|
||||||
|
effect,
|
||||||
|
sender: effect_complete_sender,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.haptic_effects[&index]
|
||||||
|
.effect
|
||||||
|
.play()
|
||||||
|
.expect("Failed to play haptic effect.");
|
||||||
|
} else {
|
||||||
|
debug!("Couldn't find connected gamepad to play haptic effect on");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop_haptic_effect(&mut self, index: usize) -> bool {
|
||||||
|
let Some(haptic_effect) = self.haptic_effects.get(&index) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let stopped_successfully = match haptic_effect.effect.stop() {
|
||||||
|
Ok(()) => true,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Failed to stop haptic effect: {:?}", e);
|
||||||
|
false
|
||||||
|
},
|
||||||
|
};
|
||||||
|
self.haptic_effects.remove(&index);
|
||||||
|
|
||||||
|
stopped_successfully
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ use std::env;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use arboard::Clipboard;
|
|
||||||
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
|
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
|
||||||
use keyboard_types::{Modifiers, ShortcutMatcher};
|
use keyboard_types::{Modifiers, ShortcutMatcher};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
|
@ -26,7 +25,7 @@ use servo::webrender_api::ScrollLocation;
|
||||||
use servo::webrender_traits::SurfmanRenderingContext;
|
use servo::webrender_traits::SurfmanRenderingContext;
|
||||||
use servo::{
|
use servo::{
|
||||||
ClipboardEventType, Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
|
ClipboardEventType, Cursor, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
|
||||||
Servo, Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode,
|
Theme, TouchEventType, TouchId, WebView, WheelDelta, WheelMode,
|
||||||
};
|
};
|
||||||
use surfman::{Context, Device, SurfaceType};
|
use surfman::{Context, Device, SurfaceType};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -37,9 +36,9 @@ use winit::keyboard::{Key as LogicalKey, ModifiersState, NamedKey};
|
||||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||||
use winit::window::Icon;
|
use winit::window::Icon;
|
||||||
|
|
||||||
|
use super::app_state::RunningAppState;
|
||||||
use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
|
use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
|
||||||
use super::keyutils::{keyboard_event_from_winit, CMD_OR_ALT};
|
use super::keyutils::{keyboard_event_from_winit, CMD_OR_ALT};
|
||||||
use super::webview::{WebView as ServoShellWebView, WebViewManager};
|
|
||||||
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||||
use crate::desktop::keyutils::CMD_OR_CONTROL;
|
use crate::desktop::keyutils::CMD_OR_CONTROL;
|
||||||
|
|
||||||
|
@ -195,25 +194,16 @@ impl Window {
|
||||||
webview.notify_keyboard_event(event);
|
webview.notify_keyboard_event(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_keyboard_input(
|
fn handle_keyboard_input(&self, state: Rc<RunningAppState>, winit_event: KeyEvent) {
|
||||||
&self,
|
|
||||||
servo: &Servo,
|
|
||||||
clipboard: &mut Option<Clipboard>,
|
|
||||||
webviews: &mut WebViewManager,
|
|
||||||
winit_event: KeyEvent,
|
|
||||||
) {
|
|
||||||
// First, handle servoshell key bindings that are not overridable by, or visible to, the page.
|
// First, handle servoshell key bindings that are not overridable by, or visible to, the page.
|
||||||
let mut keyboard_event =
|
let mut keyboard_event =
|
||||||
keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
|
keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
|
||||||
if self.handle_intercepted_key_bindings(servo, clipboard, webviews, &keyboard_event) {
|
if self.handle_intercepted_key_bindings(state.clone(), &keyboard_event) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then we deliver character and keyboard events to the page in the focused webview.
|
// Then we deliver character and keyboard events to the page in the focused webview.
|
||||||
let Some(webview) = webviews
|
let Some(webview) = state.focused_webview() else {
|
||||||
.focused_webview()
|
|
||||||
.map(|webview| webview.servo_webview.clone())
|
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -296,15 +286,10 @@ impl Window {
|
||||||
/// Handle key events before sending them to Servo.
|
/// Handle key events before sending them to Servo.
|
||||||
fn handle_intercepted_key_bindings(
|
fn handle_intercepted_key_bindings(
|
||||||
&self,
|
&self,
|
||||||
servo: &Servo,
|
state: Rc<RunningAppState>,
|
||||||
clipboard: &mut Option<Clipboard>,
|
|
||||||
webviews: &mut WebViewManager,
|
|
||||||
key_event: &KeyboardEvent,
|
key_event: &KeyboardEvent,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let Some(focused_webview) = webviews
|
let Some(focused_webview) = state.focused_webview() else {
|
||||||
.focused_webview()
|
|
||||||
.map(|webview| webview.servo_webview.clone())
|
|
||||||
else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -312,7 +297,7 @@ impl Window {
|
||||||
ShortcutMatcher::from_event(key_event.clone())
|
ShortcutMatcher::from_event(key_event.clone())
|
||||||
.shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload())
|
.shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload())
|
||||||
.shortcut(CMD_OR_CONTROL, 'W', || {
|
.shortcut(CMD_OR_CONTROL, 'W', || {
|
||||||
webviews.close_webview(servo, focused_webview.id());
|
state.close_webview(focused_webview.id());
|
||||||
})
|
})
|
||||||
.shortcut(CMD_OR_CONTROL, 'P', || {
|
.shortcut(CMD_OR_CONTROL, 'P', || {
|
||||||
let rate = env::var("SAMPLING_RATE")
|
let rate = env::var("SAMPLING_RATE")
|
||||||
|
@ -335,7 +320,9 @@ impl Window {
|
||||||
focused_webview.notify_clipboard_event(ClipboardEventType::Copy);
|
focused_webview.notify_clipboard_event(ClipboardEventType::Copy);
|
||||||
})
|
})
|
||||||
.shortcut(CMD_OR_CONTROL, 'V', || {
|
.shortcut(CMD_OR_CONTROL, 'V', || {
|
||||||
let text = clipboard
|
let text = state
|
||||||
|
.inner_mut()
|
||||||
|
.clipboard
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.and_then(|clipboard| clipboard.get_text().ok())
|
.and_then(|clipboard| clipboard.get_text().ok())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
@ -382,40 +369,40 @@ impl Window {
|
||||||
|| focused_webview.exit_fullscreen(),
|
|| focused_webview.exit_fullscreen(),
|
||||||
)
|
)
|
||||||
// Select the first 8 tabs via shortcuts
|
// Select the first 8 tabs via shortcuts
|
||||||
.shortcut(CMD_OR_CONTROL, '1', || webviews.focus_webview_by_index(0))
|
.shortcut(CMD_OR_CONTROL, '1', || state.focus_webview_by_index(0))
|
||||||
.shortcut(CMD_OR_CONTROL, '2', || webviews.focus_webview_by_index(1))
|
.shortcut(CMD_OR_CONTROL, '2', || state.focus_webview_by_index(1))
|
||||||
.shortcut(CMD_OR_CONTROL, '3', || webviews.focus_webview_by_index(2))
|
.shortcut(CMD_OR_CONTROL, '3', || state.focus_webview_by_index(2))
|
||||||
.shortcut(CMD_OR_CONTROL, '4', || webviews.focus_webview_by_index(3))
|
.shortcut(CMD_OR_CONTROL, '4', || state.focus_webview_by_index(3))
|
||||||
.shortcut(CMD_OR_CONTROL, '5', || webviews.focus_webview_by_index(4))
|
.shortcut(CMD_OR_CONTROL, '5', || state.focus_webview_by_index(4))
|
||||||
.shortcut(CMD_OR_CONTROL, '6', || webviews.focus_webview_by_index(5))
|
.shortcut(CMD_OR_CONTROL, '6', || state.focus_webview_by_index(5))
|
||||||
.shortcut(CMD_OR_CONTROL, '7', || webviews.focus_webview_by_index(6))
|
.shortcut(CMD_OR_CONTROL, '7', || state.focus_webview_by_index(6))
|
||||||
.shortcut(CMD_OR_CONTROL, '8', || webviews.focus_webview_by_index(7))
|
.shortcut(CMD_OR_CONTROL, '8', || state.focus_webview_by_index(7))
|
||||||
// Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
|
// Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
|
||||||
.shortcut(CMD_OR_CONTROL, '9', || {
|
.shortcut(CMD_OR_CONTROL, '9', || {
|
||||||
let len = webviews.webviews().len();
|
let len = state.webviews().len();
|
||||||
if len > 0 {
|
if len > 0 {
|
||||||
webviews.focus_webview_by_index(len - 1)
|
state.focus_webview_by_index(len - 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.shortcut(Modifiers::CONTROL, Key::PageDown, || {
|
.shortcut(Modifiers::CONTROL, Key::PageDown, || {
|
||||||
if let Some(index) = webviews.get_focused_webview_index() {
|
if let Some(index) = state.get_focused_webview_index() {
|
||||||
webviews.focus_webview_by_index((index + 1) % webviews.webviews().len())
|
state.focus_webview_by_index((index + 1) % state.webviews().len())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.shortcut(Modifiers::CONTROL, Key::PageUp, || {
|
.shortcut(Modifiers::CONTROL, Key::PageUp, || {
|
||||||
if let Some(index) = webviews.get_focused_webview_index() {
|
if let Some(index) = state.get_focused_webview_index() {
|
||||||
let new_index = if index == 0 {
|
let new_index = if index == 0 {
|
||||||
webviews.webviews().len() - 1
|
state.webviews().len() - 1
|
||||||
} else {
|
} else {
|
||||||
index - 1
|
index - 1
|
||||||
};
|
};
|
||||||
webviews.focus_webview_by_index(new_index)
|
state.focus_webview_by_index(new_index)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.shortcut(CMD_OR_CONTROL, 'T', || {
|
.shortcut(CMD_OR_CONTROL, 'T', || {
|
||||||
webviews.add(servo.new_webview(Url::parse("servo:newtab").unwrap()));
|
state.new_toplevel_webview(Url::parse("servo:newtab").unwrap());
|
||||||
})
|
})
|
||||||
.shortcut(CMD_OR_CONTROL, 'Q', || servo.start_shutting_down())
|
.shortcut(CMD_OR_CONTROL, 'Q', || state.servo().start_shutting_down())
|
||||||
.otherwise(|| handled = false);
|
.otherwise(|| handled = false);
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
@ -442,7 +429,7 @@ impl WindowPortsMethods for Window {
|
||||||
self.winit_window.set_title(title);
|
self.winit_window.set_title(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_resize(&self, _: &ServoShellWebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
fn request_resize(&self, _: &WebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||||
let toolbar_height = self.toolbar_height() * self.hidpi_factor();
|
let toolbar_height = self.toolbar_height() * self.hidpi_factor();
|
||||||
let toolbar_height = toolbar_height.get().ceil() as i32;
|
let toolbar_height = toolbar_height.get().ceil() as i32;
|
||||||
let total_size = PhysicalSize::new(size.width, size.height + toolbar_height);
|
let total_size = PhysicalSize::new(size.width, size.height + toolbar_height);
|
||||||
|
@ -536,23 +523,14 @@ impl WindowPortsMethods for Window {
|
||||||
self.winit_window.id()
|
self.winit_window.id()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_winit_event(
|
fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent) {
|
||||||
&self,
|
let Some(webview) = state.focused_webview() else {
|
||||||
servo: &Servo,
|
|
||||||
clipboard: &mut Option<Clipboard>,
|
|
||||||
webviews: &mut WebViewManager,
|
|
||||||
event: winit::event::WindowEvent,
|
|
||||||
) {
|
|
||||||
let Some(webview) = webviews
|
|
||||||
.focused_webview()
|
|
||||||
.map(|webview| webview.servo_webview.clone())
|
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
winit::event::WindowEvent::KeyboardInput { event, .. } => {
|
winit::event::WindowEvent::KeyboardInput { event, .. } => {
|
||||||
self.handle_keyboard_input(servo, clipboard, webviews, event)
|
self.handle_keyboard_input(state, event)
|
||||||
},
|
},
|
||||||
winit::event::WindowEvent::ModifiersChanged(modifiers) => {
|
winit::event::WindowEvent::ModifiersChanged(modifiers) => {
|
||||||
self.modifiers_state.set(modifiers.state())
|
self.modifiers_state.set(modifiers.state())
|
||||||
|
@ -615,7 +593,7 @@ impl WindowPortsMethods for Window {
|
||||||
webview.set_pinch_zoom(delta as f32 + 1.0);
|
webview.set_pinch_zoom(delta as f32 + 1.0);
|
||||||
},
|
},
|
||||||
winit::event::WindowEvent::CloseRequested => {
|
winit::event::WindowEvent::CloseRequested => {
|
||||||
servo.start_shutting_down();
|
state.servo().start_shutting_down();
|
||||||
},
|
},
|
||||||
winit::event::WindowEvent::Resized(new_size) => {
|
winit::event::WindowEvent::Resized(new_size) => {
|
||||||
if self.inner_size.get() != new_size {
|
if self.inner_size.get() != new_size {
|
||||||
|
|
|
@ -7,15 +7,13 @@
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use arboard::Clipboard;
|
|
||||||
use euclid::num::Zero;
|
use euclid::num::Zero;
|
||||||
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
|
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
|
||||||
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
|
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
|
||||||
use servo::servo_geometry::DeviceIndependentPixel;
|
use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
|
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
|
||||||
use servo::Servo;
|
|
||||||
|
|
||||||
use super::webview::{WebView, WebViewManager};
|
use super::app_state::RunningAppState;
|
||||||
use crate::desktop::window_trait::WindowPortsMethods;
|
use crate::desktop::window_trait::WindowPortsMethods;
|
||||||
|
|
||||||
pub struct Window {
|
pub struct Window {
|
||||||
|
@ -58,10 +56,6 @@ impl Window {
|
||||||
|
|
||||||
Rc::new(window)
|
Rc::new(window)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_uninit() -> Rc<dyn WindowPortsMethods> {
|
|
||||||
Self::new(Default::default(), None, None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowPortsMethods for Window {
|
impl WindowPortsMethods for Window {
|
||||||
|
@ -69,7 +63,11 @@ impl WindowPortsMethods for Window {
|
||||||
winit::window::WindowId::dummy()
|
winit::window::WindowId::dummy()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn request_resize(&self, webview: &WebView, size: DeviceIntSize) -> Option<DeviceIntSize> {
|
fn request_resize(
|
||||||
|
&self,
|
||||||
|
webview: &::servo::WebView,
|
||||||
|
size: DeviceIntSize,
|
||||||
|
) -> Option<DeviceIntSize> {
|
||||||
// Surfman doesn't support zero-sized surfaces.
|
// Surfman doesn't support zero-sized surfaces.
|
||||||
let new_size = DeviceIntSize::new(size.width.max(1), size.height.max(1));
|
let new_size = DeviceIntSize::new(size.width.max(1), size.height.max(1));
|
||||||
if self.inner_size.get() == new_size {
|
if self.inner_size.get() == new_size {
|
||||||
|
@ -81,7 +79,7 @@ impl WindowPortsMethods for Window {
|
||||||
// Because we are managing the rendering surface ourselves, there will be no other
|
// Because we are managing the rendering surface ourselves, there will be no other
|
||||||
// notification (such as from the display manager) that it has changed size, so we
|
// notification (such as from the display manager) that it has changed size, so we
|
||||||
// must notify the compositor here.
|
// must notify the compositor here.
|
||||||
webview.servo_webview.notify_rendering_context_resized();
|
webview.notify_rendering_context_resized();
|
||||||
|
|
||||||
Some(new_size)
|
Some(new_size)
|
||||||
}
|
}
|
||||||
|
@ -114,13 +112,7 @@ impl WindowPortsMethods for Window {
|
||||||
self.animation_state.get() == AnimationState::Animating
|
self.animation_state.get() == AnimationState::Animating
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_winit_event(
|
fn handle_winit_event(&self, _: Rc<RunningAppState>, _: winit::event::WindowEvent) {
|
||||||
&self,
|
|
||||||
_: &Servo,
|
|
||||||
_: &mut Option<Clipboard>,
|
|
||||||
_: &mut WebViewManager,
|
|
||||||
_: winit::event::WindowEvent,
|
|
||||||
) {
|
|
||||||
// Not expecting any winit events.
|
// Not expecting any winit events.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,14 +24,14 @@ use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::servo_url::ServoUrl;
|
use servo::servo_url::ServoUrl;
|
||||||
use servo::webrender_api::units::DevicePixel;
|
use servo::webrender_api::units::DevicePixel;
|
||||||
use servo::webrender_traits::SurfmanRenderingContext;
|
use servo::webrender_traits::SurfmanRenderingContext;
|
||||||
use servo::{LoadStatus, Servo};
|
use servo::{LoadStatus, WebView};
|
||||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||||
use winit::event_loop::ActiveEventLoop;
|
use winit::event_loop::ActiveEventLoop;
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
|
use super::app_state::RunningAppState;
|
||||||
use super::egui_glue::EguiGlow;
|
use super::egui_glue::EguiGlow;
|
||||||
use super::geometry::winit_position_to_euclid_point;
|
use super::geometry::winit_position_to_euclid_point;
|
||||||
use super::webview::{WebView, WebViewManager};
|
|
||||||
|
|
||||||
pub struct Minibrowser {
|
pub struct Minibrowser {
|
||||||
pub context: EguiGlow,
|
pub context: EguiGlow,
|
||||||
|
@ -73,6 +73,12 @@ fn truncate_with_ellipsis(input: &str, max_length: usize) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for Minibrowser {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.context.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Minibrowser {
|
impl Minibrowser {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
rendering_context: &SurfmanRenderingContext,
|
rendering_context: &SurfmanRenderingContext,
|
||||||
|
@ -173,12 +179,13 @@ impl Minibrowser {
|
||||||
/// Draws a browser tab, checking for clicks and queues appropriate `MinibrowserEvent`s.
|
/// Draws a browser tab, checking for clicks and queues appropriate `MinibrowserEvent`s.
|
||||||
/// Using a custom widget here would've been nice, but it doesn't seem as though egui
|
/// Using a custom widget here would've been nice, but it doesn't seem as though egui
|
||||||
/// supports that, so we arrange multiple Widgets in a way that they look connected.
|
/// supports that, so we arrange multiple Widgets in a way that they look connected.
|
||||||
fn browser_tab(
|
fn browser_tab(ui: &mut egui::Ui, webview: WebView, event_queue: &mut Vec<MinibrowserEvent>) {
|
||||||
ui: &mut egui::Ui,
|
let label = match (webview.page_title(), webview.url()) {
|
||||||
label: &str,
|
(Some(title), _) if !title.is_empty() => title,
|
||||||
webview: &WebView,
|
(_, Some(url)) => url.to_string(),
|
||||||
event_queue: &mut Vec<MinibrowserEvent>,
|
_ => "New Tab".into(),
|
||||||
) {
|
};
|
||||||
|
|
||||||
let old_item_spacing = ui.spacing().item_spacing;
|
let old_item_spacing = ui.spacing().item_spacing;
|
||||||
let old_visuals = ui.visuals().clone();
|
let old_visuals = ui.visuals().clone();
|
||||||
let active_bg_color = old_visuals.widgets.active.weak_bg_fill;
|
let active_bg_color = old_visuals.widgets.active.weak_bg_fill;
|
||||||
|
@ -214,10 +221,10 @@ impl Minibrowser {
|
||||||
visuals.widgets.hovered.rounding = rounding;
|
visuals.widgets.hovered.rounding = rounding;
|
||||||
visuals.widgets.inactive.rounding = rounding;
|
visuals.widgets.inactive.rounding = rounding;
|
||||||
|
|
||||||
let selected = webview.focused;
|
let selected = webview.focused();
|
||||||
let tab = ui.add(SelectableLabel::new(
|
let tab = ui.add(SelectableLabel::new(
|
||||||
selected,
|
selected,
|
||||||
truncate_with_ellipsis(label, 20),
|
truncate_with_ellipsis(&label, 20),
|
||||||
));
|
));
|
||||||
let tab = tab.on_hover_ui(|ui| {
|
let tab = tab.on_hover_ui(|ui| {
|
||||||
ui.label(label);
|
ui.label(label);
|
||||||
|
@ -244,22 +251,16 @@ impl Minibrowser {
|
||||||
let close_button = ui.add(egui::Button::new("X").fill(fill_color));
|
let close_button = ui.add(egui::Button::new("X").fill(fill_color));
|
||||||
*ui.visuals_mut() = old_visuals;
|
*ui.visuals_mut() = old_visuals;
|
||||||
if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
|
if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
|
||||||
event_queue.push(MinibrowserEvent::CloseWebView(webview.servo_webview.id()))
|
event_queue.push(MinibrowserEvent::CloseWebView(webview.id()))
|
||||||
} else if !selected && tab.clicked() {
|
} else if !selected && tab.clicked() {
|
||||||
webview.servo_webview.focus();
|
webview.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the minibrowser, but don’t paint.
|
/// Update the minibrowser, but don’t paint.
|
||||||
/// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our
|
/// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our
|
||||||
/// CentralPanel when [`Minibrowser::paint`] is called.
|
/// CentralPanel when [`Minibrowser::paint`] is called.
|
||||||
pub fn update(
|
pub fn update(&mut self, window: &Window, state: &RunningAppState, reason: &'static str) {
|
||||||
&mut self,
|
|
||||||
window: &Window,
|
|
||||||
webviews: &mut WebViewManager,
|
|
||||||
servo: Option<&Servo>,
|
|
||||||
reason: &'static str,
|
|
||||||
) {
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
trace!(
|
trace!(
|
||||||
"{:?} since last update ({})",
|
"{:?} since last update ({})",
|
||||||
|
@ -277,9 +278,7 @@ impl Minibrowser {
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
let widget_fbo = *widget_surface_fbo;
|
let widget_fbo = *widget_surface_fbo;
|
||||||
let servo_framebuffer_id = servo
|
let servo_framebuffer_id = state.servo().offscreen_framebuffer_id();
|
||||||
.as_ref()
|
|
||||||
.and_then(|servo| servo.offscreen_framebuffer_id());
|
|
||||||
let _duration = context.run(window, |ctx| {
|
let _duration = context.run(window, |ctx| {
|
||||||
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
|
// 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
|
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
|
||||||
|
@ -362,13 +361,8 @@ impl Minibrowser {
|
||||||
ui.available_size(),
|
ui.available_size(),
|
||||||
egui::Layout::left_to_right(egui::Align::Center),
|
egui::Layout::left_to_right(egui::Align::Center),
|
||||||
|ui| {
|
|ui| {
|
||||||
for (_, webview) in webviews.webviews().into_iter() {
|
for (_, webview) in state.webviews().into_iter() {
|
||||||
let label = match (&webview.title, &webview.url) {
|
Self::browser_tab(ui, webview, &mut event_queue.borrow_mut());
|
||||||
(Some(title), _) if !title.is_empty() => title,
|
|
||||||
(_, Some(url)) => &url.to_string(),
|
|
||||||
_ => "New Tab",
|
|
||||||
};
|
|
||||||
Self::browser_tab(ui, label, webview, &mut event_queue.borrow_mut());
|
|
||||||
}
|
}
|
||||||
if ui.add(Minibrowser::toolbar_button("+")).clicked() {
|
if ui.add(Minibrowser::toolbar_button("+")).clicked() {
|
||||||
event_queue.borrow_mut().push(MinibrowserEvent::NewWebView);
|
event_queue.borrow_mut().push(MinibrowserEvent::NewWebView);
|
||||||
|
@ -384,17 +378,14 @@ impl Minibrowser {
|
||||||
|
|
||||||
let scale =
|
let scale =
|
||||||
Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
|
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
egui::CentralPanel::default().show(ctx, |_| {
|
egui::CentralPanel::default().show(ctx, |_| {
|
||||||
webview.update(ctx);
|
state.for_each_active_dialog(|dialog| dialog.update(ctx));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let Some(webview) = state.focused_webview() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
CentralPanel::default()
|
CentralPanel::default()
|
||||||
.frame(Frame::none())
|
.frame(Frame::none())
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
|
@ -407,9 +398,8 @@ impl Minibrowser {
|
||||||
Point2D::new(x, y),
|
Point2D::new(x, y),
|
||||||
Size2D::new(width, height),
|
Size2D::new(width, height),
|
||||||
) * scale;
|
) * scale;
|
||||||
if rect != webview.rect {
|
if rect != webview.rect() {
|
||||||
webview.rect = rect;
|
webview.move_resize(rect);
|
||||||
webview.servo_webview.move_resize(rect)
|
|
||||||
}
|
}
|
||||||
let min = ui.cursor().min;
|
let min = ui.cursor().min;
|
||||||
let size = ui.available_size();
|
let size = ui.available_size();
|
||||||
|
@ -489,13 +479,16 @@ impl Minibrowser {
|
||||||
|
|
||||||
/// Updates the location field from the given [WebViewManager], unless the user has started
|
/// 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).
|
/// 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) -> bool {
|
pub fn update_location_in_toolbar(&mut self, state: &RunningAppState) -> bool {
|
||||||
// User edited without clicking Go?
|
// User edited without clicking Go?
|
||||||
if self.location_dirty.get() {
|
if self.location_dirty.get() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
match browser.current_url_string() {
|
let current_url_string = state
|
||||||
|
.focused_webview()
|
||||||
|
.and_then(|webview| Some(webview.url()?.to_string()));
|
||||||
|
match current_url_string {
|
||||||
Some(location) if location != *self.location.get_mut() => {
|
Some(location) if location != *self.location.get_mut() => {
|
||||||
self.location = RefCell::new(location.to_owned());
|
self.location = RefCell::new(location.to_owned());
|
||||||
true
|
true
|
||||||
|
@ -508,29 +501,32 @@ impl Minibrowser {
|
||||||
self.location_dirty.set(dirty);
|
self.location_dirty.set(dirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the spinner from the given [WebViewManager], returning true iff it has changed
|
pub fn update_load_status(&mut self, state: &RunningAppState) -> bool {
|
||||||
/// (needing an egui update).
|
let state_status = state
|
||||||
pub fn update_spinner_in_toolbar(&mut self, browser: &mut WebViewManager) -> bool {
|
.focused_webview()
|
||||||
let need_update = browser.load_status() != self.load_status;
|
.map(|webview| webview.load_status())
|
||||||
self.load_status = browser.load_status();
|
.unwrap_or(LoadStatus::Complete);
|
||||||
need_update
|
let old_status = std::mem::replace(&mut self.load_status, state_status);
|
||||||
|
old_status != self.load_status
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_status_text(&mut self, browser: &mut WebViewManager) -> bool {
|
pub fn update_status_text(&mut self, state: &RunningAppState) -> bool {
|
||||||
let need_update = browser.status_text() != self.status_text;
|
let state_status = state
|
||||||
self.status_text = browser.status_text();
|
.focused_webview()
|
||||||
need_update
|
.and_then(|webview| webview.status_text());
|
||||||
|
let old_status = std::mem::replace(&mut self.status_text, state_status);
|
||||||
|
old_status != self.status_text
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates all fields taken from the given [WebViewManager], such as the location field.
|
/// Updates all fields taken from the given [WebViewManager], such as the location field.
|
||||||
/// Returns true iff the egui needs an update.
|
/// Returns true iff the egui needs an update.
|
||||||
pub fn update_webview_data(&mut self, browser: &mut WebViewManager) -> bool {
|
pub fn update_webview_data(&mut self, state: &RunningAppState) -> bool {
|
||||||
// Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
|
// 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.
|
// 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
|
// We want to ensure that all functions are called. The "bitwise OR" operator
|
||||||
// does not short-circuit.
|
// does not short-circuit.
|
||||||
self.update_location_in_toolbar(browser) |
|
self.update_location_in_toolbar(state) |
|
||||||
self.update_spinner_in_toolbar(browser) |
|
self.update_load_status(state) |
|
||||||
self.update_status_text(browser)
|
self.update_status_text(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,13 @@
|
||||||
//! Contains files specific to the servoshell app for Desktop systems.
|
//! Contains files specific to the servoshell app for Desktop systems.
|
||||||
|
|
||||||
pub(crate) mod app;
|
pub(crate) mod app;
|
||||||
|
mod app_state;
|
||||||
pub(crate) mod cli;
|
pub(crate) mod cli;
|
||||||
mod dialog;
|
mod dialog;
|
||||||
mod egui_glue;
|
mod egui_glue;
|
||||||
mod embedder;
|
mod embedder;
|
||||||
pub(crate) mod events_loop;
|
pub(crate) mod events_loop;
|
||||||
|
mod gamepad;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
mod headed_window;
|
mod headed_window;
|
||||||
mod headless_window;
|
mod headless_window;
|
||||||
|
@ -17,5 +19,4 @@ mod keyutils;
|
||||||
mod minibrowser;
|
mod minibrowser;
|
||||||
mod protocols;
|
mod protocols;
|
||||||
mod tracing;
|
mod tracing;
|
||||||
mod webview;
|
|
||||||
mod window_trait;
|
mod window_trait;
|
||||||
|
|
|
@ -22,21 +22,7 @@ macro_rules! trace_winit_event {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log an event from servo ([servo::EmbedderMsg]) at trace level.
|
pub(crate) use trace_winit_event;
|
||||||
/// - 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)+)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) use {trace_embedder_msg, trace_winit_event};
|
|
||||||
|
|
||||||
/// Get the log target for an event, as a static string.
|
/// Get the log target for an event, as a static string.
|
||||||
pub(crate) trait LogTarget {
|
pub(crate) trait LogTarget {
|
||||||
|
@ -115,58 +101,3 @@ mod from_winit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod from_servo {
|
|
||||||
use super::LogTarget;
|
|
||||||
|
|
||||||
macro_rules! target {
|
|
||||||
($($name:literal)+) => {
|
|
||||||
concat!("servoshell<servo@", $($name),+)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogTarget for servo::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::WebResourceRequested(..) => target!("WebResourceRequested"),
|
|
||||||
Self::AllowUnload(..) => target!("AllowUnload"),
|
|
||||||
Self::Keyboard(..) => target!("Keyboard"),
|
|
||||||
Self::ClearClipboardContents(..) => target!("ClearClipboardContents"),
|
|
||||||
Self::GetClipboardContents(..) => target!("GetClipboardContents"),
|
|
||||||
Self::SetClipboardContents(..) => target!("SetClipboardContents"),
|
|
||||||
Self::SetCursor(..) => target!("SetCursor"),
|
|
||||||
Self::NewFavicon(..) => target!("NewFavicon"),
|
|
||||||
Self::HistoryChanged(..) => target!("HistoryChanged"),
|
|
||||||
Self::SetFullscreenState(..) => target!("SetFullscreenState"),
|
|
||||||
Self::NotifyLoadStatusChanged(..) => target!("NotifyLoadStatusChanged"),
|
|
||||||
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::RequestDevtoolsConnection(..) => target!("RequestDevtoolsConnection"),
|
|
||||||
Self::ReadyToPresent(..) => target!("ReadyToPresent"),
|
|
||||||
Self::EventDelivered(..) => target!("EventDelivered"),
|
|
||||||
Self::PlayGamepadHapticEffect(..) => target!("PlayGamepadHapticEffect"),
|
|
||||||
Self::StopGamepadHapticEffect(..) => target!("StopGamepadHapticEffect"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,895 +0,0 @@
|
||||||
/* 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::path::PathBuf;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::{env, thread};
|
|
||||||
|
|
||||||
use arboard::Clipboard;
|
|
||||||
use euclid::Vector2D;
|
|
||||||
use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
|
|
||||||
use gilrs::{EventType, Gilrs};
|
|
||||||
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
|
|
||||||
use log::{debug, error, info, warn};
|
|
||||||
use servo::base::id::WebViewId;
|
|
||||||
use servo::config::opts::Opts;
|
|
||||||
use servo::ipc_channel::ipc::IpcSender;
|
|
||||||
use servo::servo_url::ServoUrl;
|
|
||||||
use servo::webrender_api::units::DeviceRect;
|
|
||||||
use servo::webrender_api::ScrollLocation;
|
|
||||||
use servo::{
|
|
||||||
CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern,
|
|
||||||
GamepadEvent, GamepadHapticEffectType, GamepadIndex, GamepadInputBounds,
|
|
||||||
GamepadSupportedHapticEffects, GamepadUpdateType, LoadStatus, PermissionPrompt,
|
|
||||||
PermissionRequest, PromptCredentialsInput, PromptDefinition, PromptOrigin, PromptResult, Servo,
|
|
||||||
TouchEventType,
|
|
||||||
};
|
|
||||||
use tinyfiledialogs::{self, MessageBoxIcon, OkCancel};
|
|
||||||
|
|
||||||
use super::dialog::Dialog;
|
|
||||||
use super::keyutils::CMD_OR_CONTROL;
|
|
||||||
use super::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
|
||||||
use crate::desktop::tracing::trace_embedder_msg;
|
|
||||||
|
|
||||||
pub struct WebViewManager {
|
|
||||||
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>,
|
|
||||||
|
|
||||||
window: Rc<dyn WindowPortsMethods>,
|
|
||||||
gamepad: Option<Gilrs>,
|
|
||||||
haptic_effects: HashMap<usize, HapticEffect>,
|
|
||||||
shutdown_requested: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
// The state of each Tab/WebView
|
|
||||||
pub struct WebView {
|
|
||||||
pub rect: DeviceRect,
|
|
||||||
pub title: Option<String>,
|
|
||||||
pub url: Option<ServoUrl>,
|
|
||||||
pub focused: bool,
|
|
||||||
pub load_status: LoadStatus,
|
|
||||||
pub servo_webview: ::servo::WebView,
|
|
||||||
dialogs: Vec<Dialog>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebView {
|
|
||||||
fn new(servo_webview: ::servo::WebView) -> Self {
|
|
||||||
Self {
|
|
||||||
rect: DeviceRect::zero(),
|
|
||||||
title: None,
|
|
||||||
url: None,
|
|
||||||
focused: false,
|
|
||||||
load_status: LoadStatus::Complete,
|
|
||||||
servo_webview,
|
|
||||||
dialogs: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_file_dialog(
|
|
||||||
&mut self,
|
|
||||||
multiple: bool,
|
|
||||||
response_sender: IpcSender<Option<Vec<PathBuf>>>,
|
|
||||||
patterns: Vec<FilterPattern>,
|
|
||||||
) {
|
|
||||||
self.dialogs
|
|
||||||
.push(Dialog::new_file_dialog(multiple, response_sender, patterns));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, ctx: &egui::Context) {
|
|
||||||
self.dialogs.retain_mut(|dialog| dialog.update(ctx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ServoEventResponse {
|
|
||||||
pub need_present: bool,
|
|
||||||
pub need_update: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HapticEffect {
|
|
||||||
pub effect: Effect,
|
|
||||||
pub sender: IpcSender<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WebViewManager {
|
|
||||||
pub fn new() -> WebViewManager {
|
|
||||||
WebViewManager {
|
|
||||||
status_text: None,
|
|
||||||
webviews: HashMap::default(),
|
|
||||||
creation_order: vec![],
|
|
||||||
focused_webview_id: None,
|
|
||||||
window: crate::desktop::headless_window::Window::new_uninit(),
|
|
||||||
gamepad: match Gilrs::new() {
|
|
||||||
Ok(g) => Some(g),
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Error creating gamepad input connection ({})", e);
|
|
||||||
None
|
|
||||||
},
|
|
||||||
},
|
|
||||||
haptic_effects: HashMap::default(),
|
|
||||||
shutdown_requested: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_window(&mut self, window: Rc<dyn WindowPortsMethods>) {
|
|
||||||
self.window = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_mut(&mut self, webview_id: WebViewId) -> Option<&mut WebView> {
|
|
||||||
self.webviews.get_mut(&webview_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, webview_id: WebViewId) -> Option<&WebView> {
|
|
||||||
self.webviews.get(&webview_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add(&mut self, webview: ::servo::WebView) {
|
|
||||||
self.creation_order.push(webview.id());
|
|
||||||
self.webviews.insert(webview.id(), WebView::new(webview));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focused_webview_id(&self) -> Option<WebViewId> {
|
|
||||||
self.focused_webview_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_webview(&mut self, servo: &Servo, webview_id: WebViewId) {
|
|
||||||
// This can happen because we can trigger a close with a UI action and then get the
|
|
||||||
// close event from Servo later.
|
|
||||||
if !self.webviews.contains_key(&webview_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.webviews.retain(|&id, _| id != webview_id);
|
|
||||||
self.creation_order.retain(|&id| id != webview_id);
|
|
||||||
self.focused_webview_id = None;
|
|
||||||
match self.last_created_webview() {
|
|
||||||
Some(last_created_webview) => last_created_webview.servo_webview.focus(),
|
|
||||||
None => servo.start_shutting_down(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn last_created_webview(&self) -> Option<&WebView> {
|
|
||||||
self.creation_order
|
|
||||||
.last()
|
|
||||||
.and_then(|id| self.webviews.get(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_url_string(&self) -> Option<String> {
|
|
||||||
match self.focused_webview() {
|
|
||||||
Some(webview) => webview.url.as_ref().map(|url| url.to_string()),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focused_webview(&self) -> Option<&WebView> {
|
|
||||||
self.focused_webview_id
|
|
||||||
.and_then(|id| self.webviews.get(&id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_status(&self) -> LoadStatus {
|
|
||||||
match self.focused_webview() {
|
|
||||||
Some(webview) => webview.load_status,
|
|
||||||
None => LoadStatus::Complete,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn status_text(&self) -> Option<String> {
|
|
||||||
self.status_text.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_pending_file_dialog(&self) -> bool {
|
|
||||||
self.focused_webview().map_or(false, |webview| {
|
|
||||||
webview
|
|
||||||
.dialogs
|
|
||||||
.iter()
|
|
||||||
.any(|dialog| matches!(dialog, Dialog::File(_)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the webviews in the creation order.
|
|
||||||
pub fn webviews(&self) -> Vec<(WebViewId, &WebView)> {
|
|
||||||
let mut res = vec![];
|
|
||||||
for id in &self.creation_order {
|
|
||||||
res.push((*id, self.webviews.get(id).unwrap()))
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle updates to connected gamepads from GilRs
|
|
||||||
pub fn handle_gamepad_events(&mut self) {
|
|
||||||
let Some(webview) = self
|
|
||||||
.focused_webview()
|
|
||||||
.map(|webview| webview.servo_webview.clone())
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
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),
|
|
||||||
};
|
|
||||||
// GilRs does not yet support trigger rumble
|
|
||||||
let supported_haptic_effects = GamepadSupportedHapticEffects {
|
|
||||||
supports_dual_rumble: true,
|
|
||||||
supports_trigger_rumble: false,
|
|
||||||
};
|
|
||||||
gamepad_event = Some(GamepadEvent::Connected(
|
|
||||||
index,
|
|
||||||
name,
|
|
||||||
bounds,
|
|
||||||
supported_haptic_effects,
|
|
||||||
));
|
|
||||||
},
|
|
||||||
EventType::Disconnected => {
|
|
||||||
gamepad_event = Some(GamepadEvent::Disconnected(index));
|
|
||||||
},
|
|
||||||
EventType::ForceFeedbackEffectCompleted => {
|
|
||||||
let Some(effect) = self.haptic_effects.get(&event.id.into()) else {
|
|
||||||
warn!("Failed to find haptic effect for id {}", event.id);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
effect
|
|
||||||
.sender
|
|
||||||
.send(true)
|
|
||||||
.expect("Failed to send haptic effect completion.");
|
|
||||||
self.haptic_effects.remove(&event.id.into());
|
|
||||||
},
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(event) = gamepad_event {
|
|
||||||
webview.notify_gamepad_event(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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_haptic_effect(
|
|
||||||
&mut self,
|
|
||||||
index: usize,
|
|
||||||
params: DualRumbleEffectParams,
|
|
||||||
effect_complete_sender: IpcSender<bool>,
|
|
||||||
) {
|
|
||||||
let Some(ref mut gilrs) = self.gamepad else {
|
|
||||||
debug!("Unable to get gilrs instance!");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(connected_gamepad) = gilrs
|
|
||||||
.gamepads()
|
|
||||||
.find(|gamepad| usize::from(gamepad.0) == index)
|
|
||||||
{
|
|
||||||
let start_delay = Ticks::from_ms(params.start_delay as u32);
|
|
||||||
let duration = Ticks::from_ms(params.duration as u32);
|
|
||||||
let strong_magnitude = (params.strong_magnitude * u16::MAX as f64).round() as u16;
|
|
||||||
let weak_magnitude = (params.weak_magnitude * u16::MAX as f64).round() as u16;
|
|
||||||
|
|
||||||
let scheduling = Replay {
|
|
||||||
after: start_delay,
|
|
||||||
play_for: duration,
|
|
||||||
with_delay: Ticks::from_ms(0),
|
|
||||||
};
|
|
||||||
let effect = EffectBuilder::new()
|
|
||||||
.add_effect(BaseEffect {
|
|
||||||
kind: BaseEffectType::Strong { magnitude: strong_magnitude },
|
|
||||||
scheduling,
|
|
||||||
envelope: Default::default(),
|
|
||||||
})
|
|
||||||
.add_effect(BaseEffect {
|
|
||||||
kind: BaseEffectType::Weak { magnitude: weak_magnitude },
|
|
||||||
scheduling,
|
|
||||||
envelope: Default::default(),
|
|
||||||
})
|
|
||||||
.repeat(Repeat::For(start_delay + duration))
|
|
||||||
.add_gamepad(&connected_gamepad.1)
|
|
||||||
.finish(gilrs)
|
|
||||||
.expect("Failed to create haptic effect, ensure connected gamepad supports force feedback.");
|
|
||||||
self.haptic_effects.insert(
|
|
||||||
index,
|
|
||||||
HapticEffect {
|
|
||||||
effect,
|
|
||||||
sender: effect_complete_sender,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
self.haptic_effects[&index]
|
|
||||||
.effect
|
|
||||||
.play()
|
|
||||||
.expect("Failed to play haptic effect.");
|
|
||||||
} else {
|
|
||||||
debug!("Couldn't find connected gamepad to play haptic effect on");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop_haptic_effect(&mut self, index: usize) -> bool {
|
|
||||||
let Some(haptic_effect) = self.haptic_effects.get(&index) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let stopped_successfully = match haptic_effect.effect.stop() {
|
|
||||||
Ok(()) => true,
|
|
||||||
Err(e) => {
|
|
||||||
debug!("Failed to stop haptic effect: {:?}", e);
|
|
||||||
false
|
|
||||||
},
|
|
||||||
};
|
|
||||||
self.haptic_effects.remove(&index);
|
|
||||||
|
|
||||||
stopped_successfully
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shutdown_requested(&self) -> bool {
|
|
||||||
self.shutdown_requested
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn focus_webview_by_index(&self, index: usize) {
|
|
||||||
if let Some((_, webview)) = self.webviews().get(index) {
|
|
||||||
webview.servo_webview.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
|
|
||||||
let focused_id = self.focused_webview_id?;
|
|
||||||
self.webviews()
|
|
||||||
.iter()
|
|
||||||
.position(|webview| webview.0 == focused_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_error(&self, webview_id: WebViewId, error: String) {
|
|
||||||
let Some(webview) = self.get(webview_id) else {
|
|
||||||
return warn!("{error}");
|
|
||||||
};
|
|
||||||
webview.servo_webview.send_error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if the caller needs to manually present a new frame.
|
|
||||||
pub fn handle_servo_events(
|
|
||||||
&mut self,
|
|
||||||
servo: &mut Servo,
|
|
||||||
clipboard: &mut Option<Clipboard>,
|
|
||||||
opts: &Opts,
|
|
||||||
messages: Vec<EmbedderMsg>,
|
|
||||||
) -> ServoEventResponse {
|
|
||||||
let mut need_present =
|
|
||||||
self.load_status() != LoadStatus::Complete || self.has_pending_file_dialog();
|
|
||||||
let mut need_update = false;
|
|
||||||
for message in messages {
|
|
||||||
trace_embedder_msg!(message, "{message:?}");
|
|
||||||
|
|
||||||
match message {
|
|
||||||
EmbedderMsg::Status(_, status) => {
|
|
||||||
self.status_text = status;
|
|
||||||
need_update = true;
|
|
||||||
},
|
|
||||||
EmbedderMsg::ChangePageTitle(webview_id, title) => {
|
|
||||||
// Set the title to the target webview, and update the OS window title
|
|
||||||
// if this is the currently focused one.
|
|
||||||
if let Some(webview) = self.get_mut(webview_id) {
|
|
||||||
webview.title = title.clone();
|
|
||||||
if webview.focused {
|
|
||||||
self.window.set_title(&format!(
|
|
||||||
"{} - Servo",
|
|
||||||
title.clone().unwrap_or_default()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
need_update = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EmbedderMsg::MoveTo(_, point) => {
|
|
||||||
self.window.set_position(point);
|
|
||||||
},
|
|
||||||
EmbedderMsg::ResizeTo(webview_id, inner_size) => {
|
|
||||||
if let Some(webview) = self.get_mut(webview_id) {
|
|
||||||
if webview.rect.size() != inner_size.to_f32() {
|
|
||||||
webview.rect.set_size(inner_size.to_f32());
|
|
||||||
webview.servo_webview.move_resize(webview.rect);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(webview) = self.get(webview_id) {
|
|
||||||
self.window.request_resize(webview, inner_size);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EmbedderMsg::Prompt(webview_id, definition, origin) => {
|
|
||||||
let res = if opts.headless {
|
|
||||||
match definition {
|
|
||||||
PromptDefinition::Alert(_message, sender) => sender.send(()),
|
|
||||||
PromptDefinition::OkCancel(_message, sender) => {
|
|
||||||
sender.send(PromptResult::Primary)
|
|
||||||
},
|
|
||||||
PromptDefinition::Input(_message, default, sender) => {
|
|
||||||
sender.send(Some(default.to_owned()))
|
|
||||||
},
|
|
||||||
PromptDefinition::Credentials(sender) => {
|
|
||||||
sender.send(PromptCredentialsInput {
|
|
||||||
username: None,
|
|
||||||
password: None,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} 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::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)
|
|
||||||
},
|
|
||||||
PromptDefinition::Credentials(sender) => {
|
|
||||||
// TODO: figure out how to make the message a localized string
|
|
||||||
let username = tinyfiledialogs::input_box("", "username", "");
|
|
||||||
let password = tinyfiledialogs::input_box("", "password", "");
|
|
||||||
sender.send(PromptCredentialsInput { username, password })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
.join()
|
|
||||||
.expect("Thread spawning failed")
|
|
||||||
};
|
|
||||||
if let Err(e) = res {
|
|
||||||
self.send_error(webview_id, format!("Failed to send Prompt response: {e}"))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EmbedderMsg::AllowUnload(webview_id, sender) => {
|
|
||||||
// Always allow unload for now.
|
|
||||||
if let Err(e) = sender.send(true) {
|
|
||||||
self.send_error(
|
|
||||||
webview_id,
|
|
||||||
format!("Failed to send AllowUnload response: {e}"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EmbedderMsg::AllowNavigationRequest(_, pipeline_id, _url) => {
|
|
||||||
servo.allow_navigation_response(pipeline_id, true);
|
|
||||||
},
|
|
||||||
EmbedderMsg::AllowOpeningWebView(_, response_chan) => {
|
|
||||||
let webview = servo.new_auxiliary_webview();
|
|
||||||
match response_chan.send(Some(webview.id())) {
|
|
||||||
Ok(()) => self.add(webview),
|
|
||||||
Err(error) => warn!("Failed to send AllowOpeningWebView response: {error}"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
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;
|
|
||||||
|
|
||||||
let webview = self
|
|
||||||
.webviews
|
|
||||||
.get(&new_webview_id)
|
|
||||||
.expect("Unknown webview opened.");
|
|
||||||
webview.servo_webview.focus();
|
|
||||||
webview.servo_webview.move_resize(rect);
|
|
||||||
webview.servo_webview.raise_to_top(true);
|
|
||||||
},
|
|
||||||
EmbedderMsg::WebViewClosed(webview_id) => {
|
|
||||||
self.close_webview(servo, webview_id);
|
|
||||||
},
|
|
||||||
EmbedderMsg::WebViewFocused(webview_id) => {
|
|
||||||
if let Some(webview) = self.focused_webview_id.and_then(|id| self.get_mut(id)) {
|
|
||||||
webview.focused = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show the most recently created webview and hide all others.
|
|
||||||
// TODO: Stop doing this once we have full multiple webviews support
|
|
||||||
if let Some(webview) = self.get_mut(webview_id) {
|
|
||||||
webview.focused = true;
|
|
||||||
webview.servo_webview.show(true);
|
|
||||||
self.focused_webview_id = Some(webview_id);
|
|
||||||
need_update = true;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
EmbedderMsg::WebViewBlurred => {
|
|
||||||
for webview in self.webviews.values_mut() {
|
|
||||||
webview.focused = false;
|
|
||||||
}
|
|
||||||
self.focused_webview_id = None;
|
|
||||||
},
|
|
||||||
EmbedderMsg::Keyboard(webview_id, key_event) => {
|
|
||||||
self.handle_overridable_key_bindings(webview_id, key_event);
|
|
||||||
},
|
|
||||||
EmbedderMsg::ClearClipboardContents(_) => {
|
|
||||||
clipboard
|
|
||||||
.as_mut()
|
|
||||||
.and_then(|clipboard| clipboard.clear().ok());
|
|
||||||
},
|
|
||||||
EmbedderMsg::GetClipboardContents(_, sender) => {
|
|
||||||
let contents = clipboard
|
|
||||||
.as_mut()
|
|
||||||
.and_then(|clipboard| clipboard.get_text().ok())
|
|
||||||
.unwrap_or_default();
|
|
||||||
if let Err(e) = sender.send(contents) {
|
|
||||||
warn!("Failed to send clipboard ({})", e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EmbedderMsg::SetClipboardContents(_, text) => {
|
|
||||||
if let Some(clipboard) = clipboard.as_mut() {
|
|
||||||
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::NotifyLoadStatusChanged(webview_id, load_status) => {
|
|
||||||
if let Some(webview) = self.get_mut(webview_id) {
|
|
||||||
webview.load_status = load_status;
|
|
||||||
need_update = true;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
EmbedderMsg::HistoryChanged(webview_id, urls, current) => {
|
|
||||||
if let Some(webview) = self.get_mut(webview_id) {
|
|
||||||
webview.url = Some(urls[current].clone());
|
|
||||||
need_update = true;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
EmbedderMsg::SetFullscreenState(_, state) => {
|
|
||||||
self.window.set_fullscreen(state);
|
|
||||||
},
|
|
||||||
EmbedderMsg::WebResourceRequested(_, _web_resource_request, _response_sender) => {},
|
|
||||||
EmbedderMsg::Shutdown => {
|
|
||||||
self.shutdown_requested = true;
|
|
||||||
},
|
|
||||||
EmbedderMsg::Panic(_, _reason, _backtrace) => {},
|
|
||||||
EmbedderMsg::GetSelectedBluetoothDevice(webview_id, devices, sender) => {
|
|
||||||
let selected = platform_get_selected_devices(devices);
|
|
||||||
if let Err(e) = sender.send(selected) {
|
|
||||||
self.send_error(
|
|
||||||
webview_id,
|
|
||||||
format!("Failed to send GetSelectedBluetoothDevice response: {e}"),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
EmbedderMsg::SelectFiles(webview_id, patterns, multiple_files, sender) => {
|
|
||||||
if let Some(webview) = self.get_mut(webview_id) {
|
|
||||||
webview.add_file_dialog(multiple_files, sender, patterns);
|
|
||||||
need_update = true;
|
|
||||||
need_present = true;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
EmbedderMsg::PromptPermission(_, prompt, sender) => {
|
|
||||||
let _ = sender.send(match opts.headless {
|
|
||||||
true => PermissionRequest::Denied,
|
|
||||||
false => prompt_user(prompt),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
EmbedderMsg::ShowIME(_webview_id, _kind, _text, _multiline, _rect) => {
|
|
||||||
debug!("ShowIME received");
|
|
||||||
},
|
|
||||||
EmbedderMsg::HideIME(_webview_id) => {
|
|
||||||
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::RequestDevtoolsConnection(response_sender) => {
|
|
||||||
let _ = response_sender.send(true);
|
|
||||||
},
|
|
||||||
EmbedderMsg::ShowContextMenu(_, sender, ..) => {
|
|
||||||
let _ = sender.send(ContextMenuResult::Ignored);
|
|
||||||
},
|
|
||||||
EmbedderMsg::ReadyToPresent(_webview_ids) => {
|
|
||||||
need_present = true;
|
|
||||||
},
|
|
||||||
EmbedderMsg::EventDelivered(webview_id, event) => {
|
|
||||||
if let Some(webview) = self.get_mut(webview_id) {
|
|
||||||
if let CompositorEventVariant::MouseButtonEvent = event {
|
|
||||||
webview.servo_webview.raise_to_top(true);
|
|
||||||
webview.servo_webview.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
EmbedderMsg::PlayGamepadHapticEffect(_, index, effect, effect_complete_sender) => {
|
|
||||||
match effect {
|
|
||||||
GamepadHapticEffectType::DualRumble(params) => {
|
|
||||||
self.play_haptic_effect(index, params, effect_complete_sender);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
EmbedderMsg::StopGamepadHapticEffect(_, index, haptic_stop_sender) => {
|
|
||||||
let stopped_successfully = self.stop_haptic_effect(index);
|
|
||||||
haptic_stop_sender
|
|
||||||
.send(stopped_successfully)
|
|
||||||
.expect("Failed to send haptic stop result");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ServoEventResponse {
|
|
||||||
need_present,
|
|
||||||
need_update,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle servoshell key bindings that may have been prevented by the page in the focused webview.
|
|
||||||
fn handle_overridable_key_bindings(&mut self, webview_id: WebViewId, event: KeyboardEvent) {
|
|
||||||
let Some(webview) = self.get(webview_id) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let origin = webview.rect.min.ceil().to_i32();
|
|
||||||
let webview = &webview.servo_webview;
|
|
||||||
ShortcutMatcher::from_event(event)
|
|
||||||
.shortcut(CMD_OR_CONTROL, '=', || {
|
|
||||||
webview.set_zoom(1.1);
|
|
||||||
})
|
|
||||||
.shortcut(CMD_OR_CONTROL, '+', || {
|
|
||||||
webview.set_zoom(1.1);
|
|
||||||
})
|
|
||||||
.shortcut(CMD_OR_CONTROL, '-', || {
|
|
||||||
webview.set_zoom(1.0 / 1.1);
|
|
||||||
})
|
|
||||||
.shortcut(CMD_OR_CONTROL, '0', || {
|
|
||||||
webview.reset_zoom();
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::PageDown, || {
|
|
||||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
|
||||||
0.0,
|
|
||||||
-self.window.page_height() + 2.0 * LINE_HEIGHT,
|
|
||||||
));
|
|
||||||
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::PageUp, || {
|
|
||||||
let scroll_location = ScrollLocation::Delta(Vector2D::new(
|
|
||||||
0.0,
|
|
||||||
self.window.page_height() - 2.0 * LINE_HEIGHT,
|
|
||||||
));
|
|
||||||
webview.notify_scroll_event(scroll_location, origin, TouchEventType::Move);
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::Home, || {
|
|
||||||
webview.notify_scroll_event(ScrollLocation::Start, origin, TouchEventType::Move);
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::End, || {
|
|
||||||
webview.notify_scroll_event(ScrollLocation::End, origin, TouchEventType::Move);
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::ArrowUp, || {
|
|
||||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, 3.0 * LINE_HEIGHT));
|
|
||||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::ArrowDown, || {
|
|
||||||
let location = ScrollLocation::Delta(Vector2D::new(0.0, -3.0 * LINE_HEIGHT));
|
|
||||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::ArrowLeft, || {
|
|
||||||
let location = ScrollLocation::Delta(Vector2D::new(LINE_HEIGHT, 0.0));
|
|
||||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
|
||||||
})
|
|
||||||
.shortcut(Modifiers::empty(), Key::ArrowRight, || {
|
|
||||||
let location = ScrollLocation::Delta(Vector2D::new(-LINE_HEIGHT, 0.0));
|
|
||||||
webview.notify_scroll_event(location, origin, TouchEventType::Move);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
fn prompt_user(prompt: PermissionPrompt) -> PermissionRequest {
|
|
||||||
use tinyfiledialogs::YesNo;
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
|
@ -7,14 +7,13 @@
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use arboard::Clipboard;
|
|
||||||
use euclid::{Length, Scale};
|
use euclid::{Length, Scale};
|
||||||
use servo::compositing::windowing::WindowMethods;
|
use servo::compositing::windowing::WindowMethods;
|
||||||
use servo::servo_geometry::DeviceIndependentPixel;
|
use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
|
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
|
||||||
use servo::{Cursor, Servo};
|
use servo::{Cursor, WebView};
|
||||||
|
|
||||||
use super::webview::{WebView, WebViewManager};
|
use super::app_state::RunningAppState;
|
||||||
|
|
||||||
// This should vary by zoom level and maybe actual text size (focused or under cursor)
|
// This should vary by zoom level and maybe actual text size (focused or under cursor)
|
||||||
pub const LINE_HEIGHT: f32 = 38.0;
|
pub const LINE_HEIGHT: f32 = 38.0;
|
||||||
|
@ -31,13 +30,7 @@ pub trait WindowPortsMethods: WindowMethods {
|
||||||
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>;
|
) -> Option<Scale<f32, DeviceIndependentPixel, DevicePixel>>;
|
||||||
fn page_height(&self) -> f32;
|
fn page_height(&self) -> f32;
|
||||||
fn get_fullscreen(&self) -> bool;
|
fn get_fullscreen(&self) -> bool;
|
||||||
fn handle_winit_event(
|
fn handle_winit_event(&self, state: Rc<RunningAppState>, event: winit::event::WindowEvent);
|
||||||
&self,
|
|
||||||
servo: &Servo,
|
|
||||||
clipboard: &mut Option<Clipboard>,
|
|
||||||
webviews: &mut WebViewManager,
|
|
||||||
event: winit::event::WindowEvent,
|
|
||||||
);
|
|
||||||
fn is_animating(&self) -> bool;
|
fn is_animating(&self) -> bool;
|
||||||
fn set_title(&self, _title: &str) {}
|
fn set_title(&self, _title: &str) {}
|
||||||
/// Request a new inner size for the window, not including external decorations.
|
/// Request a new inner size for the window, not including external decorations.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue