diff --git a/CHANGELOG.md b/CHANGELOG.md index bbadd11..4aaf191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Unreleased +### Features + +- Added `PreloadScripts` component for specifying JavaScript to be executed when the page is initialized. + ### Bug Fixes - Fixed so that webview can detect pointers correctly even if it is not the root entity. diff --git a/crates/bevy_cef_core/src/browser_process/browsers.rs b/crates/bevy_cef_core/src/browser_process/browsers.rs index 8f58ca7..f8d5fd6 100644 --- a/crates/bevy_cef_core/src/browser_process/browsers.rs +++ b/crates/bevy_cef_core/src/browser_process/browsers.rs @@ -8,10 +8,11 @@ use bevy::platform::collections::HashMap; use bevy::prelude::*; use bevy_remote::BrpMessage; use cef::{ - Browser, BrowserHost, BrowserSettings, Client, CompositionUnderline, ImplBrowser, - ImplBrowserHost, ImplFrame, ImplListValue, ImplProcessMessage, ImplRequestContext, - MouseButtonType, ProcessId, Range, RequestContext, RequestContextSettings, WindowInfo, - browser_host_create_browser_sync, process_message_create, + Browser, BrowserHost, BrowserSettings, CefString, Client, CompositionUnderline, + DictionaryValue, ImplBrowser, ImplBrowserHost, ImplDictionaryValue, ImplFrame, ImplListValue, + ImplProcessMessage, ImplRequestContext, MouseButtonType, ProcessId, Range, RequestContext, + RequestContextSettings, WindowInfo, browser_host_create_browser_sync, dictionary_value_create, + process_message_create, }; use cef_dll_sys::{cef_event_flags_t, cef_mouse_button_type_t}; #[allow(deprecated)] @@ -62,6 +63,7 @@ impl Browsers { ipc_event_sender: Sender, brp_sender: Sender, system_cursor_icon_sender: SystemCursorIconSenderInner, + initialize_scripts: &[String], _window_handle: Option, ) { let mut context = Self::request_context(requester); @@ -93,10 +95,11 @@ impl Browsers { windowless_frame_rate: 60, ..Default::default() }), - None, + Self::create_extra_info(initialize_scripts).as_mut(), context.as_mut(), ) .expect("Failed to create browser"); + self.browsers.insert( webview, WebviewBrowser { @@ -415,6 +418,18 @@ impl Browsers { .get(webview) .and_then(|b| b.client.focused_frame().is_some().then_some(b)) } + + fn create_extra_info(scripts: &[String]) -> Option { + if scripts.is_empty() { + return None; + } + let extra = dictionary_value_create()?; + extra.set_string( + Some(&CefString::from(INIT_SCRIPT_KEY)), + Some(&CefString::from(scripts.join(";").as_str())), + ); + Some(extra) + } } pub fn modifiers_from_mouse_buttons<'a>(buttons: impl IntoIterator) -> u32 { diff --git a/crates/bevy_cef_core/src/render_process/render_process_handler.rs b/crates/bevy_cef_core/src/render_process/render_process_handler.rs index 2ed4cb4..f4b59ee 100644 --- a/crates/bevy_cef_core/src/render_process/render_process_handler.rs +++ b/crates/bevy_cef_core/src/render_process/render_process_handler.rs @@ -8,9 +8,10 @@ use bevy::platform::collections::HashMap; use bevy_remote::BrpResult; use cef::rc::{Rc, RcImpl}; use cef::{ - Browser, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplRenderProcessHandler, - ImplV8Context, ImplV8Value, ProcessId, ProcessMessage, V8Context, V8Propertyattribute, V8Value, - WrapRenderProcessHandler, sys, v8_value_create_function, v8_value_create_object, + Browser, CefString, DictionaryValue, Frame, ImplBrowser, ImplDictionaryValue, ImplFrame, + ImplListValue, ImplProcessMessage, ImplRenderProcessHandler, ImplV8Context, ImplV8Value, + ProcessId, ProcessMessage, V8Context, V8Propertyattribute, V8Value, WrapRenderProcessHandler, + sys, v8_value_create_function, v8_value_create_object, }; use std::os::raw::c_int; use std::sync::Mutex; @@ -18,6 +19,9 @@ use std::sync::Mutex; pub(crate) static BRP_PROMISES: Mutex> = Mutex::new(HashMap::new()); pub(crate) static LISTEN_EVENTS: Mutex> = Mutex::new(HashMap::new()); +static INIT_SCRIPTS: Mutex> = Mutex::new(HashMap::new()); +pub const INIT_SCRIPT_KEY: &str = "init_script"; + pub const PROCESS_MESSAGE_BRP: &str = "brp"; pub const PROCESS_MESSAGE_HOST_EMIT: &str = "host-emit"; pub const PROCESS_MESSAGE_JS_EMIT: &str = "js-emit"; @@ -61,52 +65,34 @@ impl Clone for RenderProcessHandlerBuilder { } impl ImplRenderProcessHandler for RenderProcessHandlerBuilder { + fn on_browser_created( + &self, + browser: Option<&mut Browser>, + extra: Option<&mut DictionaryValue>, + ) { + if let (Some(browser), Some(extra)) = (browser, extra) { + let script = extra.string(Some(&INIT_SCRIPT_KEY.into())).into_string(); + if script.is_empty() { + return; + } + let id = browser.identifier(); + INIT_SCRIPTS.lock().unwrap().insert(id, script); + } + } + fn on_context_created( &self, - _browser: Option<&mut Browser>, + browser: Option<&mut Browser>, frame: Option<&mut Frame>, context: Option<&mut V8Context>, ) { - if let Some(g) = context.and_then(|c| c.global()) + if let Some(context) = context && let Some(frame) = frame - && let Some(mut cef) = v8_value_create_object( - Some(&mut V8DefaultAccessorBuilder::build()), - Some(&mut V8DefaultInterceptorBuilder::build()), - ) - && let Some(mut brp) = v8_value_create_function( - Some(&"brp".into()), - Some(&mut BrpBuilder::build(frame.clone())), - ) - && let Some(mut emit) = v8_value_create_function( - Some(&"emit".into()), - Some(&mut EmitBuilder::build(frame.clone())), - ) - && let Some(mut listen) = v8_value_create_function( - Some(&"listen".into()), - Some(&mut ListenBuilder::build(frame.clone())), - ) + && let Some(browser) = browser { - cef.set_value_bykey( - Some(&"brp".into()), - Some(&mut brp), - V8Propertyattribute::default(), - ); - cef.set_value_bykey( - Some(&"emit".into()), - Some(&mut emit), - V8Propertyattribute::default(), - ); - cef.set_value_bykey( - Some(&"listen".into()), - Some(&mut listen), - V8Propertyattribute::default(), - ); - g.set_value_bykey( - Some(&"cef".into()), - Some(&mut cef), - V8Propertyattribute::default(), - ); - }; + inject_initialize_scripts(browser, context, frame); + inject_cef_api(context, frame); + } } fn on_process_message_received( @@ -139,6 +125,60 @@ impl ImplRenderProcessHandler for RenderProcessHandlerBuilder { } } +fn inject_initialize_scripts(browser: &mut Browser, context: &mut V8Context, frame: &mut Frame) { + let id = browser.identifier(); + if let Some(script) = INIT_SCRIPTS.lock().ok().and_then(|scripts| { + let script = scripts.get(&id)?; + Some(CefString::from(script.as_str())) + }) { + context.enter(); + frame.execute_java_script(Some(&script), Some(&(&frame.url()).into()), 0); + context.exit(); + } +} + +fn inject_cef_api(context: &mut V8Context, frame: &mut Frame) { + if let Some(g) = context.global() + && let Some(mut cef) = v8_value_create_object( + Some(&mut V8DefaultAccessorBuilder::build()), + Some(&mut V8DefaultInterceptorBuilder::build()), + ) + && let Some(mut brp) = v8_value_create_function( + Some(&"brp".into()), + Some(&mut BrpBuilder::build(frame.clone())), + ) + && let Some(mut emit) = v8_value_create_function( + Some(&"emit".into()), + Some(&mut EmitBuilder::build(frame.clone())), + ) + && let Some(mut listen) = v8_value_create_function( + Some(&"listen".into()), + Some(&mut ListenBuilder::build(frame.clone())), + ) + { + cef.set_value_bykey( + Some(&"brp".into()), + Some(&mut brp), + V8Propertyattribute::default(), + ); + cef.set_value_bykey( + Some(&"emit".into()), + Some(&mut emit), + V8Propertyattribute::default(), + ); + cef.set_value_bykey( + Some(&"listen".into()), + Some(&mut listen), + V8Propertyattribute::default(), + ); + g.set_value_bykey( + Some(&"cef".into()), + Some(&mut cef), + V8Propertyattribute::default(), + ); + } +} + fn handle_brp_message(message: &ProcessMessage, ctx: V8Context) { let Some(argument_list) = message.argument_list() else { return; diff --git a/examples/preload_scripts.rs b/examples/preload_scripts.rs new file mode 100644 index 0000000..15d2cf8 --- /dev/null +++ b/examples/preload_scripts.rs @@ -0,0 +1,44 @@ +//! You can use [`PreloadScripts`] to execute scripts when the webview is initialized. +//! +//! [`PreloadScripts`] is executed before the scripts in the HTML. + +use bevy::prelude::*; +use bevy_cef::prelude::*; + +fn main() { + App::new() + .add_plugins((DefaultPlugins, CefPlugin)) + .add_systems( + Startup, + (spawn_camera, spawn_directional_light, spawn_webview), + ) + .run(); +} + +fn spawn_camera(mut commands: Commands) { + commands.spawn(( + Camera3d::default(), + Transform::from_translation(Vec3::new(0., 0., 3.)).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn spawn_directional_light(mut commands: Commands) { + commands.spawn(( + DirectionalLight::default(), + Transform::from_translation(Vec3::new(1., 1., 1.)).looking_at(Vec3::ZERO, Vec3::Y), + )); +} + +fn spawn_webview( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + commands.spawn(( + CefWebviewUri::new("https://github.com/not-elm/bevy_cef"), + Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))), + MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())), + // Here, we add a simple script to show an alert. + PreloadScripts::from(["alert('Hello World!')"]), + )); +} diff --git a/src/common/components.rs b/src/common/components.rs index 8d1e180..dabbbdc 100644 --- a/src/common/components.rs +++ b/src/common/components.rs @@ -9,7 +9,9 @@ impl Plugin for WebviewCoreComponentsPlugin { app.register_type::() .register_type::() .register_type::() - .register_type::(); + .register_type::() + .register_type::() + .register_type::(); } } @@ -21,7 +23,7 @@ impl Plugin for WebviewCoreComponentsPlugin { /// Alternatively, you can also use [`CefWebviewUri::local`]. #[derive(Component, Debug, Clone, PartialEq, Eq, Hash, Reflect)] #[reflect(Component, Debug)] -#[require(WebviewSize, ZoomLevel, AudioMuted)] +#[require(WebviewSize, ZoomLevel, AudioMuted, PreloadScripts)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] pub struct CefWebviewUri(pub String); @@ -78,3 +80,20 @@ pub struct ZoomLevel(pub f64); #[derive(Reflect, Component, Debug, Copy, Clone, PartialEq, Default, Serialize, Deserialize)] #[reflect(Component, Debug, Default, Serialize, Deserialize)] pub struct AudioMuted(pub bool); + +/// This component is used to preload scripts in the webview. +/// +/// Scripts specified in this component are executed before the scripts in the HTML. +#[derive(Reflect, Component, Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[reflect(Component, Debug, Default, Serialize, Deserialize)] +pub struct PreloadScripts(pub Vec); + +impl From for PreloadScripts +where + L: IntoIterator, + S: Into, +{ + fn from(scripts: L) -> Self { + Self(scripts.into_iter().map(Into::into).collect()) + } +} diff --git a/src/webview.rs b/src/webview.rs index 07a463e..43cd847 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -1,5 +1,6 @@ use crate::common::{CefWebviewUri, HostWindow, IpcEventRawSender, WebviewSize}; use crate::cursor_icon::SystemCursorIconSender; +use crate::prelude::PreloadScripts; use crate::webview::mesh::MeshWebviewPlugin; use bevy::ecs::component::HookContext; use bevy::ecs::world::DeferredWorld; @@ -104,13 +105,19 @@ fn create_webview( cursor_icon_sender: Res, winit_windows: NonSend, webviews: Query< - (Entity, &CefWebviewUri, &WebviewSize, Option<&HostWindow>), + ( + Entity, + &CefWebviewUri, + &WebviewSize, + &PreloadScripts, + Option<&HostWindow>, + ), Added, >, primary_window: Query>, ) { - for (entity, uri, size, parent) in webviews.iter() { - let host_window = parent + for (entity, uri, size, initialize_scripts, host_window) in webviews.iter() { + let host_window = host_window .and_then(|w| winit_windows.get_window(w.0)) .or_else(|| winit_windows.get_window(primary_window.single().ok()?)) .and_then(|w| { @@ -125,6 +132,7 @@ fn create_webview( ipc_event_sender.0.clone(), brp_sender.clone(), cursor_icon_sender.clone(), + &initialize_scripts.0, host_window, ); }