mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Migrate Android and OHOS ports to the delegate API (#35315)
Signed-off-by: Mukilan Thiyagarajan <mukilan@igalia.com> Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
e7a6691628
commit
e0689c1f0b
9 changed files with 794 additions and 784 deletions
|
@ -613,7 +613,7 @@ impl Servo {
|
|||
)
|
||||
}
|
||||
|
||||
fn handle_window_event(&mut self, event: EmbedderEvent) {
|
||||
fn handle_window_event(&self, event: EmbedderEvent) {
|
||||
match event {
|
||||
EmbedderEvent::Idle => {},
|
||||
|
||||
|
@ -913,7 +913,7 @@ impl Servo {
|
|||
std::mem::take(&mut *self.messages_for_embedder.borrow_mut())
|
||||
}
|
||||
|
||||
pub fn handle_events(&mut self, events: impl IntoIterator<Item = EmbedderEvent>) {
|
||||
pub fn handle_events(&self, events: impl IntoIterator<Item = EmbedderEvent>) {
|
||||
if self.compositor.borrow_mut().receive_messages() {
|
||||
self.receive_messages();
|
||||
}
|
||||
|
|
|
@ -18,11 +18,11 @@ use log::{debug, error, info, warn};
|
|||
use servo::{LoadStatus, MediaSessionActionType};
|
||||
use simpleservo::{
|
||||
DeviceIntRect, EventLoopWaker, InitOptions, InputMethodType, MediaSessionPlaybackState,
|
||||
PromptResult, SERVO,
|
||||
PromptResult, APP,
|
||||
};
|
||||
|
||||
use super::app_state::{Coordinates, RunningAppState};
|
||||
use super::host_trait::HostTrait;
|
||||
use super::servo_glue::{Coordinates, ServoGlue};
|
||||
|
||||
struct HostCallbacks {
|
||||
callbacks: GlobalRef,
|
||||
|
@ -44,10 +44,10 @@ pub extern "C" fn android_main() {
|
|||
|
||||
fn call<F>(env: &mut JNIEnv, f: F)
|
||||
where
|
||||
F: Fn(&mut ServoGlue),
|
||||
F: Fn(&RunningAppState),
|
||||
{
|
||||
SERVO.with(|servo| match servo.borrow_mut().as_mut() {
|
||||
Some(ref mut servo) => (f)(servo),
|
||||
APP.with(|app| match app.borrow().as_ref() {
|
||||
Some(ref app_state) => (f)(app_state),
|
||||
None => throw(env, "Servo not available in this thread"),
|
||||
});
|
||||
}
|
||||
|
@ -673,13 +673,6 @@ impl HostTrait for HostCallbacks {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
fn on_devtools_started(&self, port: Result<u16, ()>, _token: String) {
|
||||
match port {
|
||||
Ok(p) => info!("Devtools Server running on port {}", p),
|
||||
Err(()) => error!("Error running devtools server"),
|
||||
}
|
||||
}
|
||||
|
||||
fn show_context_menu(&self, _title: Option<String>, _items: Vec<String>) {}
|
||||
|
||||
fn on_panic(&self, _reason: String, _backtrace: Option<String>) {}
|
||||
|
|
|
@ -19,14 +19,14 @@ pub use servo::{InputMethodType, MediaSessionPlaybackState, PromptResult};
|
|||
use surfman::{Connection, SurfaceType};
|
||||
|
||||
use crate::egl::android::resources::ResourceReaderInstance;
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::egl::servo_glue::{
|
||||
Coordinates, ServoEmbedderCallbacks, ServoGlue, ServoWindowCallbacks,
|
||||
use crate::egl::app_state::{
|
||||
Coordinates, RunningAppState, ServoEmbedderCallbacks, ServoWindowCallbacks,
|
||||
};
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::prefs::{parse_command_line_arguments, ArgumentParsingResult};
|
||||
|
||||
thread_local! {
|
||||
pub static SERVO: RefCell<Option<ServoGlue>> = const { RefCell::new(None) };
|
||||
pub static APP: RefCell<Option<Rc<RunningAppState>>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub struct InitOptions {
|
||||
|
@ -116,20 +116,25 @@ pub fn init(
|
|||
CompositeTarget::ContextFbo,
|
||||
);
|
||||
|
||||
SERVO.with(|s| {
|
||||
let servo_glue = ServoGlue::new(
|
||||
APP.with(|app| {
|
||||
let app_state = RunningAppState::new(
|
||||
init_opts.url,
|
||||
rendering_context,
|
||||
servo,
|
||||
window_callbacks,
|
||||
servoshell_preferences,
|
||||
);
|
||||
*s.borrow_mut() = Some(servo_glue);
|
||||
*app.borrow_mut() = Some(app_state);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deinit() {
|
||||
SERVO.with(|s| s.replace(None).unwrap().deinit());
|
||||
APP.with(|app| {
|
||||
let app = app.replace(None).unwrap();
|
||||
if let Some(app_state) = Rc::into_inner(app) {
|
||||
app_state.deinit()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
762
ports/servoshell/egl/app_state.rs
Normal file
762
ports/servoshell/egl/app_state.rs
Normal file
|
@ -0,0 +1,762 @@
|
|||
/* 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::os::raw::c_void;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use keyboard_types::{CompositionEvent, CompositionState};
|
||||
use log::{debug, error, info, warn};
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::compositing::windowing::{
|
||||
AnimationState, EmbedderCoordinates, EmbedderMethods, MouseWindowEvent, WindowMethods,
|
||||
};
|
||||
use servo::euclid::{Box2D, Point2D, Rect, Scale, Size2D, Vector2D};
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::webrender_api::units::{DeviceIntRect, DeviceIntSize, DevicePixel, DeviceRect};
|
||||
use servo::webrender_api::ScrollLocation;
|
||||
use servo::webrender_traits::SurfmanRenderingContext;
|
||||
use servo::{
|
||||
AllowOrDenyRequest, ContextMenuResult, EmbedderProxy, EventLoopWaker, InputMethodType, Key,
|
||||
KeyState, KeyboardEvent, LoadStatus, MediaSessionActionType, MediaSessionEvent, MouseButton,
|
||||
NavigationRequest, PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin,
|
||||
PromptResult, Servo, ServoDelegate, ServoError, TouchEventType, TouchId, WebView,
|
||||
WebViewDelegate,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Coordinates {
|
||||
pub viewport: Rect<i32, DevicePixel>,
|
||||
pub framebuffer: Size2D<i32, DevicePixel>,
|
||||
}
|
||||
|
||||
impl Coordinates {
|
||||
pub fn new(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
fb_width: i32,
|
||||
fb_height: i32,
|
||||
) -> Coordinates {
|
||||
Coordinates {
|
||||
viewport: Rect::new(Point2D::new(x, y), Size2D::new(width, height)),
|
||||
framebuffer: Size2D::new(fb_width, fb_height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ServoWindowCallbacks {
|
||||
host_callbacks: Box<dyn HostTrait>,
|
||||
coordinates: RefCell<Coordinates>,
|
||||
hidpi_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
|
||||
}
|
||||
|
||||
impl ServoWindowCallbacks {
|
||||
pub(super) fn new(
|
||||
host_callbacks: Box<dyn HostTrait>,
|
||||
coordinates: RefCell<Coordinates>,
|
||||
hidpi_factor: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
host_callbacks,
|
||||
coordinates,
|
||||
hidpi_factor: Scale::new(hidpi_factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RunningAppState {
|
||||
servo: Servo,
|
||||
rendering_context: SurfmanRenderingContext,
|
||||
callbacks: Rc<ServoWindowCallbacks>,
|
||||
inner: RefCell<RunningAppStateInner>,
|
||||
/// servoshell specific preferences created during startup of the application.
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
}
|
||||
|
||||
struct RunningAppStateInner {
|
||||
need_present: bool,
|
||||
/// List of top-level browsing contexts.
|
||||
/// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed,
|
||||
/// and we exit if it ever becomes empty.
|
||||
webviews: HashMap<WebViewId, WebView>,
|
||||
|
||||
/// The order in which the webviews were created.
|
||||
creation_order: Vec<WebViewId>,
|
||||
|
||||
/// The webview that is currently focused.
|
||||
/// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred.
|
||||
focused_webview_id: Option<WebViewId>,
|
||||
|
||||
context_menu_sender: Option<IpcSender<ContextMenuResult>>,
|
||||
}
|
||||
|
||||
impl ServoDelegate for RunningAppState {
|
||||
fn notify_devtools_server_started(&self, _servo: &Servo, port: u16, _token: String) {
|
||||
info!("Devtools Server running on port {port}");
|
||||
}
|
||||
|
||||
fn request_devtools_connection(&self, _servo: &Servo, request: AllowOrDenyRequest) {
|
||||
request.allow();
|
||||
}
|
||||
|
||||
fn notify_error(&self, _servo: &Servo, error: ServoError) {
|
||||
error!("Saw Servo error: {error:?}!");
|
||||
}
|
||||
}
|
||||
|
||||
impl WebViewDelegate for RunningAppState {
|
||||
fn notify_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);
|
||||
}
|
||||
|
||||
fn notify_ready_to_show(&self, webview: WebView) {
|
||||
webview.focus();
|
||||
}
|
||||
|
||||
fn notify_closed(&self, webview: WebView) {
|
||||
{
|
||||
let mut inner_mut = self.inner_mut();
|
||||
inner_mut.webviews.retain(|&id, _| id != webview.id());
|
||||
inner_mut.creation_order.retain(|&id| id != webview.id());
|
||||
inner_mut.focused_webview_id = None;
|
||||
}
|
||||
|
||||
if let Some(newest_webview) = self.newest_webview() {
|
||||
newest_webview.focus();
|
||||
} else {
|
||||
self.servo.start_shutting_down();
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_focus_changed(&self, webview: WebView, focused: bool) {
|
||||
if focused {
|
||||
self.inner_mut().focused_webview_id = Some(webview.id());
|
||||
webview.show(true);
|
||||
} else if self.inner().focused_webview_id == Some(webview.id()) {
|
||||
self.inner_mut().focused_webview_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
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 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 new_webview = self.servo.new_auxiliary_webview();
|
||||
self.add(new_webview.clone());
|
||||
Some(new_webview)
|
||||
}
|
||||
|
||||
fn request_permission(
|
||||
&self,
|
||||
_webview: WebView,
|
||||
prompt: PermissionPrompt,
|
||||
result_sender: IpcSender<PermissionRequest>,
|
||||
) {
|
||||
let message = match prompt {
|
||||
PermissionPrompt::Request(permission_name) => {
|
||||
format!("Do you want to grant permission for {:?}?", permission_name)
|
||||
},
|
||||
PermissionPrompt::Insecure(permission_name) => {
|
||||
format!(
|
||||
"The {:?} feature is only safe to use in secure context, but servo can't guarantee\n\
|
||||
that the current context is secure. Do you want to proceed and grant permission?",
|
||||
permission_name
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
let result = match self.callbacks.host_callbacks.prompt_yes_no(message, true) {
|
||||
PromptResult::Primary => PermissionRequest::Granted,
|
||||
PromptResult::Secondary | PromptResult::Dismissed => PermissionRequest::Denied,
|
||||
};
|
||||
|
||||
let _ = result_sender.send(result);
|
||||
}
|
||||
|
||||
fn request_resize_to(&self, _webview: WebView, size: DeviceIntSize) {
|
||||
warn!("Received resize event (to {size:?}). Currently only the user can resize windows");
|
||||
}
|
||||
|
||||
fn show_context_menu(
|
||||
&self,
|
||||
_webview: WebView,
|
||||
result_sender: IpcSender<ContextMenuResult>,
|
||||
title: Option<String>,
|
||||
items: Vec<String>,
|
||||
) {
|
||||
if self.inner().context_menu_sender.is_some() {
|
||||
warn!("Trying to show a context menu when a context menu is already active");
|
||||
let _ = result_sender.send(ContextMenuResult::Ignored);
|
||||
} else {
|
||||
self.inner_mut().context_menu_sender = Some(result_sender);
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.show_context_menu(title, items);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_prompt(&self, _webview: WebView, prompt: PromptDefinition, origin: PromptOrigin) {
|
||||
let cb = &self.callbacks.host_callbacks;
|
||||
let trusted = origin == PromptOrigin::Trusted;
|
||||
let _ = match prompt {
|
||||
PromptDefinition::Alert(message, response_sender) => {
|
||||
cb.prompt_alert(message, trusted);
|
||||
response_sender.send(())
|
||||
},
|
||||
PromptDefinition::OkCancel(message, response_sender) => {
|
||||
response_sender.send(cb.prompt_ok_cancel(message, trusted))
|
||||
},
|
||||
PromptDefinition::Input(message, default, response_sender) => {
|
||||
response_sender.send(cb.prompt_input(message, default, trusted))
|
||||
},
|
||||
PromptDefinition::Credentials(response_sender) => {
|
||||
warn!("implement credentials prompt for OpenHarmony OS and Android");
|
||||
response_sender.send(Default::default())
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn show_ime(
|
||||
&self,
|
||||
_webview: WebView,
|
||||
input_method_type: InputMethodType,
|
||||
text: Option<(String, i32)>,
|
||||
multiline: bool,
|
||||
position: DeviceIntRect,
|
||||
) {
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.on_ime_show(input_method_type, text, multiline, position);
|
||||
}
|
||||
|
||||
fn hide_ime(&self, _webview: WebView) {
|
||||
self.callbacks.host_callbacks.on_ime_hide();
|
||||
}
|
||||
|
||||
fn get_clipboard_contents(&self, _webview: WebView, sender: IpcSender<String>) {
|
||||
let contents = self.callbacks.host_callbacks.get_clipboard_contents();
|
||||
let _ = sender.send(contents.unwrap_or("".to_owned()));
|
||||
}
|
||||
|
||||
fn set_clipboard_contents(&self, _webview: WebView, text: String) {
|
||||
self.callbacks.host_callbacks.set_clipboard_contents(text);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl RunningAppState {
|
||||
pub(super) fn new(
|
||||
initial_url: Option<String>,
|
||||
rendering_context: SurfmanRenderingContext,
|
||||
servo: Servo,
|
||||
callbacks: Rc<ServoWindowCallbacks>,
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
) -> 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 {
|
||||
rendering_context,
|
||||
servo,
|
||||
callbacks,
|
||||
servoshell_preferences,
|
||||
inner: RefCell::new(RunningAppStateInner {
|
||||
need_present: false,
|
||||
context_menu_sender: None,
|
||||
webviews: Default::default(),
|
||||
creation_order: vec![],
|
||||
focused_webview_id: None,
|
||||
}),
|
||||
});
|
||||
|
||||
app_state.new_toplevel_webview(initial_url);
|
||||
|
||||
app_state
|
||||
}
|
||||
|
||||
pub(crate) fn new_toplevel_webview(self: &Rc<Self>, url: Url) {
|
||||
let webview = self.servo.new_webview(url);
|
||||
webview.set_delegate(self.clone());
|
||||
self.add(webview.clone());
|
||||
}
|
||||
|
||||
pub(crate) fn add(&self, webview: WebView) {
|
||||
self.inner_mut().creation_order.push(webview.id());
|
||||
self.inner_mut().webviews.insert(webview.id(), webview);
|
||||
}
|
||||
|
||||
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> {
|
||||
let webview_id = match self.inner().focused_webview_id {
|
||||
Some(id) => id,
|
||||
None => return Err("No focused WebViewId yet."),
|
||||
};
|
||||
Ok(webview_id)
|
||||
}
|
||||
|
||||
fn newest_webview(&self) -> Option<WebView> {
|
||||
self.inner()
|
||||
.creation_order
|
||||
.last()
|
||||
.and_then(|id| self.inner().webviews.get(id).cloned())
|
||||
}
|
||||
|
||||
fn active_webview(&self) -> WebView {
|
||||
self.inner()
|
||||
.focused_webview_id
|
||||
.and_then(|id| self.inner().webviews.get(&id).cloned())
|
||||
.or(self.newest_webview())
|
||||
.expect("Should always have an active WebView")
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// Returns the webrender surface management integration interface.
|
||||
/// This provides the embedder access to the current front buffer.
|
||||
pub fn surfman(&self) -> SurfmanRenderingContext {
|
||||
self.rendering_context.clone()
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
debug!("perform_updates");
|
||||
let should_continue = self.servo.spin_event_loop();
|
||||
if !should_continue {
|
||||
self.callbacks.host_callbacks.on_shutdown_complete();
|
||||
}
|
||||
debug!("done perform_updates");
|
||||
}
|
||||
|
||||
/// 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().load(url.into_url());
|
||||
}
|
||||
|
||||
/// Reload the page.
|
||||
pub fn reload(&self) {
|
||||
info!("reload");
|
||||
self.active_webview().reload();
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Redraw the page.
|
||||
pub fn refresh(&self) {
|
||||
info!("refresh");
|
||||
self.active_webview().composite();
|
||||
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().go_back(1);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Go forward in history.
|
||||
pub fn go_forward(&self) {
|
||||
info!("go_forward");
|
||||
self.active_webview().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;
|
||||
let _ = self
|
||||
.rendering_context
|
||||
.resize(Size2D::new(size.width, size.height))
|
||||
.inspect_err(|e| error!("Failed to resize rendering context: {e:?}"));
|
||||
*self.callbacks.coordinates.borrow_mut() = coordinates;
|
||||
self.active_webview().notify_rendering_context_resized();
|
||||
self.active_webview()
|
||||
.move_resize(DeviceRect::from_size(size.to_f32()));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Start scrolling.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
#[cfg(not(target_env = "ohos"))]
|
||||
pub fn scroll_start(&self, dx: f32, dy: f32, x: i32, y: i32) {
|
||||
let delta = Vector2D::new(dx, dy);
|
||||
let scroll_location = ScrollLocation::Delta(delta);
|
||||
self.active_webview().notify_scroll_event(
|
||||
scroll_location,
|
||||
Point2D::new(x, y),
|
||||
TouchEventType::Down,
|
||||
);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Scroll.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
pub fn scroll(&self, dx: f32, dy: f32, x: i32, y: i32) {
|
||||
let delta = Vector2D::new(dx, dy);
|
||||
let scroll_location = ScrollLocation::Delta(delta);
|
||||
self.active_webview().notify_scroll_event(
|
||||
scroll_location,
|
||||
Point2D::new(x, y),
|
||||
TouchEventType::Move,
|
||||
);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// End scrolling.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
#[cfg(not(target_env = "ohos"))]
|
||||
pub fn scroll_end(&self, dx: f32, dy: f32, x: i32, y: i32) {
|
||||
let delta = Vector2D::new(dx, dy);
|
||||
let scroll_location = ScrollLocation::Delta(delta);
|
||||
self.active_webview().notify_scroll_event(
|
||||
scroll_location,
|
||||
Point2D::new(x, y),
|
||||
TouchEventType::Up,
|
||||
);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Touch event: press down
|
||||
pub fn touch_down(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Down,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Touch event: move touching finger
|
||||
pub fn touch_move(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Move,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Touch event: Lift touching finger
|
||||
pub fn touch_up(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Up,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Cancel touch event
|
||||
pub fn touch_cancel(&self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Cancel,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Register a mouse movement.
|
||||
pub fn mouse_move(&self, x: f32, y: f32) {
|
||||
self.active_webview()
|
||||
.notify_pointer_move_event(Point2D::new(x, y));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Register a mouse button press.
|
||||
pub fn mouse_down(&self, x: f32, y: f32, button: MouseButton) {
|
||||
self.active_webview()
|
||||
.notify_pointer_button_event(MouseWindowEvent::MouseDown(button, Point2D::new(x, y)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Register a mouse button release.
|
||||
pub fn mouse_up(&self, x: f32, y: f32, button: MouseButton) {
|
||||
self.active_webview()
|
||||
.notify_pointer_button_event(MouseWindowEvent::MouseUp(button, Point2D::new(x, y)));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Start pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_start(&self, factor: f32, _x: u32, _y: u32) {
|
||||
self.active_webview().set_pinch_zoom(factor);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom(&self, factor: f32, _x: u32, _y: u32) {
|
||||
self.active_webview().set_pinch_zoom(factor);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// End pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_end(&self, factor: f32, _x: u32, _y: u32) {
|
||||
self.active_webview().set_pinch_zoom(factor);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
/// Perform a click.
|
||||
pub fn click(&self, x: f32, y: f32) {
|
||||
self.active_webview()
|
||||
.notify_pointer_button_event(MouseWindowEvent::Click(
|
||||
MouseButton::Left,
|
||||
Point2D::new(x, y),
|
||||
));
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn key_down(&self, key: Key) {
|
||||
let key_event = KeyboardEvent {
|
||||
state: KeyState::Down,
|
||||
key,
|
||||
..KeyboardEvent::default()
|
||||
};
|
||||
self.active_webview().notify_keyboard_event(key_event);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn key_up(&self, key: Key) {
|
||||
let key_event = KeyboardEvent {
|
||||
state: KeyState::Up,
|
||||
key,
|
||||
..KeyboardEvent::default()
|
||||
};
|
||||
self.active_webview().notify_keyboard_event(key_event);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn ime_insert_text(&self, text: String) {
|
||||
self.active_webview().notify_ime_event(CompositionEvent {
|
||||
state: CompositionState::End,
|
||||
data: text,
|
||||
});
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn notify_vsync(&self) {
|
||||
self.active_webview().notify_vsync();
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn pause_compositor(&self) {
|
||||
if let Err(e) = self.rendering_context.unbind_native_surface_from_context() {
|
||||
warn!("Unbinding native surface from context failed ({:?})", e);
|
||||
}
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn resume_compositor(&self, native_surface: *mut c_void, coords: Coordinates) {
|
||||
if native_surface.is_null() {
|
||||
panic!("null passed for native_surface");
|
||||
}
|
||||
let connection = self.rendering_context.connection();
|
||||
let native_widget = unsafe {
|
||||
connection
|
||||
.create_native_widget_from_ptr(native_surface, coords.framebuffer.to_untyped())
|
||||
};
|
||||
if let Err(e) = self
|
||||
.rendering_context
|
||||
.bind_native_surface_to_context(native_widget)
|
||||
{
|
||||
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()
|
||||
.notify_media_session_action_event(action);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn set_throttled(&self, throttled: bool) {
|
||||
info!("set_throttled");
|
||||
self.active_webview().set_throttled(throttled);
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn ime_dismissed(&self) {
|
||||
info!("ime_dismissed");
|
||||
self.active_webview().notify_ime_dismissed_event();
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
pub fn on_context_menu_closed(&self, result: ContextMenuResult) -> Result<(), &'static str> {
|
||||
if let Some(sender) = self.inner_mut().context_menu_sender.take() {
|
||||
let _ = sender.send(result);
|
||||
} else {
|
||||
warn!("Trying to close a context menu when no context menu is active");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn present_if_needed(&self) {
|
||||
if self.inner().need_present {
|
||||
self.inner_mut().need_present = false;
|
||||
self.servo.present();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ServoEmbedderCallbacks {
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
#[cfg(feature = "webxr")]
|
||||
xr_discovery: Option<servo::webxr::Discovery>,
|
||||
}
|
||||
|
||||
impl ServoEmbedderCallbacks {
|
||||
pub(super) fn new(
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
#[cfg(feature = "webxr")] xr_discovery: Option<servo::webxr::Discovery>,
|
||||
) -> Self {
|
||||
Self {
|
||||
waker,
|
||||
#[cfg(feature = "webxr")]
|
||||
xr_discovery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmbedderMethods for ServoEmbedderCallbacks {
|
||||
fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> {
|
||||
debug!("EmbedderMethods::create_event_loop_waker");
|
||||
self.waker.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
fn register_webxr(
|
||||
&mut self,
|
||||
registry: &mut servo::webxr::MainThreadRegistry,
|
||||
_embedder_proxy: EmbedderProxy,
|
||||
) {
|
||||
debug!("EmbedderMethods::register_xr");
|
||||
if let Some(discovery) = self.xr_discovery.take() {
|
||||
registry.register(discovery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowMethods for ServoWindowCallbacks {
|
||||
fn get_coordinates(&self) -> EmbedderCoordinates {
|
||||
let coords = self.coordinates.borrow();
|
||||
let screen_size = (coords.viewport.size.to_f32() / self.hidpi_factor).to_i32();
|
||||
EmbedderCoordinates {
|
||||
viewport: coords.viewport.to_box2d(),
|
||||
framebuffer: coords.framebuffer,
|
||||
window_rect: Box2D::from_origin_and_size(Point2D::zero(), screen_size),
|
||||
screen_size,
|
||||
available_screen_size: screen_size,
|
||||
hidpi_factor: self.hidpi_factor,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_animation_state(&self, state: AnimationState) {
|
||||
debug!("WindowMethods::set_animation_state: {:?}", state);
|
||||
self.host_callbacks
|
||||
.on_animating_changed(state == AnimationState::Animating);
|
||||
}
|
||||
}
|
|
@ -74,8 +74,6 @@ pub trait HostTrait {
|
|||
fn on_media_session_playback_state_change(&self, state: MediaSessionPlaybackState);
|
||||
/// Called when the media session position state is set.
|
||||
fn on_media_session_set_position_state(&self, duration: f64, position: f64, playback_rate: f64);
|
||||
/// Called when devtools server is started
|
||||
fn on_devtools_started(&self, port: Result<u16, ()>, token: String);
|
||||
/// Called when we get a panic message from constellation
|
||||
fn on_panic(&self, reason: String, backtrace: Option<String>);
|
||||
}
|
||||
|
|
|
@ -10,5 +10,5 @@ mod ohos;
|
|||
|
||||
mod log;
|
||||
|
||||
mod app_state;
|
||||
mod host_trait;
|
||||
mod servo_glue;
|
||||
|
|
|
@ -32,8 +32,8 @@ use xcomponent_sys::{
|
|||
OH_NativeXComponent_TouchEvent, OH_NativeXComponent_TouchEventType,
|
||||
};
|
||||
|
||||
use super::app_state::{Coordinates, RunningAppState};
|
||||
use super::host_trait::HostTrait;
|
||||
use super::servo_glue::{Coordinates, ServoGlue};
|
||||
|
||||
mod resources;
|
||||
mod simpleservo;
|
||||
|
@ -129,7 +129,7 @@ static PROMPT_TOAST: OnceLock<
|
|||
|
||||
impl ServoAction {
|
||||
fn dispatch_touch_event(
|
||||
servo: &mut ServoGlue,
|
||||
servo: &RunningAppState,
|
||||
kind: TouchEventType,
|
||||
x: f32,
|
||||
y: f32,
|
||||
|
@ -145,7 +145,7 @@ impl ServoAction {
|
|||
}
|
||||
|
||||
// todo: consider making this take `self`, so we don't need to needlessly clone.
|
||||
fn do_action(&self, servo: &mut ServoGlue) {
|
||||
fn do_action(&self, servo: &RunningAppState) {
|
||||
use ServoAction::*;
|
||||
match self {
|
||||
WakeUp => servo.perform_updates(),
|
||||
|
@ -244,7 +244,7 @@ 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 mut servo = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks)
|
||||
let servo = simpleservo::init(*init_opts, window.0, xc.0, wakeup, callbacks)
|
||||
.expect("Servo initialization failed");
|
||||
|
||||
info!("Surface created!");
|
||||
|
@ -261,7 +261,7 @@ 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(&mut servo);
|
||||
action.do_action(&servo);
|
||||
}
|
||||
|
||||
info!("Sender disconnected - Terminating main surface thread");
|
||||
|
@ -844,10 +844,6 @@ impl HostTrait for HostCallbacks {
|
|||
warn!("on_media_session_set_position_state not implemented");
|
||||
}
|
||||
|
||||
fn on_devtools_started(&self, port: Result<u16, ()>, token: String) {
|
||||
warn!("on_devtools_started not implemented");
|
||||
}
|
||||
|
||||
fn on_panic(&self, reason: String, backtrace: Option<String>) {
|
||||
error!("Panic: {reason},");
|
||||
if let Some(bt) = backtrace {
|
||||
|
|
|
@ -18,12 +18,12 @@ use servo::{self, resources, Servo};
|
|||
use surfman::{Connection, SurfaceType};
|
||||
use xcomponent_sys::OH_NativeXComponent;
|
||||
|
||||
use crate::egl::app_state::{
|
||||
Coordinates, RunningAppState, ServoEmbedderCallbacks, ServoWindowCallbacks,
|
||||
};
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::egl::ohos::resources::ResourceReaderInstance;
|
||||
use crate::egl::ohos::InitOpts;
|
||||
use crate::egl::servo_glue::{
|
||||
Coordinates, ServoEmbedderCallbacks, ServoGlue, ServoWindowCallbacks,
|
||||
};
|
||||
use crate::prefs::{parse_command_line_arguments, ArgumentParsingResult};
|
||||
|
||||
/// Initialize Servo. At that point, we need a valid GL context.
|
||||
|
@ -34,7 +34,7 @@ pub fn init(
|
|||
xcomponent: *mut OH_NativeXComponent,
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
callbacks: Box<dyn HostTrait>,
|
||||
) -> Result<ServoGlue, &'static str> {
|
||||
) -> Result<Rc<RunningAppState>, &'static str> {
|
||||
info!("Entered simpleservo init function");
|
||||
crate::init_tracing();
|
||||
crate::init_crypto();
|
||||
|
@ -117,7 +117,7 @@ pub fn init(
|
|||
CompositeTarget::ContextFbo,
|
||||
);
|
||||
|
||||
let servo_glue = ServoGlue::new(
|
||||
let app_state = RunningAppState::new(
|
||||
Some(options.url),
|
||||
rendering_context,
|
||||
servo,
|
||||
|
@ -125,5 +125,5 @@ pub fn init(
|
|||
servoshell_preferences,
|
||||
);
|
||||
|
||||
Ok(servo_glue)
|
||||
Ok(app_state)
|
||||
}
|
||||
|
|
|
@ -1,744 +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::collections::HashMap;
|
||||
use std::os::raw::c_void;
|
||||
use std::rc::Rc;
|
||||
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use keyboard_types::{CompositionEvent, CompositionState};
|
||||
use log::{debug, error, info, warn};
|
||||
use servo::base::id::WebViewId;
|
||||
use servo::compositing::windowing::{
|
||||
AnimationState, EmbedderCoordinates, EmbedderMethods, MouseWindowEvent, WindowMethods,
|
||||
};
|
||||
use servo::euclid::{Box2D, Point2D, Rect, Scale, Size2D, Vector2D};
|
||||
use servo::servo_geometry::DeviceIndependentPixel;
|
||||
use servo::webrender_api::units::{DevicePixel, DeviceRect};
|
||||
use servo::webrender_api::ScrollLocation;
|
||||
use servo::webrender_traits::SurfmanRenderingContext;
|
||||
use servo::{
|
||||
ContextMenuResult, EmbedderMsg, EmbedderProxy, EventLoopWaker, Key, KeyState, KeyboardEvent,
|
||||
MediaSessionActionType, MediaSessionEvent, MouseButton, PermissionPrompt, PermissionRequest,
|
||||
PromptDefinition, PromptOrigin, PromptResult, Servo, TouchEventType, TouchId, WebView,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::egl::host_trait::HostTrait;
|
||||
use crate::prefs::ServoShellPreferences;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Coordinates {
|
||||
pub viewport: Rect<i32, DevicePixel>,
|
||||
pub framebuffer: Size2D<i32, DevicePixel>,
|
||||
}
|
||||
|
||||
impl Coordinates {
|
||||
pub fn new(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
fb_width: i32,
|
||||
fb_height: i32,
|
||||
) -> Coordinates {
|
||||
Coordinates {
|
||||
viewport: Rect::new(Point2D::new(x, y), Size2D::new(width, height)),
|
||||
framebuffer: Size2D::new(fb_width, fb_height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ServoWindowCallbacks {
|
||||
host_callbacks: Box<dyn HostTrait>,
|
||||
coordinates: RefCell<Coordinates>,
|
||||
hidpi_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>,
|
||||
}
|
||||
|
||||
impl ServoWindowCallbacks {
|
||||
pub(super) fn new(
|
||||
host_callbacks: Box<dyn HostTrait>,
|
||||
coordinates: RefCell<Coordinates>,
|
||||
hidpi_factor: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
host_callbacks,
|
||||
coordinates,
|
||||
hidpi_factor: Scale::new(hidpi_factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServoGlue {
|
||||
rendering_context: SurfmanRenderingContext,
|
||||
servo: Servo,
|
||||
need_present: bool,
|
||||
callbacks: Rc<ServoWindowCallbacks>,
|
||||
context_menu_sender: Option<IpcSender<ContextMenuResult>>,
|
||||
|
||||
/// List of top-level browsing contexts.
|
||||
/// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed,
|
||||
/// and we exit if it ever becomes empty.
|
||||
webviews: HashMap<WebViewId, WebView>,
|
||||
|
||||
/// The order in which the webviews were created.
|
||||
creation_order: Vec<WebViewId>,
|
||||
|
||||
/// The webview that is currently focused.
|
||||
/// Modified by EmbedderMsg::WebViewFocused and EmbedderMsg::WebViewBlurred.
|
||||
focused_webview_id: Option<WebViewId>,
|
||||
|
||||
/// servoshell specific preferences created during startup of the application.
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl ServoGlue {
|
||||
pub(super) fn new(
|
||||
initial_url: Option<String>,
|
||||
rendering_context: SurfmanRenderingContext,
|
||||
servo: Servo,
|
||||
callbacks: Rc<ServoWindowCallbacks>,
|
||||
servoshell_preferences: ServoShellPreferences,
|
||||
) -> 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 webview = servo.new_webview(initial_url);
|
||||
let webview_id = webview.id();
|
||||
let webviews = [(webview_id, webview)].into();
|
||||
|
||||
Self {
|
||||
rendering_context,
|
||||
servo,
|
||||
need_present: false,
|
||||
callbacks,
|
||||
context_menu_sender: None,
|
||||
webviews,
|
||||
creation_order: vec![],
|
||||
focused_webview_id: Some(webview_id),
|
||||
servoshell_preferences,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_browser_id(&self) -> Result<WebViewId, &'static str> {
|
||||
let webview_id = match self.focused_webview_id {
|
||||
Some(id) => id,
|
||||
None => return Err("No focused WebViewId yet."),
|
||||
};
|
||||
Ok(webview_id)
|
||||
}
|
||||
|
||||
fn newest_webview(&self) -> Option<&WebView> {
|
||||
self.creation_order
|
||||
.last()
|
||||
.and_then(|id| self.webviews.get(id))
|
||||
}
|
||||
|
||||
fn active_webview(&self) -> &WebView {
|
||||
self.focused_webview_id
|
||||
.and_then(|id| self.webviews.get(&id))
|
||||
.or(self.newest_webview())
|
||||
.expect("Should always have an active WebView")
|
||||
}
|
||||
|
||||
/// Request shutdown. Will call on_shutdown_complete.
|
||||
pub fn request_shutdown(&mut self) {
|
||||
self.servo.start_shutting_down();
|
||||
self.maybe_perform_updates();
|
||||
}
|
||||
|
||||
/// Call after on_shutdown_complete
|
||||
pub fn deinit(self) {
|
||||
self.servo.deinit();
|
||||
}
|
||||
|
||||
/// Returns the webrender surface management integration interface.
|
||||
/// This provides the embedder access to the current front buffer.
|
||||
pub fn surfman(&self) -> SurfmanRenderingContext {
|
||||
self.rendering_context.clone()
|
||||
}
|
||||
|
||||
/// 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(&mut self) {
|
||||
debug!("perform_updates");
|
||||
self.servo.handle_events(vec![]);
|
||||
let _ = self.handle_servo_events();
|
||||
debug!("done perform_updates");
|
||||
}
|
||||
|
||||
/// Load an URL.
|
||||
pub fn load_uri(&mut 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().load(url.into_url());
|
||||
}
|
||||
|
||||
/// Reload the page.
|
||||
pub fn reload(&mut self) {
|
||||
info!("reload");
|
||||
self.active_webview().reload();
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Redraw the page.
|
||||
pub fn refresh(&mut self) {
|
||||
info!("refresh");
|
||||
self.active_webview().composite();
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Stop loading the page.
|
||||
pub fn stop(&mut self) {
|
||||
warn!("TODO can't stop won't stop");
|
||||
}
|
||||
|
||||
/// Go back in history.
|
||||
pub fn go_back(&mut self) {
|
||||
info!("go_back");
|
||||
self.active_webview().go_back(1);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Go forward in history.
|
||||
pub fn go_forward(&mut self) {
|
||||
info!("go_forward");
|
||||
self.active_webview().go_forward(1);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Let Servo know that the window has been resized.
|
||||
pub fn resize(&mut self, coordinates: Coordinates) {
|
||||
info!("resize to {:?}", coordinates);
|
||||
let size = coordinates.viewport.size;
|
||||
let _ = self
|
||||
.rendering_context
|
||||
.resize(Size2D::new(size.width, size.height))
|
||||
.inspect_err(|e| error!("Failed to resize rendering context: {e:?}"));
|
||||
*self.callbacks.coordinates.borrow_mut() = coordinates;
|
||||
self.active_webview().notify_rendering_context_resized();
|
||||
self.active_webview()
|
||||
.move_resize(DeviceRect::from_size(size.to_f32()));
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Start scrolling.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
#[cfg(not(target_env = "ohos"))]
|
||||
pub fn scroll_start(&mut self, dx: f32, dy: f32, x: i32, y: i32) {
|
||||
let delta = Vector2D::new(dx, dy);
|
||||
let scroll_location = ScrollLocation::Delta(delta);
|
||||
self.active_webview().notify_scroll_event(
|
||||
scroll_location,
|
||||
Point2D::new(x, y),
|
||||
TouchEventType::Down,
|
||||
);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Scroll.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
pub fn scroll(&mut self, dx: f32, dy: f32, x: i32, y: i32) {
|
||||
let delta = Vector2D::new(dx, dy);
|
||||
let scroll_location = ScrollLocation::Delta(delta);
|
||||
self.active_webview().notify_scroll_event(
|
||||
scroll_location,
|
||||
Point2D::new(x, y),
|
||||
TouchEventType::Move,
|
||||
);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// End scrolling.
|
||||
/// x/y are scroll coordinates.
|
||||
/// dx/dy are scroll deltas.
|
||||
#[cfg(not(target_env = "ohos"))]
|
||||
pub fn scroll_end(&mut self, dx: f32, dy: f32, x: i32, y: i32) {
|
||||
let delta = Vector2D::new(dx, dy);
|
||||
let scroll_location = ScrollLocation::Delta(delta);
|
||||
self.active_webview().notify_scroll_event(
|
||||
scroll_location,
|
||||
Point2D::new(x, y),
|
||||
TouchEventType::Up,
|
||||
);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Touch event: press down
|
||||
pub fn touch_down(&mut self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Down,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Touch event: move touching finger
|
||||
pub fn touch_move(&mut self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Move,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Touch event: Lift touching finger
|
||||
pub fn touch_up(&mut self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Up,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Cancel touch event
|
||||
pub fn touch_cancel(&mut self, x: f32, y: f32, pointer_id: i32) {
|
||||
self.active_webview().notify_touch_event(
|
||||
TouchEventType::Cancel,
|
||||
TouchId(pointer_id),
|
||||
Point2D::new(x, y),
|
||||
);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Register a mouse movement.
|
||||
pub fn mouse_move(&mut self, x: f32, y: f32) {
|
||||
self.active_webview()
|
||||
.notify_pointer_move_event(Point2D::new(x, y));
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Register a mouse button press.
|
||||
pub fn mouse_down(&mut self, x: f32, y: f32, button: MouseButton) {
|
||||
self.active_webview()
|
||||
.notify_pointer_button_event(MouseWindowEvent::MouseDown(button, Point2D::new(x, y)));
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Register a mouse button release.
|
||||
pub fn mouse_up(&mut self, x: f32, y: f32, button: MouseButton) {
|
||||
self.active_webview()
|
||||
.notify_pointer_button_event(MouseWindowEvent::MouseUp(button, Point2D::new(x, y)));
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Start pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_start(&mut self, factor: f32, _x: u32, _y: u32) {
|
||||
self.active_webview().set_pinch_zoom(factor);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom(&mut self, factor: f32, _x: u32, _y: u32) {
|
||||
self.active_webview().set_pinch_zoom(factor);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// End pinchzoom.
|
||||
/// x/y are pinch origin coordinates.
|
||||
pub fn pinchzoom_end(&mut self, factor: f32, _x: u32, _y: u32) {
|
||||
self.active_webview().set_pinch_zoom(factor);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
/// Perform a click.
|
||||
pub fn click(&mut self, x: f32, y: f32) {
|
||||
self.active_webview()
|
||||
.notify_pointer_button_event(MouseWindowEvent::Click(
|
||||
MouseButton::Left,
|
||||
Point2D::new(x, y),
|
||||
));
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn key_down(&mut self, key: Key) {
|
||||
let key_event = KeyboardEvent {
|
||||
state: KeyState::Down,
|
||||
key,
|
||||
..KeyboardEvent::default()
|
||||
};
|
||||
self.active_webview().notify_keyboard_event(key_event);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn key_up(&mut self, key: Key) {
|
||||
let key_event = KeyboardEvent {
|
||||
state: KeyState::Up,
|
||||
key,
|
||||
..KeyboardEvent::default()
|
||||
};
|
||||
self.active_webview().notify_keyboard_event(key_event);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn ime_insert_text(&mut self, text: String) {
|
||||
self.active_webview().notify_ime_event(CompositionEvent {
|
||||
state: CompositionState::End,
|
||||
data: text,
|
||||
});
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn notify_vsync(&mut self) {
|
||||
self.active_webview().notify_vsync();
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn pause_compositor(&mut self) {
|
||||
if let Err(e) = self.rendering_context.unbind_native_surface_from_context() {
|
||||
warn!("Unbinding native surface from context failed ({:?})", e);
|
||||
}
|
||||
self.maybe_perform_updates();
|
||||
}
|
||||
|
||||
pub fn resume_compositor(&mut self, native_surface: *mut c_void, coords: Coordinates) {
|
||||
if native_surface.is_null() {
|
||||
panic!("null passed for native_surface");
|
||||
}
|
||||
let connection = self.rendering_context.connection();
|
||||
let native_widget = unsafe {
|
||||
connection
|
||||
.create_native_widget_from_ptr(native_surface, coords.framebuffer.to_untyped())
|
||||
};
|
||||
if let Err(e) = self
|
||||
.rendering_context
|
||||
.bind_native_surface_to_context(native_widget)
|
||||
{
|
||||
warn!("Binding native surface to context failed ({:?})", e);
|
||||
}
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn media_session_action(&mut self, action: MediaSessionActionType) {
|
||||
info!("Media session action {:?}", action);
|
||||
self.active_webview()
|
||||
.notify_media_session_action_event(action);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn set_throttled(&mut self, throttled: bool) {
|
||||
info!("set_throttled");
|
||||
self.active_webview().set_throttled(throttled);
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn ime_dismissed(&mut self) {
|
||||
info!("ime_dismissed");
|
||||
self.active_webview().notify_ime_dismissed_event();
|
||||
self.maybe_perform_updates()
|
||||
}
|
||||
|
||||
pub fn on_context_menu_closed(
|
||||
&mut self,
|
||||
result: ContextMenuResult,
|
||||
) -> Result<(), &'static str> {
|
||||
if let Some(sender) = self.context_menu_sender.take() {
|
||||
let _ = sender.send(result);
|
||||
} else {
|
||||
warn!("Trying to close a context menu when no context menu is active");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn maybe_perform_updates(&mut self) {
|
||||
self.perform_updates();
|
||||
}
|
||||
|
||||
fn handle_servo_events(&mut self) -> Result<(), &'static str> {
|
||||
let mut need_update = false;
|
||||
let messages = self.servo.get_events();
|
||||
for message in messages {
|
||||
match message {
|
||||
EmbedderMsg::ChangePageTitle(_, title) => {
|
||||
self.callbacks.host_callbacks.on_title_changed(title);
|
||||
},
|
||||
EmbedderMsg::AllowNavigationRequest(_, pipeline_id, url) => {
|
||||
let data: bool = self
|
||||
.callbacks
|
||||
.host_callbacks
|
||||
.on_allow_navigation(url.to_string());
|
||||
self.servo.allow_navigation_response(pipeline_id, data);
|
||||
need_update = true;
|
||||
},
|
||||
EmbedderMsg::HistoryChanged(_, entries, current) => {
|
||||
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());
|
||||
},
|
||||
EmbedderMsg::NotifyLoadStatusChanged(_, load_status) => {
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.notify_load_status_changed(load_status);
|
||||
},
|
||||
EmbedderMsg::GetSelectedBluetoothDevice(_, _, sender) => {
|
||||
let _ = sender.send(None);
|
||||
},
|
||||
EmbedderMsg::AllowUnload(_, sender) => {
|
||||
let _ = sender.send(true);
|
||||
},
|
||||
EmbedderMsg::ShowContextMenu(_, sender, title, items) => {
|
||||
if self.context_menu_sender.is_some() {
|
||||
warn!(
|
||||
"Trying to show a context menu when a context menu is already active"
|
||||
);
|
||||
let _ = sender.send(ContextMenuResult::Ignored);
|
||||
} else {
|
||||
self.context_menu_sender = Some(sender);
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.show_context_menu(title, items);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::Prompt(_, definition, origin) => {
|
||||
let cb = &self.callbacks.host_callbacks;
|
||||
let trusted = origin == PromptOrigin::Trusted;
|
||||
let res = match definition {
|
||||
PromptDefinition::Alert(message, sender) => {
|
||||
cb.prompt_alert(message, trusted);
|
||||
sender.send(())
|
||||
},
|
||||
PromptDefinition::OkCancel(message, sender) => {
|
||||
sender.send(cb.prompt_ok_cancel(message, trusted))
|
||||
},
|
||||
PromptDefinition::Input(message, default, sender) => {
|
||||
sender.send(cb.prompt_input(message, default, trusted))
|
||||
},
|
||||
PromptDefinition::Credentials(_) => {
|
||||
warn!("implement credentials prompt for OpenHarmony OS and Android");
|
||||
Ok(())
|
||||
},
|
||||
};
|
||||
if let Err(e) = res {
|
||||
self.active_webview()
|
||||
.send_error(format!("Failed to send Prompt response: {e}"));
|
||||
}
|
||||
},
|
||||
EmbedderMsg::AllowOpeningWebView(_, response_chan) => {
|
||||
let new_webview = self.servo.new_auxiliary_webview();
|
||||
let new_webview_id = new_webview.id();
|
||||
self.webviews.insert(new_webview_id, new_webview);
|
||||
self.creation_order.push(new_webview_id);
|
||||
|
||||
if let Err(e) = response_chan.send(Some(new_webview_id)) {
|
||||
warn!("Failed to send AllowOpeningBrowser response: {}", e);
|
||||
};
|
||||
},
|
||||
EmbedderMsg::WebViewOpened(new_webview_id) => {
|
||||
if let Some(webview) = self.webviews.get(&new_webview_id) {
|
||||
webview.focus();
|
||||
}
|
||||
},
|
||||
EmbedderMsg::WebViewClosed(webview_id) => {
|
||||
self.webviews.retain(|&id, _| id != webview_id);
|
||||
self.creation_order.retain(|&id| id != webview_id);
|
||||
self.focused_webview_id = None;
|
||||
|
||||
if let Some(newest_webview) = self.newest_webview() {
|
||||
newest_webview.focus();
|
||||
} else {
|
||||
self.servo.start_shutting_down();
|
||||
}
|
||||
},
|
||||
EmbedderMsg::WebViewFocused(webview_id) => {
|
||||
self.focused_webview_id = Some(webview_id);
|
||||
if let Some(webview) = self.webviews.get(&webview_id) {
|
||||
webview.show(true);
|
||||
}
|
||||
},
|
||||
EmbedderMsg::WebViewBlurred => {
|
||||
self.focused_webview_id = None;
|
||||
},
|
||||
EmbedderMsg::GetClipboardContents(_, sender) => {
|
||||
let contents = self.callbacks.host_callbacks.get_clipboard_contents();
|
||||
let _ = sender.send(contents.unwrap_or("".to_owned()));
|
||||
},
|
||||
EmbedderMsg::SetClipboardContents(_, text) => {
|
||||
self.callbacks.host_callbacks.set_clipboard_contents(text);
|
||||
},
|
||||
EmbedderMsg::Shutdown => {
|
||||
self.callbacks.host_callbacks.on_shutdown_complete();
|
||||
},
|
||||
EmbedderMsg::PromptPermission(_, prompt, sender) => {
|
||||
let message = match prompt {
|
||||
PermissionPrompt::Request(permission_name) => {
|
||||
format!("Do you want to grant permission for {:?}?", permission_name)
|
||||
},
|
||||
PermissionPrompt::Insecure(permission_name) => {
|
||||
format!(
|
||||
"The {:?} feature is only safe to use in secure context, but servo can't guarantee\n\
|
||||
that the current context is secure. Do you want to proceed and grant permission?",
|
||||
permission_name
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
let result = match self.callbacks.host_callbacks.prompt_yes_no(message, true) {
|
||||
PromptResult::Primary => PermissionRequest::Granted,
|
||||
PromptResult::Secondary | PromptResult::Dismissed => {
|
||||
PermissionRequest::Denied
|
||||
},
|
||||
};
|
||||
|
||||
let _ = sender.send(result);
|
||||
},
|
||||
EmbedderMsg::ShowIME(_, kind, text, multiline, bounds) => {
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.on_ime_show(kind, text, multiline, bounds);
|
||||
},
|
||||
EmbedderMsg::HideIME(_) => {
|
||||
self.callbacks.host_callbacks.on_ime_hide();
|
||||
},
|
||||
EmbedderMsg::MediaSessionEvent(_, event) => {
|
||||
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,
|
||||
),
|
||||
};
|
||||
},
|
||||
EmbedderMsg::OnDevtoolsStarted(port, token) => {
|
||||
self.callbacks
|
||||
.host_callbacks
|
||||
.on_devtools_started(port, token);
|
||||
},
|
||||
EmbedderMsg::RequestDevtoolsConnection(result_sender) => {
|
||||
result_sender.send(true);
|
||||
},
|
||||
EmbedderMsg::Panic(_, reason, backtrace) => {
|
||||
self.callbacks.host_callbacks.on_panic(reason, backtrace);
|
||||
},
|
||||
EmbedderMsg::ReadyToPresent(_webview_ids) => {
|
||||
self.need_present = true;
|
||||
},
|
||||
EmbedderMsg::ResizeTo(_, size) => {
|
||||
warn!("Received resize event (to {size:?}). Currently only the user can resize windows");
|
||||
},
|
||||
EmbedderMsg::Keyboard(..) |
|
||||
EmbedderMsg::Status(..) |
|
||||
EmbedderMsg::SelectFiles(..) |
|
||||
EmbedderMsg::MoveTo(..) |
|
||||
EmbedderMsg::SetCursor(..) |
|
||||
EmbedderMsg::NewFavicon(..) |
|
||||
EmbedderMsg::SetFullscreenState(..) |
|
||||
EmbedderMsg::ReportProfile(..) |
|
||||
EmbedderMsg::EventDelivered(..) |
|
||||
EmbedderMsg::PlayGamepadHapticEffect(..) |
|
||||
EmbedderMsg::StopGamepadHapticEffect(..) |
|
||||
EmbedderMsg::ClearClipboardContents(..) |
|
||||
EmbedderMsg::WebResourceRequested(..) => {},
|
||||
}
|
||||
}
|
||||
|
||||
if need_update {
|
||||
self.perform_updates();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn present_if_needed(&mut self) {
|
||||
if self.need_present {
|
||||
self.need_present = false;
|
||||
self.servo.present();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ServoEmbedderCallbacks {
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
#[cfg(feature = "webxr")]
|
||||
xr_discovery: Option<servo::webxr::Discovery>,
|
||||
}
|
||||
|
||||
impl ServoEmbedderCallbacks {
|
||||
pub(super) fn new(
|
||||
waker: Box<dyn EventLoopWaker>,
|
||||
#[cfg(feature = "webxr")] xr_discovery: Option<servo::webxr::Discovery>,
|
||||
) -> Self {
|
||||
Self {
|
||||
waker,
|
||||
#[cfg(feature = "webxr")]
|
||||
xr_discovery,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmbedderMethods for ServoEmbedderCallbacks {
|
||||
fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> {
|
||||
debug!("EmbedderMethods::create_event_loop_waker");
|
||||
self.waker.clone()
|
||||
}
|
||||
|
||||
#[cfg(feature = "webxr")]
|
||||
fn register_webxr(
|
||||
&mut self,
|
||||
registry: &mut servo::webxr::MainThreadRegistry,
|
||||
_embedder_proxy: EmbedderProxy,
|
||||
) {
|
||||
debug!("EmbedderMethods::register_xr");
|
||||
if let Some(discovery) = self.xr_discovery.take() {
|
||||
registry.register(discovery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowMethods for ServoWindowCallbacks {
|
||||
fn get_coordinates(&self) -> EmbedderCoordinates {
|
||||
let coords = self.coordinates.borrow();
|
||||
let screen_size = (coords.viewport.size.to_f32() / self.hidpi_factor).to_i32();
|
||||
EmbedderCoordinates {
|
||||
viewport: coords.viewport.to_box2d(),
|
||||
framebuffer: coords.framebuffer,
|
||||
window_rect: Box2D::from_origin_and_size(Point2D::zero(), screen_size),
|
||||
screen_size,
|
||||
available_screen_size: screen_size,
|
||||
hidpi_factor: self.hidpi_factor,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_animation_state(&self, state: AnimationState) {
|
||||
debug!("WindowMethods::set_animation_state: {:?}", state);
|
||||
self.host_callbacks
|
||||
.on_animating_changed(state == AnimationState::Animating);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue