feat: add custom CEF V8 extensions support (#17)
* feat: Register extensions in render process - Add CefExtensions type to hold V8 extension code - Pass extensions through BrowserProcessAppBuilder - Register extensions in RenderProcessHandler on WebKit initialization - Decode JSON extensions from command line switch - Prefix extension names with "v8/" per CEF convention - Include actual JSON in error messages for debugging l * feat: refactor window.cef API and register as CEF extension * fix: remove debug print statements in render process handler * refactor: centralize EXTENSIONS_SWITCH constant in util.rs * fmt * refactor: implement Default trait for CefApiHandler * docs: add documentation for CefApiHandler and its JavaScript API functions --------- Co-authored-by: not-elm <elmgameinfo@gmail.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ target/
|
||||
book/
|
||||
.DS_Store**/.DS_Store
|
||||
.claude/memo.md
|
||||
docs/plans/
|
||||
docs/plans/
|
||||
.worktrees/
|
||||
@@ -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
|
||||
|
||||
|
||||
100
CLAUDE.md
100
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<String> 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<T>`
|
||||
2. **Host Emit**: Bevy app → Webview via event emission
|
||||
3. **BRP (Bevy Remote Protocol)**: Bidirectional RPC calls
|
||||
1. **JS Emit**: Webview → Bevy via `JsEmitEventPlugin<E>` where E: `DeserializeOwned + Send + Sync + 'static`
|
||||
- Events wrapped in `Receive<E>` 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
|
||||
- `crates/bevy_cef_debug_render_process`: Debug render process executable
|
||||
39
assets/extensions.html
Normal file
39
assets/extensions.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CEF Extensions Example</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 20px; background: #1a1a2e; color: #eee; }
|
||||
button { padding: 10px 20px; margin: 5px; cursor: pointer; }
|
||||
#output { margin-top: 20px; padding: 10px; background: #16213e; border-radius: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>CEF Extensions Example</h1>
|
||||
<p>Testing custom JavaScript extensions registered via register_extension.</p>
|
||||
|
||||
<button onclick="testExtension()">Test myGame.sendScore()</button>
|
||||
<button onclick="checkVersion()">Check Version</button>
|
||||
|
||||
<div id="output">Output will appear here...</div>
|
||||
|
||||
<script>
|
||||
function testExtension() {
|
||||
if (typeof myGame !== 'undefined') {
|
||||
myGame.sendScore(Math.floor(Math.random() * 100));
|
||||
document.getElementById('output').innerHTML = 'Score sent! Check Bevy console.';
|
||||
} else {
|
||||
document.getElementById('output').innerHTML = 'Error: myGame extension not found';
|
||||
}
|
||||
}
|
||||
|
||||
function checkVersion() {
|
||||
if (typeof myGame !== 'undefined' && myGame.version) {
|
||||
document.getElementById('output').innerHTML = 'myGame version: ' + myGame.version;
|
||||
} else {
|
||||
document.getElementById('output').innerHTML = 'Error: myGame.version not found';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -15,7 +15,7 @@
|
||||
<h1 id="count" style="color: aqua">0</h1>
|
||||
<script>
|
||||
const count = document.getElementById("count");
|
||||
window.cef.listen("count", (payload) => {
|
||||
cef.listen("count", (payload) => {
|
||||
count.innerText = payload;
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let count = 0;
|
||||
window.setInterval(() => {
|
||||
console.log("Emitting count:", count);
|
||||
window.cef.emit({
|
||||
cef.emit({
|
||||
count,
|
||||
});
|
||||
countElement.innerText = count;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<MessageLoopTimer>,
|
||||
config: CommandLineConfig,
|
||||
extensions: CefExtensions,
|
||||
}
|
||||
|
||||
impl BrowserProcessAppBuilder {
|
||||
pub fn build(
|
||||
message_loop_working_requester: Sender<MessageLoopTimer>,
|
||||
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<BrowserProcessHandler> {
|
||||
Some(BrowserProcessHandlerBuilder::build(
|
||||
self.message_loop_working_requester.clone(),
|
||||
self.extensions.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@@ -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<cef_dll_sys::cef_browser_process_handler_t, Self>,
|
||||
message_loop_working_requester: Sender<MessageLoopTimer>,
|
||||
extensions: CefExtensions,
|
||||
}
|
||||
|
||||
impl BrowserProcessHandlerBuilder {
|
||||
pub fn build(
|
||||
message_loop_working_requester: Sender<MessageLoopTimer>,
|
||||
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) {
|
||||
|
||||
47
crates/bevy_cef_core/src/browser_process/extensions.rs
Normal file
47
crates/bevy_cef_core/src/browser_process/extensions.rs
Normal file
@@ -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<String, String>);
|
||||
|
||||
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<String>, code: impl Into<String>) -> 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()
|
||||
}
|
||||
}
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <T>(request: BrpRequest) -> Promise<T>`.
|
||||
///
|
||||
/// 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<V8Value>]>,
|
||||
ret: Option<&mut Option<V8Value>>,
|
||||
_: 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::<Self>(object);
|
||||
}
|
||||
|
||||
fn get_raw(&self) -> *mut _cef_v8_handler_t {
|
||||
self.object.cast()
|
||||
}
|
||||
}
|
||||
|
||||
fn init_methods<I: ImplV8Handler>(object: &mut _cef_v8_handler_t) {
|
||||
object.execute = Some(execute::<I>);
|
||||
}
|
||||
|
||||
extern "C" fn execute<I: ImplV8Handler>(
|
||||
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::<Vec<_>>()
|
||||
});
|
||||
let arg_arguments = vec_arguments.as_deref();
|
||||
let mut arg_retval: Option<V8Value> = 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
|
||||
}
|
||||
166
crates/bevy_cef_core/src/render_process/cef_api_handler.rs
Normal file
166
crates/bevy_cef_core/src/render_process/cef_api_handler.rs
Normal file
@@ -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<sys::_cef_v8_handler_t, Self>,
|
||||
}
|
||||
|
||||
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<sys::_cef_v8_handler_t, Self>) {
|
||||
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<V8Value>]>,
|
||||
ret: Option<&mut Option<V8Value>>,
|
||||
_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<V8Value>]>,
|
||||
ret: Option<&mut Option<V8Value>>,
|
||||
) -> 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<V8Value>]>) -> 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<V8Value>]>) -> 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
|
||||
}
|
||||
}
|
||||
@@ -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<sys::_cef_v8_handler_t, Self>,
|
||||
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<sys::_cef_v8_handler_t, Self>) {
|
||||
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<V8Value>]>,
|
||||
_: Option<&mut Option<V8Value>>,
|
||||
_: 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()
|
||||
}
|
||||
}
|
||||
@@ -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<sys::_cef_v8_handler_t, Self>,
|
||||
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<sys::_cef_v8_handler_t, Self>) {
|
||||
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<V8Value>]>,
|
||||
_: Option<&mut Option<V8Value>>,
|
||||
_: 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()
|
||||
}
|
||||
}
|
||||
@@ -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<HashMap<String, V8Value>> = Mutex::new(HashMap::new());
|
||||
pub(crate) static LISTEN_EVENTS: Mutex<HashMap<String, V8Value>> = 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::<StdHashMap<String, String>>(&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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
71
examples/extensions.rs
Normal file
71
examples/extensions.rs
Normal file
@@ -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::<ScoreUpdate>::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<Receive<ScoreUpdate>>) {
|
||||
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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
|
||||
) {
|
||||
commands.spawn((
|
||||
CefWebviewUri::local("extensions.html"),
|
||||
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
|
||||
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
|
||||
));
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user