mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
servoshell: Add architectural support for opening multiple windows (#40883)
This change is a major re-architecture of servoshell to support multiple windows. Unfortunately it was not possible to do this incrementally, but @mukilan and I did this together so we feel more confident about these changes. The main change here is that now the `HashMap` of windows that `App` has can be filled with more than one `ServoShellWindow`. `ServoShellWindow` is a wrapper around a `PlatformWindow` which can either be headed, headless, or for embedded platforms. Embedded platforms (Android and OHOS) are only expected to have a single window, but there is no reason that more windows cannot be added. There is still a little bit more work to be done in order to fully enable mulitple windows, so this change is just the architectural preparation. This change enables the embedded and desktop versions of servoshell to start to be fully integrated so that the entire `RunningAppState` is shared between them. Testing: servoshell is the test harness so these changes are covered by the WPT tests. --------- Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
@@ -138,7 +138,7 @@ pub enum WebDriverCommandMsg {
|
||||
HandleUserPrompt(
|
||||
WebViewId,
|
||||
WebDriverUserPromptAction,
|
||||
IpcSender<Result<Option<String>, ()>>,
|
||||
IpcSender<Result<String, ()>>,
|
||||
),
|
||||
GetAlertText(WebViewId, IpcSender<Result<String, ()>>),
|
||||
SendAlertText(WebViewId, String),
|
||||
|
||||
@@ -333,9 +333,7 @@ impl Handler {
|
||||
|
||||
if handler.notify || handler.handler == WebDriverUserPromptAction::Ignore {
|
||||
// Step 6. If handler's notify is true, return annotated unexpected alert open error.
|
||||
let alert_text = wait_for_ipc_response(receiver)?
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
let alert_text = wait_for_ipc_response(receiver)?.unwrap_or_default();
|
||||
|
||||
Err(WebDriverError::new_with_data(
|
||||
ErrorStatus::UnexpectedAlertOpen,
|
||||
|
||||
@@ -4,64 +4,48 @@
|
||||
|
||||
//! Application entry point, runs the event loop.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::time::Instant;
|
||||
use std::{env, fs};
|
||||
|
||||
use ::servo::ServoBuilder;
|
||||
use crossbeam_channel::unbounded;
|
||||
use log::warn;
|
||||
use net::protocols::ProtocolRegistry;
|
||||
use servo::config::opts::Opts;
|
||||
use servo::config::prefs::Preferences;
|
||||
use servo::servo_url::ServoUrl;
|
||||
use servo::user_content_manager::{UserContentManager, UserScript};
|
||||
use servo::{EventLoopWaker, WebDriverCommandMsg, WebDriverUserPromptAction};
|
||||
use url::Url;
|
||||
use servo::{EventLoopWaker, ServoBuilder};
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use super::app_state::AppState;
|
||||
use super::event_loop::AppEvent;
|
||||
use super::{headed_window, headless_window};
|
||||
use crate::desktop::app_state::RunningAppState;
|
||||
use crate::desktop::event_loop::ServoShellEventLoop;
|
||||
use crate::desktop::protocols;
|
||||
use crate::desktop::tracing::trace_winit_event;
|
||||
use crate::desktop::window_trait::WindowPortsMethods;
|
||||
use crate::parser::get_default_url;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
use crate::running_app_state::RunningAppStateTrait;
|
||||
use crate::running_app_state::RunningAppState;
|
||||
use crate::window::PlatformWindow;
|
||||
|
||||
pub(crate) enum AppState {
|
||||
Initializing,
|
||||
Running(Rc<RunningAppState>),
|
||||
ShuttingDown,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
opts: Opts,
|
||||
preferences: Preferences,
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
proxy: Option<EventLoopProxy<AppEvent>>,
|
||||
event_loop_proxy: Option<EventLoopProxy<AppEvent>>,
|
||||
initial_url: ServoUrl,
|
||||
t_start: Instant,
|
||||
t: Instant,
|
||||
state: AppState,
|
||||
|
||||
// This is the last field of the struct to ensure that windows are dropped *after* all other
|
||||
// references to the relevant rendering contexts have been destroyed.
|
||||
// (https://github.com/servo/servo/issues/36711)
|
||||
windows: HashMap<WindowId, Rc<dyn WindowPortsMethods>>,
|
||||
}
|
||||
|
||||
/// Action to be taken by the caller of [`App::handle_events`].
|
||||
pub(crate) enum PumpResult {
|
||||
/// The caller should shut down Servo and its related context.
|
||||
Shutdown,
|
||||
Continue {
|
||||
needs_user_interface_update: bool,
|
||||
need_window_redraw: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -83,9 +67,8 @@ impl App {
|
||||
opts,
|
||||
preferences,
|
||||
servoshell_preferences: servo_shell_preferences,
|
||||
windows: HashMap::new(),
|
||||
waker: event_loop.create_event_loop_waker(),
|
||||
proxy: event_loop.event_loop_proxy(),
|
||||
event_loop_proxy: event_loop.event_loop_proxy(),
|
||||
initial_url: initial_url.clone(),
|
||||
t_start: t,
|
||||
t,
|
||||
@@ -94,27 +77,7 @@ impl App {
|
||||
}
|
||||
|
||||
/// Initialize Application once event loop start running.
|
||||
pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) {
|
||||
let headless = self.servoshell_preferences.headless;
|
||||
assert_eq!(headless, event_loop.is_none());
|
||||
|
||||
let window = match event_loop {
|
||||
Some(event_loop) => {
|
||||
let event_loop_proxy = self.proxy.take().expect("Must have a proxy available");
|
||||
Rc::new(headed_window::Window::new(
|
||||
&self.servoshell_preferences,
|
||||
event_loop,
|
||||
event_loop_proxy,
|
||||
self.initial_url.clone(),
|
||||
))
|
||||
},
|
||||
None => headless_window::Window::new(&self.servoshell_preferences),
|
||||
};
|
||||
|
||||
self.windows.insert(window.id(), window);
|
||||
|
||||
let (_, window) = self.windows.iter().next().unwrap();
|
||||
|
||||
pub fn init(&mut self, active_event_loop: Option<&ActiveEventLoop>) {
|
||||
let mut user_content_manager = UserContentManager::new();
|
||||
for script in load_userscripts(self.servoshell_preferences.userscripts_directory.as_deref())
|
||||
.expect("Loading userscripts failed")
|
||||
@@ -141,278 +104,63 @@ impl App {
|
||||
.protocol_registry(protocol_registry)
|
||||
.event_loop_waker(self.waker.clone());
|
||||
|
||||
let platform_window =
|
||||
self.create_platform_window(self.initial_url.clone(), active_event_loop);
|
||||
#[cfg(feature = "webxr")]
|
||||
let servo_builder =
|
||||
servo_builder.webxr_registry(super::webxr::XrDiscoveryWebXrRegistry::new_boxed(
|
||||
window.clone(),
|
||||
event_loop,
|
||||
platform_window.clone(),
|
||||
active_event_loop,
|
||||
&self.preferences,
|
||||
));
|
||||
|
||||
let servo = servo_builder.build();
|
||||
servo.setup_logging();
|
||||
|
||||
// Initialize WebDriver server here before `servo` is moved.
|
||||
let webdriver_receiver = self.servoshell_preferences.webdriver_port.map(|port| {
|
||||
let (embedder_sender, embedder_receiver) = unbounded();
|
||||
webdriver_server::start_server(port, embedder_sender, self.waker.clone());
|
||||
embedder_receiver
|
||||
});
|
||||
|
||||
let running_state = Rc::new(RunningAppState::new(
|
||||
servo,
|
||||
window.clone(),
|
||||
self.servoshell_preferences.clone(),
|
||||
webdriver_receiver,
|
||||
self.waker.clone(),
|
||||
));
|
||||
|
||||
running_state.create_and_focus_toplevel_webview(self.initial_url.clone().into_url());
|
||||
window.rebuild_user_interface(&running_state);
|
||||
running_state.create_window(platform_window, self.initial_url.as_url().clone());
|
||||
|
||||
self.state = AppState::Running(running_state);
|
||||
}
|
||||
|
||||
fn create_platform_window(
|
||||
&self,
|
||||
url: ServoUrl,
|
||||
active_event_loop: Option<&ActiveEventLoop>,
|
||||
) -> Rc<dyn PlatformWindow> {
|
||||
assert_eq!(
|
||||
self.servoshell_preferences.headless,
|
||||
active_event_loop.is_none()
|
||||
);
|
||||
|
||||
let Some(active_event_loop) = active_event_loop else {
|
||||
return headless_window::Window::new(&self.servoshell_preferences);
|
||||
};
|
||||
|
||||
headed_window::Window::new(
|
||||
&self.servoshell_preferences,
|
||||
active_event_loop,
|
||||
self.event_loop_proxy
|
||||
.clone()
|
||||
.expect("Should always have event loop proxy in headed mode."),
|
||||
url,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pump_servo_event_loop(&mut self) -> bool {
|
||||
let AppState::Running(state) = &self.state else {
|
||||
return false;
|
||||
};
|
||||
|
||||
self.handle_webdriver_messages();
|
||||
match state.pump_event_loop() {
|
||||
PumpResult::Shutdown => {
|
||||
state.webview_collection_mut().clear();
|
||||
self.state = AppState::ShuttingDown;
|
||||
false
|
||||
},
|
||||
PumpResult::Continue {
|
||||
needs_user_interface_update,
|
||||
need_window_redraw,
|
||||
} => {
|
||||
// TODO: These results should eventually be stored per-`Window`.
|
||||
for window in self.windows.values() {
|
||||
let updated_user_interface =
|
||||
needs_user_interface_update && window.update_user_interface_state(state);
|
||||
if updated_user_interface || need_window_redraw {
|
||||
window.request_repaint(state);
|
||||
}
|
||||
}
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_webdriver_messages(&self) {
|
||||
let AppState::Running(running_state) = &self.state else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(webdriver_receiver) = running_state.webdriver_receiver() else {
|
||||
return;
|
||||
};
|
||||
|
||||
while let Ok(msg) = webdriver_receiver.try_recv() {
|
||||
match msg {
|
||||
WebDriverCommandMsg::Shutdown => {
|
||||
running_state.servo().start_shutting_down();
|
||||
},
|
||||
WebDriverCommandMsg::IsWebViewOpen(webview_id, sender) => {
|
||||
let context = running_state.webview_by_id(webview_id);
|
||||
|
||||
if let Err(error) = sender.send(context.is_some()) {
|
||||
warn!("Failed to send response of IsWebViewOpen: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::IsBrowsingContextOpen(..) => {
|
||||
running_state.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::NewWebView(response_sender, load_status_sender) => {
|
||||
let new_webview =
|
||||
running_state.create_toplevel_webview(Url::parse("about:blank").unwrap());
|
||||
|
||||
if let Err(error) = response_sender.send(new_webview.id()) {
|
||||
warn!("Failed to send response of NewWebview: {error}");
|
||||
}
|
||||
if let Some(load_status_sender) = load_status_sender {
|
||||
running_state.set_load_status_sender(new_webview.id(), load_status_sender);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::CloseWebView(webview_id, response_sender) => {
|
||||
running_state.close_webview(webview_id);
|
||||
if let Err(error) = response_sender.send(()) {
|
||||
warn!("Failed to send response of CloseWebView: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::FocusWebView(webview_id) => {
|
||||
if let Some(webview) = running_state.webview_by_id(webview_id) {
|
||||
webview.focus_and_raise_to_top(true);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::FocusBrowsingContext(..) => {
|
||||
running_state.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::GetAllWebViews(response_sender) => {
|
||||
let webviews = running_state.webviews().iter().map(|(id, _)| *id).collect();
|
||||
|
||||
if let Err(error) = response_sender.send(webviews) {
|
||||
warn!("Failed to send response of GetAllWebViews: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GetWindowRect(_webview_id, response_sender) => {
|
||||
let window = self
|
||||
.windows
|
||||
.values()
|
||||
.next()
|
||||
.expect("Should have at least one window in servoshell");
|
||||
|
||||
if let Err(error) = response_sender.send(window.window_rect()) {
|
||||
warn!("Failed to send response of GetWindowSize: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::MaximizeWebView(webview_id, response_sender) => {
|
||||
let window = self
|
||||
.windows
|
||||
.values()
|
||||
.next()
|
||||
.expect("Should have at least one window in servoshell");
|
||||
window.maximize(
|
||||
&running_state
|
||||
.webview_by_id(webview_id)
|
||||
.expect("Webview must exists as we just verified"),
|
||||
);
|
||||
|
||||
if let Err(error) = response_sender.send(window.window_rect()) {
|
||||
warn!("Failed to send response of GetWindowSize: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::SetWindowRect(webview_id, requested_rect, size_sender) => {
|
||||
let Some(webview) = running_state.webview_by_id(webview_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let window = self
|
||||
.windows
|
||||
.values()
|
||||
.next()
|
||||
.expect("Should have at least one window in servoshell");
|
||||
let scale = window.hidpi_scale_factor();
|
||||
|
||||
let requested_physical_rect =
|
||||
(requested_rect.to_f32() * scale).round().to_i32();
|
||||
|
||||
// Step 17. Set Width/Height.
|
||||
window.request_resize(&webview, requested_physical_rect.size());
|
||||
|
||||
// Step 18. Set position of the window.
|
||||
window.set_position(requested_physical_rect.min);
|
||||
|
||||
if let Err(error) = size_sender.send(window.window_rect()) {
|
||||
warn!("Failed to send window size: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GetViewportSize(_webview_id, response_sender) => {
|
||||
let window = self
|
||||
.windows
|
||||
.values()
|
||||
.next()
|
||||
.expect("Should have at least one window in servoshell");
|
||||
|
||||
let size = window.rendering_context().size2d();
|
||||
|
||||
if let Err(error) = response_sender.send(size) {
|
||||
warn!("Failed to send response of GetViewportSize: {error}");
|
||||
}
|
||||
},
|
||||
// This is only received when start new session.
|
||||
WebDriverCommandMsg::GetFocusedWebView(sender) => {
|
||||
let focused_webview = running_state.focused_webview();
|
||||
|
||||
if let Err(error) = sender.send(focused_webview.map(|w| w.id())) {
|
||||
warn!("Failed to send response of GetFocusedWebView: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => {
|
||||
running_state.handle_webdriver_load_url(webview_id, url, load_status_sender);
|
||||
},
|
||||
WebDriverCommandMsg::Refresh(webview_id, load_status_sender) => {
|
||||
if let Some(webview) = running_state.webview_by_id(webview_id) {
|
||||
running_state.set_load_status_sender(webview_id, load_status_sender);
|
||||
webview.reload();
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GoBack(webview_id, load_status_sender) => {
|
||||
if let Some(webview) = running_state.webview_by_id(webview_id) {
|
||||
let traversal_id = webview.go_back(1);
|
||||
running_state.set_pending_traversal(traversal_id, load_status_sender);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GoForward(webview_id, load_status_sender) => {
|
||||
if let Some(webview) = running_state.webview_by_id(webview_id) {
|
||||
let traversal_id = webview.go_forward(1);
|
||||
running_state.set_pending_traversal(traversal_id, load_status_sender);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::InputEvent(webview_id, input_event, response_sender) => {
|
||||
running_state.handle_webdriver_input_event(
|
||||
webview_id,
|
||||
input_event,
|
||||
response_sender,
|
||||
);
|
||||
},
|
||||
WebDriverCommandMsg::ScriptCommand(_, ref webdriver_script_command) => {
|
||||
running_state.handle_webdriver_script_command(webdriver_script_command);
|
||||
running_state.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::CurrentUserPrompt(webview_id, response_sender) => {
|
||||
let current_dialog =
|
||||
running_state.get_current_active_dialog_webdriver_type(webview_id);
|
||||
if let Err(error) = response_sender.send(current_dialog) {
|
||||
warn!("Failed to send response of CurrentUserPrompt: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => {
|
||||
let response = if running_state.webview_has_active_dialog(webview_id) {
|
||||
let alert_text = running_state.alert_text_of_newest_dialog(webview_id);
|
||||
|
||||
match action {
|
||||
WebDriverUserPromptAction::Accept => {
|
||||
running_state.accept_active_dialogs(webview_id)
|
||||
},
|
||||
WebDriverUserPromptAction::Dismiss => {
|
||||
running_state.dismiss_active_dialogs(webview_id)
|
||||
},
|
||||
WebDriverUserPromptAction::Ignore => {},
|
||||
};
|
||||
|
||||
// Return success for AcceptAlert and DismissAlert commands.
|
||||
Ok(alert_text)
|
||||
} else {
|
||||
// Return error for AcceptAlert and DismissAlert commands
|
||||
// if there is no active dialog.
|
||||
Err(())
|
||||
};
|
||||
|
||||
if let Err(error) = response_sender.send(response) {
|
||||
warn!("Failed to send response of HandleUserPrompt: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => {
|
||||
let response = match running_state.alert_text_of_newest_dialog(webview_id) {
|
||||
Some(text) => Ok(text),
|
||||
None => Err(()),
|
||||
};
|
||||
|
||||
if let Err(error) = response_sender.send(response) {
|
||||
warn!("Failed to send response of GetAlertText: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::SendAlertText(webview_id, text) => {
|
||||
running_state.set_alert_text_of_newest_dialog(webview_id, text);
|
||||
},
|
||||
WebDriverCommandMsg::TakeScreenshot(webview_id, rect, result_sender) => {
|
||||
running_state.handle_webdriver_screenshot(webview_id, rect, result_sender);
|
||||
},
|
||||
};
|
||||
if !state.spin_event_loop() {
|
||||
self.state = AppState::ShuttingDown;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,10 +188,14 @@ impl ApplicationHandler<AppEvent> for App {
|
||||
let AppState::Running(state) = &self.state else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = self.windows.get(&window_id) else {
|
||||
return;
|
||||
};
|
||||
window.handle_winit_window_event(state.clone(), window_event);
|
||||
let window_id: u64 = window_id.into();
|
||||
if let Some(window) = state.window(window_id.into()) {
|
||||
window.platform_window().handle_winit_window_event(
|
||||
state.clone(),
|
||||
&window,
|
||||
window_event,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.pump_servo_event_loop() {
|
||||
@@ -458,10 +210,16 @@ impl ApplicationHandler<AppEvent> for App {
|
||||
let AppState::Running(state) = &self.state else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = self.windows.values().next() else {
|
||||
return;
|
||||
};
|
||||
window.handle_winit_app_event(state.clone(), app_event);
|
||||
if let Some(window_id) = app_event.window_id() {
|
||||
let window_id: u64 = window_id.into();
|
||||
if let Some(window) = state.window(window_id.into()) {
|
||||
window.platform_window().handle_winit_app_event(
|
||||
state.clone(),
|
||||
&window,
|
||||
app_event,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.pump_servo_event_loop() {
|
||||
|
||||
@@ -1,716 +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::cell::{Ref, RefCell, RefMut};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use log::{error, info};
|
||||
use servo::base::generic_channel::GenericSender;
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::config::pref;
|
||||
use servo::ipc_channel::ipc::IpcSender;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
|
||||
use servo::{
|
||||
AllowOrDenyRequest, AuthenticationRequest, Cursor, EmbedderControl, EmbedderControlId,
|
||||
GamepadHapticEffectType, InputEventId, InputEventResult, JSValue, LoadStatus,
|
||||
PermissionRequest, Servo, ServoDelegate, ServoError, SimpleDialog, TraversalId,
|
||||
WebDriverCommandMsg, WebDriverLoadStatus, WebDriverUserPrompt, WebView, WebViewBuilder,
|
||||
WebViewDelegate,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use super::app::PumpResult;
|
||||
use super::dialog::Dialog;
|
||||
use super::gamepad::GamepadSupport;
|
||||
use super::window_trait::WindowPortsMethods;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
use crate::running_app_state::{RunningAppStateBase, RunningAppStateTrait};
|
||||
|
||||
pub(crate) enum AppState {
|
||||
Initializing,
|
||||
Running(Rc<RunningAppState>),
|
||||
ShuttingDown,
|
||||
}
|
||||
|
||||
pub(crate) struct RunningAppState {
|
||||
base: RunningAppStateBase,
|
||||
inner: RefCell<RunningAppStateInner>,
|
||||
}
|
||||
|
||||
pub struct RunningAppStateInner {
|
||||
/// 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 Servo needs to repaint its display. Currently this is global
|
||||
/// because every `WebView` shares a `RenderingContext`.
|
||||
need_repaint: bool,
|
||||
|
||||
/// Whether or not the amount of dialogs on the currently rendered webview
|
||||
/// has just changed.
|
||||
dialog_amount_changed: bool,
|
||||
|
||||
/// List of webviews that have favicon textures which are not yet uploaded
|
||||
/// to the GPU by egui.
|
||||
pending_favicon_loads: Vec<WebViewId>,
|
||||
|
||||
/// A list of showing [`InputMethod`] interfaces.
|
||||
visible_input_methods: Vec<EmbedderControlId>,
|
||||
}
|
||||
|
||||
impl Drop for RunningAppState {
|
||||
fn drop(&mut self) {
|
||||
self.servo().deinit();
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningAppStateTrait for RunningAppState {
|
||||
fn base(&self) -> &RunningAppStateBase {
|
||||
&self.base
|
||||
}
|
||||
|
||||
fn base_mut(&mut self) -> &mut RunningAppStateBase {
|
||||
&mut self.base
|
||||
}
|
||||
|
||||
fn webview_by_id(&self, id: WebViewId) -> Option<WebView> {
|
||||
self.webview_collection().get(id).cloned()
|
||||
}
|
||||
|
||||
fn dismiss_embedder_controls_for_webview(&self, webview_id: WebViewId) {
|
||||
let _ = self.inner_mut().dialogs.remove(&webview_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningAppState {
|
||||
pub fn new(
|
||||
servo: Servo,
|
||||
window: Rc<dyn WindowPortsMethods>,
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
webdriver_receiver: Option<Receiver<WebDriverCommandMsg>>,
|
||||
) -> RunningAppState {
|
||||
servo.set_delegate(Rc::new(ServoShellServoDelegate));
|
||||
let gamepad_support = if pref!(dom_gamepad_enabled) {
|
||||
GamepadSupport::maybe_new()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
RunningAppState {
|
||||
base: RunningAppStateBase::new(servoshell_preferences, servo, webdriver_receiver),
|
||||
inner: RefCell::new(RunningAppStateInner {
|
||||
dialogs: Default::default(),
|
||||
window,
|
||||
gamepad_support,
|
||||
need_update: false,
|
||||
need_repaint: false,
|
||||
dialog_amount_changed: false,
|
||||
pending_favicon_loads: Default::default(),
|
||||
visible_input_methods: Default::default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_and_focus_toplevel_webview(self: &Rc<Self>, url: Url) {
|
||||
let webview = self.create_toplevel_webview(url);
|
||||
webview.focus_and_raise_to_top(true);
|
||||
}
|
||||
|
||||
pub(crate) fn create_toplevel_webview(self: &Rc<Self>, url: Url) -> WebView {
|
||||
let webview = WebViewBuilder::new(self.servo(), self.inner().window.rendering_context())
|
||||
.url(url)
|
||||
.hidpi_scale_factor(self.inner().window.hidpi_scale_factor())
|
||||
.delegate(self.clone())
|
||||
.build();
|
||||
|
||||
webview.notify_theme_change(self.inner().window.theme());
|
||||
self.add_webview(webview.clone());
|
||||
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 hidpi_scale_factor_changed(&self) {
|
||||
let new_scale_factor = self.inner().window.hidpi_scale_factor();
|
||||
for webview in self.webview_collection().values() {
|
||||
webview.set_hidpi_scale_factor(new_scale_factor);
|
||||
}
|
||||
}
|
||||
|
||||
/// Repaint the Servo view is necessary, returning true if anything was actually
|
||||
/// painted or false otherwise. Something may not be painted if Servo is waiting
|
||||
/// for a stable image to paint.
|
||||
pub(crate) fn repaint_servo_if_necessary(&self) {
|
||||
if !self.inner().need_repaint {
|
||||
return;
|
||||
}
|
||||
let Some(webview) = self.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
webview.paint();
|
||||
|
||||
let mut inner_mut = self.inner_mut();
|
||||
inner_mut.window.rendering_context().present();
|
||||
inner_mut.need_repaint = false;
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
if !self.servo().spin_event_loop() {
|
||||
return PumpResult::Shutdown;
|
||||
}
|
||||
|
||||
// Delegate handlers may have asked us to present or update compositor contents.
|
||||
// Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid.
|
||||
let need_window_redraw = self.inner().need_repaint ||
|
||||
self.has_active_dialog() ||
|
||||
self.inner().dialog_amount_changed;
|
||||
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
|
||||
|
||||
self.inner_mut().dialog_amount_changed = false;
|
||||
|
||||
if self.servoshell_preferences().exit_after_stable_image &&
|
||||
self.base().achieved_stable_image.get()
|
||||
{
|
||||
self.servo().start_shutting_down();
|
||||
}
|
||||
|
||||
PumpResult::Continue {
|
||||
needs_user_interface_update: need_update,
|
||||
need_window_redraw,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn for_each_active_dialog(&self, callback: impl Fn(&mut Dialog) -> bool) {
|
||||
let last_created_webview_id = self
|
||||
.webview_collection()
|
||||
.newest()
|
||||
.map(|webview_view| webview_view.id());
|
||||
let Some(webview_id) = self
|
||||
.focused_webview()
|
||||
.as_ref()
|
||||
.map(WebView::id)
|
||||
.or(last_created_webview_id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut inner = self.inner_mut();
|
||||
|
||||
// If a dialog is open, clear any Servo cursor. TODO: This should restore the
|
||||
// cursor too, when all dialogs close. In general, we need a better cursor
|
||||
// management strategy.
|
||||
if inner
|
||||
.dialogs
|
||||
.get(&webview_id)
|
||||
.is_some_and(|dialogs| !dialogs.is_empty())
|
||||
{
|
||||
inner.window.set_cursor(Cursor::Default);
|
||||
}
|
||||
|
||||
if let Some(dialogs) = inner.dialogs.get_mut(&webview_id) {
|
||||
let length = dialogs.len();
|
||||
dialogs.retain_mut(callback);
|
||||
if length != dialogs.len() {
|
||||
inner.dialog_amount_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
if !self.webview_collection().contains(webview_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.inner_mut().dialogs.remove(&webview_id);
|
||||
|
||||
self.webview_collection_mut().remove(webview_id);
|
||||
|
||||
match self.webview_collection().newest() {
|
||||
Some(newest_webview) => {
|
||||
newest_webview.focus();
|
||||
},
|
||||
None if self.servoshell_preferences().webdriver_port.is_none() => {
|
||||
self.servo().start_shutting_down()
|
||||
},
|
||||
None => {
|
||||
// For WebDriver, don't shut down when last webview closed
|
||||
// https://github.com/servo/servo/issues/37408
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 add_dialog(&self, webview: servo::WebView, dialog: Dialog) {
|
||||
let mut inner_mut = self.inner_mut();
|
||||
inner_mut
|
||||
.dialogs
|
||||
.entry(webview.id())
|
||||
.or_default()
|
||||
.push(dialog);
|
||||
inner_mut.need_update = true;
|
||||
}
|
||||
|
||||
pub(crate) fn has_active_dialog(&self) -> bool {
|
||||
let last_created_webview_id = self
|
||||
.webview_collection()
|
||||
.newest()
|
||||
.map(|webview_view| webview_view.id());
|
||||
let Some(webview_id) = self
|
||||
.focused_webview()
|
||||
.as_ref()
|
||||
.map(WebView::id)
|
||||
.or(last_created_webview_id)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let inner = self.inner();
|
||||
inner
|
||||
.dialogs
|
||||
.get(&webview_id)
|
||||
.is_some_and(|dialogs| !dialogs.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn webview_has_active_dialog(&self, webview_id: WebViewId) -> bool {
|
||||
self.inner()
|
||||
.dialogs
|
||||
.get(&webview_id)
|
||||
.is_some_and(|dialogs| !dialogs.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_active_dialog_webdriver_type(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
) -> Option<WebDriverUserPrompt> {
|
||||
let inner = self.inner();
|
||||
let dialogs = inner.dialogs.get(&webview_id)?;
|
||||
dialogs
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|dialog| dialog.webdriver_dialog_type())
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
pub(crate) fn accept_active_dialogs(&self, webview_id: WebViewId) {
|
||||
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) {
|
||||
dialogs.drain(..).for_each(|dialog| {
|
||||
dialog.accept();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn dismiss_active_dialogs(&self, webview_id: WebViewId) {
|
||||
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) {
|
||||
dialogs.drain(..).for_each(|dialog| {
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn alert_text_of_newest_dialog(&self, webview_id: WebViewId) -> Option<String> {
|
||||
self.inner()
|
||||
.dialogs
|
||||
.get(&webview_id)
|
||||
.and_then(|dialogs| dialogs.last())
|
||||
.and_then(|dialog| dialog.message())
|
||||
}
|
||||
|
||||
pub(crate) fn set_alert_text_of_newest_dialog(&self, webview_id: WebViewId, text: String) {
|
||||
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview_id) {
|
||||
if let Some(dialog) = dialogs.last_mut() {
|
||||
dialog.set_message(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_simple_dialog(&self, webview: servo::WebView, dialog: SimpleDialog) {
|
||||
self.interrupt_webdriver_script_evaluation();
|
||||
|
||||
// Dialogs block the page load, so need need to notify WebDriver
|
||||
let webview_id = webview.id();
|
||||
if let Some(sender) = self
|
||||
.base()
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.get(&webview_id)
|
||||
{
|
||||
let _ = sender.send(WebDriverLoadStatus::Blocked);
|
||||
};
|
||||
|
||||
if self.servoshell_preferences().headless &&
|
||||
self.servoshell_preferences().webdriver_port.is_none()
|
||||
{
|
||||
// TODO: Avoid copying this from the default trait impl?
|
||||
// Return the DOM-specified default value for when we **cannot show simple dialogs**.
|
||||
let _ = match dialog {
|
||||
SimpleDialog::Alert {
|
||||
response_sender, ..
|
||||
} => response_sender.send(Default::default()),
|
||||
SimpleDialog::Confirm {
|
||||
response_sender, ..
|
||||
} => response_sender.send(Default::default()),
|
||||
SimpleDialog::Prompt {
|
||||
response_sender, ..
|
||||
} => response_sender.send(Default::default()),
|
||||
};
|
||||
return;
|
||||
}
|
||||
let dialog = Dialog::new_simple_dialog(dialog);
|
||||
self.add_dialog(webview, dialog);
|
||||
}
|
||||
|
||||
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
|
||||
let focused_id = self.webview_collection().focused_id()?;
|
||||
self.webviews()
|
||||
.iter()
|
||||
.position(|webview| webview.0 == focused_id)
|
||||
}
|
||||
|
||||
/// Interrupt any ongoing WebDriver-based script evaluation.
|
||||
///
|
||||
/// From <https://w3c.github.io/webdriver/#dfn-execute-a-function-body>:
|
||||
/// > The rules to execute a function body are as follows. The algorithm returns
|
||||
/// > an ECMAScript completion record.
|
||||
/// >
|
||||
/// > If at any point during the algorithm a user prompt appears, immediately return
|
||||
/// > Completion { Type: normal, Value: null, Target: empty }, but continue to run the
|
||||
/// > other steps of this algorithm in parallel.
|
||||
fn interrupt_webdriver_script_evaluation(&self) {
|
||||
if let Some(sender) = &self
|
||||
.base()
|
||||
.webdriver_senders
|
||||
.borrow()
|
||||
.script_evaluation_interrupt_sender
|
||||
{
|
||||
sender.send(Ok(JSValue::Null)).unwrap_or_else(|err| {
|
||||
info!(
|
||||
"Notify dialog appear failed. Maybe the channel to webdriver is closed: {err}"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a list of all webviews that have favicons that have not yet been loaded by egui.
|
||||
pub(crate) fn take_pending_favicon_loads(&self) -> Vec<WebViewId> {
|
||||
mem::take(&mut self.inner_mut().pending_favicon_loads)
|
||||
}
|
||||
}
|
||||
|
||||
struct ServoShellServoDelegate;
|
||||
impl ServoDelegate for ServoShellServoDelegate {
|
||||
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 screen_geometry(&self, _webview: WebView) -> Option<servo::ScreenGeometry> {
|
||||
Some(self.inner().window.screen_geometry())
|
||||
}
|
||||
|
||||
fn notify_status_text_changed(&self, _webview: servo::WebView, _status: Option<String>) {
|
||||
self.inner_mut().need_update = true;
|
||||
}
|
||||
|
||||
fn notify_history_changed(&self, _webview: WebView, _entries: Vec<Url>, _current: usize) {
|
||||
self.inner_mut().need_update = true;
|
||||
}
|
||||
|
||||
fn notify_page_title_changed(&self, webview: servo::WebView, _: Option<String>) {
|
||||
if webview.focused() {
|
||||
self.inner_mut().need_update = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_traversal_complete(&self, _webview: servo::WebView, traversal_id: TraversalId) {
|
||||
let mut webdriver_state = self.base().webdriver_senders.borrow_mut();
|
||||
if let Entry::Occupied(entry) = webdriver_state.pending_traversals.entry(traversal_id) {
|
||||
let sender = entry.remove();
|
||||
let _ = sender.send(WebDriverLoadStatus::Complete);
|
||||
}
|
||||
}
|
||||
|
||||
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, requested_outer_size: DeviceIntSize) {
|
||||
// We need to update compositor's view later as we not sure about resizing result.
|
||||
self.inner()
|
||||
.window
|
||||
.request_resize(&webview, requested_outer_size);
|
||||
}
|
||||
|
||||
fn request_authentication(
|
||||
&self,
|
||||
webview: WebView,
|
||||
authentication_request: AuthenticationRequest,
|
||||
) {
|
||||
if self.servoshell_preferences().headless &&
|
||||
self.servoshell_preferences().webdriver_port.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.add_dialog(
|
||||
webview,
|
||||
Dialog::new_authentication_dialog(authentication_request),
|
||||
);
|
||||
}
|
||||
|
||||
fn request_open_auxiliary_webview(
|
||||
&self,
|
||||
parent_webview: servo::WebView,
|
||||
) -> Option<servo::WebView> {
|
||||
let webview =
|
||||
WebViewBuilder::new_auxiliary(self.servo(), self.inner().window.rendering_context())
|
||||
.hidpi_scale_factor(self.inner().window.hidpi_scale_factor())
|
||||
.delegate(parent_webview.delegate())
|
||||
.build();
|
||||
|
||||
webview.notify_theme_change(self.inner().window.theme());
|
||||
// When WebDriver is enabled, do not focus and raise the WebView to the top,
|
||||
// as that is what the specification expects. Otherwise, we would like `window.open()`
|
||||
// to create a new foreground tab
|
||||
if self.servoshell_preferences().webdriver_port.is_none() {
|
||||
webview.focus_and_raise_to_top(true);
|
||||
}
|
||||
self.add_webview(webview.clone());
|
||||
Some(webview)
|
||||
}
|
||||
|
||||
fn notify_closed(&self, webview: servo::WebView) {
|
||||
self.close_webview(webview.id());
|
||||
}
|
||||
|
||||
fn notify_focus_changed(&self, webview: servo::WebView, focused: bool) {
|
||||
if focused {
|
||||
webview.show(true);
|
||||
self.inner_mut().need_update = true;
|
||||
self.webview_collection_mut()
|
||||
.set_focused(Some(webview.id()));
|
||||
} else if self.webview_collection().focused_id() == Some(webview.id()) {
|
||||
self.webview_collection_mut().set_focused(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_input_event_handled(
|
||||
&self,
|
||||
webview: WebView,
|
||||
id: InputEventId,
|
||||
result: InputEventResult,
|
||||
) {
|
||||
self.inner()
|
||||
.window
|
||||
.notify_input_event_handled(&webview, id, result);
|
||||
|
||||
if let Some(response_sender) = self
|
||||
.base()
|
||||
.pending_webdriver_events
|
||||
.borrow_mut()
|
||||
.remove(&id)
|
||||
{
|
||||
let _ = response_sender.send(());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if status == LoadStatus::Complete {
|
||||
if let Some(sender) = self
|
||||
.base()
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.remove(&webview.id())
|
||||
{
|
||||
let _ = sender.send(WebDriverLoadStatus::Complete);
|
||||
}
|
||||
self.maybe_request_screenshot(webview);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_fullscreen_state_changed(&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: GenericSender<Option<String>>,
|
||||
) {
|
||||
self.add_dialog(
|
||||
webview,
|
||||
Dialog::new_device_selection_dialog(devices, response_sender),
|
||||
);
|
||||
}
|
||||
|
||||
fn request_permission(&self, webview: servo::WebView, permission_request: PermissionRequest) {
|
||||
if self.servoshell_preferences().headless &&
|
||||
self.servoshell_preferences().webdriver_port.is_none()
|
||||
{
|
||||
permission_request.deny();
|
||||
return;
|
||||
}
|
||||
|
||||
let permission_dialog = Dialog::new_permission_request_dialog(permission_request);
|
||||
self.add_dialog(webview, permission_dialog);
|
||||
}
|
||||
|
||||
fn notify_new_frame_ready(&self, _webview: servo::WebView) {
|
||||
self.inner_mut().need_repaint = true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) {
|
||||
if self.servoshell_preferences().headless &&
|
||||
self.servoshell_preferences().webdriver_port.is_none()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let control_id = embedder_control.id();
|
||||
match embedder_control {
|
||||
EmbedderControl::SelectElement(prompt) => {
|
||||
// FIXME: Reading the toolbar height is needed here to properly position the select dialog.
|
||||
// But if the toolbar height changes while the dialog is open then the position won't be updated
|
||||
let offset = self.inner().window.toolbar_height();
|
||||
self.add_dialog(webview, Dialog::new_select_element_dialog(prompt, offset));
|
||||
},
|
||||
EmbedderControl::ColorPicker(color_picker) => {
|
||||
// FIXME: Reading the toolbar height is needed here to properly position the select dialog.
|
||||
// But if the toolbar height changes while the dialog is open then the position won't be updated
|
||||
let offset = self.inner().window.toolbar_height();
|
||||
self.add_dialog(
|
||||
webview,
|
||||
Dialog::new_color_picker_dialog(color_picker, offset),
|
||||
);
|
||||
},
|
||||
EmbedderControl::InputMethod(input_method_control) => {
|
||||
self.inner_mut().visible_input_methods.push(control_id);
|
||||
self.inner().window.show_ime(input_method_control);
|
||||
},
|
||||
EmbedderControl::FilePicker(file_picker) => {
|
||||
self.add_dialog(webview, Dialog::new_file_dialog(file_picker));
|
||||
},
|
||||
EmbedderControl::SimpleDialog(simple_dialog) => {
|
||||
self.show_simple_dialog(webview, simple_dialog);
|
||||
},
|
||||
EmbedderControl::ContextMenu(prompt) => {
|
||||
let offset = self.inner().window.toolbar_height();
|
||||
self.add_dialog(webview, Dialog::new_context_menu(prompt, offset));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_embedder_control(&self, webview: WebView, control_id: EmbedderControlId) {
|
||||
{
|
||||
let mut inner_mut = self.inner_mut();
|
||||
if let Some(index) = inner_mut
|
||||
.visible_input_methods
|
||||
.iter()
|
||||
.position(|visible_id| *visible_id == control_id)
|
||||
{
|
||||
inner_mut.visible_input_methods.remove(index);
|
||||
inner_mut.window.hide_ime();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dialogs) = self.inner_mut().dialogs.get_mut(&webview.id()) {
|
||||
dialogs.retain(|dialog| dialog.embedder_control_id() != Some(control_id));
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_favicon_changed(&self, webview: WebView) {
|
||||
let mut inner = self.inner_mut();
|
||||
inner.pending_favicon_loads.push(webview.id());
|
||||
inner.need_repaint = true;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ use servo::{
|
||||
AlertResponse, AuthenticationRequest, ColorPicker, ConfirmResponse, ContextMenu,
|
||||
ContextMenuItem, EmbedderControlId, FilePicker, PermissionRequest, PromptResponse, RgbColor,
|
||||
SelectElement, SelectElementOption, SelectElementOptionOrOptgroup, SimpleDialog,
|
||||
WebDriverUserPrompt,
|
||||
};
|
||||
|
||||
/// The minimum width of many UI elements including dialog boxes and menus,
|
||||
@@ -146,40 +145,6 @@ impl Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn accept(&self) {
|
||||
#[allow(clippy::single_match)]
|
||||
match self {
|
||||
Dialog::SimpleDialog(dialog) => {
|
||||
dialog.accept();
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dismiss(&self) {
|
||||
#[allow(clippy::single_match)]
|
||||
match self {
|
||||
Dialog::SimpleDialog(dialog) => {
|
||||
dialog.dismiss();
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message(&self) -> Option<String> {
|
||||
#[allow(clippy::single_match)]
|
||||
match self {
|
||||
Dialog::SimpleDialog(dialog) => Some(dialog.message().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_message(&mut self, text: String) {
|
||||
if let Dialog::SimpleDialog(dialog) = self {
|
||||
dialog.set_message(text);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns false if the dialog has been closed, or true otherwise.
|
||||
pub fn update(&mut self, ctx: &egui::Context) -> bool {
|
||||
match self {
|
||||
@@ -712,25 +677,6 @@ impl Dialog {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn webdriver_dialog_type(&self) -> Option<WebDriverUserPrompt> {
|
||||
// From <https://w3c.github.io/webdriver/#dfn-handle-any-user-prompts>
|
||||
// > Step 3: If the current user prompt is an alert dialog, set type to "alert". Otherwise,
|
||||
// > if the current user prompt is a beforeunload dialog, set type to
|
||||
// > "beforeUnload". Otherwise, if the current user prompt is a confirm dialog, set
|
||||
// > type to "confirm". Otherwise, if the current user prompt is a prompt dialog,
|
||||
// > set type to "prompt".
|
||||
match self {
|
||||
Dialog::SimpleDialog(SimpleDialog::Alert { .. }) => Some(WebDriverUserPrompt::Alert),
|
||||
Dialog::SimpleDialog(SimpleDialog::Confirm { .. }) => {
|
||||
Some(WebDriverUserPrompt::Confirm)
|
||||
},
|
||||
Dialog::SimpleDialog(SimpleDialog::Prompt { .. }) => Some(WebDriverUserPrompt::Prompt),
|
||||
Dialog::File { .. } => Some(WebDriverUserPrompt::File),
|
||||
Dialog::SelectElement { .. } => Some(WebDriverUserPrompt::Default),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn embedder_control_id(&self) -> Option<EmbedderControlId> {
|
||||
match self {
|
||||
Dialog::SelectElement { maybe_prompt, .. } => {
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::time;
|
||||
use log::warn;
|
||||
use servo::EventLoopWaker;
|
||||
use winit::event_loop::{EventLoop, EventLoop as WinitEventLoop, EventLoopProxy};
|
||||
use winit::window::WindowId;
|
||||
|
||||
use super::app::App;
|
||||
|
||||
@@ -26,6 +27,15 @@ impl From<egui_winit::accesskit_winit::Event> for AppEvent {
|
||||
}
|
||||
}
|
||||
|
||||
impl AppEvent {
|
||||
pub(crate) fn window_id(&self) -> Option<WindowId> {
|
||||
match self {
|
||||
AppEvent::Waker => None,
|
||||
AppEvent::Accessibility(event) => Some(event.window_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A headed or headless event loop. Headless event loops are necessary for environments without a
|
||||
/// display server. Ideally, we could use the headed winit event loop in both modes, but on Linux,
|
||||
/// the event loop requires a display server, which prevents running servoshell in a console.
|
||||
|
||||
@@ -25,11 +25,12 @@ use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
|
||||
use winit::window::Window;
|
||||
|
||||
use super::app_state::RunningAppState;
|
||||
use super::geometry::winit_position_to_euclid_point;
|
||||
use crate::desktop::event_loop::AppEvent;
|
||||
use crate::desktop::headed_window;
|
||||
use crate::prefs::{EXPERIMENTAL_PREFS, ServoShellPreferences};
|
||||
use crate::running_app_state::RunningAppStateTrait;
|
||||
use crate::running_app_state::RunningAppState;
|
||||
use crate::window::ServoShellWindow;
|
||||
|
||||
/// The user interface of a headed servoshell. Currently this is implemented via
|
||||
/// egui.
|
||||
@@ -94,7 +95,7 @@ impl Drop for Gui {
|
||||
}
|
||||
|
||||
impl Gui {
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
winit_window: &Window,
|
||||
event_loop: &ActiveEventLoop,
|
||||
event_loop_proxy: EventLoopProxy<AppEvent>,
|
||||
@@ -147,19 +148,12 @@ impl Gui {
|
||||
std::mem::take(&mut self.event_queue)
|
||||
}
|
||||
|
||||
pub fn on_window_event(
|
||||
pub(crate) fn on_window_event(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
app_state: &RunningAppState,
|
||||
winit_window: &Window,
|
||||
event: &WindowEvent,
|
||||
) -> EventResponse {
|
||||
let mut result = self.context.on_window_event(window, event);
|
||||
|
||||
if app_state.has_active_dialog() {
|
||||
result.consumed = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
let mut result = self.context.on_window_event(winit_window, event);
|
||||
result.consumed &= match event {
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
let scale = Scale::<_, DeviceIndependentPixel, _>::new(
|
||||
@@ -293,7 +287,12 @@ impl Gui {
|
||||
}
|
||||
|
||||
/// Update the user interface, but do not paint the updated state.
|
||||
pub fn update(&mut self, winit_window: &Window, state: &RunningAppState) {
|
||||
pub(crate) fn update(
|
||||
&mut self,
|
||||
state: &RunningAppState,
|
||||
window: &ServoShellWindow,
|
||||
headed_window: &headed_window::Window,
|
||||
) {
|
||||
let Self {
|
||||
rendering_context,
|
||||
context,
|
||||
@@ -305,8 +304,9 @@ impl Gui {
|
||||
..
|
||||
} = self;
|
||||
|
||||
let winit_window = headed_window.winit_window();
|
||||
context.run(winit_window, |ctx| {
|
||||
load_pending_favicons(ctx, state, favicon_textures);
|
||||
load_pending_favicons(ctx, window, favicon_textures);
|
||||
|
||||
// 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
|
||||
@@ -416,7 +416,7 @@ impl Gui {
|
||||
ui.available_size(),
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
for (id, webview) in state.webviews().into_iter() {
|
||||
for (id, webview) in window.webviews().into_iter() {
|
||||
let favicon = favicon_textures
|
||||
.get(&id)
|
||||
.map(|(_, favicon)| favicon)
|
||||
@@ -439,14 +439,14 @@ impl Gui {
|
||||
let scale =
|
||||
Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
|
||||
|
||||
state.for_each_active_dialog(|dialog| dialog.update(ctx));
|
||||
headed_window.for_each_active_dialog(window, |dialog| dialog.update(ctx));
|
||||
|
||||
// If the top parts of the GUI changed size, then update the size of the WebView and also
|
||||
// the size of its RenderingContext.
|
||||
let rect = ctx.available_rect();
|
||||
let size = Size2D::new(rect.width(), rect.height()) * scale;
|
||||
let rect = Box2D::from_origin_and_size(Point2D::origin(), size);
|
||||
if let Some(webview) = state.focused_webview() &&
|
||||
if let Some(webview) = window.focused_webview() &&
|
||||
rect != webview.rect()
|
||||
{
|
||||
webview.move_resize(rect);
|
||||
@@ -466,7 +466,7 @@ impl Gui {
|
||||
.show(|ui| ui.add(Label::new(status_text.clone()).extend()));
|
||||
}
|
||||
|
||||
state.repaint_servo_if_necessary();
|
||||
window.repaint_webviews();
|
||||
|
||||
if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
|
||||
ctx.layer_painter(LayerId::background()).add(PaintCallback {
|
||||
@@ -485,7 +485,7 @@ impl Gui {
|
||||
}
|
||||
|
||||
/// Paint the GUI, as of the last update.
|
||||
pub fn paint(&mut self, window: &Window) {
|
||||
pub(crate) fn paint(&mut self, window: &Window) {
|
||||
self.rendering_context
|
||||
.parent_context()
|
||||
.prepare_for_rendering();
|
||||
@@ -495,13 +495,13 @@ impl Gui {
|
||||
|
||||
/// Updates the location field from the given [`RunningAppState`], unless the user has started
|
||||
/// editing it without clicking Go, returning true iff it has changed (needing an egui update).
|
||||
fn update_location_in_toolbar(&mut self, state: &RunningAppState) -> bool {
|
||||
fn update_location_in_toolbar(&mut self, window: &ServoShellWindow) -> bool {
|
||||
// User edited without clicking Go?
|
||||
if self.location_dirty {
|
||||
return false;
|
||||
}
|
||||
|
||||
let current_url_string = state
|
||||
let current_url_string = window
|
||||
.focused_webview()
|
||||
.and_then(|webview| Some(webview.url()?.to_string()));
|
||||
match current_url_string {
|
||||
@@ -517,8 +517,8 @@ impl Gui {
|
||||
self.location_dirty = dirty;
|
||||
}
|
||||
|
||||
fn update_load_status(&mut self, state: &RunningAppState) -> bool {
|
||||
let state_status = state
|
||||
fn update_load_status(&mut self, window: &ServoShellWindow) -> bool {
|
||||
let state_status = window
|
||||
.focused_webview()
|
||||
.map(|webview| webview.load_status())
|
||||
.unwrap_or(LoadStatus::Complete);
|
||||
@@ -526,16 +526,16 @@ impl Gui {
|
||||
old_status != self.load_status
|
||||
}
|
||||
|
||||
fn update_status_text(&mut self, state: &RunningAppState) -> bool {
|
||||
let state_status = state
|
||||
fn update_status_text(&mut self, window: &ServoShellWindow) -> bool {
|
||||
let state_status = window
|
||||
.focused_webview()
|
||||
.and_then(|webview| webview.status_text());
|
||||
let old_status = std::mem::replace(&mut self.status_text, state_status);
|
||||
old_status != self.status_text
|
||||
}
|
||||
|
||||
fn update_can_go_back_and_forward(&mut self, state: &RunningAppState) -> bool {
|
||||
let (can_go_back, can_go_forward) = state
|
||||
fn update_can_go_back_and_forward(&mut self, window: &ServoShellWindow) -> bool {
|
||||
let (can_go_back, can_go_forward) = window
|
||||
.focused_webview()
|
||||
.map(|webview| (webview.can_go_back(), webview.can_go_forward()))
|
||||
.unwrap_or((false, false));
|
||||
@@ -544,17 +544,17 @@ impl Gui {
|
||||
old_can_go_back != self.can_go_back || old_can_go_forward != self.can_go_forward
|
||||
}
|
||||
|
||||
/// Updates all fields taken from the given [`RunningAppState`], such as the location field.
|
||||
/// Updates all fields taken from the given [`ServoShellWindow`], such as the location field.
|
||||
/// Returns true iff the egui needs an update.
|
||||
pub fn update_webview_data(&mut self, state: &RunningAppState) -> bool {
|
||||
pub(crate) fn update_webview_data(&mut self, window: &ServoShellWindow) -> bool {
|
||||
// Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
|
||||
// because logical OR would short-circuit if any of the functions return true.
|
||||
// We want to ensure that all functions are called. The "bitwise OR" operator
|
||||
// does not short-circuit.
|
||||
self.update_location_in_toolbar(state) |
|
||||
self.update_load_status(state) |
|
||||
self.update_status_text(state) |
|
||||
self.update_can_go_back_and_forward(state)
|
||||
self.update_location_in_toolbar(window) |
|
||||
self.update_load_status(window) |
|
||||
self.update_status_text(window) |
|
||||
self.update_can_go_back_and_forward(window)
|
||||
}
|
||||
|
||||
/// Returns true if a redraw is required after handling the provided event.
|
||||
@@ -619,11 +619,11 @@ fn embedder_image_to_egui_image(image: &Image) -> egui::ColorImage {
|
||||
/// Uploads all favicons that have not yet been processed to the GPU.
|
||||
fn load_pending_favicons(
|
||||
ctx: &egui::Context,
|
||||
state: &RunningAppState,
|
||||
window: &ServoShellWindow,
|
||||
texture_cache: &mut HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
|
||||
) {
|
||||
for id in state.take_pending_favicon_loads() {
|
||||
let Some(webview) = state.webview_by_id(id) else {
|
||||
for id in window.take_pending_favicon_loads() {
|
||||
let Some(webview) = window.webview_by_id(id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(favicon) = webview.favicon() else {
|
||||
|
||||
@@ -17,6 +17,7 @@ use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vec
|
||||
use keyboard_types::ShortcutMatcher;
|
||||
use log::{debug, info, warn};
|
||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
|
||||
use servo::base::generic_channel::GenericSender;
|
||||
use servo::servo_geometry::{
|
||||
DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel,
|
||||
};
|
||||
@@ -25,11 +26,12 @@ use servo::webrender_api::units::{
|
||||
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint,
|
||||
};
|
||||
use servo::{
|
||||
Cursor, ImeEvent, InputEvent, InputEventId, InputEventResult, InputMethodControl, Key,
|
||||
KeyState, KeyboardEvent, Modifiers, MouseButton as ServoMouseButton, MouseButtonAction,
|
||||
MouseButtonEvent, MouseLeftViewportEvent, MouseMoveEvent, NamedKey, OffscreenRenderingContext,
|
||||
RenderingContext, ScreenGeometry, Theme, TouchEvent, TouchEventType, TouchId,
|
||||
WebRenderDebugOption, WebView, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext,
|
||||
AuthenticationRequest, Cursor, EmbedderControl, EmbedderControlId, ImeEvent, InputEvent,
|
||||
InputEventId, InputEventResult, InputMethodControl, Key, KeyState, KeyboardEvent, Modifiers,
|
||||
MouseButton as ServoMouseButton, MouseButtonAction, MouseButtonEvent, MouseLeftViewportEvent,
|
||||
MouseMoveEvent, NamedKey, OffscreenRenderingContext, PermissionRequest, RenderingContext,
|
||||
ScreenGeometry, Theme, TouchEvent, TouchEventType, TouchId, WebRenderDebugOption, WebView,
|
||||
WebViewId, WheelDelta, WheelEvent, WheelMode, WindowRenderingContext,
|
||||
};
|
||||
use url::Url;
|
||||
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize};
|
||||
@@ -48,18 +50,20 @@ use {
|
||||
objc2_foundation::MainThreadMarker,
|
||||
};
|
||||
|
||||
use super::app_state::RunningAppState;
|
||||
use super::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
|
||||
use super::keyutils::{CMD_OR_ALT, keyboard_event_from_winit};
|
||||
use super::window_trait::{LINE_HEIGHT, LINE_WIDTH, WindowPortsMethods};
|
||||
use crate::desktop::accelerated_gl_media::setup_gl_accelerated_media;
|
||||
use crate::desktop::dialog::Dialog;
|
||||
use crate::desktop::event_loop::AppEvent;
|
||||
use crate::desktop::gui::{Gui, GuiCommand};
|
||||
use crate::desktop::keyutils::CMD_OR_CONTROL;
|
||||
use crate::desktop::window_trait::MIN_WINDOW_INNER_SIZE;
|
||||
use crate::parser::location_bar_input_to_url;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
use crate::running_app_state::RunningAppStateTrait;
|
||||
use crate::running_app_state::RunningAppState;
|
||||
use crate::window::{
|
||||
LINE_HEIGHT, LINE_WIDTH, MIN_WINDOW_INNER_SIZE, PlatformWindow, ServoShellWindow,
|
||||
ServoShellWindowId,
|
||||
};
|
||||
|
||||
pub(crate) const INITIAL_WINDOW_TITLE: &str = "Servo";
|
||||
|
||||
@@ -77,7 +81,6 @@ pub struct Window {
|
||||
device_pixel_ratio_override: Option<f32>,
|
||||
xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
|
||||
modifiers_state: Cell<ModifiersState>,
|
||||
|
||||
/// The `RenderingContext` of Servo itself. This is used to render Servo results
|
||||
/// temporarily until they can be blitted into the egui scene.
|
||||
rendering_context: Rc<OffscreenRenderingContext>,
|
||||
@@ -92,24 +95,26 @@ pub struct Window {
|
||||
/// When these are handled, they will optionally be used to trigger keybindings that
|
||||
/// are overridable by web content.
|
||||
pending_keyboard_events: RefCell<HashMap<InputEventId, KeyboardEvent>>,
|
||||
|
||||
// Keep this as the last field of the struct to ensure that the rendering context is
|
||||
// dropped first.
|
||||
// (https://github.com/servo/servo/issues/36711)
|
||||
winit_window: winit::window::Window,
|
||||
|
||||
/// The last title set on this window. We need to store this value here, as `winit::Window::title`
|
||||
/// is not supported very many platforms.
|
||||
last_title: RefCell<String>,
|
||||
/// The current set of open dialogs.
|
||||
dialogs: RefCell<HashMap<WebViewId, Vec<Dialog>>>,
|
||||
/// A list of showing [`InputMethod`] interfaces.
|
||||
visible_input_methods: RefCell<Vec<EmbedderControlId>>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub fn new(
|
||||
pub(crate) fn new(
|
||||
servoshell_preferences: &ServoShellPreferences,
|
||||
event_loop: &ActiveEventLoop,
|
||||
event_loop_proxy: EventLoopProxy<AppEvent>,
|
||||
initial_url: ServoUrl,
|
||||
) -> Window {
|
||||
) -> Rc<Self> {
|
||||
let no_native_titlebar = servoshell_preferences.no_native_titlebar;
|
||||
let inner_size = servoshell_preferences.initial_window_size;
|
||||
let window_attr = winit::window::Window::default_attributes()
|
||||
@@ -193,7 +198,7 @@ impl Window {
|
||||
));
|
||||
|
||||
debug!("Created window {:?}", winit_window.id());
|
||||
Window {
|
||||
Rc::new(Window {
|
||||
gui,
|
||||
winit_window,
|
||||
webview_relative_mouse_point: Cell::new(Point2D::zero()),
|
||||
@@ -211,18 +216,29 @@ impl Window {
|
||||
pending_keyboard_events: Default::default(),
|
||||
rendering_context,
|
||||
last_title: RefCell::new(String::from(INITIAL_WINDOW_TITLE)),
|
||||
}
|
||||
dialogs: Default::default(),
|
||||
visible_input_methods: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_keyboard_input(&self, state: Rc<RunningAppState>, winit_event: KeyEvent) {
|
||||
pub(crate) fn winit_window(&self) -> &winit::window::Window {
|
||||
&self.winit_window
|
||||
}
|
||||
|
||||
fn handle_keyboard_input(
|
||||
&self,
|
||||
state: Rc<RunningAppState>,
|
||||
window: &ServoShellWindow,
|
||||
winit_event: KeyEvent,
|
||||
) {
|
||||
// First, handle servoshell key bindings that are not overridable by, or visible to, the page.
|
||||
let keyboard_event = keyboard_event_from_winit(&winit_event, self.modifiers_state.get());
|
||||
if self.handle_intercepted_key_bindings(state.clone(), &keyboard_event) {
|
||||
if self.handle_intercepted_key_bindings(state.clone(), window, &keyboard_event) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Then we deliver character and keyboard events to the page in the focused webview.
|
||||
let Some(webview) = state.focused_webview() else {
|
||||
let Some(webview) = window.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -316,9 +332,10 @@ impl Window {
|
||||
fn handle_intercepted_key_bindings(
|
||||
&self,
|
||||
state: Rc<RunningAppState>,
|
||||
window: &ServoShellWindow,
|
||||
key_event: &KeyboardEvent,
|
||||
) -> bool {
|
||||
let Some(focused_webview) = state.focused_webview() else {
|
||||
let Some(focused_webview) = window.focused_webview() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -326,7 +343,7 @@ impl Window {
|
||||
ShortcutMatcher::from_event(key_event.event.clone())
|
||||
.shortcut(CMD_OR_CONTROL, 'R', || focused_webview.reload())
|
||||
.shortcut(CMD_OR_CONTROL, 'W', || {
|
||||
state.close_webview(focused_webview.id());
|
||||
window.close_webview(focused_webview.id());
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'P', || {
|
||||
let rate = env::var("SAMPLING_RATE")
|
||||
@@ -396,34 +413,35 @@ impl Window {
|
||||
|| focused_webview.exit_fullscreen(),
|
||||
)
|
||||
// Select the first 8 tabs via shortcuts
|
||||
.shortcut(CMD_OR_CONTROL, '1', || state.focus_webview_by_index(0))
|
||||
.shortcut(CMD_OR_CONTROL, '2', || state.focus_webview_by_index(1))
|
||||
.shortcut(CMD_OR_CONTROL, '3', || state.focus_webview_by_index(2))
|
||||
.shortcut(CMD_OR_CONTROL, '4', || state.focus_webview_by_index(3))
|
||||
.shortcut(CMD_OR_CONTROL, '5', || state.focus_webview_by_index(4))
|
||||
.shortcut(CMD_OR_CONTROL, '6', || state.focus_webview_by_index(5))
|
||||
.shortcut(CMD_OR_CONTROL, '7', || state.focus_webview_by_index(6))
|
||||
.shortcut(CMD_OR_CONTROL, '8', || state.focus_webview_by_index(7))
|
||||
.shortcut(CMD_OR_CONTROL, '1', || window.focus_webview_by_index(0))
|
||||
.shortcut(CMD_OR_CONTROL, '2', || window.focus_webview_by_index(1))
|
||||
.shortcut(CMD_OR_CONTROL, '3', || window.focus_webview_by_index(2))
|
||||
.shortcut(CMD_OR_CONTROL, '4', || window.focus_webview_by_index(3))
|
||||
.shortcut(CMD_OR_CONTROL, '5', || window.focus_webview_by_index(4))
|
||||
.shortcut(CMD_OR_CONTROL, '6', || window.focus_webview_by_index(5))
|
||||
.shortcut(CMD_OR_CONTROL, '7', || window.focus_webview_by_index(6))
|
||||
.shortcut(CMD_OR_CONTROL, '8', || window.focus_webview_by_index(7))
|
||||
// Cmd/Ctrl 9 is a bit different in that it focuses the last tab instead of the 9th
|
||||
.shortcut(CMD_OR_CONTROL, '9', || {
|
||||
let len = state.webviews().len();
|
||||
let len = window.webviews().len();
|
||||
if len > 0 {
|
||||
state.focus_webview_by_index(len - 1)
|
||||
window.focus_webview_by_index(len - 1)
|
||||
}
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageDown), || {
|
||||
if let Some(index) = state.get_focused_webview_index() {
|
||||
state.focus_webview_by_index((index + 1) % state.webviews().len())
|
||||
if let Some(index) = window.get_focused_webview_index() {
|
||||
window.focus_webview_by_index((index + 1) % window.webviews().len())
|
||||
}
|
||||
})
|
||||
.shortcut(Modifiers::CONTROL, Key::Named(NamedKey::PageUp), || {
|
||||
if let Some(index) = state.get_focused_webview_index() {
|
||||
let len = state.webviews().len();
|
||||
state.focus_webview_by_index((index + len - 1) % len);
|
||||
if let Some(index) = window.get_focused_webview_index() {
|
||||
let len = window.webviews().len();
|
||||
window.focus_webview_by_index((index + len - 1) % len);
|
||||
}
|
||||
})
|
||||
.shortcut(CMD_OR_CONTROL, 'T', || {
|
||||
state.create_and_focus_toplevel_webview(
|
||||
window.create_and_focus_toplevel_webview(
|
||||
state.clone(),
|
||||
Url::parse("servo:newtab")
|
||||
.expect("Should be able to unconditionally parse 'servo:newtab' as URL"),
|
||||
);
|
||||
@@ -450,7 +468,7 @@ impl Window {
|
||||
}
|
||||
|
||||
/// Takes any events generated during `egui` updates and performs their actions.
|
||||
fn handle_servoshell_ui_events(&self, state: Rc<RunningAppState>) {
|
||||
fn handle_servoshell_ui_events(&self, state: Rc<RunningAppState>, window: &ServoShellWindow) {
|
||||
let mut gui = self.gui.borrow_mut();
|
||||
for event in gui.take_commands() {
|
||||
match event {
|
||||
@@ -458,52 +476,122 @@ impl Window {
|
||||
gui.update_location_dirty(false);
|
||||
let Some(url) = location_bar_input_to_url(
|
||||
&location.clone(),
|
||||
&state.servoshell_preferences().searchpage,
|
||||
&state.servoshell_preferences.searchpage,
|
||||
) else {
|
||||
warn!("failed to parse location");
|
||||
break;
|
||||
};
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
if let Some(focused_webview) = window.focused_webview() {
|
||||
focused_webview.load(url.into_url());
|
||||
}
|
||||
},
|
||||
GuiCommand::Back => {
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
if let Some(focused_webview) = window.focused_webview() {
|
||||
focused_webview.go_back(1);
|
||||
}
|
||||
},
|
||||
GuiCommand::Forward => {
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
if let Some(focused_webview) = window.focused_webview() {
|
||||
focused_webview.go_forward(1);
|
||||
}
|
||||
},
|
||||
GuiCommand::Reload => {
|
||||
gui.update_location_dirty(false);
|
||||
if let Some(focused_webview) = state.focused_webview() {
|
||||
if let Some(focused_webview) = window.focused_webview() {
|
||||
focused_webview.reload();
|
||||
}
|
||||
},
|
||||
GuiCommand::ReloadAll => {
|
||||
gui.update_location_dirty(false);
|
||||
for (_, webview) in state.webviews() {
|
||||
for (_, webview) in window.webviews() {
|
||||
webview.reload();
|
||||
}
|
||||
},
|
||||
GuiCommand::NewWebView => {
|
||||
gui.update_location_dirty(false);
|
||||
let url = Url::parse("servo:newtab").expect("Should always be able to parse");
|
||||
state.create_and_focus_toplevel_webview(url);
|
||||
window.create_and_focus_toplevel_webview(state.clone(), url);
|
||||
},
|
||||
GuiCommand::CloseWebView(id) => {
|
||||
gui.update_location_dirty(false);
|
||||
state.close_webview(id);
|
||||
window.close_webview(id);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_ime(&self, input_method: InputMethodControl) {
|
||||
let position = input_method.position();
|
||||
self.winit_window.set_ime_allowed(true);
|
||||
self.winit_window.set_ime_cursor_area(
|
||||
LogicalPosition::new(
|
||||
position.min.x,
|
||||
position.min.y + (self.toolbar_height().0 as i32),
|
||||
),
|
||||
LogicalSize::new(
|
||||
position.max.x - position.min.x,
|
||||
position.max.y - position.min.y,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn for_each_active_dialog(
|
||||
&self,
|
||||
window: &ServoShellWindow,
|
||||
callback: impl Fn(&mut Dialog) -> bool,
|
||||
) {
|
||||
let Some(focused_webview) = window.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
let mut dialogs = self.dialogs.borrow_mut();
|
||||
let Some(dialogs) = dialogs.get_mut(&focused_webview.id()) else {
|
||||
return;
|
||||
};
|
||||
if dialogs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a dialog is open, clear any Servo cursor. TODO: This should restore the
|
||||
// cursor too, when all dialogs close. In general, we need a better cursor
|
||||
// management strategy.
|
||||
self.set_cursor(Cursor::Default);
|
||||
|
||||
let length = dialogs.len();
|
||||
dialogs.retain_mut(callback);
|
||||
if length != dialogs.len() {
|
||||
window.set_needs_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
fn add_dialog(&self, webview_id: WebViewId, dialog: Dialog) {
|
||||
self.dialogs
|
||||
.borrow_mut()
|
||||
.entry(webview_id)
|
||||
.or_default()
|
||||
.push(dialog)
|
||||
}
|
||||
|
||||
fn remove_dialog(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) {
|
||||
let mut dialogs = self.dialogs.borrow_mut();
|
||||
if let Some(dialogs) = dialogs.get_mut(&webview_id) {
|
||||
dialogs.retain(|dialog| dialog.embedder_control_id() != Some(embedder_control_id));
|
||||
}
|
||||
dialogs.retain(|_, dialogs| !dialogs.is_empty());
|
||||
}
|
||||
|
||||
fn has_active_dialog_for_webview(&self, webview_id: WebViewId) -> bool {
|
||||
// First lazily clean up any empty dialog vectors.
|
||||
let mut dialogs = self.dialogs.borrow_mut();
|
||||
dialogs.retain(|_, dialogs| !dialogs.is_empty());
|
||||
dialogs.contains_key(&webview_id)
|
||||
}
|
||||
|
||||
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
|
||||
self.gui.borrow().toolbar_height()
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPortsMethods for Window {
|
||||
impl PlatformWindow for Window {
|
||||
fn screen_geometry(&self) -> ScreenGeometry {
|
||||
let hidpi_factor = self.hidpi_scale_factor();
|
||||
let toolbar_size = Size2D::new(0.0, (self.toolbar_height() * self.hidpi_scale_factor()).0);
|
||||
@@ -536,12 +624,12 @@ impl WindowPortsMethods for Window {
|
||||
.unwrap_or_else(|| self.device_hidpi_scale_factor())
|
||||
}
|
||||
|
||||
fn rebuild_user_interface(&self, state: &RunningAppState) {
|
||||
self.gui.borrow_mut().update(&self.winit_window, state);
|
||||
fn rebuild_user_interface(&self, state: &RunningAppState, window: &ServoShellWindow) {
|
||||
self.gui.borrow_mut().update(state, window, self);
|
||||
}
|
||||
|
||||
fn update_user_interface_state(&self, state: &RunningAppState) -> bool {
|
||||
let title = state
|
||||
fn update_user_interface_state(&self, _: &RunningAppState, window: &ServoShellWindow) -> bool {
|
||||
let title = window
|
||||
.focused_webview()
|
||||
.and_then(|webview| {
|
||||
webview
|
||||
@@ -556,15 +644,21 @@ impl WindowPortsMethods for Window {
|
||||
*self.last_title.borrow_mut() = title;
|
||||
}
|
||||
|
||||
self.gui.borrow_mut().update_webview_data(state)
|
||||
self.gui.borrow_mut().update_webview_data(window)
|
||||
}
|
||||
|
||||
fn handle_winit_window_event(&self, state: Rc<RunningAppState>, event: WindowEvent) {
|
||||
fn handle_winit_window_event(
|
||||
&self,
|
||||
state: Rc<RunningAppState>,
|
||||
window: &ServoShellWindow,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
if event == WindowEvent::RedrawRequested {
|
||||
// WARNING: do not defer painting or presenting to some later tick of the event
|
||||
// loop or servoshell may become unresponsive! (servo#30312)
|
||||
let _ = self.rendering_context().make_current();
|
||||
let mut gui = self.gui.borrow_mut();
|
||||
gui.update(&self.winit_window, &state);
|
||||
gui.update(&state, window, self);
|
||||
gui.paint(&self.winit_window);
|
||||
}
|
||||
|
||||
@@ -587,20 +681,20 @@ impl WindowPortsMethods for Window {
|
||||
.borrow()
|
||||
.set_zoom_factor(effective_egui_zoom_factor);
|
||||
|
||||
state.hidpi_scale_factor_changed();
|
||||
window.hidpi_scale_factor_changed();
|
||||
|
||||
// Request a winit redraw event, so we can recomposite, update and paint
|
||||
// the GUI, and present the new frame.
|
||||
self.winit_window.request_redraw();
|
||||
},
|
||||
ref event => {
|
||||
let response =
|
||||
self.gui
|
||||
.borrow_mut()
|
||||
.on_window_event(&self.winit_window, &state, event);
|
||||
let response = self
|
||||
.gui
|
||||
.borrow_mut()
|
||||
.on_window_event(&self.winit_window, event);
|
||||
|
||||
if let WindowEvent::Resized(_) = event {
|
||||
self.rebuild_user_interface(&state);
|
||||
self.rebuild_user_interface(&state, window);
|
||||
}
|
||||
|
||||
if response.repaint && *event != WindowEvent::RedrawRequested {
|
||||
@@ -613,6 +707,19 @@ impl WindowPortsMethods for Window {
|
||||
},
|
||||
}
|
||||
|
||||
if matches!(
|
||||
event,
|
||||
WindowEvent::CursorMoved { .. } |
|
||||
WindowEvent::MouseInput { .. } |
|
||||
WindowEvent::MouseWheel { .. } |
|
||||
WindowEvent::KeyboardInput { .. }
|
||||
) && window
|
||||
.focused_webview()
|
||||
.is_some_and(|webview| self.has_active_dialog_for_webview(webview.id()))
|
||||
{
|
||||
consumed = true;
|
||||
}
|
||||
|
||||
if !consumed {
|
||||
// Make sure to handle early resize events even when there are no webviews yet
|
||||
if let WindowEvent::Resized(new_inner_size) = event {
|
||||
@@ -625,10 +732,10 @@ impl WindowPortsMethods for Window {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(webview) = state.focused_webview() {
|
||||
if let Some(webview) = window.focused_webview() {
|
||||
match event {
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
self.handle_keyboard_input(state.clone(), event)
|
||||
self.handle_keyboard_input(state.clone(), window, event)
|
||||
},
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
self.modifiers_state.set(modifiers.state())
|
||||
@@ -689,7 +796,7 @@ impl WindowPortsMethods for Window {
|
||||
);
|
||||
},
|
||||
WindowEvent::CloseRequested => {
|
||||
state.servo().start_shutting_down();
|
||||
window.schedule_close();
|
||||
},
|
||||
WindowEvent::ThemeChanged(theme) => {
|
||||
webview.notify_theme_change(match theme {
|
||||
@@ -732,10 +839,15 @@ impl WindowPortsMethods for Window {
|
||||
}
|
||||
|
||||
// Consume and handle any events from the servoshell UI.
|
||||
self.handle_servoshell_ui_events(state.clone());
|
||||
self.handle_servoshell_ui_events(state.clone(), window);
|
||||
}
|
||||
|
||||
fn handle_winit_app_event(&self, state: Rc<RunningAppState>, app_event: AppEvent) {
|
||||
fn handle_winit_app_event(
|
||||
&self,
|
||||
state: Rc<RunningAppState>,
|
||||
window: &ServoShellWindow,
|
||||
app_event: AppEvent,
|
||||
) {
|
||||
if let AppEvent::Accessibility(ref event) = app_event {
|
||||
if self
|
||||
.gui
|
||||
@@ -747,11 +859,20 @@ impl WindowPortsMethods for Window {
|
||||
}
|
||||
|
||||
// Consume and handle any events from user interface interaction.
|
||||
self.handle_servoshell_ui_events(state.clone());
|
||||
self.handle_servoshell_ui_events(state.clone(), window);
|
||||
}
|
||||
|
||||
fn request_repaint(&self, _: &RunningAppState) {
|
||||
fn request_repaint(&self, window: &ServoShellWindow) {
|
||||
self.winit_window.request_redraw();
|
||||
|
||||
// FIXME: This is a workaround for dialogs, which do not seem to animate, unless we
|
||||
// constantly repaint the egui scene.
|
||||
if window
|
||||
.focused_webview()
|
||||
.is_some_and(|webview| self.has_active_dialog_for_webview(webview.id()))
|
||||
{
|
||||
window.set_needs_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
fn request_resize(&self, _: &WebView, new_outer_size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
@@ -875,8 +996,9 @@ impl WindowPortsMethods for Window {
|
||||
self.winit_window.set_cursor_visible(true);
|
||||
}
|
||||
|
||||
fn id(&self) -> winit::window::WindowId {
|
||||
self.winit_window.id()
|
||||
fn id(&self) -> ServoShellWindowId {
|
||||
let id: u64 = self.winit_window.id().into();
|
||||
id.into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
@@ -903,33 +1025,10 @@ impl WindowPortsMethods for Window {
|
||||
Rc::new(XRWindow { winit_window, pose })
|
||||
}
|
||||
|
||||
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
|
||||
self.gui.borrow().toolbar_height()
|
||||
}
|
||||
|
||||
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
|
||||
self.rendering_context.clone()
|
||||
}
|
||||
|
||||
fn show_ime(&self, input_method: InputMethodControl) {
|
||||
let position = input_method.position();
|
||||
self.winit_window.set_ime_allowed(true);
|
||||
self.winit_window.set_ime_cursor_area(
|
||||
LogicalPosition::new(
|
||||
position.min.x,
|
||||
position.min.y + (self.toolbar_height().0 as i32),
|
||||
),
|
||||
LogicalSize::new(
|
||||
position.max.x - position.min.x,
|
||||
position.max.y - position.min.y,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fn hide_ime(&self) {
|
||||
self.winit_window.set_ime_allowed(false);
|
||||
}
|
||||
|
||||
fn theme(&self) -> servo::Theme {
|
||||
match self.winit_window.theme() {
|
||||
Some(winit::window::Theme::Dark) => servo::Theme::Dark,
|
||||
@@ -969,6 +1068,96 @@ impl WindowPortsMethods for Window {
|
||||
webview.set_page_zoom(1.0);
|
||||
});
|
||||
}
|
||||
|
||||
fn focused(&self) -> bool {
|
||||
self.winit_window.has_focus()
|
||||
}
|
||||
|
||||
fn show_embedder_control(&self, webview_id: WebViewId, embedder_control: EmbedderControl) {
|
||||
let control_id = embedder_control.id();
|
||||
match embedder_control {
|
||||
EmbedderControl::SelectElement(prompt) => {
|
||||
// FIXME: Reading the toolbar height is needed here to properly position the select dialog.
|
||||
// But if the toolbar height changes while the dialog is open then the position won't be updated
|
||||
let offset = self.gui.borrow().toolbar_height();
|
||||
self.add_dialog(
|
||||
webview_id,
|
||||
Dialog::new_select_element_dialog(prompt, offset),
|
||||
);
|
||||
},
|
||||
EmbedderControl::ColorPicker(color_picker) => {
|
||||
// FIXME: Reading the toolbar height is needed here to properly position the select dialog.
|
||||
// But if the toolbar height changes while the dialog is open then the position won't be updated
|
||||
let offset = self.gui.borrow().toolbar_height();
|
||||
self.add_dialog(
|
||||
webview_id,
|
||||
Dialog::new_color_picker_dialog(color_picker, offset),
|
||||
);
|
||||
},
|
||||
EmbedderControl::InputMethod(input_method_control) => {
|
||||
self.visible_input_methods.borrow_mut().push(control_id);
|
||||
self.show_ime(input_method_control);
|
||||
},
|
||||
EmbedderControl::FilePicker(file_picker) => {
|
||||
self.add_dialog(webview_id, Dialog::new_file_dialog(file_picker));
|
||||
},
|
||||
EmbedderControl::SimpleDialog(simple_dialog) => {
|
||||
self.add_dialog(webview_id, Dialog::new_simple_dialog(simple_dialog));
|
||||
},
|
||||
EmbedderControl::ContextMenu(prompt) => {
|
||||
let offset = self.gui.borrow().toolbar_height();
|
||||
self.add_dialog(webview_id, Dialog::new_context_menu(prompt, offset));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_embedder_control(&self, webview_id: WebViewId, embedder_control_id: EmbedderControlId) {
|
||||
{
|
||||
let mut visible_input_methods = self.visible_input_methods.borrow_mut();
|
||||
if let Some(index) = visible_input_methods
|
||||
.iter()
|
||||
.position(|visible_id| *visible_id == embedder_control_id)
|
||||
{
|
||||
visible_input_methods.remove(index);
|
||||
self.winit_window.set_ime_allowed(false);
|
||||
}
|
||||
}
|
||||
self.remove_dialog(webview_id, embedder_control_id);
|
||||
}
|
||||
|
||||
fn show_bluetooth_device_dialog(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
devices: Vec<String>,
|
||||
response_sender: GenericSender<Option<String>>,
|
||||
) {
|
||||
self.add_dialog(
|
||||
webview_id,
|
||||
Dialog::new_device_selection_dialog(devices, response_sender),
|
||||
);
|
||||
}
|
||||
|
||||
fn show_permission_dialog(&self, webview_id: WebViewId, permission_request: PermissionRequest) {
|
||||
self.add_dialog(
|
||||
webview_id,
|
||||
Dialog::new_permission_request_dialog(permission_request),
|
||||
);
|
||||
}
|
||||
|
||||
fn show_http_authentication_dialog(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
authentication_request: AuthenticationRequest,
|
||||
) {
|
||||
self.add_dialog(
|
||||
webview_id,
|
||||
Dialog::new_authentication_dialog(authentication_request),
|
||||
);
|
||||
}
|
||||
|
||||
fn dismiss_embedder_controls_for_webview(&self, webview_id: WebViewId) {
|
||||
self.dialogs.borrow_mut().remove(&webview_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn winit_phase_to_touch_event_type(phase: TouchPhase) -> TouchEventType {
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
use std::cell::Cell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use euclid::num::Zero;
|
||||
use euclid::{Length, Point2D, Scale, Size2D};
|
||||
use euclid::{Point2D, Scale, Size2D};
|
||||
use servo::servo_geometry::{
|
||||
DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel,
|
||||
};
|
||||
@@ -19,9 +18,8 @@ use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize,
|
||||
use servo::{RenderingContext, ScreenGeometry, SoftwareRenderingContext, WebView};
|
||||
use winit::dpi::PhysicalSize;
|
||||
|
||||
use super::app_state::RunningAppState;
|
||||
use crate::desktop::window_trait::{MIN_WINDOW_INNER_SIZE, WindowPortsMethods};
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
use crate::window::{MIN_WINDOW_INNER_SIZE, PlatformWindow, ServoShellWindow, ServoShellWindowId};
|
||||
|
||||
pub struct Window {
|
||||
fullscreen: Cell<bool>,
|
||||
@@ -34,8 +32,7 @@ pub struct Window {
|
||||
}
|
||||
|
||||
impl Window {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc<dyn WindowPortsMethods> {
|
||||
pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc<Self> {
|
||||
let size = servoshell_preferences.initial_window_size;
|
||||
|
||||
let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override;
|
||||
@@ -67,9 +64,9 @@ impl Window {
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowPortsMethods for Window {
|
||||
fn id(&self) -> winit::window::WindowId {
|
||||
winit::window::WindowId::dummy()
|
||||
impl PlatformWindow for Window {
|
||||
fn id(&self) -> ServoShellWindowId {
|
||||
0.into()
|
||||
}
|
||||
|
||||
fn screen_geometry(&self) -> servo::ScreenGeometry {
|
||||
@@ -87,8 +84,8 @@ impl WindowPortsMethods for Window {
|
||||
self.window_position.set(point);
|
||||
}
|
||||
|
||||
fn request_repaint(&self, state: &RunningAppState) {
|
||||
state.repaint_servo_if_necessary();
|
||||
fn request_repaint(&self, window: &ServoShellWindow) {
|
||||
window.repaint_webviews();
|
||||
}
|
||||
|
||||
fn request_resize(&self, webview: &WebView, new_size: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
@@ -138,10 +135,6 @@ impl WindowPortsMethods for Window {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
|
||||
Length::zero()
|
||||
}
|
||||
|
||||
fn window_rect(&self) -> DeviceIndependentIntRect {
|
||||
convert_rect_to_css_pixel(
|
||||
DeviceIntRect::from_origin_and_size(self.window_position.get(), self.inner_size.get()),
|
||||
@@ -165,4 +158,8 @@ impl WindowPortsMethods for Window {
|
||||
self.screen_size.height as u32,
|
||||
));
|
||||
}
|
||||
|
||||
fn focused(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
mod accelerated_gl_media;
|
||||
pub(crate) mod app;
|
||||
mod app_state;
|
||||
pub(crate) mod cli;
|
||||
mod dialog;
|
||||
pub(crate) mod dialog;
|
||||
pub(crate) mod event_loop;
|
||||
mod gamepad;
|
||||
pub(crate) mod gamepad;
|
||||
pub mod geometry;
|
||||
mod gui;
|
||||
mod headed_window;
|
||||
@@ -20,4 +19,3 @@ mod protocols;
|
||||
mod tracing;
|
||||
#[cfg(feature = "webxr")]
|
||||
mod webxr;
|
||||
mod window_trait;
|
||||
|
||||
@@ -15,7 +15,7 @@ use servo::webxr::glwindow::GlWindowDiscovery;
|
||||
use servo::webxr::openxr::{AppInfo, OpenXrDiscovery};
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
|
||||
use super::window_trait::WindowPortsMethods;
|
||||
use crate::window::PlatformWindow;
|
||||
|
||||
enum XrDiscovery {
|
||||
GlWindow(GlWindowDiscovery),
|
||||
@@ -29,7 +29,7 @@ pub(crate) struct XrDiscoveryWebXrRegistry {
|
||||
|
||||
impl XrDiscoveryWebXrRegistry {
|
||||
pub(crate) fn new_boxed(
|
||||
window: Rc<dyn WindowPortsMethods>,
|
||||
window: Rc<dyn PlatformWindow>,
|
||||
event_loop: Option<&ActiveEventLoop>,
|
||||
preferences: &Preferences,
|
||||
) -> Box<Self> {
|
||||
|
||||
@@ -1,92 +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/. */
|
||||
|
||||
//! Definition of Window.
|
||||
//! Implemented by headless and headed windows.
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use euclid::{Length, Scale};
|
||||
use servo::servo_geometry::{DeviceIndependentIntRect, DeviceIndependentPixel};
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
|
||||
use servo::{
|
||||
Cursor, InputEventId, InputEventResult, InputMethodControl, RenderingContext, ScreenGeometry,
|
||||
WebView,
|
||||
};
|
||||
use winit::event::WindowEvent;
|
||||
|
||||
use super::app_state::RunningAppState;
|
||||
use crate::desktop::event_loop::AppEvent;
|
||||
|
||||
// This should vary by zoom level and maybe actual text size (focused or under cursor)
|
||||
pub(crate) const LINE_HEIGHT: f32 = 76.0;
|
||||
pub(crate) const LINE_WIDTH: f32 = 76.0;
|
||||
|
||||
/// <https://github.com/web-platform-tests/wpt/blob/9320b1f724632c52929a3fdb11bdaf65eafc7611/webdriver/tests/classic/set_window_rect/set.py#L287-L290>
|
||||
/// "A window size of 10x10px shouldn't be supported by any browser."
|
||||
pub(crate) const MIN_WINDOW_INNER_SIZE: DeviceIntSize = DeviceIntSize::new(100, 100);
|
||||
|
||||
pub trait WindowPortsMethods {
|
||||
fn id(&self) -> winit::window::WindowId;
|
||||
fn screen_geometry(&self) -> ScreenGeometry;
|
||||
fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
|
||||
fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
|
||||
fn get_fullscreen(&self) -> bool;
|
||||
/// Request that the `Window` rebuild its user interface, if it has one. This should
|
||||
/// not repaint, but should prepare the user interface for painting when it is
|
||||
/// actually requested.
|
||||
fn rebuild_user_interface(&self, _: &RunningAppState) {}
|
||||
/// Inform the `Window` that the state of a `WebView` has changed and that it should
|
||||
/// do an incremental update of user interface state. Returns `true` if the user
|
||||
/// interface actually changed and a rebuild and repaint is needed, `false` otherwise.
|
||||
fn update_user_interface_state(&self, _: &RunningAppState) -> bool {
|
||||
false
|
||||
}
|
||||
/// Handle a winit [`WindowEvent`]. Returns `true` if the event loop should continue
|
||||
/// and `false` otherwise.
|
||||
///
|
||||
/// TODO: This should be handled internally in the winit window if possible so that it
|
||||
/// makes more sense when we are mixing headed and headless windows.
|
||||
fn handle_winit_window_event(&self, _: Rc<RunningAppState>, _: WindowEvent) {}
|
||||
/// Handle a winit [`AppEvent`]. Returns `true` if the event loop should continue and
|
||||
/// `false` otherwise.
|
||||
///
|
||||
/// TODO: This should be handled internally in the winit window if possible so that it
|
||||
/// makes more sense when we are mixing headed and headless windows.
|
||||
fn handle_winit_app_event(&self, _: Rc<RunningAppState>, _: AppEvent) {}
|
||||
/// Request that the window redraw itself. It is up to the window to do this
|
||||
/// once the windowing system is ready. If this is a headless window, the redraw
|
||||
/// will happen immediately.
|
||||
fn request_repaint(&self, _: &RunningAppState);
|
||||
/// Request a new outer size for the window, including external decorations.
|
||||
/// This should be the same as `window.outerWidth` and `window.outerHeight``
|
||||
fn request_resize(&self, webview: &WebView, outer_size: DeviceIntSize)
|
||||
-> Option<DeviceIntSize>;
|
||||
fn set_position(&self, _point: DeviceIntPoint) {}
|
||||
fn set_fullscreen(&self, _state: bool) {}
|
||||
fn set_cursor(&self, _cursor: Cursor) {}
|
||||
#[cfg(feature = "webxr")]
|
||||
fn new_glwindow(
|
||||
&self,
|
||||
event_loop: &winit::event_loop::ActiveEventLoop,
|
||||
) -> Rc<dyn servo::webxr::glwindow::GlWindow>;
|
||||
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel>;
|
||||
/// This returns [`RenderingContext`] matching the viewport.
|
||||
fn rendering_context(&self) -> Rc<dyn RenderingContext>;
|
||||
fn show_ime(&self, _input_method: InputMethodControl) {}
|
||||
fn hide_ime(&self) {}
|
||||
fn theme(&self) -> servo::Theme {
|
||||
servo::Theme::Light
|
||||
}
|
||||
fn window_rect(&self) -> DeviceIndependentIntRect;
|
||||
fn maximize(&self, webview: &WebView);
|
||||
|
||||
fn notify_input_event_handled(
|
||||
&self,
|
||||
_webview: &WebView,
|
||||
_id: InputEventId,
|
||||
_result: InputEventResult,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,52 @@
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod resources;
|
||||
mod simpleservo;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::mem;
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use android_logger::{self, Config, FilterBuilder};
|
||||
use dpi::PhysicalSize;
|
||||
use euclid::{Point2D, Rect, Size2D};
|
||||
use jni::objects::{GlobalRef, JClass, JObject, JString, JValue, JValueOwned};
|
||||
use jni::sys::{jboolean, jfloat, jint, jobject};
|
||||
use jni::{JNIEnv, JavaVM};
|
||||
use keyboard_types::{Key, NamedKey};
|
||||
use log::{debug, error, info, warn};
|
||||
use raw_window_handle::{
|
||||
AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
|
||||
AndroidDisplayHandle, AndroidNdkWindowHandle, DisplayHandle, RawDisplayHandle, RawWindowHandle,
|
||||
WindowHandle,
|
||||
};
|
||||
use resources::ResourceReaderInstance;
|
||||
use servo::webrender_api::units::DevicePixel;
|
||||
use servo::{
|
||||
AlertResponse, EventLoopWaker, InputMethodControl, LoadStatus, MediaSessionActionType,
|
||||
MouseButton, PermissionRequest, PrefValue, SimpleDialog, WebView,
|
||||
self, EventLoopWaker, InputMethodControl, LoadStatus, MediaSessionActionType, MouseButton,
|
||||
PrefValue,
|
||||
};
|
||||
use simpleservo::{APP, InitOptions, MediaSessionPlaybackState};
|
||||
pub use servo::{MediaSessionPlaybackState, WindowRenderingContext};
|
||||
|
||||
use super::app_state::{Coordinates, RunningAppState};
|
||||
use super::app::{App, AppInitOptions, VsyncRefreshDriver};
|
||||
use super::host_trait::HostTrait;
|
||||
use crate::prefs::EXPERIMENTAL_PREFS;
|
||||
use crate::running_app_state::RunningAppStateTrait;
|
||||
use crate::prefs::{ArgumentParsingResult, EXPERIMENTAL_PREFS, parse_command_line_arguments};
|
||||
|
||||
thread_local! {
|
||||
pub static APP: RefCell<Option<Rc<App>>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
struct InitOptions {
|
||||
args: Vec<String>,
|
||||
url: Option<String>,
|
||||
viewport_rect: Rect<i32, DevicePixel>,
|
||||
density: f32,
|
||||
#[cfg(feature = "webxr")]
|
||||
xr_discovery: Option<servo::webxr::Discovery>,
|
||||
window_handle: RawWindowHandle,
|
||||
display_handle: RawDisplayHandle,
|
||||
}
|
||||
|
||||
struct HostCallbacks {
|
||||
callbacks: GlobalRef,
|
||||
@@ -51,10 +72,10 @@ pub extern "C" fn android_main() {
|
||||
|
||||
fn call<F>(env: &mut JNIEnv, f: F)
|
||||
where
|
||||
F: FnOnce(&RunningAppState),
|
||||
F: FnOnce(&App),
|
||||
{
|
||||
APP.with(|app| match app.borrow().as_ref() {
|
||||
Some(ref app_state) => (f)(app_state),
|
||||
Some(app) => (f)(app),
|
||||
None => throw(env, "Servo not available in this thread"),
|
||||
});
|
||||
}
|
||||
@@ -64,11 +85,13 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_version<'local>(
|
||||
env: JNIEnv<'local>,
|
||||
_class: JClass<'local>,
|
||||
) -> JString<'local> {
|
||||
let v = crate::VERSION;
|
||||
env.new_string(&v)
|
||||
let version = crate::VERSION;
|
||||
env.new_string(version)
|
||||
.unwrap_or_else(|_str| JObject::null().into())
|
||||
}
|
||||
|
||||
/// Initialize Servo. At that point, we need a valid GL context. In the future, this will
|
||||
/// be done in multiple steps.
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>(
|
||||
mut env: JNIEnv<'local>,
|
||||
@@ -78,7 +101,8 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>(
|
||||
callbacks_obj: JObject<'local>,
|
||||
surface: JObject<'local>,
|
||||
) {
|
||||
let (opts, log, log_str, _gst_debug_str) = match get_options(&mut env, &opts, &surface) {
|
||||
let (mut init_opts, log, log_str, _gst_debug_str) = match get_options(&mut env, &opts, &surface)
|
||||
{
|
||||
Ok((opts, log, log_str, gst_debug_str)) => (opts, log, log_str, gst_debug_str),
|
||||
Err(err) => {
|
||||
throw(&mut env, &err);
|
||||
@@ -141,12 +165,68 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_init<'local>(
|
||||
},
|
||||
};
|
||||
|
||||
let wakeup = Box::new(WakeupCallback::new(callbacks_ref.clone(), &env));
|
||||
let callbacks = Box::new(HostCallbacks::new(callbacks_ref, &env));
|
||||
let event_loop_waker = Box::new(WakeupCallback::new(callbacks_ref.clone(), &env));
|
||||
let host = Box::new(HostCallbacks::new(callbacks_ref, &env));
|
||||
|
||||
if let Err(err) = simpleservo::init(opts, wakeup, callbacks) {
|
||||
throw(&mut env, err)
|
||||
crate::init_crypto();
|
||||
servo::resources::set(Box::new(ResourceReaderInstance::new()));
|
||||
|
||||
// `parse_command_line_arguments` expects the first argument to be the binary name.
|
||||
let mut args = mem::take(&mut init_opts.args);
|
||||
args.insert(0, "servo".to_string());
|
||||
|
||||
let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) {
|
||||
ArgumentParsingResult::ContentProcess(..) => {
|
||||
unreachable!("Android does not have support for multiprocess yet.")
|
||||
},
|
||||
ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
|
||||
(opts, preferences, servoshell_preferences)
|
||||
},
|
||||
ArgumentParsingResult::Exit => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
ArgumentParsingResult::ErrorParsing => std::process::exit(1),
|
||||
};
|
||||
|
||||
preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true));
|
||||
|
||||
crate::init_tracing(servoshell_preferences.tracing_filter.as_deref());
|
||||
|
||||
let (display_handle, window_handle) = unsafe {
|
||||
(
|
||||
DisplayHandle::borrow_raw(init_opts.display_handle),
|
||||
WindowHandle::borrow_raw(init_opts.window_handle),
|
||||
)
|
||||
};
|
||||
|
||||
let size = init_opts.viewport_rect.size;
|
||||
let refresh_driver = Rc::new(VsyncRefreshDriver::default());
|
||||
let rendering_context = Rc::new(
|
||||
WindowRenderingContext::new_with_refresh_driver(
|
||||
display_handle,
|
||||
window_handle,
|
||||
PhysicalSize::new(size.width as u32, size.height as u32),
|
||||
refresh_driver.clone(),
|
||||
)
|
||||
.expect("Could not create RenderingContext"),
|
||||
);
|
||||
|
||||
APP.with(|app| {
|
||||
*app.borrow_mut() = Some(App::new(AppInitOptions {
|
||||
host,
|
||||
event_loop_waker,
|
||||
viewport_rect: init_opts.viewport_rect,
|
||||
hidpi_scale_factor: init_opts.density,
|
||||
rendering_context,
|
||||
refresh_driver,
|
||||
initial_url: init_opts.url,
|
||||
opts,
|
||||
preferences,
|
||||
servoshell_preferences,
|
||||
#[cfg(feature = "webxr")]
|
||||
xr_discovery: init_opts.xr_discovery,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -178,7 +258,9 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_deinit<'local>(
|
||||
_class: JClass<'local>,
|
||||
) {
|
||||
debug!("deinit");
|
||||
simpleservo::deinit();
|
||||
APP.with(|app| {
|
||||
*app.borrow_mut() = None;
|
||||
});
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -187,10 +269,10 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_resize<'local>(
|
||||
_: JClass<'local>,
|
||||
coordinates: JObject<'local>,
|
||||
) {
|
||||
let coords = jni_coords_to_rust_coords(&mut env, &coordinates);
|
||||
debug!("resize {:#?}", coords);
|
||||
match coords {
|
||||
Ok(coords) => call(&mut env, |s| s.resize(coords.clone())),
|
||||
let viewport_rect = jni_coordinate_to_rust_viewport_rect(&mut env, &coordinates);
|
||||
debug!("resize {viewport_rect:#?}");
|
||||
match viewport_rect {
|
||||
Ok(viewport_rect) => call(&mut env, |s| s.resize(viewport_rect)),
|
||||
Err(error) => throw(&mut env, &error),
|
||||
}
|
||||
}
|
||||
@@ -201,9 +283,8 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_performUpdates<'local>(
|
||||
_class: JClass<'local>,
|
||||
) {
|
||||
debug!("performUpdates");
|
||||
call(&mut env, |s| {
|
||||
s.perform_updates();
|
||||
s.present_if_needed();
|
||||
call(&mut env, |app| {
|
||||
app.spin_event_loop();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -474,14 +555,14 @@ pub extern "C" fn Java_org_servo_servoview_JNIServo_resumeCompositor<'local>(
|
||||
coordinates: JObject<'local>,
|
||||
) {
|
||||
debug!("resumeCompositor");
|
||||
let coords = match jni_coords_to_rust_coords(&mut env, &coordinates) {
|
||||
Ok(coords) => coords,
|
||||
let viewport_rect = match jni_coordinate_to_rust_viewport_rect(&mut env, &coordinates) {
|
||||
Ok(viewport_rect) => viewport_rect,
|
||||
Err(error) => return throw(&mut env, &error),
|
||||
};
|
||||
|
||||
let (_, window_handle) = display_and_window_handle(&mut env, &surface);
|
||||
call(&mut env, |s| {
|
||||
s.resume_compositor(window_handle, coords.clone())
|
||||
call(&mut env, |app| {
|
||||
app.resume_compositor(window_handle, viewport_rect);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -540,7 +621,9 @@ impl HostCallbacks {
|
||||
let jvm = env.get_java_vm().unwrap();
|
||||
HostCallbacks { callbacks, jvm }
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for HostCallbacks {
|
||||
fn show_alert(&self, message: String) {
|
||||
let mut env = self.jvm.get_env().unwrap();
|
||||
let Ok(string) = new_string_as_jvalue(&mut env, &message) else {
|
||||
@@ -554,44 +637,6 @@ impl HostCallbacks {
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for HostCallbacks {
|
||||
fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
|
||||
warn!("Permissions prompt not implemented. Denied.");
|
||||
request.deny();
|
||||
}
|
||||
|
||||
fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog) {
|
||||
let _ = match dialog {
|
||||
SimpleDialog::Alert {
|
||||
message,
|
||||
response_sender,
|
||||
..
|
||||
} => {
|
||||
debug!("SimpleDialog::Alert");
|
||||
// TODO: Indicate that this message is untrusted, and what origin it came from.
|
||||
self.show_alert(message);
|
||||
response_sender.send(AlertResponse::Ok)
|
||||
},
|
||||
SimpleDialog::Confirm {
|
||||
message,
|
||||
response_sender,
|
||||
..
|
||||
} => {
|
||||
warn!("Confirm dialog not implemented. Cancelled. {}", message);
|
||||
response_sender.send(Default::default())
|
||||
},
|
||||
SimpleDialog::Prompt {
|
||||
message,
|
||||
response_sender,
|
||||
..
|
||||
} => {
|
||||
warn!("Prompt dialog not implemented. Cancelled. {}", message);
|
||||
response_sender.send(Default::default())
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn notify_load_status_changed(&self, load_status: LoadStatus) {
|
||||
debug!("notify_load_status_changed: {load_status:?}");
|
||||
@@ -632,24 +677,6 @@ impl HostTrait for HostCallbacks {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn on_allow_navigation(&self, url: String) -> bool {
|
||||
debug!("on_allow_navigation");
|
||||
let mut env = self.jvm.get_env().unwrap();
|
||||
let Ok(url_string) = new_string_as_jvalue(&mut env, &url) else {
|
||||
return false;
|
||||
};
|
||||
let allow = env.call_method(
|
||||
self.callbacks.as_obj(),
|
||||
"onAllowNavigation",
|
||||
"(Ljava/lang/String;)Z",
|
||||
&[(&url_string).into()],
|
||||
);
|
||||
match allow {
|
||||
Ok(allow) => allow.z().unwrap(),
|
||||
Err(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_url_changed(&self, url: String) {
|
||||
debug!("on_url_changed");
|
||||
let mut env = self.jvm.get_env().unwrap();
|
||||
@@ -783,10 +810,10 @@ fn new_string_as_jvalue<'local>(
|
||||
Ok(JValueOwned::from(jstring))
|
||||
}
|
||||
|
||||
fn jni_coords_to_rust_coords<'local>(
|
||||
fn jni_coordinate_to_rust_viewport_rect<'local>(
|
||||
env: &mut JNIEnv<'local>,
|
||||
obj: &JObject<'local>,
|
||||
) -> Result<Coordinates, String> {
|
||||
) -> Result<Rect<i32, DevicePixel>, String> {
|
||||
let x = get_non_null_field(env, obj, "x", "I")?
|
||||
.i()
|
||||
.map_err(|_| "x not an int")? as i32;
|
||||
@@ -799,7 +826,7 @@ fn jni_coords_to_rust_coords<'local>(
|
||||
let height = get_non_null_field(env, obj, "height", "I")?
|
||||
.i()
|
||||
.map_err(|_| "height not an int")? as i32;
|
||||
Ok(Coordinates::new(x, y, width, height))
|
||||
Ok(Rect::new(Point2D::new(x, y), Size2D::new(width, height)))
|
||||
}
|
||||
|
||||
fn get_field<'local>(
|
||||
@@ -877,7 +904,7 @@ fn get_options<'local>(
|
||||
)?
|
||||
.l()
|
||||
.map_err(|_| "coordinates is not an object")?;
|
||||
let coordinates = jni_coords_to_rust_coords(env, &coordinates)?;
|
||||
let viewport_rect = jni_coordinate_to_rust_viewport_rect(env, &coordinates)?;
|
||||
|
||||
let mut args: Vec<String> = match args {
|
||||
Some(args) => serde_json::from_str(&args)
|
||||
@@ -893,7 +920,7 @@ fn get_options<'local>(
|
||||
let opts = InitOptions {
|
||||
args,
|
||||
url,
|
||||
coordinates,
|
||||
viewport_rect,
|
||||
density,
|
||||
xr_discovery: None,
|
||||
window_handle,
|
||||
@@ -1,138 +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::cell::RefCell;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crossbeam_channel::unbounded;
|
||||
use dpi::PhysicalSize;
|
||||
use raw_window_handle::{DisplayHandle, RawDisplayHandle, RawWindowHandle, WindowHandle};
|
||||
use servo::{self, EventLoopWaker, ServoBuilder, resources};
|
||||
pub use servo::{MediaSessionPlaybackState, WindowRenderingContext};
|
||||
|
||||
use crate::egl::android::resources::ResourceReaderInstance;
|
||||
#[cfg(feature = "webxr")]
|
||||
use crate::egl::app_state::XrDiscoveryWebXrRegistry;
|
||||
use crate::egl::app_state::{
|
||||
Coordinates, RunningAppState, ServoWindowCallbacks, VsyncRefreshDriver,
|
||||
};
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments};
|
||||
|
||||
thread_local! {
|
||||
pub static APP: RefCell<Option<Rc<RunningAppState>>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub struct InitOptions {
|
||||
pub args: Vec<String>,
|
||||
pub url: Option<String>,
|
||||
pub coordinates: Coordinates,
|
||||
pub density: f32,
|
||||
#[cfg(feature = "webxr")]
|
||||
pub xr_discovery: Option<servo::webxr::Discovery>,
|
||||
pub window_handle: RawWindowHandle,
|
||||
pub display_handle: RawDisplayHandle,
|
||||
}
|
||||
|
||||
/// Initialize Servo. At that point, we need a valid GL context.
|
||||
/// In the future, this will be done in multiple steps.
|
||||
pub fn init(
|
||||
mut init_opts: InitOptions,
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
callbacks: Box<dyn HostTrait>,
|
||||
) -> Result<(), &'static str> {
|
||||
crate::init_crypto();
|
||||
resources::set(Box::new(ResourceReaderInstance::new()));
|
||||
|
||||
// `parse_command_line_arguments` expects the first argument to be the binary name.
|
||||
let mut args = mem::take(&mut init_opts.args);
|
||||
args.insert(0, "servo".to_string());
|
||||
|
||||
let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) {
|
||||
ArgumentParsingResult::ContentProcess(..) => {
|
||||
unreachable!("Android does not have support for multiprocess yet.")
|
||||
},
|
||||
ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
|
||||
(opts, preferences, servoshell_preferences)
|
||||
},
|
||||
ArgumentParsingResult::Exit => {
|
||||
std::process::exit(0);
|
||||
},
|
||||
ArgumentParsingResult::ErrorParsing => std::process::exit(1),
|
||||
};
|
||||
|
||||
preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true));
|
||||
|
||||
crate::init_tracing(servoshell_preferences.tracing_filter.as_deref());
|
||||
|
||||
let (display_handle, window_handle) = unsafe {
|
||||
(
|
||||
DisplayHandle::borrow_raw(init_opts.display_handle),
|
||||
WindowHandle::borrow_raw(init_opts.window_handle),
|
||||
)
|
||||
};
|
||||
|
||||
let size = init_opts.coordinates.viewport.size;
|
||||
let refresh_driver = Rc::new(VsyncRefreshDriver::default());
|
||||
let rendering_context = Rc::new(
|
||||
WindowRenderingContext::new_with_refresh_driver(
|
||||
display_handle,
|
||||
window_handle,
|
||||
PhysicalSize::new(size.width as u32, size.height as u32),
|
||||
refresh_driver.clone(),
|
||||
)
|
||||
.expect("Could not create RenderingContext"),
|
||||
);
|
||||
|
||||
let window_callbacks = Rc::new(ServoWindowCallbacks::new(
|
||||
callbacks,
|
||||
RefCell::new(init_opts.coordinates),
|
||||
));
|
||||
|
||||
let servo_builder = ServoBuilder::default()
|
||||
.opts(opts)
|
||||
.preferences(preferences)
|
||||
.event_loop_waker(waker.clone());
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
let servo_builder = servo_builder.webxr_registry(Box::new(XrDiscoveryWebXrRegistry::new(
|
||||
init_opts.xr_discovery,
|
||||
)));
|
||||
|
||||
let servo = servo_builder.build();
|
||||
|
||||
// Initialize WebDriver server if port is specified
|
||||
let webdriver_receiver = servoshell_preferences.webdriver_port.map(|port| {
|
||||
let (embedder_sender, embedder_receiver) = unbounded();
|
||||
webdriver_server::start_server(port, embedder_sender, waker);
|
||||
log::info!("WebDriver server started on port {port}");
|
||||
embedder_receiver
|
||||
});
|
||||
|
||||
APP.with(|app| {
|
||||
let app_state = RunningAppState::new(
|
||||
init_opts.url,
|
||||
init_opts.density,
|
||||
rendering_context,
|
||||
servo,
|
||||
window_callbacks,
|
||||
Some(refresh_driver),
|
||||
servoshell_preferences,
|
||||
webdriver_receiver,
|
||||
);
|
||||
*app.borrow_mut() = Some(app_state);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deinit() {
|
||||
APP.with(|app| {
|
||||
let app = app.replace(None).unwrap();
|
||||
if let Some(app_state) = Rc::into_inner(app) {
|
||||
app_state.deinit()
|
||||
}
|
||||
});
|
||||
}
|
||||
662
ports/servoshell/egl/app.rs
Normal file
662
ports/servoshell/egl/app.rs
Normal file
@@ -0,0 +1,662 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use dpi::PhysicalSize;
|
||||
use euclid::{Rect, Scale};
|
||||
use keyboard_types::{CompositionEvent, CompositionState, Key, KeyState, NamedKey};
|
||||
use log::{info, warn};
|
||||
use raw_window_handle::{RawWindowHandle, WindowHandle};
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::config::opts::Opts;
|
||||
use servo::config::prefs::Preferences;
|
||||
use servo::servo_geometry::{
|
||||
DeviceIndependentIntRect, DeviceIndependentPixel, convert_rect_to_css_pixel,
|
||||
};
|
||||
use servo::webrender_api::units::{DeviceIntSize, DevicePixel, DevicePoint, DeviceVector2D};
|
||||
use servo::{
|
||||
AlertResponse, EmbedderControl, EmbedderControlId, EventLoopWaker, ImeEvent, InputEvent,
|
||||
KeyboardEvent, LoadStatus, MediaSessionActionType, MediaSessionEvent, MouseButton,
|
||||
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, RefreshDriver, RenderingContext,
|
||||
ScreenGeometry, Scroll, Servo, ServoBuilder, SimpleDialog, TouchEvent, TouchEventType, TouchId,
|
||||
WebView, WindowRenderingContext,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
use crate::running_app_state::RunningAppState;
|
||||
use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId};
|
||||
|
||||
pub(super) struct EmbeddedPlatformWindow {
|
||||
host: Box<dyn HostTrait>,
|
||||
rendering_context: Rc<WindowRenderingContext>,
|
||||
refresh_driver: Rc<VsyncRefreshDriver>,
|
||||
viewport_rect: RefCell<Rect<i32, DevicePixel>>,
|
||||
/// The HiDPI scaling factor to use for the display of [`WebView`]s.
|
||||
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
|
||||
/// A list of showing [`InputMethod`] interfaces.
|
||||
visible_input_methods: RefCell<Vec<EmbedderControlId>>,
|
||||
/// The current title of the focused WebView in this window.
|
||||
current_title: RefCell<Option<String>>,
|
||||
/// The current URL of the focused WebView in this window.
|
||||
current_url: RefCell<Option<Url>>,
|
||||
/// Whether or not the focused WebView is currently able to go back.
|
||||
current_can_go_back: Cell<bool>,
|
||||
/// Whether or not the focused WebView is currently able to go forward.
|
||||
current_can_go_forward: Cell<bool>,
|
||||
/// The current load status of the focused WebView.
|
||||
current_load_status: Cell<Option<LoadStatus>>,
|
||||
}
|
||||
|
||||
impl PlatformWindow for EmbeddedPlatformWindow {
|
||||
fn id(&self) -> ServoShellWindowId {
|
||||
0.into()
|
||||
}
|
||||
|
||||
fn screen_geometry(&self) -> ScreenGeometry {
|
||||
let viewport_rect = self.viewport_rect.borrow();
|
||||
ScreenGeometry {
|
||||
size: viewport_rect.size,
|
||||
available_size: viewport_rect.size,
|
||||
window_rect: viewport_rect.to_box2d(),
|
||||
}
|
||||
}
|
||||
|
||||
fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
self.hidpi_scale_factor
|
||||
}
|
||||
|
||||
fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||
self.hidpi_scale_factor
|
||||
}
|
||||
|
||||
fn get_fullscreen(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn rebuild_user_interface(&self, _: &RunningAppState, _: &ServoShellWindow) {}
|
||||
|
||||
#[cfg_attr(target_os = "android", expect(unused_variables))]
|
||||
fn update_user_interface_state(
|
||||
&self,
|
||||
state: &RunningAppState,
|
||||
window: &ServoShellWindow,
|
||||
) -> bool {
|
||||
let Some(focused_webview) = window.focused_webview() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let new_title = focused_webview.page_title();
|
||||
let title_changed = new_title != *self.current_title.borrow();
|
||||
if title_changed {
|
||||
*self.current_title.borrow_mut() = new_title.clone();
|
||||
self.host.on_title_changed(new_title);
|
||||
}
|
||||
|
||||
let new_url = focused_webview.url();
|
||||
let url_changed = new_url != *self.current_url.borrow();
|
||||
if url_changed {
|
||||
let new_url_string = new_url.as_ref().map(Url::to_string).unwrap_or_default();
|
||||
*self.current_url.borrow_mut() = new_url;
|
||||
self.host.on_url_changed(new_url_string);
|
||||
}
|
||||
|
||||
let new_back_forward = (
|
||||
focused_webview.can_go_back(),
|
||||
focused_webview.can_go_forward(),
|
||||
);
|
||||
let old_back_forward = (
|
||||
self.current_can_go_back.get(),
|
||||
self.current_can_go_forward.get(),
|
||||
);
|
||||
let back_forward_changed = new_back_forward != old_back_forward;
|
||||
if back_forward_changed {
|
||||
self.current_can_go_back.set(new_back_forward.0);
|
||||
self.current_can_go_forward.set(new_back_forward.1);
|
||||
self.host
|
||||
.on_history_changed(new_back_forward.0, new_back_forward.1);
|
||||
}
|
||||
|
||||
let new_load_status = focused_webview.load_status();
|
||||
let load_status_changed = Some(new_load_status) != self.current_load_status.get();
|
||||
if load_status_changed {
|
||||
self.host.notify_load_status_changed(new_load_status);
|
||||
|
||||
#[cfg(all(feature = "tracing", feature = "tracing-hitrace"))]
|
||||
if new_load_status == LoadStatus::Complete {
|
||||
let (sender, receiver) =
|
||||
ipc_channel::ipc::channel().expect("Could not create channel");
|
||||
state.servo().create_memory_report(sender);
|
||||
std::thread::spawn(move || {
|
||||
let result = receiver.recv().expect("Could not get memory report");
|
||||
let reports = result
|
||||
.results
|
||||
.first()
|
||||
.expect("We should have some memory report");
|
||||
for report in &reports.reports {
|
||||
let path = String::from("servo_memory_profiling:") + &report.path.join("/");
|
||||
hitrace::trace_metric_str(&path, report.size as i64);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
title_changed || url_changed || back_forward_changed || load_status_changed
|
||||
}
|
||||
|
||||
fn request_repaint(&self, window: &ServoShellWindow) {
|
||||
window.repaint_webviews();
|
||||
}
|
||||
|
||||
fn request_resize(&self, _: &WebView, _: DeviceIntSize) -> Option<DeviceIntSize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
|
||||
self.rendering_context.clone()
|
||||
}
|
||||
|
||||
fn window_rect(&self) -> DeviceIndependentIntRect {
|
||||
convert_rect_to_css_pixel(
|
||||
self.viewport_rect.borrow().to_box2d(),
|
||||
self.hidpi_scale_factor(),
|
||||
)
|
||||
}
|
||||
|
||||
fn focused(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn show_embedder_control(&self, _: WebViewId, embedder_control: EmbedderControl) {
|
||||
let control_id = embedder_control.id();
|
||||
match embedder_control {
|
||||
EmbedderControl::InputMethod(input_method_control) => {
|
||||
self.visible_input_methods.borrow_mut().push(control_id);
|
||||
self.host.on_ime_show(input_method_control);
|
||||
},
|
||||
EmbedderControl::SimpleDialog(simple_dialog) => match simple_dialog {
|
||||
SimpleDialog::Alert {
|
||||
message,
|
||||
response_sender,
|
||||
..
|
||||
} => {
|
||||
self.host.show_alert(message);
|
||||
let _ = response_sender.send(AlertResponse::Ok);
|
||||
},
|
||||
SimpleDialog::Confirm {
|
||||
response_sender, ..
|
||||
} => {
|
||||
let _ = response_sender.send(Default::default());
|
||||
},
|
||||
SimpleDialog::Prompt {
|
||||
response_sender, ..
|
||||
} => {
|
||||
let _ = response_sender.send(Default::default());
|
||||
},
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_embedder_control(&self, _: WebViewId, control_id: servo::EmbedderControlId) {
|
||||
let mut visible_input_methods = self.visible_input_methods.borrow_mut();
|
||||
if let Some(index) = visible_input_methods
|
||||
.iter()
|
||||
.position(|visible_id| *visible_id == control_id)
|
||||
{
|
||||
visible_input_methods.remove(index);
|
||||
self.host.on_ime_hide();
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_media_session_event(&self, event: MediaSessionEvent) {
|
||||
match event {
|
||||
MediaSessionEvent::SetMetadata(metadata) => {
|
||||
self.host
|
||||
.on_media_session_metadata(metadata.title, metadata.artist, metadata.album)
|
||||
},
|
||||
MediaSessionEvent::PlaybackStateChange(state) => {
|
||||
self.host.on_media_session_playback_state_change(state)
|
||||
},
|
||||
MediaSessionEvent::SetPositionState(position_state) => {
|
||||
self.host.on_media_session_set_position_state(
|
||||
position_state.duration,
|
||||
position_state.position,
|
||||
position_state.playback_rate,
|
||||
)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn notify_crashed(&self, _webview: WebView, reason: String, backtrace: Option<String>) {
|
||||
self.host.on_panic(reason, backtrace);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct VsyncRefreshDriver {
|
||||
start_frame_callbacks: RefCell<Vec<Box<dyn Fn() + Send>>>,
|
||||
}
|
||||
|
||||
impl VsyncRefreshDriver {
|
||||
fn notify_vsync(&self) {
|
||||
let start_frame_callbacks: Vec<_> =
|
||||
self.start_frame_callbacks.borrow_mut().drain(..).collect();
|
||||
for start_frame_callback in start_frame_callbacks {
|
||||
start_frame_callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RefreshDriver for VsyncRefreshDriver {
|
||||
fn observe_next_frame(&self, new_start_frame_callback: Box<dyn Fn() + Send + 'static>) {
|
||||
self.start_frame_callbacks
|
||||
.borrow_mut()
|
||||
.push(new_start_frame_callback);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct AppInitOptions {
|
||||
pub host: Box<dyn HostTrait>,
|
||||
pub event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
pub viewport_rect: Rect<i32, DevicePixel>,
|
||||
pub hidpi_scale_factor: f32,
|
||||
pub rendering_context: Rc<WindowRenderingContext>,
|
||||
pub refresh_driver: Rc<VsyncRefreshDriver>,
|
||||
pub initial_url: Option<String>,
|
||||
pub opts: Opts,
|
||||
pub preferences: Preferences,
|
||||
pub servoshell_preferences: ServoShellPreferences,
|
||||
#[cfg(feature = "webxr")]
|
||||
pub xr_discovery: Option<servo::webxr::Discovery>,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
state: Rc<RunningAppState>,
|
||||
platform_window: Rc<EmbeddedPlatformWindow>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl App {
|
||||
pub(super) fn new(init: AppInitOptions) -> Rc<Self> {
|
||||
let mut servo_builder = ServoBuilder::default()
|
||||
.opts(init.opts)
|
||||
.preferences(init.preferences)
|
||||
.event_loop_waker(init.event_loop_waker.clone());
|
||||
#[cfg(feature = "webxr")]
|
||||
let servo_builder = servo_builder
|
||||
.webxr_registry(Box::new(XrDiscoveryWebXrRegistry::new(init.xr_discovery)));
|
||||
let servo = servo_builder.build();
|
||||
|
||||
let initial_url = init.initial_url.and_then(|string| Url::parse(&string).ok());
|
||||
let initial_url = initial_url
|
||||
.or_else(|| Url::parse(&init.servoshell_preferences.homepage).ok())
|
||||
.or_else(|| Url::parse("about:blank").ok())
|
||||
.expect("Failed to parse initial URL");
|
||||
|
||||
let state = Rc::new(RunningAppState::new(
|
||||
servo,
|
||||
init.servoshell_preferences,
|
||||
init.event_loop_waker,
|
||||
));
|
||||
|
||||
let platform_window = Rc::new(EmbeddedPlatformWindow {
|
||||
host: init.host,
|
||||
rendering_context: init.rendering_context,
|
||||
refresh_driver: init.refresh_driver,
|
||||
viewport_rect: RefCell::new(init.viewport_rect),
|
||||
hidpi_scale_factor: Scale::new(init.hidpi_scale_factor),
|
||||
visible_input_methods: Default::default(),
|
||||
current_title: Default::default(),
|
||||
current_url: Default::default(),
|
||||
current_can_go_back: Default::default(),
|
||||
current_can_go_forward: Default::default(),
|
||||
current_load_status: Default::default(),
|
||||
});
|
||||
state.create_window(platform_window.clone(), initial_url);
|
||||
|
||||
Rc::new(Self {
|
||||
state,
|
||||
platform_window,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn servo(&self) -> &Servo {
|
||||
&self.state.servo
|
||||
}
|
||||
|
||||
pub(crate) fn servoshell_preferences(&self) -> &ServoShellPreferences {
|
||||
&self.state.servoshell_preferences
|
||||
}
|
||||
|
||||
pub(crate) fn focused_or_newest_webview(&self) -> Option<WebView> {
|
||||
self.state.any_window().focused_or_newest_webview()
|
||||
}
|
||||
|
||||
pub(crate) fn create_and_focus_toplevel_webview(self: &Rc<Self>, url: Url) -> WebView {
|
||||
self.state
|
||||
.any_window()
|
||||
.create_and_focus_toplevel_webview(self.state.clone(), url)
|
||||
}
|
||||
|
||||
/// The focused webview will not be immediately valid via `focused_or_newest_webview()`
|
||||
pub(crate) fn focus_webview(&self, id: WebViewId) {
|
||||
if let Some(webview) = self.state.webview_by_id(id) {
|
||||
webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Request shutdown. Will call on_shutdown_complete.
|
||||
pub fn request_shutdown(&self) {
|
||||
self.state.servo.start_shutting_down();
|
||||
self.spin_event_loop();
|
||||
}
|
||||
|
||||
/// This is the Servo heartbeat. This needs to be called
|
||||
/// everytime wakeup is called or when embedder wants Servo
|
||||
/// to act on its pending events.
|
||||
pub fn spin_event_loop(&self) {
|
||||
if !self.state.spin_event_loop() {
|
||||
self.platform_window.host.on_shutdown_complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load an URL.
|
||||
pub fn load_uri(&self, url: &str) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
let Some(url) = crate::parser::location_bar_input_to_url(
|
||||
url,
|
||||
&self.servoshell_preferences().searchpage,
|
||||
) else {
|
||||
warn!("Cannot parse URL");
|
||||
return;
|
||||
};
|
||||
webview.load(url.into_url());
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload the page.
|
||||
pub fn reload(&self) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.reload();
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop loading the page.
|
||||
pub fn stop(&self) {
|
||||
warn!("TODO can't stop won't stop");
|
||||
}
|
||||
|
||||
/// Go back in history.
|
||||
pub fn go_back(&self) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.go_back(1);
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Go forward in history.
|
||||
pub fn go_forward(&self) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.go_forward(1);
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Let Servo know that the window has been resized.
|
||||
pub fn resize(&self, viewport_rect: Rect<i32, DevicePixel>) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
info!("Setting viewport to {viewport_rect:?}");
|
||||
let size = viewport_rect.size;
|
||||
webview.move_resize(size.to_f32().into());
|
||||
webview.resize(PhysicalSize::new(size.width as u32, size.height as u32));
|
||||
}
|
||||
*self.platform_window.viewport_rect.borrow_mut() = viewport_rect;
|
||||
self.spin_event_loop();
|
||||
}
|
||||
|
||||
/// Scroll.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
pub fn scroll(&self, dx: f32, dy: f32, x: f32, y: f32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
let scroll = Scroll::Delta(DeviceVector2D::new(dx, dy).into());
|
||||
let point = DevicePoint::new(x, y).into();
|
||||
webview.notify_scroll_event(scroll, point);
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch event: press down
|
||||
pub fn touch_down(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Down,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch event: move touching finger
|
||||
pub fn touch_move(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Move,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch event: Lift touching finger
|
||||
pub fn touch_up(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Up,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel touch event
|
||||
pub fn touch_cancel(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Cancel,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a mouse movement.
|
||||
pub fn mouse_move(&self, x: f32, y: f32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a mouse button press.
|
||||
pub fn mouse_down(&self, x: f32, y: f32, button: MouseButton) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
|
||||
MouseButtonAction::Down,
|
||||
button,
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a mouse button release.
|
||||
pub fn mouse_up(&self, x: f32, y: f32, button: MouseButton) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
|
||||
MouseButtonAction::Up,
|
||||
button,
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Start pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_start(&self, factor: f32, x: f32, y: f32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.pinch_zoom(factor, DevicePoint::new(x, y));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom(&self, factor: f32, x: f32, y: f32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.pinch_zoom(factor, DevicePoint::new(x, y));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
/// End pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_end(&self, factor: f32, x: f32, y: f32) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.pinch_zoom(factor, DevicePoint::new(x, y));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_down(&self, key: Key) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key(
|
||||
KeyState::Down,
|
||||
key,
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key_up(&self, key: Key) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key(
|
||||
KeyState::Up,
|
||||
key,
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ime_insert_text(&self, text: String) {
|
||||
// In OHOS, we get empty text after the intended text.
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(webview) = self.focused_or_newest_webview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key(
|
||||
KeyState::Down,
|
||||
Key::Named(NamedKey::Process),
|
||||
)));
|
||||
webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(CompositionEvent {
|
||||
state: CompositionState::End,
|
||||
data: text,
|
||||
})));
|
||||
webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key(
|
||||
KeyState::Up,
|
||||
Key::Named(NamedKey::Process),
|
||||
)));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
|
||||
pub fn media_session_action(&self, action: MediaSessionActionType) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_media_session_action_event(action);
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_throttled(&self, throttled: bool) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.set_throttled(throttled);
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ime_dismissed(&self) {
|
||||
if let Some(webview) = self.focused_or_newest_webview() {
|
||||
webview.notify_input_event(InputEvent::Ime(ImeEvent::Dismissed));
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_vsync(&self) {
|
||||
self.platform_window.refresh_driver.notify_vsync();
|
||||
self.spin_event_loop();
|
||||
}
|
||||
|
||||
pub fn pause_compositor(&self) {
|
||||
if let Err(e) = self.platform_window.rendering_context.take_window() {
|
||||
warn!("Unbinding native surface from context failed ({:?})", e);
|
||||
}
|
||||
self.spin_event_loop();
|
||||
}
|
||||
|
||||
pub fn resume_compositor(
|
||||
&self,
|
||||
window_handle: RawWindowHandle,
|
||||
viewport_rect: Rect<i32, DevicePixel>,
|
||||
) {
|
||||
let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) };
|
||||
let size = viewport_rect.size.to_u32();
|
||||
if let Err(error) = self
|
||||
.platform_window
|
||||
.rendering_context
|
||||
.set_window(window_handle, PhysicalSize::new(size.width, size.height))
|
||||
{
|
||||
warn!("Binding native surface to context failed ({error:?})");
|
||||
}
|
||||
self.spin_event_loop();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
pub(crate) struct XrDiscoveryWebXrRegistry {
|
||||
xr_discovery: RefCell<Option<servo::webxr::Discovery>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
#[cfg_attr(target_env = "ohos", allow(dead_code))]
|
||||
impl XrDiscoveryWebXrRegistry {
|
||||
pub(crate) fn new(xr_discovery: Option<servo::webxr::Discovery>) -> Self {
|
||||
Self {
|
||||
xr_discovery: RefCell::new(xr_discovery),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
impl servo::webxr::WebXrRegistry for XrDiscoveryWebXrRegistry {
|
||||
fn register(&self, registry: &mut servo::webxr::MainThreadRegistry) {
|
||||
log::debug!("XrDiscoveryWebXrRegistry::register");
|
||||
if let Some(discovery) = self.xr_discovery.take() {
|
||||
registry.register(discovery);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,833 +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::cell::{Ref, RefCell, RefMut};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crossbeam_channel::Receiver;
|
||||
use dpi::PhysicalSize;
|
||||
use euclid::{Point2D, Rect, Scale, Size2D};
|
||||
use keyboard_types::{CompositionEvent, CompositionState, Key, KeyState, NamedKey};
|
||||
use log::{debug, error, info, warn};
|
||||
use raw_window_handle::{RawWindowHandle, WindowHandle};
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::webrender_api::units::{
|
||||
DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint, DeviceVector2D,
|
||||
};
|
||||
use servo::{
|
||||
EmbedderControl, EmbedderControlId, ImeEvent, InputEvent, InputEventId, InputEventResult,
|
||||
KeyboardEvent, LoadStatus, MediaSessionActionType, MediaSessionEvent, MouseButton,
|
||||
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, NavigationRequest, PermissionRequest,
|
||||
RefreshDriver, RenderingContext, ScreenGeometry, Scroll, Servo, TouchEvent, TouchEventType,
|
||||
TouchId, TraversalId, WebDriverCommandMsg, WebDriverLoadStatus, WebView, WebViewBuilder,
|
||||
WebViewDelegate, WindowRenderingContext,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
use crate::running_app_state::{RunningAppStateBase, RunningAppStateTrait};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Coordinates {
|
||||
pub viewport: Rect<i32, DevicePixel>,
|
||||
}
|
||||
|
||||
impl Coordinates {
|
||||
pub fn new(x: i32, y: i32, width: i32, height: i32) -> Coordinates {
|
||||
Coordinates {
|
||||
viewport: Rect::new(Point2D::new(x, y), Size2D::new(width, height)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn origin(&self) -> Point2D<i32, DevicePixel> {
|
||||
self.viewport.origin
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Size2D<i32, DevicePixel> {
|
||||
self.viewport.size
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ServoWindowCallbacks {
|
||||
host_callbacks: Box<dyn HostTrait>,
|
||||
coordinates: RefCell<Coordinates>,
|
||||
}
|
||||
|
||||
impl ServoWindowCallbacks {
|
||||
pub(super) fn new(
|
||||
host_callbacks: Box<dyn HostTrait>,
|
||||
coordinates: RefCell<Coordinates>,
|
||||
) -> Self {
|
||||
Self {
|
||||
host_callbacks,
|
||||
coordinates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RunningAppState {
|
||||
base: RunningAppStateBase,
|
||||
rendering_context: Rc<WindowRenderingContext>,
|
||||
callbacks: Rc<ServoWindowCallbacks>,
|
||||
refresh_driver: Option<Rc<VsyncRefreshDriver>>,
|
||||
inner: RefCell<RunningAppStateInner>,
|
||||
}
|
||||
|
||||
struct RunningAppStateInner {
|
||||
need_present: bool,
|
||||
|
||||
/// The HiDPI scaling factor to use for the display of [`WebView`]s.
|
||||
hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
|
||||
|
||||
/// A list of showing [`InputMethod`] interfaces.
|
||||
visible_input_methods: Vec<EmbedderControlId>,
|
||||
}
|
||||
|
||||
impl WebViewDelegate for RunningAppState {
|
||||
fn screen_geometry(&self, _webview: WebView) -> Option<ScreenGeometry> {
|
||||
let coord = self.callbacks.coordinates.borrow();
|
||||
let available_size = coord.size();
|
||||
let screen_size = coord.size();
|
||||
Some(ScreenGeometry {
|
||||
size: screen_size,
|
||||
available_size,
|
||||
window_rect: DeviceIntRect::from_origin_and_size(coord.origin(), coord.size()),
|
||||
})
|
||||
}
|
||||
|
||||
fn notify_page_title_changed(&self, _webview: servo::WebView, title: Option<String>) {
|
||||
self.callbacks.host_callbacks.on_title_changed(title);
|
||||
}
|
||||
|
||||
fn notify_history_changed(&self, _webview: WebView, entries: Vec<Url>, current: usize) {
|
||||
let can_go_back = current > 0;
|
||||
let can_go_forward = current < entries.len() - 1;
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.on_history_changed(can_go_back, can_go_forward);
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.on_url_changed(entries[current].clone().to_string());
|
||||
}
|
||||
|
||||
fn notify_load_status_changed(&self, webview: WebView, load_status: LoadStatus) {
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.notify_load_status_changed(load_status);
|
||||
|
||||
if load_status == LoadStatus::Complete {
|
||||
if let Some(sender) = self
|
||||
.base()
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.remove(&webview.id())
|
||||
{
|
||||
let _ = sender.send(WebDriverLoadStatus::Complete);
|
||||
}
|
||||
self.maybe_request_screenshot(webview);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if load_status == LoadStatus::Complete {
|
||||
#[cfg(feature = "tracing-hitrace")]
|
||||
let (snd, recv) = ipc_channel::ipc::channel().expect("Could not create channel");
|
||||
self.servo().create_memory_report(snd);
|
||||
std::thread::spawn(move || {
|
||||
let result = recv.recv().expect("Could not get memory report");
|
||||
let reports = result
|
||||
.results
|
||||
.first()
|
||||
.expect("We should have some memory report");
|
||||
for report in &reports.reports {
|
||||
let path = String::from("servo_memory_profiling:") + &report.path.join("/");
|
||||
hitrace::trace_metric_str(&path, report.size as i64);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_closed(&self, webview: WebView) {
|
||||
self.webview_collection_mut().remove(webview.id());
|
||||
|
||||
if let Some(newest_webview) = self.webview_collection().newest() {
|
||||
newest_webview.focus();
|
||||
} else {
|
||||
self.servo().start_shutting_down();
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_focus_changed(&self, webview: WebView, focused: bool) {
|
||||
if focused {
|
||||
self.webview_collection_mut()
|
||||
.set_focused(Some(webview.id()));
|
||||
webview.show(true);
|
||||
} else if self.webview_collection().focused_id() == Some(webview.id()) {
|
||||
self.webview_collection_mut().set_focused(None);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_traversal_complete(&self, _webview: servo::WebView, traversal_id: TraversalId) {
|
||||
let mut webdriver_state = self.base().webdriver_senders.borrow_mut();
|
||||
if let std::collections::hash_map::Entry::Occupied(entry) =
|
||||
webdriver_state.pending_traversals.entry(traversal_id)
|
||||
{
|
||||
let sender = entry.remove();
|
||||
let _ = sender.send(WebDriverLoadStatus::Complete);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_media_session_event(&self, _webview: WebView, event: MediaSessionEvent) {
|
||||
match event {
|
||||
MediaSessionEvent::SetMetadata(metadata) => self
|
||||
.callbacks
|
||||
.host_callbacks
|
||||
.on_media_session_metadata(metadata.title, metadata.artist, metadata.album),
|
||||
MediaSessionEvent::PlaybackStateChange(state) => self
|
||||
.callbacks
|
||||
.host_callbacks
|
||||
.on_media_session_playback_state_change(state),
|
||||
MediaSessionEvent::SetPositionState(position_state) => self
|
||||
.callbacks
|
||||
.host_callbacks
|
||||
.on_media_session_set_position_state(
|
||||
position_state.duration,
|
||||
position_state.position,
|
||||
position_state.playback_rate,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn notify_crashed(&self, _webview: WebView, reason: String, backtrace: Option<String>) {
|
||||
self.callbacks.host_callbacks.on_panic(reason, backtrace);
|
||||
}
|
||||
|
||||
fn notify_new_frame_ready(&self, _webview: WebView) {
|
||||
self.inner_mut().need_present = true;
|
||||
}
|
||||
|
||||
fn notify_input_event_handled(
|
||||
&self,
|
||||
_webview: WebView,
|
||||
id: InputEventId,
|
||||
_result: InputEventResult,
|
||||
) {
|
||||
if let Some(response_sender) = self
|
||||
.base()
|
||||
.pending_webdriver_events
|
||||
.borrow_mut()
|
||||
.remove(&id)
|
||||
{
|
||||
let _ = response_sender.send(());
|
||||
}
|
||||
}
|
||||
|
||||
fn request_navigation(&self, _webview: WebView, navigation_request: NavigationRequest) {
|
||||
if self
|
||||
.callbacks
|
||||
.host_callbacks
|
||||
.on_allow_navigation(navigation_request.url.to_string())
|
||||
{
|
||||
navigation_request.allow();
|
||||
} else {
|
||||
navigation_request.deny();
|
||||
}
|
||||
}
|
||||
|
||||
fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option<WebView> {
|
||||
let webview = WebViewBuilder::new_auxiliary(self.servo(), self.rendering_context.clone())
|
||||
.delegate(parent_webview.delegate())
|
||||
.hidpi_scale_factor(self.inner().hidpi_scale_factor)
|
||||
.build();
|
||||
self.add_webview(webview.clone());
|
||||
Some(webview)
|
||||
}
|
||||
|
||||
fn request_permission(&self, webview: WebView, request: PermissionRequest) {
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.request_permission(webview, request);
|
||||
}
|
||||
|
||||
fn request_resize_to(&self, _webview: WebView, size: DeviceIntSize) {
|
||||
warn!("Received resize event (to {size:?}). Currently only the user can resize windows");
|
||||
}
|
||||
|
||||
fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) {
|
||||
let control_id = embedder_control.id();
|
||||
match embedder_control {
|
||||
EmbedderControl::InputMethod(input_method_control) => {
|
||||
self.inner_mut().visible_input_methods.push(control_id);
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.on_ime_show(input_method_control);
|
||||
},
|
||||
EmbedderControl::SimpleDialog(simple_dialog) => self
|
||||
.callbacks
|
||||
.host_callbacks
|
||||
.show_simple_dialog(webview, simple_dialog),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_embedder_control(&self, _webview: WebView, control_id: servo::EmbedderControlId) {
|
||||
let mut inner_mut = self.inner_mut();
|
||||
if let Some(index) = inner_mut
|
||||
.visible_input_methods
|
||||
.iter()
|
||||
.position(|visible_id| *visible_id == control_id)
|
||||
{
|
||||
inner_mut.visible_input_methods.remove(index);
|
||||
self.callbacks.host_callbacks.on_ime_hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct VsyncRefreshDriver {
|
||||
start_frame_callbacks: RefCell<Vec<Box<dyn Fn() + Send>>>,
|
||||
}
|
||||
|
||||
impl VsyncRefreshDriver {
|
||||
fn notify_vsync(&self) {
|
||||
let start_frame_callbacks: Vec<_> =
|
||||
self.start_frame_callbacks.borrow_mut().drain(..).collect();
|
||||
for start_frame_callback in start_frame_callbacks {
|
||||
start_frame_callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RefreshDriver for VsyncRefreshDriver {
|
||||
fn observe_next_frame(&self, new_start_frame_callback: Box<dyn Fn() + Send + 'static>) {
|
||||
self.start_frame_callbacks
|
||||
.borrow_mut()
|
||||
.push(new_start_frame_callback);
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningAppStateTrait for RunningAppState {
|
||||
fn base(&self) -> &RunningAppStateBase {
|
||||
&self.base
|
||||
}
|
||||
|
||||
fn base_mut(&mut self) -> &mut RunningAppStateBase {
|
||||
&mut self.base
|
||||
}
|
||||
|
||||
fn webview_by_id(&self, id: WebViewId) -> Option<WebView> {
|
||||
self.webview_collection().get(id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl RunningAppState {
|
||||
pub(super) fn new(
|
||||
initial_url: Option<String>,
|
||||
hidpi_scale_factor: f32,
|
||||
rendering_context: Rc<WindowRenderingContext>,
|
||||
servo: Servo,
|
||||
callbacks: Rc<ServoWindowCallbacks>,
|
||||
refresh_driver: Option<Rc<VsyncRefreshDriver>>,
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
webdriver_receiver: Option<Receiver<WebDriverCommandMsg>>,
|
||||
) -> Rc<Self> {
|
||||
let initial_url = initial_url.and_then(|string| Url::parse(&string).ok());
|
||||
let initial_url = initial_url
|
||||
.or_else(|| Url::parse(&servoshell_preferences.homepage).ok())
|
||||
.or_else(|| Url::parse("about:blank").ok())
|
||||
.unwrap();
|
||||
|
||||
let app_state = Rc::new(Self {
|
||||
base: RunningAppStateBase::new(servoshell_preferences, servo, webdriver_receiver),
|
||||
rendering_context,
|
||||
callbacks,
|
||||
refresh_driver,
|
||||
inner: RefCell::new(RunningAppStateInner {
|
||||
need_present: false,
|
||||
hidpi_scale_factor: Scale::new(hidpi_scale_factor),
|
||||
visible_input_methods: Default::default(),
|
||||
}),
|
||||
});
|
||||
|
||||
app_state.create_and_focus_toplevel_webview(initial_url);
|
||||
app_state
|
||||
}
|
||||
|
||||
pub(crate) fn create_and_focus_toplevel_webview(self: &Rc<Self>, url: Url) -> WebView {
|
||||
let webview = WebViewBuilder::new(self.servo(), self.rendering_context.clone())
|
||||
.url(url)
|
||||
.hidpi_scale_factor(self.inner().hidpi_scale_factor)
|
||||
.delegate(self.clone())
|
||||
.build();
|
||||
|
||||
webview.focus();
|
||||
self.add_webview(webview.clone());
|
||||
webview
|
||||
}
|
||||
|
||||
/// The focused webview will not be immediately valid via `active_webview()`
|
||||
pub(crate) fn focus_webview(&self, id: WebViewId) {
|
||||
if let Some(webview) = self.webview_collection().get(id) {
|
||||
webview.focus();
|
||||
} else {
|
||||
error!("We could not find the webview with this id {id}");
|
||||
}
|
||||
}
|
||||
|
||||
fn inner(&self) -> Ref<'_, RunningAppStateInner> {
|
||||
self.inner.borrow()
|
||||
}
|
||||
|
||||
fn inner_mut(&self) -> RefMut<'_, RunningAppStateInner> {
|
||||
self.inner.borrow_mut()
|
||||
}
|
||||
|
||||
fn get_browser_id(&self) -> Result<WebViewId, &'static str> {
|
||||
self.webview_collection()
|
||||
.focused_id()
|
||||
.ok_or("No focused WebViewId yet.")
|
||||
}
|
||||
|
||||
/// Request shutdown. Will call on_shutdown_complete.
|
||||
pub fn request_shutdown(&self) {
|
||||
self.servo().start_shutting_down();
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Call after on_shutdown_complete
|
||||
pub fn deinit(self) {
|
||||
self.servo().deinit();
|
||||
}
|
||||
|
||||
/// This is the Servo heartbeat. This needs to be called
|
||||
/// everytime wakeup is called or when embedder wants Servo
|
||||
/// to act on its pending events.
|
||||
pub fn perform_updates(&self) {
|
||||
let should_continue = self.servo().spin_event_loop();
|
||||
if !should_continue {
|
||||
self.callbacks.host_callbacks.on_shutdown_complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// Load an URL.
|
||||
pub fn load_uri(&self, url: &str) {
|
||||
info!("load_uri: {}", url);
|
||||
|
||||
let Some(url) = crate::parser::location_bar_input_to_url(
|
||||
url,
|
||||
&self.servoshell_preferences().searchpage,
|
||||
) else {
|
||||
warn!("Cannot parse URL");
|
||||
return;
|
||||
};
|
||||
|
||||
self.active_webview_or_panic().load(url.into_url());
|
||||
}
|
||||
|
||||
/// Reload the page.
|
||||
pub fn reload(&self) {
|
||||
info!("reload");
|
||||
self.active_webview_or_panic().reload();
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Stop loading the page.
|
||||
pub fn stop(&self) {
|
||||
warn!("TODO can't stop won't stop");
|
||||
}
|
||||
|
||||
/// Go back in history.
|
||||
pub fn go_back(&self) {
|
||||
info!("go_back");
|
||||
self.active_webview_or_panic().go_back(1);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Go forward in history.
|
||||
pub fn go_forward(&self) {
|
||||
info!("go_forward");
|
||||
self.active_webview_or_panic().go_forward(1);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Let Servo know that the window has been resized.
|
||||
pub fn resize(&self, coordinates: Coordinates) {
|
||||
info!("resize to {:?}", coordinates,);
|
||||
let size = coordinates.viewport.size;
|
||||
self.active_webview_or_panic()
|
||||
.move_resize(size.to_f32().into());
|
||||
self.active_webview_or_panic()
|
||||
.resize(PhysicalSize::new(size.width as u32, size.height as u32));
|
||||
*self.callbacks.coordinates.borrow_mut() = coordinates;
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Scroll.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
pub fn scroll(&self, dx: f32, dy: f32, x: f32, y: f32) {
|
||||
let scroll = Scroll::Delta(DeviceVector2D::new(dx, dy).into());
|
||||
let point = DevicePoint::new(x, y).into();
|
||||
self.active_webview_or_panic()
|
||||
.notify_scroll_event(scroll, point);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// WebDriver message handling methods
|
||||
pub(crate) fn handle_webdriver_messages(self: &Rc<Self>) {
|
||||
if let Some(webdriver_receiver) = &self.webdriver_receiver() {
|
||||
while let Ok(msg) = webdriver_receiver.try_recv() {
|
||||
match msg {
|
||||
WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => {
|
||||
self.handle_webdriver_load_url(webview_id, url, load_status_sender);
|
||||
},
|
||||
WebDriverCommandMsg::NewWebView(response_sender, load_status_sender) => {
|
||||
info!("Creating new webview via WebDriver");
|
||||
let new_webview = self
|
||||
.create_and_focus_toplevel_webview(Url::parse("about:blank").unwrap());
|
||||
|
||||
if let Err(error) = response_sender.send(new_webview.id()) {
|
||||
warn!("Failed to send response of NewWebview: {error}");
|
||||
}
|
||||
if let Some(load_status_sender) = load_status_sender {
|
||||
self.set_load_status_sender(new_webview.id(), load_status_sender);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::CloseWebView(webview_id, response_sender) => {
|
||||
info!("(Not Implemented) Closing webview {}", webview_id);
|
||||
},
|
||||
WebDriverCommandMsg::FocusWebView(webview_id) => {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let focus_id = webview.focus();
|
||||
info!("Successfully focused webview {}", webview_id);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::IsWebViewOpen(webview_id, response_sender) => {
|
||||
let context = self.webview_by_id(webview_id);
|
||||
|
||||
if let Err(error) = response_sender.send(context.is_some()) {
|
||||
warn!("Failed to send response of IsWebViewOpen: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::IsBrowsingContextOpen(..) => {
|
||||
self.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::GetFocusedWebView(response_sender) => {
|
||||
let focused_id = self.webview_collection().focused_id();
|
||||
|
||||
if let Err(error) = response_sender.send(focused_id) {
|
||||
warn!("Failed to send response of GetFocusedWebView: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::Refresh(webview_id, load_status_sender) => {
|
||||
info!("Refreshing webview {}", webview_id);
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
self.set_load_status_sender(webview_id, load_status_sender);
|
||||
webview.reload();
|
||||
} else {
|
||||
warn!("WebView {} not found for Refresh command", webview_id);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GoBack(webview_id, load_status_sender) => {
|
||||
info!("Going back in webview {}", webview_id);
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let traversal_id = webview.go_back(1);
|
||||
self.set_pending_traversal(traversal_id, load_status_sender);
|
||||
} else {
|
||||
warn!("WebView {} not found for GoBack command", webview_id);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GoForward(webview_id, load_status_sender) => {
|
||||
info!("Going forward in webview {}", webview_id);
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let traversal_id = webview.go_forward(1);
|
||||
self.set_pending_traversal(traversal_id, load_status_sender);
|
||||
} else {
|
||||
warn!("WebView {} not found for GoForward command", webview_id);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GetAllWebViews(response_sender) => {
|
||||
let webviews = self
|
||||
.webviews()
|
||||
.iter()
|
||||
.map(|(id, _)| *id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Err(error) = response_sender.send(webviews) {
|
||||
warn!("Failed to send response of GetAllWebViews: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::ScriptCommand(_, ref webdriver_script_command) => {
|
||||
self.handle_webdriver_script_command(webdriver_script_command);
|
||||
self.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::CurrentUserPrompt(webview_id, response_sender) => {
|
||||
info!("Handling CurrentUserPrompt for webview {}", webview_id);
|
||||
if let Err(error) = response_sender.send(None) {
|
||||
warn!("Failed to send response of CurrentUserPrompt: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => {
|
||||
info!(
|
||||
"Handling HandleUserPrompt for webview {} with action {:?}",
|
||||
webview_id, action
|
||||
);
|
||||
|
||||
if let Err(error) = response_sender.send(Err(())) {
|
||||
warn!("Failed to send response of HandleUserPrompt: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => {
|
||||
info!("Handling GetAlertText for webview {}", webview_id);
|
||||
let _ = response_sender.send(Err(()));
|
||||
},
|
||||
WebDriverCommandMsg::SendAlertText(webview_id, text) => {
|
||||
info!(
|
||||
"Handling SendAlertText for webview {} with text: {}",
|
||||
webview_id, text
|
||||
);
|
||||
},
|
||||
WebDriverCommandMsg::GetViewportSize(webview_id, response_sender) => {
|
||||
info!("Handling GetViewportSize for webview {}", webview_id);
|
||||
let _ = response_sender.send(self.rendering_context.size2d());
|
||||
},
|
||||
WebDriverCommandMsg::InputEvent(webview_id, input_event, response_sender) => {
|
||||
self.handle_webdriver_input_event(webview_id, input_event, response_sender);
|
||||
},
|
||||
WebDriverCommandMsg::TakeScreenshot(webview_id, rect, result_sender) => {
|
||||
self.handle_webdriver_screenshot(webview_id, rect, result_sender);
|
||||
},
|
||||
_ => {
|
||||
info!("Received unsupported WebDriver command: {:?}", msg);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Touch event: press down
|
||||
pub fn touch_down(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Down,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Touch event: move touching finger
|
||||
pub fn touch_move(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Move,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Touch event: Lift touching finger
|
||||
pub fn touch_up(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Up,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Cancel touch event
|
||||
pub fn touch_cancel(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::Touch(TouchEvent::new(
|
||||
TouchEventType::Cancel,
|
||||
TouchId(pointer_id),
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Register a mouse movement.
|
||||
pub fn mouse_move(&self, x: f32, y: f32) {
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::MouseMove(MouseMoveEvent::new(
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Register a mouse button press.
|
||||
pub fn mouse_down(&self, x: f32, y: f32, button: MouseButton) {
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
|
||||
MouseButtonAction::Down,
|
||||
button,
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Register a mouse button release.
|
||||
pub fn mouse_up(&self, x: f32, y: f32, button: MouseButton) {
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::MouseButton(MouseButtonEvent::new(
|
||||
MouseButtonAction::Up,
|
||||
button,
|
||||
DevicePoint::new(x, y).into(),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Start pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_start(&self, factor: f32, x: f32, y: f32) {
|
||||
self.active_webview_or_panic()
|
||||
.pinch_zoom(factor, DevicePoint::new(x, y));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom(&self, factor: f32, x: f32, y: f32) {
|
||||
self.active_webview_or_panic()
|
||||
.pinch_zoom(factor, DevicePoint::new(x, y));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// End pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_end(&self, factor: f32, x: f32, y: f32) {
|
||||
self.active_webview_or_panic()
|
||||
.pinch_zoom(factor, DevicePoint::new(x, y));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn key_down(&self, key: Key) {
|
||||
let key_event = KeyboardEvent::from_state_and_key(KeyState::Down, key);
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::Keyboard(key_event));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn key_up(&self, key: Key) {
|
||||
let key_event = KeyboardEvent::from_state_and_key(KeyState::Up, key);
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::Keyboard(key_event));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn ime_insert_text(&self, text: String) {
|
||||
// In OHOS, we get empty text after the intended text.
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
let active_webview = self.active_webview_or_panic();
|
||||
active_webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key(
|
||||
KeyState::Down,
|
||||
Key::Named(NamedKey::Process),
|
||||
)));
|
||||
active_webview.notify_input_event(InputEvent::Ime(ImeEvent::Composition(
|
||||
CompositionEvent {
|
||||
state: CompositionState::End,
|
||||
data: text,
|
||||
},
|
||||
)));
|
||||
active_webview.notify_input_event(InputEvent::Keyboard(KeyboardEvent::from_state_and_key(
|
||||
KeyState::Up,
|
||||
Key::Named(NamedKey::Process),
|
||||
)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn notify_vsync(&self) {
|
||||
if let Some(refresh_driver) = &self.refresh_driver {
|
||||
refresh_driver.notify_vsync();
|
||||
};
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn pause_compositor(&self) {
|
||||
if let Err(e) = self.rendering_context.take_window() {
|
||||
warn!("Unbinding native surface from context failed ({:?})", e);
|
||||
}
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn resume_compositor(&self, window_handle: RawWindowHandle, coords: Coordinates) {
|
||||
let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) };
|
||||
let size = coords.viewport.size.to_u32();
|
||||
if let Err(e) = self
|
||||
.rendering_context
|
||||
.set_window(window_handle, PhysicalSize::new(size.width, size.height))
|
||||
{
|
||||
warn!("Binding native surface to context failed ({:?})", e);
|
||||
}
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn media_session_action(&self, action: MediaSessionActionType) {
|
||||
info!("Media session action {:?}", action);
|
||||
self.active_webview_or_panic()
|
||||
.notify_media_session_action_event(action);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn set_throttled(&self, throttled: bool) {
|
||||
info!("set_throttled");
|
||||
self.active_webview_or_panic().set_throttled(throttled);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn ime_dismissed(&self) {
|
||||
info!("ime_dismissed");
|
||||
self.active_webview_or_panic()
|
||||
.notify_input_event(InputEvent::Ime(ImeEvent::Dismissed));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn present_if_needed(&self) {
|
||||
if !self.inner().need_present {
|
||||
return;
|
||||
}
|
||||
|
||||
self.inner_mut().need_present = false;
|
||||
self.active_webview_or_panic().paint();
|
||||
self.rendering_context.present();
|
||||
|
||||
if self.servoshell_preferences().exit_after_stable_image &&
|
||||
self.base().achieved_stable_image.get()
|
||||
{
|
||||
self.request_shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
pub(crate) struct XrDiscoveryWebXrRegistry {
|
||||
xr_discovery: RefCell<Option<servo::webxr::Discovery>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
#[cfg_attr(target_env = "ohos", allow(dead_code))]
|
||||
impl XrDiscoveryWebXrRegistry {
|
||||
pub(crate) fn new(xr_discovery: Option<servo::webxr::Discovery>) -> Self {
|
||||
Self {
|
||||
xr_discovery: RefCell::new(xr_discovery),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
impl servo::webxr::WebXrRegistry for XrDiscoveryWebXrRegistry {
|
||||
fn register(&self, registry: &mut servo::webxr::MainThreadRegistry) {
|
||||
debug!("XrDiscoveryWebXrRegistry::register");
|
||||
if let Some(discovery) = self.xr_discovery.take() {
|
||||
registry.register(discovery);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
ports/servoshell/egl/gamepad.rs
Normal file
34
ports/servoshell/egl/gamepad.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use servo::ipc_channel::ipc::IpcSender;
|
||||
use servo::{GamepadHapticEffectType, WebView};
|
||||
|
||||
/// A dummy version of [`crate::desktop::GamepadSupport`] used to avoid conditional compilation in
|
||||
/// servoshell and as a skeleton to implement gamepad support for platforms that do not
|
||||
/// currently support it.
|
||||
pub(crate) struct GamepadSupport;
|
||||
|
||||
impl GamepadSupport {
|
||||
pub(crate) fn maybe_new() -> Option<Self> {
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn handle_gamepad_events(&mut self, _active_webview: WebView) {
|
||||
unreachable!("Dummy gamepad support should never be called.");
|
||||
}
|
||||
|
||||
pub(crate) fn play_haptic_effect(
|
||||
&mut self,
|
||||
_index: usize,
|
||||
_effect_type: GamepadHapticEffectType,
|
||||
_effect_complete_sender: IpcSender<bool>,
|
||||
) {
|
||||
unreachable!("Dummy gamepad support should never be called.");
|
||||
}
|
||||
|
||||
pub(crate) fn stop_haptic_effect(&mut self, _index: usize) -> bool {
|
||||
unreachable!("Dummy gamepad support should never be called.");
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,11 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use servo::{
|
||||
InputMethodControl, LoadStatus, MediaSessionPlaybackState, PermissionRequest, SimpleDialog,
|
||||
WebView,
|
||||
};
|
||||
use servo::{InputMethodControl, LoadStatus, MediaSessionPlaybackState};
|
||||
|
||||
/// Callbacks implemented by embedder. Called by our RunningAppState, generally on behalf of Servo.
|
||||
pub trait HostTrait {
|
||||
/// Content in a [`WebView`] is requesting permission to access a feature requiring
|
||||
/// permission from the user. The embedder should allow or deny the request, either by
|
||||
/// reading a cached value or querying the user for permission via the user interface.
|
||||
fn request_permission(&self, _webview: WebView, _: PermissionRequest);
|
||||
/// Show the user a [simple dialog](https://html.spec.whatwg.org/multipage/#simple-dialogs) (`alert()`, `confirm()`,
|
||||
/// or `prompt()`). Since their messages are controlled by web content, they should be presented to the user in a
|
||||
/// way that makes them impossible to mistake for browser UI.
|
||||
/// TODO: This API needs to be reworked to match the new model of how responses are sent.
|
||||
fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog);
|
||||
fn show_alert(&self, message: String);
|
||||
/// Notify that the load status of the page has changed.
|
||||
/// Started:
|
||||
/// - "Reload button" should be disabled.
|
||||
@@ -30,8 +19,6 @@ pub trait HostTrait {
|
||||
fn notify_load_status_changed(&self, load_status: LoadStatus);
|
||||
/// Page title has changed.
|
||||
fn on_title_changed(&self, title: Option<String>);
|
||||
/// Allow Navigation.
|
||||
fn on_allow_navigation(&self, url: String) -> bool;
|
||||
/// Page URL has changed.
|
||||
fn on_url_changed(&self, url: String);
|
||||
/// Back/forward state has changed.
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
|
||||
mod app;
|
||||
pub(crate) mod gamepad;
|
||||
mod host_trait;
|
||||
mod log;
|
||||
#[cfg(target_env = "ohos")]
|
||||
mod ohos;
|
||||
|
||||
mod log;
|
||||
|
||||
mod app_state;
|
||||
mod host_trait;
|
||||
|
||||
@@ -3,31 +3,44 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod resources;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::os::raw::c_void;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::sync::{LazyLock, Mutex, Once, OnceLock, mpsc};
|
||||
use std::thread;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{fs, thread};
|
||||
|
||||
use dpi::PhysicalSize;
|
||||
use euclid::{Point2D, Rect, Size2D};
|
||||
use keyboard_types::{Key, NamedKey};
|
||||
use log::{LevelFilter, debug, error, info, trace, warn};
|
||||
use napi_derive_ohos::napi;
|
||||
use napi_ohos::bindgen_prelude::{Function, JsObjectValue, Object};
|
||||
use napi_ohos::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
||||
use napi_ohos::{Env, JsString, JsValue};
|
||||
use ohos_abilitykit_sys::runtime::application_context;
|
||||
use ohos_ime::{
|
||||
AttachOptions, CreateImeProxyError, CreateTextEditorProxyError, Ime, ImeProxy,
|
||||
RawTextEditorProxy,
|
||||
};
|
||||
use ohos_ime_sys::types::InputMethod_EnterKeyType;
|
||||
use ohos_window_manager_sys::display_manager;
|
||||
use raw_window_handle::{
|
||||
DisplayHandle, OhosDisplayHandle, OhosNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
|
||||
WindowHandle,
|
||||
};
|
||||
use servo::style::Zero;
|
||||
use servo::webrender_api::units::DevicePixel;
|
||||
use servo::{
|
||||
AlertResponse, EventLoopWaker, InputMethodControl, InputMethodType, LoadStatus,
|
||||
MediaSessionPlaybackState, PermissionRequest, SimpleDialog, WebView, WebViewId,
|
||||
self, EventLoopWaker, InputMethodControl, InputMethodType, LoadStatus,
|
||||
MediaSessionPlaybackState, WebViewId, WindowRenderingContext,
|
||||
};
|
||||
use xcomponent_sys::{
|
||||
OH_NativeXComponent, OH_NativeXComponent_Callback, OH_NativeXComponent_GetKeyEvent,
|
||||
@@ -39,12 +52,213 @@ use xcomponent_sys::{
|
||||
OH_NativeXComponent_TouchEvent, OH_NativeXComponent_TouchEventType,
|
||||
};
|
||||
|
||||
use super::app_state::{Coordinates, RunningAppState};
|
||||
use super::app::{App, AppInitOptions, VsyncRefreshDriver};
|
||||
use super::host_trait::HostTrait;
|
||||
use crate::running_app_state::RunningAppStateTrait;
|
||||
use crate::egl::ohos::resources::ResourceReaderInstance;
|
||||
use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments};
|
||||
|
||||
mod resources;
|
||||
mod simpleservo;
|
||||
/// Queue length for the thread-safe function to submit URL updates to ArkTS
|
||||
const UPDATE_URL_QUEUE_SIZE: usize = 1;
|
||||
/// Queue length for the thread-safe function to submit prompts to ArkTS
|
||||
///
|
||||
/// We can submit alerts in a non-blocking fashion, but alerts will always come from the
|
||||
/// embedder thread. Specifying 4 as a max queue size seems reasonable for now, and can
|
||||
/// be adjusted later.
|
||||
const PROMPT_QUEUE_SIZE: usize = 4;
|
||||
// Todo: Need to check if OnceLock is suitable, or if the TS function can be destroyed, e.g.
|
||||
// if the activity gets suspended.
|
||||
static SET_URL_BAR_CB: OnceLock<
|
||||
ThreadsafeFunction<String, (), String, napi_ohos::Status, false, false, UPDATE_URL_QUEUE_SIZE>,
|
||||
> = OnceLock::new();
|
||||
static TERMINATE_CALLBACK: OnceLock<
|
||||
ThreadsafeFunction<(), (), (), napi_ohos::Status, false, false, 1>,
|
||||
> = OnceLock::new();
|
||||
static PROMPT_TOAST: OnceLock<
|
||||
ThreadsafeFunction<String, (), String, napi_ohos::Status, false, false, PROMPT_QUEUE_SIZE>,
|
||||
> = OnceLock::new();
|
||||
|
||||
/// Currently we do not support different contexts for different windows but we might want to change tabs.
|
||||
/// For this we store the window context for every tab and change the compositor by hand.
|
||||
static NATIVE_WEBVIEWS: Mutex<Vec<NativeWebViewComponents>> = Mutex::new(Vec::new());
|
||||
|
||||
static SERVO_CHANNEL: OnceLock<Sender<ServoAction>> = OnceLock::new();
|
||||
|
||||
pub(crate) fn get_raw_window_handle(
|
||||
xcomponent: *mut OH_NativeXComponent,
|
||||
window: *mut c_void,
|
||||
) -> (RawWindowHandle, Rect<i32, DevicePixel>) {
|
||||
let window_size = unsafe { get_xcomponent_size(xcomponent, window) }
|
||||
.expect("Could not get native window size");
|
||||
let window_origin = unsafe { get_xcomponent_offset(xcomponent, window) }
|
||||
.expect("Could not get native window offset");
|
||||
let viewport_rect = Rect::new(window_origin, window_size);
|
||||
let native_window = NonNull::new(window).expect("Could not get native window");
|
||||
let window_handle = RawWindowHandle::OhosNdk(OhosNdkWindowHandle::new(native_window));
|
||||
(window_handle, viewport_rect)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NativeValues {
|
||||
cache_dir: String,
|
||||
display_density: f32,
|
||||
device_type: ohos_deviceinfo::OhosDeviceType,
|
||||
os_full_name: String,
|
||||
}
|
||||
|
||||
/// Gets the resource and cache directory from the native c methods.
|
||||
fn get_native_values() -> NativeValues {
|
||||
let cache_dir = {
|
||||
const BUFFER_SIZE: i32 = 100;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(BUFFER_SIZE as usize);
|
||||
let mut write_length = 0;
|
||||
unsafe {
|
||||
application_context::OH_AbilityRuntime_ApplicationContextGetCacheDir(
|
||||
buffer.as_mut_ptr().cast(),
|
||||
BUFFER_SIZE,
|
||||
&mut write_length,
|
||||
)
|
||||
.expect("Call to cache dir failed");
|
||||
buffer.set_len(write_length as usize);
|
||||
String::from_utf8(buffer).expect("UTF-8")
|
||||
}
|
||||
};
|
||||
let display_density = unsafe {
|
||||
let mut density: f32 = 0_f32;
|
||||
display_manager::OH_NativeDisplayManager_GetDefaultDisplayDensityPixels(&mut density)
|
||||
.expect("Could not get displaydensity");
|
||||
density
|
||||
};
|
||||
|
||||
NativeValues {
|
||||
cache_dir,
|
||||
display_density,
|
||||
device_type: ohos_deviceinfo::get_device_type(),
|
||||
os_full_name: String::from(ohos_deviceinfo::get_os_full_name().unwrap_or("Undefined")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the servoshell [`App`]. At that point, we need a valid GL context. In the
|
||||
/// future, this will be done in multiple steps.
|
||||
fn init_app(
|
||||
options: InitOpts,
|
||||
native_window: *mut c_void,
|
||||
xcomponent: *mut OH_NativeXComponent,
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
host: Box<dyn HostTrait>,
|
||||
) -> Result<Rc<App>, &'static str> {
|
||||
info!("Entered servoshell init function");
|
||||
crate::init_crypto();
|
||||
|
||||
let native_values = get_native_values();
|
||||
info!("Device Type {:?}", native_values.device_type);
|
||||
info!("OS Full Name {:?}", native_values.os_full_name);
|
||||
info!("ResourceDir {:?}", options.resource_dir);
|
||||
|
||||
let resource_dir = PathBuf::from(&options.resource_dir).join("servo");
|
||||
debug!("Resources are located at: {:?}", resource_dir);
|
||||
servo::resources::set(Box::new(ResourceReaderInstance::new(resource_dir.clone())));
|
||||
|
||||
// It would be nice if `from_cmdline_args()` could accept str slices, to avoid allocations here.
|
||||
// Then again, this code could and maybe even should be disabled in production builds.
|
||||
let mut args = vec!["servoshell".to_string()];
|
||||
args.extend(
|
||||
options
|
||||
.commandline_args
|
||||
.split("\u{1f}")
|
||||
.map(|arg| arg.to_string()),
|
||||
);
|
||||
debug!("Servo commandline args: {:?}", args);
|
||||
|
||||
let config_dir = PathBuf::from(&native_values.cache_dir).join("servo");
|
||||
debug!("Configs are located at: {:?}", config_dir);
|
||||
let _ = crate::prefs::DEFAULT_CONFIG_DIR
|
||||
.set(config_dir.clone())
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Default Prefs Dir already previously filled. Got error {}",
|
||||
e.display()
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure cache dir exists before copy `prefs.json`
|
||||
let _ = crate::prefs::default_config_dir().inspect(|path| {
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).unwrap_or_else(|e| {
|
||||
log::error!("Failed to create config directory at {:?}: {:?}", path, e)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Try copy `prefs.json` from {this.context.resource_prefsDir}/servo/
|
||||
// to `config_dir` if none exist
|
||||
let source_prefs = resource_dir.join("prefs.json");
|
||||
let target_prefs = config_dir.join("prefs.json");
|
||||
if !target_prefs.exists() && source_prefs.exists() {
|
||||
debug!("Copy {:?} to {:?}", source_prefs, target_prefs);
|
||||
fs::copy(&source_prefs, &target_prefs).unwrap_or_else(|e| {
|
||||
debug!("Copy failed! {:?}", e);
|
||||
0
|
||||
});
|
||||
}
|
||||
|
||||
let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) {
|
||||
ArgumentParsingResult::ContentProcess(..) => {
|
||||
unreachable!("OHOS does not have support for multiprocess yet.")
|
||||
},
|
||||
ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
|
||||
(opts, preferences, servoshell_preferences)
|
||||
},
|
||||
ArgumentParsingResult::Exit => std::process::exit(0),
|
||||
ArgumentParsingResult::ErrorParsing => std::process::exit(1),
|
||||
};
|
||||
|
||||
if native_values.device_type == ohos_deviceinfo::OhosDeviceType::Phone {
|
||||
preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true));
|
||||
}
|
||||
|
||||
if servoshell_preferences.log_to_file {
|
||||
let mut servo_log = PathBuf::from(&native_values.cache_dir);
|
||||
servo_log.push("servo.log");
|
||||
if crate::egl::ohos::LOGGER.set_file_writer(servo_log).is_err() {
|
||||
warn!("Could not set log file");
|
||||
}
|
||||
}
|
||||
|
||||
crate::init_tracing(servoshell_preferences.tracing_filter.as_deref());
|
||||
#[cfg(target_env = "ohos")]
|
||||
crate::egl::ohos::set_log_filter(servoshell_preferences.log_filter.as_deref());
|
||||
|
||||
let (window_handle, viewport_rect) = get_raw_window_handle(xcomponent, native_window);
|
||||
let display_handle = RawDisplayHandle::Ohos(OhosDisplayHandle::new());
|
||||
let display_handle = unsafe { DisplayHandle::borrow_raw(display_handle) };
|
||||
let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) };
|
||||
|
||||
let viewport_size = viewport_rect.size;
|
||||
let refresh_driver = Rc::new(VsyncRefreshDriver::default());
|
||||
let rendering_context = Rc::new(
|
||||
WindowRenderingContext::new_with_refresh_driver(
|
||||
display_handle,
|
||||
window_handle,
|
||||
PhysicalSize::new(viewport_size.width as u32, viewport_size.height as u32),
|
||||
refresh_driver.clone(),
|
||||
)
|
||||
.expect("Could not create RenderingContext"),
|
||||
);
|
||||
Ok(App::new(AppInitOptions {
|
||||
host,
|
||||
event_loop_waker,
|
||||
viewport_rect,
|
||||
hidpi_scale_factor: native_values.display_density as f32,
|
||||
rendering_context,
|
||||
refresh_driver,
|
||||
initial_url: Some(options.url),
|
||||
opts,
|
||||
preferences,
|
||||
servoshell_preferences,
|
||||
#[cfg(feature = "webxr")]
|
||||
xr_discovery: None,
|
||||
}))
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Debug)]
|
||||
@@ -114,26 +328,6 @@ pub(super) enum ServoAction {
|
||||
NewWebview(XComponentWrapper, WindowWrapper),
|
||||
}
|
||||
|
||||
/// Queue length for the thread-safe function to submit URL updates to ArkTS
|
||||
const UPDATE_URL_QUEUE_SIZE: usize = 1;
|
||||
/// Queue length for the thread-safe function to submit prompts to ArkTS
|
||||
///
|
||||
/// We can submit alerts in a non-blocking fashion, but alerts will always come from the
|
||||
/// embedder thread. Specifying 4 as a max queue size seems reasonable for now, and can
|
||||
/// be adjusted later.
|
||||
const PROMPT_QUEUE_SIZE: usize = 4;
|
||||
// Todo: Need to check if OnceLock is suitable, or if the TS function can be destroyed, e.g.
|
||||
// if the activity gets suspended.
|
||||
static SET_URL_BAR_CB: OnceLock<
|
||||
ThreadsafeFunction<String, (), String, napi_ohos::Status, false, false, UPDATE_URL_QUEUE_SIZE>,
|
||||
> = OnceLock::new();
|
||||
static TERMINATE_CALLBACK: OnceLock<
|
||||
ThreadsafeFunction<(), (), (), napi_ohos::Status, false, false, 1>,
|
||||
> = OnceLock::new();
|
||||
static PROMPT_TOAST: OnceLock<
|
||||
ThreadsafeFunction<String, (), String, napi_ohos::Status, false, false, PROMPT_QUEUE_SIZE>,
|
||||
> = OnceLock::new();
|
||||
|
||||
/// Storing webview related items
|
||||
struct NativeWebViewComponents {
|
||||
/// The id of the related webview
|
||||
@@ -144,18 +338,8 @@ struct NativeWebViewComponents {
|
||||
window: WindowWrapper,
|
||||
}
|
||||
|
||||
/// Currently we do not support different contexts for different windows but we might want to change tabs.
|
||||
/// For this we store the window context for every tab and change the compositor by hand.
|
||||
static NATIVE_WEBVIEWS: Mutex<Vec<NativeWebViewComponents>> = Mutex::new(Vec::new());
|
||||
|
||||
impl ServoAction {
|
||||
fn dispatch_touch_event(
|
||||
servo: &RunningAppState,
|
||||
kind: TouchEventType,
|
||||
x: f32,
|
||||
y: f32,
|
||||
pointer_id: i32,
|
||||
) {
|
||||
fn dispatch_touch_event(servo: &App, kind: TouchEventType, x: f32, y: f32, pointer_id: i32) {
|
||||
match kind {
|
||||
TouchEventType::Down => servo.touch_down(x, y, pointer_id),
|
||||
TouchEventType::Up => servo.touch_up(x, y, pointer_id),
|
||||
@@ -166,12 +350,11 @@ impl ServoAction {
|
||||
}
|
||||
|
||||
// todo: consider making this take `self`, so we don't need to needlessly clone.
|
||||
fn do_action(&self, servo: &Rc<RunningAppState>) {
|
||||
fn do_action(&self, servo: &Rc<App>) {
|
||||
use ServoAction::*;
|
||||
match self {
|
||||
WakeUp => {
|
||||
servo.perform_updates();
|
||||
servo.present_if_needed();
|
||||
servo.spin_event_loop();
|
||||
},
|
||||
LoadUrl(url) => servo.load_uri(url.as_str()),
|
||||
GoBack => servo.go_back(),
|
||||
@@ -206,23 +389,26 @@ impl ServoAction {
|
||||
},
|
||||
Vsync => {
|
||||
servo.notify_vsync();
|
||||
servo.present_if_needed();
|
||||
},
|
||||
Resize { width, height } => servo.resize(Coordinates::new(0, 0, *width, *height)),
|
||||
Resize { width, height } => {
|
||||
servo.resize(Rect::new(Point2D::origin(), Size2D::new(*width, *height)))
|
||||
},
|
||||
FocusWebview(arkts_id) => {
|
||||
if let Some(native_webview_components) =
|
||||
NATIVE_WEBVIEWS.lock().unwrap().get(*arkts_id as usize)
|
||||
{
|
||||
if servo.active_webview_or_panic().id() != native_webview_components.id {
|
||||
let webview = servo
|
||||
.focused_or_newest_webview()
|
||||
.expect("Should always start with at least one WebView");
|
||||
if webview.id() != native_webview_components.id {
|
||||
servo.focus_webview(native_webview_components.id);
|
||||
servo.pause_compositor();
|
||||
let (window_handle, _, coordinates) = simpleservo::get_raw_window_handle(
|
||||
let (window_handle, viewport_rect) = get_raw_window_handle(
|
||||
native_webview_components.xcomponent.0,
|
||||
native_webview_components.window.0,
|
||||
);
|
||||
servo.resume_compositor(window_handle, coordinates);
|
||||
let url = servo
|
||||
.active_webview_or_panic()
|
||||
servo.resume_compositor(window_handle, viewport_rect);
|
||||
let url = webview
|
||||
.url()
|
||||
.map(|u| u.to_string())
|
||||
.unwrap_or(String::from("about:blank"));
|
||||
@@ -236,12 +422,11 @@ impl ServoAction {
|
||||
},
|
||||
NewWebview(xcomponent, window) => {
|
||||
servo.pause_compositor();
|
||||
servo.create_and_focus_toplevel_webview("about:blank".parse().unwrap());
|
||||
let (window_handle, _, coordinates) =
|
||||
simpleservo::get_raw_window_handle(xcomponent.0, window.0);
|
||||
let webview =
|
||||
servo.create_and_focus_toplevel_webview("about:blank".parse().unwrap());
|
||||
let (window_handle, viewport_rect) = get_raw_window_handle(xcomponent.0, window.0);
|
||||
|
||||
servo.resume_compositor(window_handle, coordinates);
|
||||
let webview = servo.newest_webview().expect("There should always be one");
|
||||
servo.resume_compositor(window_handle, viewport_rect);
|
||||
let id = webview.id();
|
||||
NATIVE_WEBVIEWS
|
||||
.lock()
|
||||
@@ -286,8 +471,6 @@ unsafe extern "C" fn on_vsync_cb(
|
||||
}
|
||||
}
|
||||
|
||||
static SERVO_CHANNEL: OnceLock<Sender<ServoAction>> = OnceLock::new();
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window: *mut c_void) {
|
||||
info!("on_surface_created_cb");
|
||||
@@ -320,14 +503,17 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window
|
||||
} else {
|
||||
panic!("Servos GL thread received another event before it was initialized")
|
||||
};
|
||||
let servo = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks)
|
||||
let servo = init_app(*init_opts, window.0, xc.0, wakeup, callbacks)
|
||||
.expect("Servo initialization failed");
|
||||
|
||||
let id = servo
|
||||
.focused_or_newest_webview()
|
||||
.expect("Should always start with at least one WebView")
|
||||
.id();
|
||||
NATIVE_WEBVIEWS
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(NativeWebViewComponents {
|
||||
id: servo.active_webview_or_panic().id(),
|
||||
id,
|
||||
xcomponent: xc,
|
||||
window,
|
||||
});
|
||||
@@ -347,8 +533,6 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window
|
||||
while let Ok(action) = rx.recv() {
|
||||
trace!("Wakeup message received!");
|
||||
action.do_action(&servo);
|
||||
// Also handle any pending WebDriver messages
|
||||
servo.handle_webdriver_messages();
|
||||
}
|
||||
|
||||
info!("Sender disconnected - Terminating main surface thread");
|
||||
@@ -369,7 +553,7 @@ extern "C" fn on_surface_created_cb(xcomponent: *mut OH_NativeXComponent, window
|
||||
unsafe fn get_xcomponent_offset(
|
||||
xcomponent: *mut OH_NativeXComponent,
|
||||
native_window: *mut c_void,
|
||||
) -> Result<(i32, i32), i32> {
|
||||
) -> Result<Point2D<i32, DevicePixel>, i32> {
|
||||
let mut x: f64 = 0.0;
|
||||
let mut y: f64 = 0.0;
|
||||
|
||||
@@ -380,11 +564,9 @@ unsafe fn get_xcomponent_offset(
|
||||
error!("OH_NativeXComponent_GetXComponentOffset failed with {result}");
|
||||
return Err(result);
|
||||
}
|
||||
|
||||
Ok((
|
||||
(x.round() as i64).try_into().expect("X offset too large"),
|
||||
(y.round() as i64).try_into().expect("Y offset too large"),
|
||||
))
|
||||
let x = (x.round() as i64).try_into().expect("X offset too large");
|
||||
let y = (y.round() as i64).try_into().expect("Y offset too large");
|
||||
Ok(Point2D::new(x, y))
|
||||
}
|
||||
|
||||
/// Returns the size of the surface
|
||||
@@ -396,7 +578,7 @@ unsafe fn get_xcomponent_offset(
|
||||
unsafe fn get_xcomponent_size(
|
||||
xcomponent: *mut OH_NativeXComponent,
|
||||
native_window: *mut c_void,
|
||||
) -> Result<euclid::default::Size2D<i32>, i32> {
|
||||
) -> Result<Size2D<i32, DevicePixel>, i32> {
|
||||
let mut width: u64 = 0;
|
||||
let mut height: u64 = 0;
|
||||
let result = unsafe {
|
||||
@@ -414,7 +596,7 @@ unsafe fn get_xcomponent_size(
|
||||
|
||||
let width: i32 = width.try_into().expect("Width too large");
|
||||
let height: i32 = height.try_into().expect("Height too large");
|
||||
Ok(euclid::Size2D::new(width, height))
|
||||
Ok(Size2D::new(width, height))
|
||||
}
|
||||
|
||||
extern "C" fn on_surface_changed_cb(
|
||||
@@ -652,7 +834,7 @@ fn debug_jsobject(obj: &Object, obj_name: &str) -> napi_ohos::Result<()> {
|
||||
#[napi(module_exports)]
|
||||
fn init(exports: Object, env: Env) -> napi_ohos::Result<()> {
|
||||
initialize_logging_once();
|
||||
info!("simpleservo init function called");
|
||||
info!("servoshell init function called");
|
||||
if let Ok(xcomponent) = exports.get_named_property::<Object>("__NATIVE_XCOMPONENT_OBJ__") {
|
||||
register_xcomponent_callbacks(&env, &xcomponent)?;
|
||||
}
|
||||
@@ -836,19 +1018,6 @@ impl HostCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_alert(&self, message: String) {
|
||||
match PROMPT_TOAST.get() {
|
||||
Some(prompt_fn) => {
|
||||
let status = prompt_fn.call(message, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
if status != napi_ohos::Status::Ok {
|
||||
// Queue could be full.
|
||||
error!("show_alert failed with {status}");
|
||||
}
|
||||
},
|
||||
None => error!("PROMPT_TOAST not set. Dropping message {message}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_create_ime_proxy(
|
||||
&self,
|
||||
input_type: InputMethodType,
|
||||
@@ -891,51 +1060,27 @@ impl Ime for ServoIme {
|
||||
|
||||
#[allow(unused)]
|
||||
impl HostTrait for HostCallbacks {
|
||||
fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
|
||||
warn!("Permissions prompt not implemented. Denied.");
|
||||
request.deny();
|
||||
}
|
||||
|
||||
fn show_simple_dialog(&self, _webview: WebView, dialog: SimpleDialog) {
|
||||
let _ = match dialog {
|
||||
SimpleDialog::Alert {
|
||||
message,
|
||||
response_sender,
|
||||
..
|
||||
} => {
|
||||
debug!("SimpleDialog::Alert");
|
||||
|
||||
// forward it to tracing
|
||||
#[cfg(feature = "tracing-hitrace")]
|
||||
{
|
||||
if message.contains("TESTCASE_PROFILING") {
|
||||
if let Some((tag, number)) = message.rsplit_once(":") {
|
||||
hitrace::trace_metric_str(tag, number.parse::<i64>().unwrap_or(-1));
|
||||
}
|
||||
}
|
||||
fn show_alert(&self, message: String) {
|
||||
// forward it to tracing
|
||||
#[cfg(feature = "tracing-hitrace")]
|
||||
{
|
||||
if message.contains("TESTCASE_PROFILING") {
|
||||
if let Some((tag, number)) = message.rsplit_once(":") {
|
||||
hitrace::trace_metric_str(tag, number.parse::<i64>().unwrap_or(-1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Indicate that this message is untrusted, and what origin it came from.
|
||||
self.show_alert(message);
|
||||
response_sender.send(AlertResponse::Ok)
|
||||
match PROMPT_TOAST.get() {
|
||||
Some(prompt_fn) => {
|
||||
let status = prompt_fn.call(message, ThreadsafeFunctionCallMode::NonBlocking);
|
||||
if status != napi_ohos::Status::Ok {
|
||||
// Queue could be full.
|
||||
error!("show_alert failed with {status}");
|
||||
}
|
||||
},
|
||||
SimpleDialog::Confirm {
|
||||
message,
|
||||
response_sender,
|
||||
..
|
||||
} => {
|
||||
warn!("Confirm dialog not implemented. Cancelled. {}", message);
|
||||
response_sender.send(Default::default())
|
||||
},
|
||||
SimpleDialog::Prompt {
|
||||
message,
|
||||
response_sender,
|
||||
..
|
||||
} => {
|
||||
warn!("Prompt dialog not implemented. Cancelled. {}", message);
|
||||
response_sender.send(Default::default())
|
||||
},
|
||||
};
|
||||
None => error!("PROMPT_TOAST not set. Dropping message {message}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_load_status_changed(&self, load_status: LoadStatus) {
|
||||
@@ -961,10 +1106,6 @@ impl HostTrait for HostCallbacks {
|
||||
warn!("on_title_changed not implemented")
|
||||
}
|
||||
|
||||
fn on_allow_navigation(&self, url: String) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_url_changed(&self, url: String) {
|
||||
debug!("Hosttrait `on_url_changed` called with new url: {url}");
|
||||
match SET_URL_BAR_CB.get() {
|
||||
@@ -1,228 +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::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::os::raw::c_void;
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crossbeam_channel::unbounded;
|
||||
use dpi::PhysicalSize;
|
||||
use log::{debug, info, warn};
|
||||
use ohos_abilitykit_sys::runtime::application_context;
|
||||
use ohos_window_manager_sys::display_manager;
|
||||
use raw_window_handle::{
|
||||
DisplayHandle, OhosDisplayHandle, OhosNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
|
||||
WindowHandle,
|
||||
};
|
||||
use servo::{self, EventLoopWaker, ServoBuilder, WindowRenderingContext, resources};
|
||||
use xcomponent_sys::OH_NativeXComponent;
|
||||
|
||||
use crate::egl::app_state::{
|
||||
Coordinates, RunningAppState, ServoWindowCallbacks, VsyncRefreshDriver,
|
||||
};
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::egl::ohos::InitOpts;
|
||||
use crate::egl::ohos::resources::ResourceReaderInstance;
|
||||
use crate::prefs::{ArgumentParsingResult, parse_command_line_arguments};
|
||||
|
||||
pub(crate) fn get_raw_window_handle(
|
||||
xcomponent: *mut OH_NativeXComponent,
|
||||
window: *mut c_void,
|
||||
) -> (RawWindowHandle, euclid::default::Size2D<i32>, Coordinates) {
|
||||
let window_size = unsafe { super::get_xcomponent_size(xcomponent, window) }
|
||||
.expect("Could not get native window size");
|
||||
let (x, y) = unsafe { super::get_xcomponent_offset(xcomponent, window) }
|
||||
.expect("Could not get native window offset");
|
||||
let coordinates = Coordinates::new(x, y, window_size.width, window_size.height);
|
||||
let native_window = NonNull::new(window).expect("Could not get native window");
|
||||
let window_handle = RawWindowHandle::OhosNdk(OhosNdkWindowHandle::new(native_window));
|
||||
(window_handle, window_size, coordinates)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NativeValues {
|
||||
cache_dir: String,
|
||||
display_density: f32,
|
||||
device_type: ohos_deviceinfo::OhosDeviceType,
|
||||
os_full_name: String,
|
||||
}
|
||||
|
||||
/// Gets the resource and cache directory from the native c methods.
|
||||
fn get_native_values() -> NativeValues {
|
||||
let cache_dir = {
|
||||
const BUFFER_SIZE: i32 = 100;
|
||||
let mut buffer: Vec<u8> = Vec::with_capacity(BUFFER_SIZE as usize);
|
||||
let mut write_length = 0;
|
||||
unsafe {
|
||||
application_context::OH_AbilityRuntime_ApplicationContextGetCacheDir(
|
||||
buffer.as_mut_ptr().cast(),
|
||||
BUFFER_SIZE,
|
||||
&mut write_length,
|
||||
)
|
||||
.expect("Call to cache dir failed");
|
||||
buffer.set_len(write_length as usize);
|
||||
String::from_utf8(buffer).expect("UTF-8")
|
||||
}
|
||||
};
|
||||
let display_density = unsafe {
|
||||
let mut density: f32 = 0_f32;
|
||||
display_manager::OH_NativeDisplayManager_GetDefaultDisplayDensityPixels(&mut density)
|
||||
.expect("Could not get displaydensity");
|
||||
density
|
||||
};
|
||||
|
||||
NativeValues {
|
||||
cache_dir,
|
||||
display_density,
|
||||
device_type: ohos_deviceinfo::get_device_type(),
|
||||
os_full_name: String::from(ohos_deviceinfo::get_os_full_name().unwrap_or("Undefined")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize Servo. At that point, we need a valid GL context.
|
||||
/// In the future, this will be done in multiple steps.
|
||||
pub fn init(
|
||||
options: InitOpts,
|
||||
native_window: *mut c_void,
|
||||
xcomponent: *mut OH_NativeXComponent,
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
callbacks: Box<dyn HostTrait>,
|
||||
) -> Result<Rc<RunningAppState>, &'static str> {
|
||||
info!("Entered simpleservo init function");
|
||||
crate::init_crypto();
|
||||
|
||||
let native_values = get_native_values();
|
||||
info!("Device Type {:?}", native_values.device_type);
|
||||
info!("OS Full Name {:?}", native_values.os_full_name);
|
||||
info!("ResourceDir {:?}", options.resource_dir);
|
||||
|
||||
let resource_dir = PathBuf::from(&options.resource_dir).join("servo");
|
||||
debug!("Resources are located at: {:?}", resource_dir);
|
||||
resources::set(Box::new(ResourceReaderInstance::new(resource_dir.clone())));
|
||||
|
||||
// It would be nice if `from_cmdline_args()` could accept str slices, to avoid allocations here.
|
||||
// Then again, this code could and maybe even should be disabled in production builds.
|
||||
let mut args = vec!["servoshell".to_string()];
|
||||
args.extend(
|
||||
options
|
||||
.commandline_args
|
||||
.split("\u{1f}")
|
||||
.map(|arg| arg.to_string()),
|
||||
);
|
||||
debug!("Servo commandline args: {:?}", args);
|
||||
|
||||
let config_dir = PathBuf::from(&native_values.cache_dir).join("servo");
|
||||
debug!("Configs are located at: {:?}", config_dir);
|
||||
let _ = crate::prefs::DEFAULT_CONFIG_DIR
|
||||
.set(config_dir.clone())
|
||||
.inspect_err(|e| {
|
||||
warn!(
|
||||
"Default Prefs Dir already previously filled. Got error {}",
|
||||
e.display()
|
||||
);
|
||||
});
|
||||
|
||||
// Ensure cache dir exists before copy `prefs.json`
|
||||
let _ = crate::prefs::default_config_dir().inspect(|path| {
|
||||
if !path.exists() {
|
||||
fs::create_dir_all(path).unwrap_or_else(|e| {
|
||||
log::error!("Failed to create config directory at {:?}: {:?}", path, e)
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
// Try copy `prefs.json` from {this.context.resource_prefsDir}/servo/
|
||||
// to `config_dir` if none exist
|
||||
let source_prefs = resource_dir.join("prefs.json");
|
||||
let target_prefs = config_dir.join("prefs.json");
|
||||
if !target_prefs.exists() && source_prefs.exists() {
|
||||
debug!("Copy {:?} to {:?}", source_prefs, target_prefs);
|
||||
fs::copy(&source_prefs, &target_prefs).unwrap_or_else(|e| {
|
||||
debug!("Copy failed! {:?}", e);
|
||||
0
|
||||
});
|
||||
}
|
||||
|
||||
let (opts, mut preferences, servoshell_preferences) = match parse_command_line_arguments(args) {
|
||||
ArgumentParsingResult::ContentProcess(..) => {
|
||||
unreachable!("OHOS does not have support for multiprocess yet.")
|
||||
},
|
||||
ArgumentParsingResult::ChromeProcess(opts, preferences, servoshell_preferences) => {
|
||||
(opts, preferences, servoshell_preferences)
|
||||
},
|
||||
ArgumentParsingResult::Exit => std::process::exit(0),
|
||||
ArgumentParsingResult::ErrorParsing => std::process::exit(1),
|
||||
};
|
||||
|
||||
if native_values.device_type == ohos_deviceinfo::OhosDeviceType::Phone {
|
||||
preferences.set_value("viewport_meta_enabled", servo::PrefValue::Bool(true));
|
||||
}
|
||||
|
||||
if servoshell_preferences.log_to_file {
|
||||
let mut servo_log = PathBuf::from(&native_values.cache_dir);
|
||||
servo_log.push("servo.log");
|
||||
if crate::egl::ohos::LOGGER.set_file_writer(servo_log).is_err() {
|
||||
warn!("Could not set log file");
|
||||
}
|
||||
}
|
||||
|
||||
crate::init_tracing(servoshell_preferences.tracing_filter.as_deref());
|
||||
#[cfg(target_env = "ohos")]
|
||||
crate::egl::ohos::set_log_filter(servoshell_preferences.log_filter.as_deref());
|
||||
|
||||
let (window_handle, window_size, coordinates) =
|
||||
get_raw_window_handle(xcomponent, native_window);
|
||||
|
||||
let display_handle = RawDisplayHandle::Ohos(OhosDisplayHandle::new());
|
||||
let display_handle = unsafe { DisplayHandle::borrow_raw(display_handle) };
|
||||
|
||||
let window_handle = unsafe { WindowHandle::borrow_raw(window_handle) };
|
||||
|
||||
let refresh_driver = Rc::new(VsyncRefreshDriver::default());
|
||||
let rendering_context = Rc::new(
|
||||
WindowRenderingContext::new_with_refresh_driver(
|
||||
display_handle,
|
||||
window_handle,
|
||||
PhysicalSize::new(window_size.width as u32, window_size.height as u32),
|
||||
refresh_driver.clone(),
|
||||
)
|
||||
.expect("Could not create RenderingContext"),
|
||||
);
|
||||
|
||||
info!("before ServoWindowCallbacks...");
|
||||
|
||||
let window_callbacks = Rc::new(ServoWindowCallbacks::new(
|
||||
callbacks,
|
||||
RefCell::new(coordinates),
|
||||
));
|
||||
|
||||
let servo = ServoBuilder::default()
|
||||
.opts(opts)
|
||||
.preferences(preferences)
|
||||
.event_loop_waker(waker.clone())
|
||||
.build();
|
||||
|
||||
// Initialize WebDriver server if port is specified
|
||||
let webdriver_receiver = servoshell_preferences.webdriver_port.map(|port| {
|
||||
let (embedder_sender, embedder_receiver) = unbounded();
|
||||
webdriver_server::start_server(port, embedder_sender, waker);
|
||||
log::info!("WebDriver server started on port {port}");
|
||||
embedder_receiver
|
||||
});
|
||||
|
||||
let app_state = RunningAppState::new(
|
||||
Some(options.url),
|
||||
native_values.display_density as f32,
|
||||
rendering_context,
|
||||
servo,
|
||||
window_callbacks,
|
||||
Some(refresh_driver),
|
||||
servoshell_preferences,
|
||||
webdriver_receiver,
|
||||
);
|
||||
|
||||
Ok(app_state)
|
||||
}
|
||||
@@ -19,9 +19,16 @@ mod egl;
|
||||
mod panic_hook;
|
||||
mod parser;
|
||||
mod prefs;
|
||||
#[cfg(not(any(target_os = "android", target_env = "ohos")))]
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod resources;
|
||||
mod running_app_state;
|
||||
mod webdriver;
|
||||
mod window;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_env = "ohos")))]
|
||||
pub(crate) use crate::desktop::gamepad::GamepadSupport;
|
||||
#[cfg(any(target_os = "android", target_env = "ohos"))]
|
||||
pub(crate) use crate::egl::gamepad::GamepadSupport;
|
||||
|
||||
pub mod platform {
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -25,8 +25,8 @@ fn main() {
|
||||
if #[cfg(not(any(target_os = "android", target_env = "ohos")))] {
|
||||
servoshell::main()
|
||||
} else {
|
||||
// Android: see ports/servoshell/egl/android/simpleservo.rs.
|
||||
// OpenHarmony: see ports/servoshell/egl/ohos/simpleservo.rs.
|
||||
// Android: see ports/servoshell/egl/android/mod.rs.
|
||||
// OpenHarmony: see ports/servoshell/egl/ohos/mod.rs.
|
||||
println!(
|
||||
"Cannot run the servoshell `bin` executable on platforms such as \
|
||||
Android or OpenHarmony. On these platforms you need to compile \
|
||||
|
||||
@@ -4,25 +4,34 @@
|
||||
|
||||
//! Shared state and methods for desktop and EGL implementations.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::cell::{Cell, Ref, RefCell};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::rc::Rc;
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use crossbeam_channel::{Receiver, Sender, unbounded};
|
||||
use euclid::Rect;
|
||||
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||
use log::{error, info, warn};
|
||||
use servo::base::generic_channel::GenericSender;
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::config::pref;
|
||||
use servo::ipc_channel::ipc::IpcSender;
|
||||
use servo::style_traits::CSSPixel;
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize};
|
||||
use servo::{
|
||||
InputEvent, InputEventId, ScreenshotCaptureError, Servo, TraversalId, WebDriverCommandMsg,
|
||||
WebDriverJSResult, WebDriverLoadStatus, WebDriverScriptCommand, WebDriverSenders, WebView,
|
||||
AllowOrDenyRequest, AuthenticationRequest, EmbedderControl, EmbedderControlId, EventLoopWaker,
|
||||
GamepadHapticEffectType, InputEvent, InputEventId, InputEventResult, JSValue, LoadStatus,
|
||||
MediaSessionEvent, PermissionRequest, ScreenshotCaptureError, Servo, ServoDelegate, ServoError,
|
||||
TraversalId, WebDriverCommandMsg, WebDriverJSResult, WebDriverLoadStatus,
|
||||
WebDriverScriptCommand, WebDriverSenders, WebView, WebViewBuilder, WebViewDelegate,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::GamepadSupport;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
use crate::webdriver::WebDriverEmbedderControls;
|
||||
use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WebViewCollection {
|
||||
@@ -32,7 +41,7 @@ pub struct WebViewCollection {
|
||||
webviews: HashMap<WebViewId, WebView>,
|
||||
|
||||
/// The order in which the webviews were created.
|
||||
creation_order: Vec<WebViewId>,
|
||||
pub(crate) creation_order: Vec<WebViewId>,
|
||||
|
||||
/// The webview that is currently focused.
|
||||
/// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred.
|
||||
@@ -85,46 +94,35 @@ impl WebViewCollection {
|
||||
.and_then(|id| self.webviews.get(id))
|
||||
}
|
||||
|
||||
/// Gets the "active" webview: the focused webview if there is one,
|
||||
/// otherwise the most recently created webview.
|
||||
#[allow(dead_code)]
|
||||
pub fn active(&self) -> Option<&WebView> {
|
||||
self.focused().or_else(|| self.newest())
|
||||
}
|
||||
|
||||
pub fn all_in_creation_order(&self) -> impl Iterator<Item = (WebViewId, &WebView)> {
|
||||
self.creation_order
|
||||
.iter()
|
||||
.filter_map(move |id| self.webviews.get(id).map(|webview| (*id, webview)))
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.webviews.clear();
|
||||
self.creation_order.clear();
|
||||
self.focused_webview_id = None;
|
||||
}
|
||||
|
||||
/// Returns an iterator over all webview references (in arbitrary order).
|
||||
pub fn values(&self) -> impl Iterator<Item = &WebView> {
|
||||
self.webviews.values()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.webviews.len()
|
||||
}
|
||||
|
||||
/// Returns true if the collection contains no webviews.
|
||||
#[allow(dead_code)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.webviews.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RunningAppStateBase {
|
||||
pub(crate) webview_collection: RefCell<WebViewCollection>,
|
||||
pub(crate) struct RunningAppState {
|
||||
/// Gamepad support, which may be `None` if it failed to initialize.
|
||||
gamepad_support: RefCell<Option<GamepadSupport>>,
|
||||
|
||||
/// The [`WebDriverSenders`] used to reply to pending WebDriver requests.
|
||||
pub(crate) webdriver_senders: RefCell<WebDriverSenders>,
|
||||
|
||||
/// When running in WebDriver mode, [`WebDriverEmbedderControls`] is a virtual container
|
||||
/// for all embedder controls. This overrides the normal behavior where these controls
|
||||
/// are shown in the GUI or not processed at all in headless mode.
|
||||
pub(crate) webdriver_embedder_controls: WebDriverEmbedderControls,
|
||||
|
||||
/// A [`HashMap`] of pending WebDriver events. It is the WebDriver embedder's responsibility
|
||||
/// to inform the WebDriver server when the event has been fully handled. This map is used
|
||||
/// to report back to WebDriver when that happens.
|
||||
@@ -143,17 +141,45 @@ pub struct RunningAppStateBase {
|
||||
/// Whether or not the application has achieved stable image output. This is used
|
||||
/// for the `exit_after_stable_image` option.
|
||||
pub(crate) achieved_stable_image: Rc<Cell<bool>>,
|
||||
|
||||
/// The set of [`ServoShellWindow`]s that currently exist for this instance of servoshell.
|
||||
// This is the last field of the struct to ensure that windows are dropped *after* all
|
||||
// other references to the relevant rendering contexts have been destroyed.
|
||||
// See https://github.com/servo/servo/issues/36711.
|
||||
windows: RefCell<HashMap<ServoShellWindowId, Rc<ServoShellWindow>>>,
|
||||
}
|
||||
|
||||
impl RunningAppStateBase {
|
||||
pub fn new(
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
impl Drop for RunningAppState {
|
||||
fn drop(&mut self) {
|
||||
self.servo.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningAppState {
|
||||
pub(crate) fn new(
|
||||
servo: Servo,
|
||||
webdriver_receiver: Option<Receiver<WebDriverCommandMsg>>,
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
event_loop_waker: Box<dyn EventLoopWaker>,
|
||||
) -> Self {
|
||||
servo.set_delegate(Rc::new(ServoShellServoDelegate));
|
||||
|
||||
let gamepad_support = if pref!(dom_gamepad_enabled) {
|
||||
GamepadSupport::maybe_new()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let webdriver_receiver = servoshell_preferences.webdriver_port.map(|port| {
|
||||
let (embedder_sender, embedder_receiver) = unbounded();
|
||||
webdriver_server::start_server(port, embedder_sender, event_loop_waker);
|
||||
embedder_receiver
|
||||
});
|
||||
|
||||
Self {
|
||||
webview_collection: RefCell::default(),
|
||||
windows: Default::default(),
|
||||
gamepad_support: RefCell::new(gamepad_support),
|
||||
webdriver_senders: RefCell::default(),
|
||||
webdriver_embedder_controls: Default::default(),
|
||||
pending_webdriver_events: Default::default(),
|
||||
webdriver_receiver,
|
||||
servoshell_preferences,
|
||||
@@ -161,200 +187,134 @@ impl RunningAppStateBase {
|
||||
achieved_stable_image: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait RunningAppStateTrait {
|
||||
fn base(&self) -> &RunningAppStateBase;
|
||||
#[expect(dead_code)]
|
||||
fn base_mut(&mut self) -> &mut RunningAppStateBase;
|
||||
fn webview_by_id(&self, _: WebViewId) -> Option<WebView>;
|
||||
fn dismiss_embedder_controls_for_webview(&self, _webview_id: WebViewId) {}
|
||||
|
||||
fn servoshell_preferences(&self) -> &ServoShellPreferences {
|
||||
&self.base().servoshell_preferences
|
||||
}
|
||||
|
||||
fn servo(&self) -> &Servo {
|
||||
&self.base().servo
|
||||
}
|
||||
|
||||
fn webdriver_receiver(&self) -> Option<&Receiver<WebDriverCommandMsg>> {
|
||||
self.base().webdriver_receiver.as_ref()
|
||||
}
|
||||
|
||||
fn webview_collection(&self) -> std::cell::Ref<'_, WebViewCollection> {
|
||||
self.base().webview_collection.borrow()
|
||||
}
|
||||
|
||||
fn webview_collection_mut(&self) -> std::cell::RefMut<'_, WebViewCollection> {
|
||||
self.base().webview_collection.borrow_mut()
|
||||
}
|
||||
|
||||
/// Returns all [`WebView`]s in creation order.
|
||||
fn webviews(&self) -> Vec<(WebViewId, WebView)> {
|
||||
self.webview_collection()
|
||||
.all_in_creation_order()
|
||||
.map(|(id, webview)| (id, webview.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_webview(&self, webview: WebView) {
|
||||
let webview_id = webview.id();
|
||||
self.webview_collection_mut().add(webview);
|
||||
let total = self.webview_collection().len();
|
||||
info!("Added webview with ID: {webview_id:?}, total webviews: {total}");
|
||||
}
|
||||
|
||||
fn focused_webview(&self) -> Option<WebView> {
|
||||
self.webview_collection().focused().cloned()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn newest_webview(&self) -> Option<WebView> {
|
||||
self.webview_collection().newest().cloned()
|
||||
}
|
||||
|
||||
/// Gets the "active" [`WebView`]: the focused [`WebView`] if there is one,
|
||||
/// otherwise the most recently created [`WebView`].
|
||||
#[allow(dead_code)]
|
||||
fn active_webview(&self) -> Option<WebView> {
|
||||
self.webview_collection().active().cloned()
|
||||
}
|
||||
|
||||
/// Gets the "active" [`WebView`], panicking if there is none.
|
||||
/// This is a convenience method for platforms that assume there's always an active [`WebView`].
|
||||
#[allow(dead_code)]
|
||||
fn active_webview_or_panic(&self) -> WebView {
|
||||
self.active_webview()
|
||||
.expect("Should always have an active WebView")
|
||||
}
|
||||
|
||||
fn set_pending_traversal(
|
||||
&self,
|
||||
traversal_id: TraversalId,
|
||||
sender: GenericSender<WebDriverLoadStatus>,
|
||||
pub(crate) fn create_window(
|
||||
self: &Rc<Self>,
|
||||
platform_window: Rc<dyn PlatformWindow>,
|
||||
initial_url: Url,
|
||||
) {
|
||||
self.base()
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.pending_traversals
|
||||
.insert(traversal_id, sender);
|
||||
let window = Rc::new(ServoShellWindow::new(platform_window));
|
||||
window.create_and_focus_toplevel_webview(self.clone(), initial_url);
|
||||
self.windows.borrow_mut().insert(window.id(), window);
|
||||
}
|
||||
|
||||
fn set_load_status_sender(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
sender: GenericSender<WebDriverLoadStatus>,
|
||||
) {
|
||||
self.base()
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.insert(webview_id, sender);
|
||||
pub(crate) fn windows<'a>(
|
||||
&'a self,
|
||||
) -> Ref<'a, HashMap<ServoShellWindowId, Rc<ServoShellWindow>>> {
|
||||
self.windows.borrow()
|
||||
}
|
||||
|
||||
fn remove_load_status_sender(&self, webview_id: WebViewId) {
|
||||
self.base()
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.remove(&webview_id);
|
||||
/// Get any [`ServoShellWindow`] from this state's collection of windows. This is used for
|
||||
/// WebDriver, which currently doesn't have great support for per-window operation.
|
||||
pub(crate) fn any_window(&self) -> Rc<ServoShellWindow> {
|
||||
self.windows
|
||||
.borrow()
|
||||
.values()
|
||||
.next()
|
||||
.expect("Should always have at least one window open when running WebDriver")
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn set_script_command_interrupt_sender(&self, sender: Option<IpcSender<WebDriverJSResult>>) {
|
||||
self.base()
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.script_evaluation_interrupt_sender = sender;
|
||||
pub(crate) fn focused_window(&self) -> Option<Rc<ServoShellWindow>> {
|
||||
self.windows
|
||||
.borrow()
|
||||
.values()
|
||||
.find(|window| window.focused())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn handle_webdriver_input_event(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
input_event: InputEvent,
|
||||
response_sender: Option<Sender<()>>,
|
||||
) {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let event_id = webview.notify_input_event(input_event);
|
||||
if let Some(response_sender) = response_sender {
|
||||
self.base()
|
||||
.pending_webdriver_events
|
||||
.borrow_mut()
|
||||
.insert(event_id, response_sender);
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) fn window(&self, id: ServoShellWindowId) -> Option<Rc<ServoShellWindow>> {
|
||||
self.windows.borrow().get(&id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn webview_by_id(&self, webview_id: WebViewId) -> Option<WebView> {
|
||||
self.maybe_window_for_webview_id(webview_id)?
|
||||
.webview_by_id(webview_id)
|
||||
}
|
||||
|
||||
pub(crate) fn webdriver_receiver(&self) -> Option<&Receiver<WebDriverCommandMsg>> {
|
||||
self.webdriver_receiver.as_ref()
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// Returns true if the event loop should continue spinning and false if it should exit.
|
||||
pub(crate) fn spin_event_loop(self: &Rc<Self>) -> bool {
|
||||
self.handle_webdriver_messages();
|
||||
|
||||
if pref!(dom_gamepad_enabled) {
|
||||
self.handle_gamepad_events();
|
||||
}
|
||||
|
||||
if !self.servo.spin_event_loop() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for window in self.windows.borrow().values() {
|
||||
window.update_and_request_repaint_if_necessary(self);
|
||||
}
|
||||
|
||||
if self.servoshell_preferences.exit_after_stable_image && self.achieved_stable_image.get() {
|
||||
self.servo.start_shutting_down();
|
||||
}
|
||||
|
||||
// When a ServoShellWindow has no more WebViews, close it. When no more windows are open, exit
|
||||
// the application. Do not do this when running WebDriver, which expects to keep running with
|
||||
// no WebView open.
|
||||
if self.servoshell_preferences.webdriver_port.is_none() {
|
||||
self.windows
|
||||
.borrow_mut()
|
||||
.retain(|_, window| !window.should_close());
|
||||
if self.windows.borrow().is_empty() {
|
||||
self.servo.start_shutting_down();
|
||||
}
|
||||
} else {
|
||||
error!("Could not find WebView ({webview_id:?}) for WebDriver event: {input_event:?}");
|
||||
};
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_webdriver_screenshot(
|
||||
pub(crate) fn maybe_window_for_webview_id(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
rect: Option<Rect<f32, CSSPixel>>,
|
||||
result_sender: Sender<Result<RgbaImage, ScreenshotCaptureError>>,
|
||||
) {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let rect = rect.map(|rect| rect.to_box2d().into());
|
||||
webview.take_screenshot(rect, move |result| {
|
||||
if let Err(error) = result_sender.send(result) {
|
||||
warn!("Failed to send response to TakeScreenshot: {error}");
|
||||
}
|
||||
});
|
||||
} else if let Err(error) =
|
||||
result_sender.send(Err(ScreenshotCaptureError::WebViewDoesNotExist))
|
||||
{
|
||||
error!("Failed to send response to TakeScreenshot: {error}");
|
||||
) -> Option<Rc<ServoShellWindow>> {
|
||||
for window in self.windows.borrow().values() {
|
||||
if window.contains_webview(webview_id) {
|
||||
return Some(window.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_webdriver_script_command(&self, script_command: &WebDriverScriptCommand) {
|
||||
match script_command {
|
||||
WebDriverScriptCommand::ExecuteScript(_webview_id, response_sender) |
|
||||
WebDriverScriptCommand::ExecuteAsyncScript(_webview_id, response_sender) => {
|
||||
// Give embedder a chance to interrupt the script command.
|
||||
// Webdriver only handles 1 script command at a time, so we can
|
||||
// safely set a new interrupt sender and remove the previous one here.
|
||||
self.set_script_command_interrupt_sender(Some(response_sender.clone()));
|
||||
},
|
||||
WebDriverScriptCommand::AddLoadStatusSender(webview_id, load_status_sender) => {
|
||||
self.set_load_status_sender(*webview_id, load_status_sender.clone());
|
||||
},
|
||||
WebDriverScriptCommand::RemoveLoadStatusSender(webview_id) => {
|
||||
self.remove_load_status_sender(*webview_id);
|
||||
},
|
||||
_ => {
|
||||
self.set_script_command_interrupt_sender(None);
|
||||
},
|
||||
}
|
||||
pub(crate) fn window_for_webview_id(&self, webview_id: WebViewId) -> Rc<ServoShellWindow> {
|
||||
self.maybe_window_for_webview_id(webview_id)
|
||||
.expect("Looking for unexpected WebView: {webview_id:?}")
|
||||
}
|
||||
|
||||
fn handle_webdriver_load_url(
|
||||
pub(crate) fn platform_window_for_webview_id(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
url: Url,
|
||||
load_status_sender: GenericSender<WebDriverLoadStatus>,
|
||||
) {
|
||||
let Some(webview) = self.webview_by_id(webview_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.dismiss_embedder_controls_for_webview(webview_id);
|
||||
|
||||
info!("Loading URL in webview {}: {}", webview_id, url);
|
||||
self.set_load_status_sender(webview_id, load_status_sender);
|
||||
webview.load(url);
|
||||
) -> Rc<dyn PlatformWindow> {
|
||||
self.window_for_webview_id(webview_id).platform_window()
|
||||
}
|
||||
|
||||
/// If we are exiting after achieving a stable image or we want to save the display of the
|
||||
/// [`WebView`] to an image file, request a screenshot of the [`WebView`].
|
||||
fn maybe_request_screenshot(&self, webview: WebView) {
|
||||
let output_path = self.servoshell_preferences().output_image_path.clone();
|
||||
if !self.servoshell_preferences().exit_after_stable_image && output_path.is_none() {
|
||||
let output_path = self.servoshell_preferences.output_image_path.clone();
|
||||
if !self.servoshell_preferences.exit_after_stable_image && output_path.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Never request more than a single screenshot for now.
|
||||
let achieved_stable_image = self.base().achieved_stable_image.clone();
|
||||
let achieved_stable_image = self.achieved_stable_image.clone();
|
||||
if achieved_stable_image.get() {
|
||||
return;
|
||||
}
|
||||
@@ -382,4 +342,387 @@ pub trait RunningAppStateTrait {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_pending_traversal(
|
||||
&self,
|
||||
traversal_id: TraversalId,
|
||||
sender: GenericSender<WebDriverLoadStatus>,
|
||||
) {
|
||||
self.webdriver_senders
|
||||
.borrow_mut()
|
||||
.pending_traversals
|
||||
.insert(traversal_id, sender);
|
||||
}
|
||||
|
||||
pub(crate) fn set_load_status_sender(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
sender: GenericSender<WebDriverLoadStatus>,
|
||||
) {
|
||||
self.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.insert(webview_id, sender);
|
||||
}
|
||||
|
||||
fn remove_load_status_sender(&self, webview_id: WebViewId) {
|
||||
self.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.remove(&webview_id);
|
||||
}
|
||||
|
||||
fn set_script_command_interrupt_sender(&self, sender: Option<IpcSender<WebDriverJSResult>>) {
|
||||
self.webdriver_senders
|
||||
.borrow_mut()
|
||||
.script_evaluation_interrupt_sender = sender;
|
||||
}
|
||||
|
||||
pub(crate) fn handle_webdriver_input_event(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
input_event: InputEvent,
|
||||
response_sender: Option<Sender<()>>,
|
||||
) {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let event_id = webview.notify_input_event(input_event);
|
||||
if let Some(response_sender) = response_sender {
|
||||
self.pending_webdriver_events
|
||||
.borrow_mut()
|
||||
.insert(event_id, response_sender);
|
||||
}
|
||||
} else {
|
||||
error!("Could not find WebView ({webview_id:?}) for WebDriver event: {input_event:?}");
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn handle_webdriver_screenshot(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
rect: Option<Rect<f32, CSSPixel>>,
|
||||
result_sender: Sender<Result<RgbaImage, ScreenshotCaptureError>>,
|
||||
) {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let rect = rect.map(|rect| rect.to_box2d().into());
|
||||
webview.take_screenshot(rect, move |result| {
|
||||
if let Err(error) = result_sender.send(result) {
|
||||
warn!("Failed to send response to TakeScreenshot: {error}");
|
||||
}
|
||||
});
|
||||
} else if let Err(error) =
|
||||
result_sender.send(Err(ScreenshotCaptureError::WebViewDoesNotExist))
|
||||
{
|
||||
error!("Failed to send response to TakeScreenshot: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_webdriver_script_command(&self, script_command: &WebDriverScriptCommand) {
|
||||
match script_command {
|
||||
WebDriverScriptCommand::ExecuteScript(_webview_id, response_sender) |
|
||||
WebDriverScriptCommand::ExecuteAsyncScript(_webview_id, response_sender) => {
|
||||
// Give embedder a chance to interrupt the script command.
|
||||
// Webdriver only handles 1 script command at a time, so we can
|
||||
// safely set a new interrupt sender and remove the previous one here.
|
||||
self.set_script_command_interrupt_sender(Some(response_sender.clone()));
|
||||
},
|
||||
WebDriverScriptCommand::AddLoadStatusSender(webview_id, load_status_sender) => {
|
||||
self.set_load_status_sender(*webview_id, load_status_sender.clone());
|
||||
},
|
||||
WebDriverScriptCommand::RemoveLoadStatusSender(webview_id) => {
|
||||
self.remove_load_status_sender(*webview_id);
|
||||
},
|
||||
_ => {
|
||||
self.set_script_command_interrupt_sender(None);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_webdriver_load_url(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
url: Url,
|
||||
load_status_sender: GenericSender<WebDriverLoadStatus>,
|
||||
) {
|
||||
let Some(webview) = self.webview_by_id(webview_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.platform_window_for_webview_id(webview_id)
|
||||
.dismiss_embedder_controls_for_webview(webview_id);
|
||||
|
||||
info!("Loading URL in webview {}: {}", webview_id, url);
|
||||
self.set_load_status_sender(webview_id, load_status_sender);
|
||||
webview.load(url);
|
||||
}
|
||||
|
||||
pub(crate) fn handle_gamepad_events(&self) {
|
||||
let Some(active_webview) = self
|
||||
.focused_window()
|
||||
.and_then(|window| window.focused_webview())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if let Some(gamepad_support) = self.gamepad_support.borrow_mut().as_mut() {
|
||||
gamepad_support.handle_gamepad_events(active_webview);
|
||||
}
|
||||
}
|
||||
|
||||
/// Interrupt any ongoing WebDriver-based script evaluation.
|
||||
///
|
||||
/// From <https://w3c.github.io/webdriver/#dfn-execute-a-function-body>:
|
||||
/// > The rules to execute a function body are as follows. The algorithm returns
|
||||
/// > an ECMAScript completion record.
|
||||
/// >
|
||||
/// > If at any point during the algorithm a user prompt appears, immediately return
|
||||
/// > Completion { Type: normal, Value: null, Target: empty }, but continue to run the
|
||||
/// > other steps of this algorithm in parallel.
|
||||
fn interrupt_webdriver_script_evaluation(&self) {
|
||||
if let Some(sender) = &self
|
||||
.webdriver_senders
|
||||
.borrow()
|
||||
.script_evaluation_interrupt_sender
|
||||
{
|
||||
sender.send(Ok(JSValue::Null)).unwrap_or_else(|err| {
|
||||
info!(
|
||||
"Notify dialog appear failed. Maybe the channel to webdriver is closed: {err}"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WebViewDelegate for RunningAppState {
|
||||
fn screen_geometry(&self, webview: WebView) -> Option<servo::ScreenGeometry> {
|
||||
Some(
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.screen_geometry(),
|
||||
)
|
||||
}
|
||||
|
||||
fn notify_status_text_changed(&self, webview: WebView, _status: Option<String>) {
|
||||
self.window_for_webview_id(webview.id()).set_needs_update();
|
||||
}
|
||||
|
||||
fn notify_history_changed(&self, webview: WebView, _entries: Vec<Url>, _current: usize) {
|
||||
self.window_for_webview_id(webview.id()).set_needs_update();
|
||||
}
|
||||
|
||||
fn notify_page_title_changed(&self, webview: WebView, _: Option<String>) {
|
||||
self.window_for_webview_id(webview.id()).set_needs_update();
|
||||
}
|
||||
|
||||
fn notify_traversal_complete(&self, _webview: WebView, traversal_id: TraversalId) {
|
||||
let mut webdriver_state = self.webdriver_senders.borrow_mut();
|
||||
if let Entry::Occupied(entry) = webdriver_state.pending_traversals.entry(traversal_id) {
|
||||
let sender = entry.remove();
|
||||
let _ = sender.send(WebDriverLoadStatus::Complete);
|
||||
}
|
||||
}
|
||||
|
||||
fn request_move_to(&self, webview: WebView, new_position: DeviceIntPoint) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.set_position(new_position);
|
||||
}
|
||||
|
||||
fn request_resize_to(&self, webview: WebView, requested_outer_size: DeviceIntSize) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.request_resize(&webview, requested_outer_size);
|
||||
}
|
||||
|
||||
fn request_authentication(
|
||||
&self,
|
||||
webview: WebView,
|
||||
authentication_request: AuthenticationRequest,
|
||||
) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.show_http_authentication_dialog(webview.id(), authentication_request);
|
||||
}
|
||||
|
||||
fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option<WebView> {
|
||||
let window = self.window_for_webview_id(parent_webview.id());
|
||||
let platform_window = window.platform_window();
|
||||
|
||||
let webview =
|
||||
WebViewBuilder::new_auxiliary(&self.servo, platform_window.rendering_context())
|
||||
.hidpi_scale_factor(platform_window.hidpi_scale_factor())
|
||||
.delegate(parent_webview.delegate())
|
||||
.build();
|
||||
|
||||
webview.notify_theme_change(platform_window.theme());
|
||||
|
||||
// When WebDriver is enabled, do not focus and raise the WebView to the top,
|
||||
// as that is what the specification expects. Otherwise, we would like `window.open()`
|
||||
// to create a new foreground tab
|
||||
if self.servoshell_preferences.webdriver_port.is_none() {
|
||||
webview.focus_and_raise_to_top(true);
|
||||
}
|
||||
|
||||
window.add_webview(webview.clone());
|
||||
|
||||
Some(webview)
|
||||
}
|
||||
|
||||
fn notify_closed(&self, webview: WebView) {
|
||||
self.window_for_webview_id(webview.id())
|
||||
.close_webview(webview.id())
|
||||
}
|
||||
|
||||
fn notify_focus_changed(&self, webview: WebView, focused: bool) {
|
||||
self.window_for_webview_id(webview.id())
|
||||
.notify_focus_changed(webview, focused);
|
||||
}
|
||||
|
||||
fn notify_input_event_handled(
|
||||
&self,
|
||||
webview: WebView,
|
||||
id: InputEventId,
|
||||
result: InputEventResult,
|
||||
) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.notify_input_event_handled(&webview, id, result);
|
||||
if let Some(response_sender) = self.pending_webdriver_events.borrow_mut().remove(&id) {
|
||||
let _ = response_sender.send(());
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_cursor_changed(&self, webview: WebView, cursor: servo::Cursor) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.set_cursor(cursor);
|
||||
}
|
||||
|
||||
fn notify_load_status_changed(&self, webview: WebView, status: LoadStatus) {
|
||||
self.window_for_webview_id(webview.id()).set_needs_update();
|
||||
|
||||
if status == LoadStatus::Complete {
|
||||
if let Some(sender) = self
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.remove(&webview.id())
|
||||
{
|
||||
let _ = sender.send(WebDriverLoadStatus::Complete);
|
||||
}
|
||||
self.maybe_request_screenshot(webview);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_fullscreen_state_changed(&self, webview: WebView, fullscreen_state: bool) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.set_fullscreen(fullscreen_state);
|
||||
}
|
||||
|
||||
fn show_bluetooth_device_dialog(
|
||||
&self,
|
||||
webview: WebView,
|
||||
devices: Vec<String>,
|
||||
response_sender: GenericSender<Option<String>>,
|
||||
) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.show_bluetooth_device_dialog(webview.id(), devices, response_sender);
|
||||
}
|
||||
|
||||
fn request_permission(&self, webview: WebView, permission_request: PermissionRequest) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.show_permission_dialog(webview.id(), permission_request);
|
||||
}
|
||||
|
||||
fn notify_new_frame_ready(&self, webview: WebView) {
|
||||
self.window_for_webview_id(webview.id()).set_needs_repaint();
|
||||
}
|
||||
|
||||
fn play_gamepad_haptic_effect(
|
||||
&self,
|
||||
_webview: WebView,
|
||||
index: usize,
|
||||
effect_type: GamepadHapticEffectType,
|
||||
effect_complete_sender: IpcSender<bool>,
|
||||
) {
|
||||
match self.gamepad_support.borrow_mut().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: WebView,
|
||||
index: usize,
|
||||
haptic_stop_sender: IpcSender<bool>,
|
||||
) {
|
||||
let stopped = match self.gamepad_support.borrow_mut().as_mut() {
|
||||
Some(gamepad_support) => gamepad_support.stop_haptic_effect(index),
|
||||
None => false,
|
||||
};
|
||||
let _ = haptic_stop_sender.send(stopped);
|
||||
}
|
||||
|
||||
fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) {
|
||||
if self.servoshell_preferences.webdriver_port.is_some() {
|
||||
if matches!(&embedder_control, EmbedderControl::SimpleDialog(..)) {
|
||||
self.interrupt_webdriver_script_evaluation();
|
||||
|
||||
// Dialogs block the page load, so need need to notify WebDriver
|
||||
if let Some(sender) = self
|
||||
.webdriver_senders
|
||||
.borrow_mut()
|
||||
.load_status_senders
|
||||
.get(&webview.id())
|
||||
{
|
||||
let _ = sender.send(WebDriverLoadStatus::Blocked);
|
||||
};
|
||||
}
|
||||
|
||||
self.webdriver_embedder_controls
|
||||
.show_embedder_control(webview.id(), embedder_control);
|
||||
return;
|
||||
}
|
||||
|
||||
self.window_for_webview_id(webview.id())
|
||||
.show_embedder_control(webview, embedder_control);
|
||||
}
|
||||
|
||||
fn hide_embedder_control(&self, webview: WebView, embedder_control_id: EmbedderControlId) {
|
||||
if self.servoshell_preferences.webdriver_port.is_some() {
|
||||
self.webdriver_embedder_controls
|
||||
.hide_embedder_control(webview.id(), embedder_control_id);
|
||||
return;
|
||||
}
|
||||
|
||||
self.window_for_webview_id(webview.id())
|
||||
.hide_embedder_control(webview, embedder_control_id);
|
||||
}
|
||||
|
||||
fn notify_favicon_changed(&self, webview: WebView) {
|
||||
self.window_for_webview_id(webview.id())
|
||||
.notify_favicon_changed(webview);
|
||||
}
|
||||
|
||||
fn notify_media_session_event(&self, webview: WebView, event: MediaSessionEvent) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.notify_media_session_event(event);
|
||||
}
|
||||
|
||||
fn notify_crashed(&self, webview: WebView, reason: String, backtrace: Option<String>) {
|
||||
self.platform_window_for_webview_id(webview.id())
|
||||
.notify_crashed(webview, reason, backtrace);
|
||||
}
|
||||
}
|
||||
|
||||
struct ServoShellServoDelegate;
|
||||
impl ServoDelegate for ServoShellServoDelegate {
|
||||
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:?}!");
|
||||
}
|
||||
}
|
||||
|
||||
308
ports/servoshell/webdriver.rs
Normal file
308
ports/servoshell/webdriver.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use log::warn;
|
||||
use servo::{
|
||||
EmbedderControl, EmbedderControlId, SimpleDialog, WebDriverCommandMsg, WebDriverUserPrompt,
|
||||
WebDriverUserPromptAction, WebViewId,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::running_app_state::RunningAppState;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct WebDriverEmbedderControls {
|
||||
embedder_controls: RefCell<HashMap<WebViewId, Vec<EmbedderControl>>>,
|
||||
}
|
||||
|
||||
impl WebDriverEmbedderControls {
|
||||
pub(crate) fn show_embedder_control(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
embedder_control: EmbedderControl,
|
||||
) {
|
||||
self.embedder_controls
|
||||
.borrow_mut()
|
||||
.entry(webview_id)
|
||||
.or_default()
|
||||
.push(embedder_control)
|
||||
}
|
||||
|
||||
pub(crate) fn hide_embedder_control(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
embedder_control_id: EmbedderControlId,
|
||||
) {
|
||||
let mut embedder_controls = self.embedder_controls.borrow_mut();
|
||||
if let Some(controls) = embedder_controls.get_mut(&webview_id) {
|
||||
controls.retain(|control| control.id() != embedder_control_id);
|
||||
}
|
||||
embedder_controls.retain(|_, controls| !controls.is_empty());
|
||||
}
|
||||
|
||||
pub(crate) fn current_active_dialog_webdriver_type(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
) -> Option<WebDriverUserPrompt> {
|
||||
// From <https://w3c.github.io/webdriver/#dfn-handle-any-user-prompts>
|
||||
// > Step 3: If the current user prompt is an alert dialog, set type to "alert". Otherwise,
|
||||
// > if the current user prompt is a beforeunload dialog, set type to
|
||||
// > "beforeUnload". Otherwise, if the current user prompt is a confirm dialog, set
|
||||
// > type to "confirm". Otherwise, if the current user prompt is a prompt dialog,
|
||||
// > set type to "prompt".
|
||||
let embedder_controls = self.embedder_controls.borrow();
|
||||
match embedder_controls.get(&webview_id)?.last()? {
|
||||
EmbedderControl::SimpleDialog(SimpleDialog::Alert { .. }) => {
|
||||
Some(WebDriverUserPrompt::Alert)
|
||||
},
|
||||
EmbedderControl::SimpleDialog(SimpleDialog::Confirm { .. }) => {
|
||||
Some(WebDriverUserPrompt::Confirm)
|
||||
},
|
||||
EmbedderControl::SimpleDialog(SimpleDialog::Prompt { .. }) => {
|
||||
Some(WebDriverUserPrompt::Prompt)
|
||||
},
|
||||
EmbedderControl::FilePicker { .. } => Some(WebDriverUserPrompt::File),
|
||||
EmbedderControl::SelectElement { .. } => Some(WebDriverUserPrompt::Default),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Respond to the most recently added dialog if it was a `SimpleDialog` and return
|
||||
/// its message string or return an error if there is no active dialog or the most
|
||||
/// recently added dialog is not a `SimpleDialog`.
|
||||
pub(crate) fn respond_to_active_simple_dialog(
|
||||
&self,
|
||||
webview_id: WebViewId,
|
||||
action: WebDriverUserPromptAction,
|
||||
) -> Result<String, ()> {
|
||||
let mut embedder_controls = self.embedder_controls.borrow_mut();
|
||||
let Some(controls) = embedder_controls.get_mut(&webview_id) else {
|
||||
return Err(());
|
||||
};
|
||||
let Some(last_control) = controls.last() else {
|
||||
return Err(());
|
||||
};
|
||||
let EmbedderControl::SimpleDialog(simple_dialog) = last_control else {
|
||||
return Err(());
|
||||
};
|
||||
|
||||
let result_text = simple_dialog.message().to_owned();
|
||||
match action {
|
||||
WebDriverUserPromptAction::Accept => simple_dialog.accept(),
|
||||
WebDriverUserPromptAction::Dismiss => simple_dialog.dismiss(),
|
||||
WebDriverUserPromptAction::Ignore => return Ok(result_text),
|
||||
};
|
||||
|
||||
controls.pop();
|
||||
Ok(result_text)
|
||||
}
|
||||
|
||||
pub(crate) fn alert_text_of_newest_dialog(&self, webview_id: WebViewId) -> Option<String> {
|
||||
let embedder_controls = self.embedder_controls.borrow();
|
||||
match embedder_controls.get(&webview_id)?.last()? {
|
||||
EmbedderControl::SimpleDialog(simple_dialog) => {
|
||||
Some(simple_dialog.message().to_owned())
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_alert_text_of_newest_dialog(&self, webview_id: WebViewId, text: String) {
|
||||
let mut embedder_controls = self.embedder_controls.borrow_mut();
|
||||
let Some(controls) = embedder_controls.get_mut(&webview_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(last_control) = controls.last_mut() else {
|
||||
return;
|
||||
};
|
||||
// FIXME: This should be setting the prompt result of the dialog and not the
|
||||
// message text according to the WebDriver specification at:
|
||||
// <https://www.w3.org/TR/webdriver2/#dfn-send-alert-text>
|
||||
if let EmbedderControl::SimpleDialog(simple_dialog) = last_control {
|
||||
simple_dialog.set_message(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningAppState {
|
||||
pub(crate) fn handle_webdriver_messages(self: &Rc<Self>) {
|
||||
let Some(webdriver_receiver) = self.webdriver_receiver() else {
|
||||
return;
|
||||
};
|
||||
|
||||
while let Ok(msg) = webdriver_receiver.try_recv() {
|
||||
match msg {
|
||||
WebDriverCommandMsg::Shutdown => {
|
||||
self.servo().start_shutting_down();
|
||||
},
|
||||
WebDriverCommandMsg::IsWebViewOpen(webview_id, sender) => {
|
||||
let context = self.webview_by_id(webview_id);
|
||||
|
||||
if let Err(error) = sender.send(context.is_some()) {
|
||||
warn!("Failed to send response of IsWebViewOpen: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::IsBrowsingContextOpen(..) => {
|
||||
self.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::NewWebView(response_sender, load_status_sender) => {
|
||||
let new_webview = self
|
||||
.any_window()
|
||||
.create_toplevel_webview(self.clone(), Url::parse("about:blank").unwrap());
|
||||
|
||||
if let Err(error) = response_sender.send(new_webview.id()) {
|
||||
warn!("Failed to send response of NewWebview: {error}");
|
||||
}
|
||||
if let Some(load_status_sender) = load_status_sender {
|
||||
self.set_load_status_sender(new_webview.id(), load_status_sender);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::CloseWebView(webview_id, response_sender) => {
|
||||
self.window_for_webview_id(webview_id)
|
||||
.close_webview(webview_id);
|
||||
if let Err(error) = response_sender.send(()) {
|
||||
warn!("Failed to send response of CloseWebView: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::FocusWebView(webview_id) => {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
webview.focus_and_raise_to_top(true);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::FocusBrowsingContext(..) => {
|
||||
self.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::GetAllWebViews(response_sender) => {
|
||||
let webviews = self
|
||||
.windows()
|
||||
.values()
|
||||
.flat_map(|window| window.webview_ids())
|
||||
.collect();
|
||||
if let Err(error) = response_sender.send(webviews) {
|
||||
warn!("Failed to send response of GetAllWebViews: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GetWindowRect(webview_id, response_sender) => {
|
||||
let platform_window = self.platform_window_for_webview_id(webview_id);
|
||||
if let Err(error) = response_sender.send(platform_window.window_rect()) {
|
||||
warn!("Failed to send response of GetWindowSize: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::MaximizeWebView(webview_id, response_sender) => {
|
||||
let Some(webview) = self.webview_by_id(webview_id) else {
|
||||
continue;
|
||||
};
|
||||
let platform_window = self.platform_window_for_webview_id(webview_id);
|
||||
platform_window.maximize(&webview);
|
||||
|
||||
if let Err(error) = response_sender.send(platform_window.window_rect()) {
|
||||
warn!("Failed to send response of GetWindowSize: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::SetWindowRect(webview_id, requested_rect, size_sender) => {
|
||||
let Some(webview) = self.webview_by_id(webview_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let platform_window = self.platform_window_for_webview_id(webview_id);
|
||||
let scale = platform_window.hidpi_scale_factor();
|
||||
|
||||
let requested_physical_rect =
|
||||
(requested_rect.to_f32() * scale).round().to_i32();
|
||||
|
||||
// Step 17. Set Width/Height.
|
||||
platform_window.request_resize(&webview, requested_physical_rect.size());
|
||||
|
||||
// Step 18. Set position of the window.
|
||||
platform_window.set_position(requested_physical_rect.min);
|
||||
|
||||
if let Err(error) = size_sender.send(platform_window.window_rect()) {
|
||||
warn!("Failed to send window size: {error}");
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GetViewportSize(webview_id, response_sender) => {
|
||||
let platform_window = self.platform_window_for_webview_id(webview_id);
|
||||
let size = platform_window.rendering_context().size2d();
|
||||
if let Err(error) = response_sender.send(size) {
|
||||
warn!("Failed to send response of GetViewportSize: {error}");
|
||||
}
|
||||
},
|
||||
// This is only received when start new session.
|
||||
WebDriverCommandMsg::GetFocusedWebView(sender) => {
|
||||
let focused_webview = self.any_window().focused_webview();
|
||||
if let Err(error) = sender.send(focused_webview.map(|w| w.id())) {
|
||||
warn!("Failed to send response of GetFocusedWebView: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::LoadUrl(webview_id, url, load_status_sender) => {
|
||||
self.handle_webdriver_load_url(webview_id, url, load_status_sender);
|
||||
},
|
||||
WebDriverCommandMsg::Refresh(webview_id, load_status_sender) => {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
self.set_load_status_sender(webview_id, load_status_sender);
|
||||
webview.reload();
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GoBack(webview_id, load_status_sender) => {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let traversal_id = webview.go_back(1);
|
||||
self.set_pending_traversal(traversal_id, load_status_sender);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::GoForward(webview_id, load_status_sender) => {
|
||||
if let Some(webview) = self.webview_by_id(webview_id) {
|
||||
let traversal_id = webview.go_forward(1);
|
||||
self.set_pending_traversal(traversal_id, load_status_sender);
|
||||
}
|
||||
},
|
||||
WebDriverCommandMsg::InputEvent(webview_id, input_event, response_sender) => {
|
||||
self.handle_webdriver_input_event(webview_id, input_event, response_sender);
|
||||
},
|
||||
WebDriverCommandMsg::ScriptCommand(_, ref webdriver_script_command) => {
|
||||
self.handle_webdriver_script_command(webdriver_script_command);
|
||||
self.servo().execute_webdriver_command(msg);
|
||||
},
|
||||
WebDriverCommandMsg::CurrentUserPrompt(webview_id, response_sender) => {
|
||||
let current_dialog = self
|
||||
.webdriver_embedder_controls
|
||||
.current_active_dialog_webdriver_type(webview_id);
|
||||
if let Err(error) = response_sender.send(current_dialog) {
|
||||
warn!("Failed to send response of CurrentUserPrompt: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::HandleUserPrompt(webview_id, action, response_sender) => {
|
||||
let controls = &self.webdriver_embedder_controls;
|
||||
let result = controls.respond_to_active_simple_dialog(webview_id, action);
|
||||
if let Err(error) = response_sender.send(result) {
|
||||
warn!("Failed to send response of HandleUserPrompt: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::GetAlertText(webview_id, response_sender) => {
|
||||
let response = match self
|
||||
.webdriver_embedder_controls
|
||||
.alert_text_of_newest_dialog(webview_id)
|
||||
{
|
||||
Some(text) => Ok(text),
|
||||
None => Err(()),
|
||||
};
|
||||
|
||||
if let Err(error) = response_sender.send(response) {
|
||||
warn!("Failed to send response of GetAlertText: {error}");
|
||||
};
|
||||
},
|
||||
WebDriverCommandMsg::SendAlertText(webview_id, text) => {
|
||||
self.webdriver_embedder_controls
|
||||
.set_alert_text_of_newest_dialog(webview_id, text);
|
||||
},
|
||||
WebDriverCommandMsg::TakeScreenshot(webview_id, rect, result_sender) => {
|
||||
self.handle_webdriver_screenshot(webview_id, rect, result_sender);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
373
ports/servoshell/window.rs
Normal file
373
ports/servoshell/window.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use euclid::Scale;
|
||||
use servo::base::generic_channel::GenericSender;
|
||||
use servo::servo_geometry::{DeviceIndependentIntRect, DeviceIndependentPixel};
|
||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
|
||||
use servo::{
|
||||
AuthenticationRequest, Cursor, EmbedderControl, EmbedderControlId, InputEventId,
|
||||
InputEventResult, MediaSessionEvent, PermissionRequest, RenderingContext, ScreenGeometry,
|
||||
WebView, WebViewBuilder, WebViewId,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::running_app_state::{RunningAppState, WebViewCollection};
|
||||
|
||||
// This should vary by zoom level and maybe actual text size (focused or under cursor)
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) const LINE_HEIGHT: f32 = 76.0;
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) const LINE_WIDTH: f32 = 76.0;
|
||||
|
||||
/// <https://github.com/web-platform-tests/wpt/blob/9320b1f724632c52929a3fdb11bdaf65eafc7611/webdriver/tests/classic/set_window_rect/set.py#L287-L290>
|
||||
/// "A window size of 10x10px shouldn't be supported by any browser."
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) const MIN_WINDOW_INNER_SIZE: DeviceIntSize = DeviceIntSize::new(100, 100);
|
||||
|
||||
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct ServoShellWindowId(u64);
|
||||
|
||||
impl From<u64> for ServoShellWindowId {
|
||||
fn from(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ServoShellWindow {
|
||||
/// The [`WebView`]s that have been added to this window.
|
||||
pub(crate) webview_collection: RefCell<WebViewCollection>,
|
||||
/// A handle to the [`PlatformWindow`] that servoshell is rendering in.
|
||||
platform_window: Rc<dyn PlatformWindow>,
|
||||
/// Whether or not this window should be closed at the end of the spin of the next event loop.
|
||||
close_scheduled: Cell<bool>,
|
||||
/// Whether or not the application interface needs to be updated.
|
||||
needs_update: Cell<bool>,
|
||||
/// Whether or not Servo needs to repaint its display. Currently this is global
|
||||
/// because every `WebView` shares a `RenderingContext`.
|
||||
needs_repaint: Cell<bool>,
|
||||
/// List of webviews that have favicon textures which are not yet uploaded
|
||||
/// to the GPU by egui.
|
||||
pending_favicon_loads: RefCell<Vec<WebViewId>>,
|
||||
}
|
||||
|
||||
impl ServoShellWindow {
|
||||
pub(crate) fn new(platform_window: Rc<dyn PlatformWindow>) -> Self {
|
||||
Self {
|
||||
webview_collection: Default::default(),
|
||||
platform_window,
|
||||
close_scheduled: Default::default(),
|
||||
needs_update: Default::default(),
|
||||
needs_repaint: Default::default(),
|
||||
pending_favicon_loads: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> ServoShellWindowId {
|
||||
self.platform_window().id()
|
||||
}
|
||||
|
||||
pub(crate) fn create_and_focus_toplevel_webview(
|
||||
&self,
|
||||
state: Rc<RunningAppState>,
|
||||
url: Url,
|
||||
) -> WebView {
|
||||
let webview = self.create_toplevel_webview(state, url);
|
||||
webview.focus_and_raise_to_top(true);
|
||||
webview
|
||||
}
|
||||
|
||||
pub(crate) fn create_toplevel_webview(&self, state: Rc<RunningAppState>, url: Url) -> WebView {
|
||||
let webview = WebViewBuilder::new(state.servo(), self.platform_window.rendering_context())
|
||||
.url(url)
|
||||
.hidpi_scale_factor(self.platform_window.hidpi_scale_factor())
|
||||
.delegate(state.clone())
|
||||
.build();
|
||||
|
||||
webview.notify_theme_change(self.platform_window.theme());
|
||||
self.add_webview(webview.clone());
|
||||
webview
|
||||
}
|
||||
|
||||
/// Repaint the focused [`WebView`].
|
||||
pub(crate) fn repaint_webviews(&self) {
|
||||
let Some(webview) = self.focused_webview() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.platform_window()
|
||||
.rendering_context()
|
||||
.make_current()
|
||||
.unwrap();
|
||||
webview.paint();
|
||||
self.platform_window().rendering_context().present();
|
||||
}
|
||||
|
||||
/// Whether or not this [`ServoShellWindow`] has any [`WebView`]s.
|
||||
pub(crate) fn should_close(&self) -> bool {
|
||||
self.webview_collection.borrow().is_empty() || self.close_scheduled.get()
|
||||
}
|
||||
|
||||
pub(crate) fn contains_webview(&self, id: WebViewId) -> bool {
|
||||
self.webview_collection.borrow().contains(id)
|
||||
}
|
||||
|
||||
pub(crate) fn webview_by_id(&self, id: WebViewId) -> Option<WebView> {
|
||||
self.webview_collection.borrow().get(id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn set_needs_update(&self) {
|
||||
self.needs_update.set(true);
|
||||
}
|
||||
|
||||
pub(crate) fn set_needs_repaint(&self) {
|
||||
self.needs_repaint.set(true)
|
||||
}
|
||||
|
||||
pub(crate) fn schedule_close(&self) {
|
||||
self.close_scheduled.set(true)
|
||||
}
|
||||
|
||||
pub(crate) fn platform_window(&self) -> Rc<dyn PlatformWindow> {
|
||||
self.platform_window.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn focused(&self) -> bool {
|
||||
self.platform_window.focused()
|
||||
}
|
||||
|
||||
pub(crate) fn add_webview(&self, webview: WebView) {
|
||||
self.webview_collection.borrow_mut().add(webview);
|
||||
self.set_needs_repaint();
|
||||
}
|
||||
|
||||
pub(crate) fn webview_ids(&self) -> Vec<WebViewId> {
|
||||
self.webview_collection.borrow().creation_order.clone()
|
||||
}
|
||||
|
||||
/// Returns all [`WebView`]s in creation order.
|
||||
pub(crate) fn webviews(&self) -> Vec<(WebViewId, WebView)> {
|
||||
self.webview_collection
|
||||
.borrow()
|
||||
.all_in_creation_order()
|
||||
.map(|(id, webview)| (id, webview.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) fn focus_webview_by_index(&self, index: usize) {
|
||||
if let Some((_, webview)) = self.webviews().get(index) {
|
||||
webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) fn get_focused_webview_index(&self) -> Option<usize> {
|
||||
let focused_id = self.webview_collection.borrow().focused_id()?;
|
||||
self.webviews()
|
||||
.iter()
|
||||
.position(|webview| webview.0 == focused_id)
|
||||
}
|
||||
|
||||
pub(crate) fn update_and_request_repaint_if_necessary(&self, state: &RunningAppState) {
|
||||
let updated_user_interface = self.needs_update.take() &&
|
||||
self.platform_window
|
||||
.update_user_interface_state(state, self);
|
||||
|
||||
// Delegate handlers may have asked us to present or update compositor contents.
|
||||
// Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid.
|
||||
let needs_repaint = self.needs_repaint.take();
|
||||
if updated_user_interface || needs_repaint {
|
||||
self.platform_window.request_repaint(self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the given [`WebView`] via its [`WebViewId`].
|
||||
///
|
||||
/// Note: This can happen because we can trigger a close with a UI action and then get
|
||||
/// the close notification via the [`WebViewDelegate`] later.
|
||||
pub(crate) fn close_webview(&self, webview_id: WebViewId) {
|
||||
let mut webview_collection = self.webview_collection.borrow_mut();
|
||||
if webview_collection.remove(webview_id).is_none() {
|
||||
return;
|
||||
}
|
||||
self.platform_window
|
||||
.dismiss_embedder_controls_for_webview(webview_id);
|
||||
|
||||
if let Some(newest_webview) = webview_collection.newest() {
|
||||
newest_webview.focus();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify_focus_changed(&self, webview: WebView, focused: bool) {
|
||||
let mut webview_collection = self.webview_collection.borrow_mut();
|
||||
if focused {
|
||||
webview.show(true);
|
||||
self.set_needs_update();
|
||||
webview_collection.set_focused(Some(webview.id()));
|
||||
} else if webview_collection.focused_id() == Some(webview.id()) {
|
||||
webview_collection.set_focused(None);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notify_favicon_changed(&self, webview: WebView) {
|
||||
self.pending_favicon_loads.borrow_mut().push(webview.id());
|
||||
self.set_needs_repaint();
|
||||
}
|
||||
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) fn hidpi_scale_factor_changed(&self) {
|
||||
let new_scale_factor = self.platform_window.hidpi_scale_factor();
|
||||
for webview in self.webview_collection.borrow().values() {
|
||||
webview.set_hidpi_scale_factor(new_scale_factor);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn focused_webview(&self) -> Option<WebView> {
|
||||
self.webview_collection.borrow().focused().cloned()
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
not(any(target_os = "android", target_env = "ohos")),
|
||||
expect(dead_code)
|
||||
)]
|
||||
pub(crate) fn focused_or_newest_webview(&self) -> Option<WebView> {
|
||||
let webview_collection = self.webview_collection.borrow();
|
||||
webview_collection
|
||||
.focused()
|
||||
.or(webview_collection.newest())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Return a list of all webviews that have favicons that have not yet been loaded by egui.
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
pub(crate) fn take_pending_favicon_loads(&self) -> Vec<WebViewId> {
|
||||
std::mem::take(&mut *self.pending_favicon_loads.borrow_mut())
|
||||
}
|
||||
|
||||
pub(crate) fn show_embedder_control(
|
||||
&self,
|
||||
webview: WebView,
|
||||
embedder_control: EmbedderControl,
|
||||
) {
|
||||
self.platform_window
|
||||
.show_embedder_control(webview.id(), embedder_control);
|
||||
self.set_needs_update();
|
||||
self.set_needs_repaint();
|
||||
}
|
||||
|
||||
pub(crate) fn hide_embedder_control(
|
||||
&self,
|
||||
webview: WebView,
|
||||
embedder_control: EmbedderControlId,
|
||||
) {
|
||||
self.platform_window
|
||||
.hide_embedder_control(webview.id(), embedder_control);
|
||||
self.set_needs_update();
|
||||
self.set_needs_repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// A `PlatformWindow` abstracts away the differents kinds of platform windows that might
|
||||
/// be used in a servoshell execution. This currently includes headed (winit) and headless
|
||||
/// windows.
|
||||
pub(crate) trait PlatformWindow {
|
||||
fn id(&self) -> ServoShellWindowId;
|
||||
fn screen_geometry(&self) -> ScreenGeometry;
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
fn device_hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
|
||||
fn hidpi_scale_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel>;
|
||||
#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
|
||||
fn get_fullscreen(&self) -> bool;
|
||||
/// Request that the `Window` rebuild its user interface, if it has one. This should
|
||||
/// not repaint, but should prepare the user interface for painting when it is
|
||||
/// actually requested.
|
||||
fn rebuild_user_interface(&self, _: &RunningAppState, _: &ServoShellWindow) {}
|
||||
/// Inform the `Window` that the state of a `WebView` has changed and that it should
|
||||
/// do an incremental update of user interface state. Returns `true` if the user
|
||||
/// interface actually changed and a rebuild and repaint is needed, `false` otherwise.
|
||||
fn update_user_interface_state(&self, _: &RunningAppState, _: &ServoShellWindow) -> bool {
|
||||
false
|
||||
}
|
||||
/// Handle a winit [`WindowEvent`]. Returns `true` if the event loop should continue
|
||||
/// and `false` otherwise.
|
||||
///
|
||||
/// TODO: This should be handled internally in the winit window if possible so that it
|
||||
/// makes more sense when we are mixing headed and headless windows.
|
||||
#[cfg(not(any(target_os = "android", target_env = "ohos")))]
|
||||
fn handle_winit_window_event(
|
||||
&self,
|
||||
_: Rc<RunningAppState>,
|
||||
_: &ServoShellWindow,
|
||||
_: winit::event::WindowEvent,
|
||||
) {
|
||||
}
|
||||
/// Handle a winit [`AppEvent`]. Returns `true` if the event loop should continue and
|
||||
/// `false` otherwise.
|
||||
///
|
||||
/// TODO: This should be handled internally in the winit window if possible so that it
|
||||
/// makes more sense when we are mixing headed and headless windows.
|
||||
#[cfg(not(any(target_os = "android", target_env = "ohos")))]
|
||||
fn handle_winit_app_event(
|
||||
&self,
|
||||
_: Rc<RunningAppState>,
|
||||
_: &ServoShellWindow,
|
||||
_: crate::desktop::event_loop::AppEvent,
|
||||
) {
|
||||
}
|
||||
/// Request that the window redraw itself. It is up to the window to do this
|
||||
/// once the windowing system is ready. If this is a headless window, the redraw
|
||||
/// will happen immediately.
|
||||
fn request_repaint(&self, _: &ServoShellWindow);
|
||||
/// Request a new outer size for the window, including external decorations.
|
||||
/// This should be the same as `window.outerWidth` and `window.outerHeight``
|
||||
fn request_resize(&self, webview: &WebView, outer_size: DeviceIntSize)
|
||||
-> Option<DeviceIntSize>;
|
||||
fn set_position(&self, _point: DeviceIntPoint) {}
|
||||
fn set_fullscreen(&self, _state: bool) {}
|
||||
fn set_cursor(&self, _cursor: Cursor) {}
|
||||
#[cfg(all(
|
||||
feature = "webxr",
|
||||
not(any(target_os = "android", target_env = "ohos"))
|
||||
))]
|
||||
fn new_glwindow(
|
||||
&self,
|
||||
event_loop: &winit::event_loop::ActiveEventLoop,
|
||||
) -> Rc<dyn servo::webxr::glwindow::GlWindow>;
|
||||
/// This returns [`RenderingContext`] matching the viewport.
|
||||
fn rendering_context(&self) -> Rc<dyn RenderingContext>;
|
||||
fn theme(&self) -> servo::Theme {
|
||||
servo::Theme::Light
|
||||
}
|
||||
fn window_rect(&self) -> DeviceIndependentIntRect;
|
||||
fn maximize(&self, _: &WebView) {}
|
||||
fn focused(&self) -> bool;
|
||||
|
||||
fn show_embedder_control(&self, _: WebViewId, _: EmbedderControl) {}
|
||||
fn hide_embedder_control(&self, _: WebViewId, _: EmbedderControlId) {}
|
||||
fn dismiss_embedder_controls_for_webview(&self, _: WebViewId) {}
|
||||
fn show_bluetooth_device_dialog(
|
||||
&self,
|
||||
_: WebViewId,
|
||||
_devices: Vec<String>,
|
||||
_: GenericSender<Option<String>>,
|
||||
) {
|
||||
}
|
||||
fn show_permission_dialog(&self, _: WebViewId, _: PermissionRequest) {}
|
||||
fn show_http_authentication_dialog(&self, _: WebViewId, _: AuthenticationRequest) {}
|
||||
|
||||
fn notify_input_event_handled(
|
||||
&self,
|
||||
_webview: &WebView,
|
||||
_id: InputEventId,
|
||||
_result: InputEventResult,
|
||||
) {
|
||||
}
|
||||
|
||||
fn notify_media_session_event(&self, _: MediaSessionEvent) {}
|
||||
fn notify_crashed(&self, _: WebView, _reason: String, _backtrace: Option<String>) {}
|
||||
}
|
||||
@@ -232,19 +232,6 @@ public class MainActivity extends Activity implements Servo.Client {
|
||||
mCanGoBack = canGoBack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAllowNavigation(String url) {
|
||||
if (url.startsWith("market://")) {
|
||||
try {
|
||||
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Log.e("onAllowNavigation", e.toString());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onRedrawing(boolean redrawing) {
|
||||
if (redrawing) {
|
||||
mIdleText.setText("LOOP");
|
||||
|
||||
@@ -178,8 +178,6 @@ public class Servo {
|
||||
public interface Client {
|
||||
void onAlert(String message);
|
||||
|
||||
boolean onAllowNavigation(String url);
|
||||
|
||||
void onLoadStarted();
|
||||
|
||||
void onLoadEnded();
|
||||
@@ -260,10 +258,6 @@ public class Servo {
|
||||
mRunCallback.inUIThread(() -> mClient.onImeHide());
|
||||
}
|
||||
|
||||
public boolean onAllowNavigation(String url) {
|
||||
return mClient.onAllowNavigation(url);
|
||||
}
|
||||
|
||||
public void onLoadStarted() {
|
||||
mRunCallback.inUIThread(() -> mClient.onLoadStarted());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user