add: preload scripts (#6)

This commit is contained in:
elm
2025-08-19 19:54:19 +09:00
committed by GitHub
parent 1d3512c6b8
commit 6031e1c9d4
6 changed files with 182 additions and 52 deletions

View File

@@ -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.

View File

@@ -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<IpcEventRaw>,
brp_sender: Sender<BrpMessage>,
system_cursor_icon_sender: SystemCursorIconSenderInner,
initialize_scripts: &[String],
_window_handle: Option<RawWindowHandle>,
) {
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<DictionaryValue> {
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<Item = &'a MouseButton>) -> u32 {

View File

@@ -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<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
pub(crate) static LISTEN_EVENTS: Mutex<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
static INIT_SCRIPTS: Mutex<HashMap<c_int, String>> = 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;

View File

@@ -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<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
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!')"]),
));
}

View File

@@ -9,7 +9,9 @@ impl Plugin for WebviewCoreComponentsPlugin {
app.register_type::<WebviewSize>()
.register_type::<CefWebviewUri>()
.register_type::<HostWindow>()
.register_type::<ZoomLevel>();
.register_type::<ZoomLevel>()
.register_type::<AudioMuted>()
.register_type::<PreloadScripts>();
}
}
@@ -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<String>);
impl<L, S> From<L> for PreloadScripts
where
L: IntoIterator<Item = S>,
S: Into<String>,
{
fn from(scripts: L) -> Self {
Self(scripts.into_iter().map(Into::into).collect())
}
}

View File

@@ -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<SystemCursorIconSender>,
winit_windows: NonSend<WinitWindows>,
webviews: Query<
(Entity, &CefWebviewUri, &WebviewSize, Option<&HostWindow>),
(
Entity,
&CefWebviewUri,
&WebviewSize,
&PreloadScripts,
Option<&HostWindow>,
),
Added<CefWebviewUri>,
>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
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,
);
}