diff --git a/.gitignore b/.gitignore index b5e851c..c34a0fc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ target/ book/ .DS_Store**/.DS_Store .claude/memo.md -docs/plans/ \ No newline at end of file +docs/plans/ +.worktrees/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 19dc31c..60d250e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ ### Features - Added `PreloadScripts` component for specifying JavaScript to be executed when the page is initialized. +- Added `CefExtensions` type for registering custom JavaScript APIs via CEF's `register_extension` + - Extensions are global and load before any page scripts + - New `extensions` example demonstrating custom JS APIs +- Refactored `window.cef` API (`brp`, `emit`, `listen`) to be registered as a CEF extension during `on_web_kit_initialized` + - The API is now available earlier in the page lifecycle ### Bug Fixes diff --git a/CLAUDE.md b/CLAUDE.md index 5216bc6..475edec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,93 +10,87 @@ This is `bevy_cef`, a Bevy plugin that integrates the Chromium Embedded Framewor ### Multi-Process Design - **Browser Process**: Main application process running Bevy (`bevy_cef_core::browser_process`) -- **Render Process**: Separate CEF render process (`bevy_cef_core::render_process`) +- **Render Process**: Separate CEF render process (`bevy_cef_core::render_process`) - Communication through IPC channels and Bevy Remote Protocol (BRP) ### Core Components -- `CefWebviewUri`: Component specifying webview URL (remote or local via `cef://localhost/`) -- `WebviewSize`: Controls webview rendering dimensions (default 800x800) -- `WebviewExtendStandardMaterial`: Material for rendering webviews on 3D meshes -- `HostWindow`: Optional parent window specification -- `ZoomLevel`: Webview zoom control -- `AudioMuted`: Audio muting control +- `CefWebviewUri`: URL specification (`CefWebviewUri::new("url")` or `CefWebviewUri::local("file.html")`) +- `WebviewSize`: Rendering dimensions (default 800x800), controls texture resolution not 3D size +- `WebviewExtendStandardMaterial`: Primary material for 3D mesh rendering +- `WebviewSpriteMaterial`: Material for 2D sprite rendering +- `HostWindow`: Optional parent window (defaults to PrimaryWindow) +- `ZoomLevel`: f64 zoom control (0.0 = default) +- `AudioMuted`: bool for audio control +- `PreloadScripts`: Vec of scripts to execute before page scripts +- `CefExtensions`: Custom JS extensions via `register_extension` (global to all webviews) ### Plugin Architecture -The main `CefPlugin` orchestrates several sub-plugins: -- `LocalHostPlugin`: Serves local assets via custom scheme -- `MessageLoopPlugin`: CEF message loop integration +The main `CefPlugin` accepts `CommandLineConfig` for CEF command-line switches and `CefExtensions` for custom JavaScript APIs. Sub-plugins: +- `LocalHostPlugin`: Serves local assets via `cef://localhost/` scheme +- `MessageLoopPlugin`: CEF message loop integration (macOS uses `CefDoMessageLoopWork()`) - `WebviewCoreComponentsPlugin`: Core component registration -- `WebviewPlugin`: Main webview management -- `IpcPlugin`: Inter-process communication -- `KeyboardPlugin`, `NavigationPlugin`, `ZoomPlugin`, `AudioMutePlugin`: Feature-specific functionality +- `WebviewPlugin`: Webview lifecycle and DevTools +- `IpcPlugin`: IPC containing `IpcRawEventPlugin` and `HostEmitPlugin` +- `KeyboardPlugin`, `SystemCursorIconPlugin`, `NavigationPlugin`, `ZoomPlugin`, `AudioMutePlugin` +- `RemotePlugin`: Auto-added for BRP support if not present ### IPC System Three communication patterns: -1. **JS Emit**: Webview → Bevy app via `JsEmitEventPlugin` -2. **Host Emit**: Bevy app → Webview via event emission -3. **BRP (Bevy Remote Protocol)**: Bidirectional RPC calls +1. **JS Emit**: Webview → Bevy via `JsEmitEventPlugin` where E: `DeserializeOwned + Send + Sync + 'static` + - Events wrapped in `Receive` EntityEvent + - JavaScript: `window.cef.emit('event_name', data)` +2. **Host Emit**: Bevy → Webview via `HostEmitEvent` (EntityEvent) + - JavaScript: `window.cef.listen('event_name', callback)` +3. **BRP**: Bidirectional RPC via `bevy_remote` + - JavaScript: `await window.cef.brp({ method: 'method_name', params: {...} })` + +### EntityEvent Pattern +Navigation and DevTools events are `EntityEvent` types requiring explicit `webview: Entity` field: +- `HostEmitEvent`, `RequestGoBack`, `RequestGoForward`, `RequestShowDevTool`, `RequestCloseDevtool` ## Development Commands -### Code Quality ```bash # Fix and format code make fix -# Which runs: -# cargo clippy --fix --allow-dirty --allow-staged --workspace --all --all-features -# cargo fmt --all -``` -### Development Setup -The build system automatically handles CEF dependencies on macOS with debug feature: -- Installs `bevy_cef_debug_render_process` tool -- Installs `export-cef-dir` tool -- Downloads/extracts CEF framework to `$HOME/.local/share/cef` +# Run examples (macOS requires debug feature) +cargo run --example simple --features debug -### Manual Installation -```bash # Install debug render process tool make install -# Or: cargo install --path ./crates/bevy_cef_debug_render_process --force ``` -## Key Examples - -- `examples/simple.rs`: Basic webview on 3D plane -- `examples/js_emit.rs`: JavaScript to Bevy communication -- `examples/host_emit.rs`: Bevy to JavaScript communication -- `examples/brp.rs`: Bidirectional RPC with devtools -- `examples/navigation.rs`: Page navigation controls -- `examples/zoom_level.rs`: Zoom functionality -- `examples/sprite.rs`: Webview as 2D sprite -- `examples/devtool.rs`: Chrome DevTools integration +### Debug Tools Setup (macOS) +Manual installation required before running with `debug` feature: +```bash +cargo install export-cef-dir +export-cef-dir --force $HOME/.local/share +cargo install bevy_cef_debug_render_process +mv $HOME/.cargo/bin/bevy_cef_debug_render_process "$HOME/.local/share/Chromium Embedded Framework.framework/Libraries/bevy_cef_debug_render_process" +``` ## Local Asset Loading -Local HTML/assets are served via the custom `cef://localhost/` scheme: +Local HTML/assets served via `cef://localhost/` scheme: - Place assets in `assets/` directory - Reference as `CefWebviewUri::local("filename.html")` -- Or manually: `cef://localhost/filename.html` ## Testing -No automated tests are present in this codebase. Testing is done through the example applications. - -### Manually Testing - -- Run tests with `cargo test --workspace --all-features` +No automated tests. Testing done through examples: +- `cargo test --workspace --all-features` (for any future tests) +- Examples: simple, js_emit, host_emit, brp, navigation, zoom_level, sprite, devtool, custom_material, preload_scripts, extensions ## Platform Notes -- Currently focused on macOS development (see Cargo.toml target-specific dependencies) -- CEF framework must be available at `$HOME/.local/share/cef` -- Uses `objc` crate for macOS-specific window handling -- DYLD environment variables required for CEF library loading +- Primary platform: macOS (uses `objc` crate for window handling) +- CEF framework location: `$HOME/.local/share/Chromium Embedded Framework.framework` +- Windows/Linux: Infrastructure ready but needs testing +- Key resources (`Browsers`, library loaders) are `NonSend` - CEF is not thread-safe ## Workspace Structure -This is a Cargo workspace with: - Root crate: `bevy_cef` (public API) - `crates/bevy_cef_core`: Core CEF integration logic -- `crates/bevy_cef_debug_render_process`: Debug render process executable -- `examples/demo`: Standalone demo application \ No newline at end of file +- `crates/bevy_cef_debug_render_process`: Debug render process executable \ No newline at end of file diff --git a/assets/extensions.html b/assets/extensions.html new file mode 100644 index 0000000..35044a3 --- /dev/null +++ b/assets/extensions.html @@ -0,0 +1,39 @@ + + + + CEF Extensions Example + + + +

CEF Extensions Example

+

Testing custom JavaScript extensions registered via register_extension.

+ + + + +
Output will appear here...
+ + + + diff --git a/assets/host_emit.html b/assets/host_emit.html index 3607ecd..1e81df4 100644 --- a/assets/host_emit.html +++ b/assets/host_emit.html @@ -15,7 +15,7 @@

0

diff --git a/assets/js_emit.html b/assets/js_emit.html index 081125f..e9b60f9 100644 --- a/assets/js_emit.html +++ b/assets/js_emit.html @@ -17,7 +17,7 @@ let count = 0; window.setInterval(() => { console.log("Emitting count:", count); - window.cef.emit({ + cef.emit({ count, }); countElement.innerText = count; diff --git a/crates/bevy_cef_core/src/browser_process.rs b/crates/bevy_cef_core/src/browser_process.rs index b7ff9c6..8e4905e 100644 --- a/crates/bevy_cef_core/src/browser_process.rs +++ b/crates/bevy_cef_core/src/browser_process.rs @@ -5,6 +5,7 @@ mod client_handler; mod command_line_config; mod context_menu_handler; mod display_handler; +mod extensions; mod localhost; mod message_pump; mod renderer_handler; @@ -16,6 +17,7 @@ pub use browsers::*; pub use client_handler::*; pub use command_line_config::*; pub use context_menu_handler::*; +pub use extensions::*; pub use localhost::*; pub use message_pump::*; pub use renderer_handler::*; diff --git a/crates/bevy_cef_core/src/browser_process/app.rs b/crates/bevy_cef_core/src/browser_process/app.rs index 3171f36..e1cf3dd 100644 --- a/crates/bevy_cef_core/src/browser_process/app.rs +++ b/crates/bevy_cef_core/src/browser_process/app.rs @@ -1,3 +1,4 @@ +use crate::browser_process::CefExtensions; use crate::browser_process::CommandLineConfig; use crate::browser_process::MessageLoopTimer; use crate::browser_process::browser_process_handler::BrowserProcessHandlerBuilder; @@ -17,17 +18,20 @@ pub struct BrowserProcessAppBuilder { object: *mut RcImpl<_cef_app_t, Self>, message_loop_working_requester: Sender, config: CommandLineConfig, + extensions: CefExtensions, } impl BrowserProcessAppBuilder { pub fn build( message_loop_working_requester: Sender, config: CommandLineConfig, + extensions: CefExtensions, ) -> cef::App { cef::App::new(Self { object: core::ptr::null_mut(), message_loop_working_requester, config, + extensions, }) } } @@ -43,6 +47,7 @@ impl Clone for BrowserProcessAppBuilder { object, message_loop_working_requester: self.message_loop_working_requester.clone(), config: self.config.clone(), + extensions: self.extensions.clone(), } } } @@ -65,7 +70,6 @@ impl ImplApp for BrowserProcessAppBuilder { let Some(command_line) = command_line else { return; }; - for switch in &self.config.switches { command_line.append_switch(Some(&(*switch).into())); } @@ -84,6 +88,7 @@ impl ImplApp for BrowserProcessAppBuilder { fn browser_process_handler(&self) -> Option { Some(BrowserProcessHandlerBuilder::build( self.message_loop_working_requester.clone(), + self.extensions.clone(), )) } diff --git a/crates/bevy_cef_core/src/browser_process/browser_process_handler.rs b/crates/bevy_cef_core/src/browser_process/browser_process_handler.rs index e0fcbfb..3739d8f 100644 --- a/crates/bevy_cef_core/src/browser_process/browser_process_handler.rs +++ b/crates/bevy_cef_core/src/browser_process/browser_process_handler.rs @@ -1,4 +1,4 @@ -use crate::prelude::MessageLoopTimer; +use crate::prelude::{CefExtensions, EXTENSIONS_SWITCH, MessageLoopTimer}; use cef::rc::{Rc, RcImpl}; use cef::*; use std::sync::mpsc::Sender; @@ -9,15 +9,18 @@ use std::sync::mpsc::Sender; pub struct BrowserProcessHandlerBuilder { object: *mut RcImpl, message_loop_working_requester: Sender, + extensions: CefExtensions, } impl BrowserProcessHandlerBuilder { pub fn build( message_loop_working_requester: Sender, + extensions: CefExtensions, ) -> BrowserProcessHandler { BrowserProcessHandler::new(Self { object: core::ptr::null_mut(), message_loop_working_requester, + extensions, }) } } @@ -48,6 +51,7 @@ impl Clone for BrowserProcessHandlerBuilder { Self { object, message_loop_working_requester: self.message_loop_working_requester.clone(), + extensions: self.extensions.clone(), } } } @@ -64,6 +68,15 @@ impl ImplBrowserProcessHandler for BrowserProcessHandlerBuilder { command_line.append_switch(Some(&"ignore-certificate-errors".into())); command_line.append_switch(Some(&"ignore-ssl-errors".into())); command_line.append_switch(Some(&"enable-logging=stderr".into())); + // Pass extensions to render process via command line + if !self.extensions.is_empty() + && let Ok(json) = serde_json::to_string(&self.extensions.0) + { + command_line.append_switch_with_value( + Some(&EXTENSIONS_SWITCH.into()), + Some(&json.as_str().into()), + ); + } } fn on_schedule_message_pump_work(&self, delay_ms: i64) { diff --git a/crates/bevy_cef_core/src/browser_process/extensions.rs b/crates/bevy_cef_core/src/browser_process/extensions.rs new file mode 100644 index 0000000..7f04c2e --- /dev/null +++ b/crates/bevy_cef_core/src/browser_process/extensions.rs @@ -0,0 +1,47 @@ +use bevy::platform::collections::HashMap; +use serde::{Deserialize, Serialize}; + +/// Custom JavaScript extensions to register via CEF's `register_extension`. +/// +/// Extensions are global to all webviews and loaded before any page scripts run. +/// Use existing `window.cef.emit()`, `window.cef.listen()`, and `window.cef.brp()` +/// APIs within your extension code for Bevy communication. +/// +/// # Example +/// +/// ```no_run +/// use bevy_cef_core::prelude::*; +/// +/// let extensions = CefExtensions::new() +/// .add("myGame", r#" +/// var myGame = { +/// sendScore: function(score) { +/// window.cef.emit('score_update', { score: score }); +/// } +/// }; +/// "#); +/// ``` +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct CefExtensions(pub HashMap); + +impl CefExtensions { + /// Creates a new empty extensions collection. + pub fn new() -> Self { + Self::default() + } + + /// Adds a JavaScript extension. + /// + /// # Arguments + /// * `name` - Extension name (will be prefixed with `v8/` internally) + /// * `code` - JavaScript code defining the extension's API + pub fn add(mut self, name: impl Into, code: impl Into) -> Self { + self.0.insert(name.into(), code.into()); + self + } + + /// Returns true if no extensions are registered. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/crates/bevy_cef_core/src/lib.rs b/crates/bevy_cef_core/src/lib.rs index d32f523..b0e20c0 100644 --- a/crates/bevy_cef_core/src/lib.rs +++ b/crates/bevy_cef_core/src/lib.rs @@ -10,8 +10,6 @@ pub mod prelude { #[cfg(target_os = "macos")] pub use crate::debug::*; pub use crate::render_process::app::*; - pub use crate::render_process::brp::*; - pub use crate::render_process::emit::*; pub use crate::render_process::execute_render_process; pub use crate::render_process::render_process_handler::*; pub use crate::util::*; diff --git a/crates/bevy_cef_core/src/render_process.rs b/crates/bevy_cef_core/src/render_process.rs index bebbef5..a423060 100644 --- a/crates/bevy_cef_core/src/render_process.rs +++ b/crates/bevy_cef_core/src/render_process.rs @@ -3,9 +3,7 @@ use cef::args::Args; use cef::{api_hash, execute_process, sys}; pub mod app; -pub mod brp; -pub mod emit; -pub mod listen; +pub mod cef_api_handler; pub mod render_process_handler; /// Execute the CEF render process. diff --git a/crates/bevy_cef_core/src/render_process/brp.rs b/crates/bevy_cef_core/src/render_process/brp.rs deleted file mode 100644 index c310cf4..0000000 --- a/crates/bevy_cef_core/src/render_process/brp.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::prelude::{BRP_PROMISES, PROCESS_MESSAGE_BRP, v8_value_to_json}; -use cef::rc::{ConvertParam, ConvertReturnValue, Rc, RcImpl}; -use cef::{ - CefString, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Handler, ImplV8Value, - ProcessId, V8Handler, V8Value, WrapV8Handler, process_message_create, sys, - v8_value_create_promise, v8_value_create_string, -}; -use cef_dll_sys::{_cef_v8_handler_t, _cef_v8_value_t, cef_process_id_t, cef_string_t}; -use std::os::raw::c_int; -use std::ptr::write; - -/// Implements the `window.brp` function in JavaScript. -/// -/// The function definition is `async (request: BrpRequest) -> Promise`. -/// -/// The flow from the execution of the function to the return of the result to the Javascript side is as follows: -/// 1. Send `BrpRequest` to the browser process. -/// 2. The browser process receives `BrpResult` and sends it to the render process. -/// 3. The render process receives the result in `on_process_message_received`. -/// 4. The render process resolves the result to the `Promise` of `window.brp`. -pub struct BrpBuilder { - object: *mut RcImpl<_cef_v8_handler_t, Self>, - frame: Frame, -} - -impl BrpBuilder { - pub fn build(frame: Frame) -> V8Handler { - V8Handler::new(Self { - object: core::ptr::null_mut(), - frame, - }) - } -} - -impl Rc for BrpBuilder { - fn as_base(&self) -> &sys::cef_base_ref_counted_t { - unsafe { - let base = &*self.object; - std::mem::transmute(&base.cef_object) - } - } -} - -impl WrapV8Handler for BrpBuilder { - fn wrap_rc(&mut self, object: *mut RcImpl<_cef_v8_handler_t, Self>) { - self.object = object; - } -} - -impl Clone for BrpBuilder { - fn clone(&self) -> Self { - let object = unsafe { - let rc_impl = &mut *self.object; - rc_impl.interface.add_ref(); - rc_impl - }; - Self { - object, - frame: self.frame.clone(), - } - } -} - -impl ImplV8Handler for BrpBuilder { - fn execute( - &self, - _: Option<&CefString>, - _: Option<&mut V8Value>, - arguments: Option<&[Option]>, - ret: Option<&mut Option>, - _: Option<&mut CefString>, - ) -> c_int { - if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_BRP.into())) - && let Some(promise) = v8_value_create_promise() - { - if let Some(arguments_list) = process.argument_list() - && let Some(arguments) = arguments - && let Some(Some(arg)) = arguments.first() - && let Some(brp_request) = v8_value_to_json(arg) - && let Ok(brp_request) = serde_json::to_string(&brp_request) - && let Some(ret) = ret - { - let id = uuid::Uuid::new_v4().to_string(); - arguments_list.set_string(0, Some(&id.as_str().into())); - arguments_list.set_string(1, Some(&brp_request.as_str().into())); - self.frame.send_process_message( - ProcessId::from(cef_process_id_t::PID_BROWSER), - Some(&mut process), - ); - ret.replace(promise.clone()); - let mut promises = BRP_PROMISES.lock().unwrap(); - promises.insert(id, promise); - } else { - let mut exception = - v8_value_create_string(Some(&"Failed to execute BRP request".into())); - promise.resolve_promise(exception.as_mut()); - } - } - 1 - } - - fn init_methods(object: &mut _cef_v8_handler_t) { - init_methods::(object); - } - - fn get_raw(&self) -> *mut _cef_v8_handler_t { - self.object.cast() - } -} - -fn init_methods(object: &mut _cef_v8_handler_t) { - object.execute = Some(execute::); -} - -extern "C" fn execute( - self_: *mut _cef_v8_handler_t, - name: *const cef_string_t, - object: *mut _cef_v8_value_t, - arguments_count: usize, - arguments: *const *mut _cef_v8_value_t, - retval: *mut *mut _cef_v8_value_t, - exception: *mut cef_string_t, -) -> c_int { - let (arg_self_, arg_name, arg_object, arg_arguments_count, arg_arguments, _, arg_exception) = ( - self_, - name, - object, - arguments_count, - arguments, - retval, - exception, - ); - let arg_self_: &RcImpl<_, I> = RcImpl::get(arg_self_); - let arg_name = if arg_name.is_null() { - None - } else { - Some(arg_name.into()) - }; - let arg_name = arg_name.as_ref(); - let mut arg_object = - unsafe { arg_object.as_mut() }.map(|arg| (arg as *mut _cef_v8_value_t).wrap_result()); - let arg_object = arg_object.as_mut(); - let vec_arguments = unsafe { arg_arguments.as_ref() }.map(|arg| { - let arg = - unsafe { std::slice::from_raw_parts(std::ptr::from_ref(arg), arg_arguments_count) }; - arg.iter() - .map(|arg| { - if arg.is_null() { - None - } else { - Some((*arg).wrap_result()) - } - }) - .collect::>() - }); - let arg_arguments = vec_arguments.as_deref(); - let mut arg_retval: Option = None; - let arg = Some(&mut arg_retval); - let mut arg_exception = if arg_exception.is_null() { - None - } else { - Some(arg_exception.into()) - }; - let arg_exception = arg_exception.as_mut(); - let r = ImplV8Handler::execute( - &arg_self_.interface, - arg_name, - arg_object, - arg_arguments, - arg, - arg_exception, - ); - if let Some(ret) = arg_retval { - // When the result is received, the pointer should be updated here - // and the exception should also be updated. - unsafe { - write(retval, ret.into_raw()); - } - } - r -} diff --git a/crates/bevy_cef_core/src/render_process/cef_api_handler.rs b/crates/bevy_cef_core/src/render_process/cef_api_handler.rs new file mode 100644 index 0000000..f780e3b --- /dev/null +++ b/crates/bevy_cef_core/src/render_process/cef_api_handler.rs @@ -0,0 +1,166 @@ +use crate::prelude::{BRP_PROMISES, LISTEN_EVENTS, PROCESS_MESSAGE_BRP, PROCESS_MESSAGE_JS_EMIT}; +use crate::util::{IntoString, v8_value_to_json}; +use cef::rc::{Rc, RcImpl}; +use cef::{ + CefString, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Context, ImplV8Handler, + ImplV8Value, ProcessId, V8Value, WrapV8Handler, process_message_create, sys, + v8_context_get_current_context, v8_value_create_promise, v8_value_create_string, +}; +use cef_dll_sys::cef_process_id_t; +use std::os::raw::c_int; + +/// Handles the `window.cef` JavaScript API functions. +/// +/// This handler is registered as a CEF extension during `on_web_kit_initialized` +/// and provides three native functions: +/// - `__cef_brp`: Async Bevy Remote Protocol requests +/// - `__cef_emit`: Send events from JavaScript to Bevy +/// - `__cef_listen`: Register callbacks for events from Bevy +/// +/// The Frame is obtained dynamically via `v8_context_get_current_context().frame()` +/// since extensions are global and not bound to a specific context. +pub struct CefApiHandler { + object: *mut RcImpl, +} + +impl Default for CefApiHandler { + fn default() -> Self { + Self { + object: core::ptr::null_mut(), + } + } +} + +impl Rc for CefApiHandler { + fn as_base(&self) -> &sys::cef_base_ref_counted_t { + unsafe { + let base = &*self.object; + std::mem::transmute(&base.cef_object) + } + } +} + +impl WrapV8Handler for CefApiHandler { + fn wrap_rc(&mut self, object: *mut RcImpl) { + self.object = object; + } +} + +impl Clone for CefApiHandler { + fn clone(&self) -> Self { + let object = unsafe { + let rc_impl = &mut *self.object; + rc_impl.interface.add_ref(); + rc_impl + }; + Self { object } + } +} + +impl ImplV8Handler for CefApiHandler { + fn execute( + &self, + name: Option<&CefString>, + _object: Option<&mut V8Value>, + arguments: Option<&[Option]>, + ret: Option<&mut Option>, + _exception: Option<&mut CefString>, + ) -> c_int { + let Some(name) = name else { return 0 }; + let name_str = name.to_string(); + + match name_str.as_str() { + "__cef_brp" => self.execute_brp(arguments, ret), + "__cef_emit" => self.execute_emit(arguments), + "__cef_listen" => self.execute_listen(arguments), + _ => 0, + } + } + + #[inline] + fn get_raw(&self) -> *mut sys::_cef_v8_handler_t { + self.object.cast() + } +} + +impl CefApiHandler { + fn execute_brp( + &self, + arguments: Option<&[Option]>, + ret: Option<&mut Option>, + ) -> c_int { + let Some(context) = v8_context_get_current_context() else { + return 0; + }; + let Some(frame) = context.frame() else { + return 0; + }; + + if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_BRP.into())) + && let Some(promise) = v8_value_create_promise() + { + if let Some(arguments_list) = process.argument_list() + && let Some(arguments) = arguments + && let Some(Some(arg)) = arguments.first() + && let Some(brp_request) = v8_value_to_json(arg) + && let Ok(brp_request) = serde_json::to_string(&brp_request) + && let Some(ret) = ret + { + let id = uuid::Uuid::new_v4().to_string(); + arguments_list.set_string(0, Some(&id.as_str().into())); + arguments_list.set_string(1, Some(&brp_request.as_str().into())); + frame.send_process_message( + ProcessId::from(cef_process_id_t::PID_BROWSER), + Some(&mut process), + ); + ret.replace(promise.clone()); + let mut promises = BRP_PROMISES.lock().unwrap(); + promises.insert(id, promise); + } else { + let mut exception = + v8_value_create_string(Some(&"Failed to execute BRP request".into())); + promise.resolve_promise(exception.as_mut()); + } + } + 1 + } + + fn execute_emit(&self, arguments: Option<&[Option]>) -> c_int { + let Some(context) = v8_context_get_current_context() else { + return 0; + }; + let Some(frame) = context.frame() else { + return 0; + }; + + if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_JS_EMIT.into())) + && let Some(arguments_list) = process.argument_list() + && let Some(arguments) = arguments + && let Some(Some(arg)) = arguments.first() + && let Some(arg) = v8_value_to_json(arg) + && let Ok(arg) = serde_json::to_string(&arg) + { + arguments_list.set_string(0, Some(&arg.as_str().into())); + frame.send_process_message( + ProcessId::from(cef_process_id_t::PID_BROWSER), + Some(&mut process), + ); + } + 1 + } + + fn execute_listen(&self, arguments: Option<&[Option]>) -> c_int { + if let Some(arguments) = arguments + && let Some(Some(id)) = arguments.first() + && id.is_string().is_positive() + && let Some(Some(callback)) = arguments.get(1) + && callback.is_function().is_positive() + { + LISTEN_EVENTS + .lock() + .unwrap() + .insert(id.string_value().into_string(), callback.clone()); + } + 1 + } +} diff --git a/crates/bevy_cef_core/src/render_process/emit.rs b/crates/bevy_cef_core/src/render_process/emit.rs deleted file mode 100644 index 6f55f05..0000000 --- a/crates/bevy_cef_core/src/render_process/emit.rs +++ /dev/null @@ -1,82 +0,0 @@ -use crate::prelude::PROCESS_MESSAGE_JS_EMIT; -use crate::util::v8_value_to_json; -use cef::rc::{Rc, RcImpl}; -use cef::{ - CefString, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ImplV8Handler, ProcessId, - V8Handler, V8Value, WrapV8Handler, process_message_create, sys, -}; -use cef_dll_sys::cef_process_id_t; -use std::os::raw::c_int; - -pub struct EmitBuilder { - object: *mut RcImpl, - frame: Frame, -} - -impl EmitBuilder { - pub fn build(frame: Frame) -> V8Handler { - V8Handler::new(Self { - object: core::ptr::null_mut(), - frame, - }) - } -} - -impl Rc for EmitBuilder { - fn as_base(&self) -> &sys::cef_base_ref_counted_t { - unsafe { - let base = &*self.object; - std::mem::transmute(&base.cef_object) - } - } -} - -impl WrapV8Handler for EmitBuilder { - fn wrap_rc(&mut self, object: *mut RcImpl) { - self.object = object; - } -} - -impl Clone for EmitBuilder { - fn clone(&self) -> Self { - let object = unsafe { - let rc_impl = &mut *self.object; - rc_impl.interface.add_ref(); - rc_impl - }; - Self { - object, - frame: self.frame.clone(), - } - } -} - -impl ImplV8Handler for EmitBuilder { - fn execute( - &self, - _: Option<&CefString>, - _: Option<&mut V8Value>, - arguments: Option<&[Option]>, - _: Option<&mut Option>, - _: Option<&mut CefString>, - ) -> c_int { - if let Some(mut process) = process_message_create(Some(&PROCESS_MESSAGE_JS_EMIT.into())) - && let Some(arguments_list) = process.argument_list() - && let Some(arguments) = arguments - && let Some(Some(arg)) = arguments.first() - && let Some(arg) = v8_value_to_json(arg) - && let Ok(arg) = serde_json::to_string(&arg) - { - arguments_list.set_string(0, Some(&arg.as_str().into())); - self.frame.send_process_message( - ProcessId::from(cef_process_id_t::PID_BROWSER), - Some(&mut process), - ); - } - 1 - } - - fn get_raw(&self) -> *mut sys::_cef_v8_handler_t { - self.object.cast() - } -} diff --git a/crates/bevy_cef_core/src/render_process/listen.rs b/crates/bevy_cef_core/src/render_process/listen.rs deleted file mode 100644 index d744478..0000000 --- a/crates/bevy_cef_core/src/render_process/listen.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::prelude::LISTEN_EVENTS; -use crate::util::IntoString; -use cef::rc::{Rc, RcImpl}; -use cef::{CefString, Frame, ImplV8Handler, ImplV8Value, V8Handler, V8Value, WrapV8Handler, sys}; -use std::os::raw::c_int; - -pub struct ListenBuilder { - object: *mut RcImpl, - frame: Frame, -} - -impl ListenBuilder { - pub fn build(frame: Frame) -> V8Handler { - V8Handler::new(Self { - object: core::ptr::null_mut(), - frame, - }) - } -} - -impl Rc for ListenBuilder { - fn as_base(&self) -> &sys::cef_base_ref_counted_t { - unsafe { - let base = &*self.object; - std::mem::transmute(&base.cef_object) - } - } -} - -impl WrapV8Handler for ListenBuilder { - fn wrap_rc(&mut self, object: *mut RcImpl) { - self.object = object; - } -} - -impl Clone for ListenBuilder { - fn clone(&self) -> Self { - let object = unsafe { - let rc_impl = &mut *self.object; - rc_impl.interface.add_ref(); - rc_impl - }; - Self { - object, - frame: self.frame.clone(), - } - } -} - -impl ImplV8Handler for ListenBuilder { - fn execute( - &self, - _: Option<&CefString>, - _: Option<&mut V8Value>, - arguments: Option<&[Option]>, - _: Option<&mut Option>, - _: Option<&mut CefString>, - ) -> c_int { - if let Some(arguments) = arguments - && let Some(Some(id)) = arguments.first() - && 0 < id.is_string() - && let Some(Some(callback)) = arguments.get(1) - && 0 < callback.is_function() - { - LISTEN_EVENTS - .lock() - .unwrap() - .insert(id.string_value().into_string(), callback.clone()); - } - 1 - } - - #[inline] - fn get_raw(&self) -> *mut sys::_cef_v8_handler_t { - self.object.cast() - } -} 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 4b98259..38b91fe 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 @@ -1,6 +1,5 @@ -use crate::prelude::{EmitBuilder, IntoString}; -use crate::render_process::brp::BrpBuilder; -use crate::render_process::listen::ListenBuilder; +use crate::prelude::{EXTENSIONS_SWITCH, IntoString}; +use crate::render_process::cef_api_handler::CefApiHandler; use crate::util::json_to_v8; use crate::util::v8_accessor::V8DefaultAccessorBuilder; use crate::util::v8_interceptor::V8DefaultInterceptorBuilder; @@ -8,14 +7,30 @@ use bevy::platform::collections::HashMap; use bevy_remote::BrpResult; use cef::rc::{Rc, RcImpl}; use cef::{ - 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, + Browser, CefString, DictionaryValue, Frame, ImplBrowser, ImplCommandLine, ImplDictionaryValue, + ImplFrame, ImplListValue, ImplProcessMessage, ImplRenderProcessHandler, ImplV8Context, + ImplV8Value, ProcessId, ProcessMessage, V8Context, V8Handler, V8Value, + WrapRenderProcessHandler, command_line_get_global, register_extension, sys, + v8_value_create_object, }; +use std::collections::HashMap as StdHashMap; use std::os::raw::c_int; use std::sync::Mutex; +const CEF_API_EXTENSION_NAME: &str = "v8/bevy-cef-api"; +const CEF_API_EXTENSION_CODE: &str = r#" +var cef; +if (!cef) cef = {}; +(function() { + native function __cef_brp(); + native function __cef_emit(); + native function __cef_listen(); + cef.brp = __cef_brp; + cef.emit = __cef_emit; + cef.listen = __cef_listen; +})(); +"#; + pub(crate) static BRP_PROMISES: Mutex> = Mutex::new(HashMap::new()); pub(crate) static LISTEN_EVENTS: Mutex> = Mutex::new(HashMap::new()); @@ -65,6 +80,11 @@ impl Clone for RenderProcessHandlerBuilder { } impl ImplRenderProcessHandler for RenderProcessHandlerBuilder { + fn on_web_kit_initialized(&self) { + register_cef_api_extension(); + register_extensions_from_command_line(); + } + fn on_browser_created( &self, browser: Option<&mut Browser>, @@ -91,7 +111,6 @@ impl ImplRenderProcessHandler for RenderProcessHandlerBuilder { && let Some(browser) = browser { inject_initialize_scripts(browser, context, frame); - inject_cef_api(context, frame); } } @@ -137,46 +156,12 @@ fn inject_initialize_scripts(browser: &mut Browser, context: &mut V8Context, fra } } -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 register_cef_api_extension() { + register_extension( + Some(&CEF_API_EXTENSION_NAME.into()), + Some(&CEF_API_EXTENSION_CODE.into()), + Some(&mut V8Handler::new(CefApiHandler::default())), + ); } fn handle_brp_message(message: &ProcessMessage, ctx: V8Context) { @@ -229,3 +214,32 @@ fn handle_listen_message(message: &ProcessMessage, mut ctx: V8Context) { } ctx.exit(); } + +fn register_extensions_from_command_line() { + let Some(cmd_line) = command_line_get_global() else { + return; + }; + if cmd_line.has_switch(Some(&EXTENSIONS_SWITCH.into())) == 0 { + return; + } + let json = cmd_line + .switch_value(Some(&EXTENSIONS_SWITCH.into())) + .into_string(); + if json.is_empty() { + return; + } + + let Ok(extensions) = serde_json::from_str::>(&json) else { + eprintln!("bevy_cef: failed to parse extensions JSON: {}", json); + return; + }; + + for (name, code) in extensions { + let full_name = format!("v8/{}", name); + register_extension( + Some(&full_name.as_str().into()), + Some(&code.as_str().into()), + None, + ); + } +} diff --git a/crates/bevy_cef_core/src/util.rs b/crates/bevy_cef_core/src/util.rs index 2b8298d..2a2d090 100644 --- a/crates/bevy_cef_core/src/util.rs +++ b/crates/bevy_cef_core/src/util.rs @@ -18,6 +18,8 @@ use cef_dll_sys::cef_scheme_options_t::{ use std::env::home_dir; use std::path::PathBuf; +pub const EXTENSIONS_SWITCH: &str = "bevy-cef-extensions"; + pub const SCHEME_CEF: &str = "cef"; pub const HOST_CEF: &str = "localhost"; diff --git a/examples/extensions.rs b/examples/extensions.rs new file mode 100644 index 0000000..5bb69ef --- /dev/null +++ b/examples/extensions.rs @@ -0,0 +1,71 @@ +//! Example demonstrating custom JavaScript extensions via CEF's register_extension. +//! +//! This example shows how to create global JavaScript APIs that are available +//! in all webviews before any page scripts run. + +use bevy::prelude::*; +use bevy_cef::prelude::*; +use serde::Deserialize; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins, + CefPlugin { + extensions: CefExtensions::new().add( + "myGame", + r#" + var myGame = { + version: "1.0.0", + sendScore: function(score) { + window.cef.emit('score_update', { score: score }); + } + }; + "#, + ), + ..Default::default() + }, + JsEmitEventPlugin::::default(), + )) + .add_systems( + Startup, + (spawn_camera, spawn_directional_light, spawn_webview), + ) + .add_observer(on_score_update) + .run(); +} + +#[derive(Deserialize, Debug)] +struct ScoreUpdate { + score: u32, +} + +fn on_score_update(trigger: On>) { + info!("Received score update: {:?}", trigger.score); +} + +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::local("extensions.html"), + Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))), + MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())), + )); +} diff --git a/src/common/message_loop.rs b/src/common/message_loop.rs index ee3e257..5fbaa26 100644 --- a/src/common/message_loop.rs +++ b/src/common/message_loop.rs @@ -10,6 +10,7 @@ use cef::{Settings, api_hash, execute_process, initialize, shutdown, sys}; /// - macOS: Calls [`CefDoMessageLoopWork`](https://cef-builds.spotifycdn.com/docs/106.1/cef__app_8h.html#a830ae43dcdffcf4e719540204cefdb61) every frame. pub struct MessageLoopPlugin { pub config: CommandLineConfig, + pub extensions: CefExtensions, } impl Plugin for MessageLoopPlugin { @@ -21,7 +22,8 @@ impl Plugin for MessageLoopPlugin { let args = Args::new(); let (tx, rx) = std::sync::mpsc::channel(); - let mut cef_app = BrowserProcessAppBuilder::build(tx, self.config.clone()); + let mut cef_app = + BrowserProcessAppBuilder::build(tx, self.config.clone(), self.extensions.clone()); let ret = execute_process( Some(args.as_main_args()), Some(&mut cef_app), diff --git a/src/lib.rs b/src/lib.rs index 4a7b39f..8051034 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,12 +16,12 @@ use crate::mute::AudioMutePlugin; use crate::prelude::{IpcPlugin, NavigationPlugin, WebviewPlugin}; use crate::zoom::ZoomPlugin; use bevy::prelude::*; -use bevy_cef_core::prelude::CommandLineConfig; +use bevy_cef_core::prelude::{CefExtensions, CommandLineConfig}; use bevy_remote::RemotePlugin; pub mod prelude { pub use crate::{CefPlugin, RunOnMainThread, common::*, navigation::*, webview::prelude::*}; - pub use bevy_cef_core::prelude::CommandLineConfig; + pub use bevy_cef_core::prelude::{CefExtensions, CommandLineConfig}; } pub struct RunOnMainThread; @@ -29,6 +29,7 @@ pub struct RunOnMainThread; #[derive(Debug, Default)] pub struct CefPlugin { pub command_line_config: CommandLineConfig, + pub extensions: CefExtensions, } impl Plugin for CefPlugin { @@ -37,6 +38,7 @@ impl Plugin for CefPlugin { LocalHostPlugin, MessageLoopPlugin { config: self.command_line_config.clone(), + extensions: self.extensions.clone(), }, WebviewCoreComponentsPlugin, WebviewPlugin,