This commit is contained in:
not-elm
2025-08-10 21:28:45 +09:00
commit 23bdc65da3
91 changed files with 20122 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
---
allowed-tools: Bash(cargo test:*),
description: Create new tests
---
1. Reading $ARGUMENTS and understanding the implementation details.
2. Creating new tests for $ARGUMENTS in `tests` module in the same file.
3. Run `cargo test --workspace --all-features` to ensure all tests pass.
4. If any tests fail, fix the issues and re-run the tests.
## Contracts
- You have to write the rust-doc that describes the test case for each test function.
以下の手順で問題を修正してください。
## 現在発生している問題
以下のようなHTMLにCEFのカスタムリソースハンドラを使って`cef://localhost/brp.html`経由でアクセスしています。
このHTMLからvideoリソースを読み込むと、`cef://localhost/test.mov`というリクエストURLでローカルResourceHandlerのopenメソッドが呼ばれ、response_headers, readメソッドでヘッダーとレスポンスボディが返されることが期待されます。
初回と回目までのreadメソッドは呼ばれるのですが、回目以降のreadメソッドが呼ばれず、何度もopenメソッドが呼ばれてしまっているため、正常にレスポンスが返却できるように修正してください。
```htm
<html>
<body>
<video controls>
<source src="test.mov">
</video>
</body>
</html>
```
## 手順
1. [CEFのResourceHandler](https://cef-builds.spotifycdn.com/docs/122.0/classCefResourceHandler.html)の仕様を深く読み込み理解する
2. `crates/bevy_cef_core/src/browser_process/localhost.rs`以下にResourceHandlerを使ってローカルリソースを読み込むコードが書いています。深く読み込んで現状の実装を理解してください。
3. openメソッドが複数回呼ばれ、readメソッドが回目以降呼ばれない原因を調査する
4. 原因を特定し、修正する

7
.claude/settings.json Normal file
View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": ["WebFetch", "WebSearch"],
"deny": ["Read(./.env)", "Read(./secrets/**)"]
}
}

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
target/
.idea/
book/

102
CLAUDE.md Normal file
View File

@@ -0,0 +1,102 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is `bevy_cef`, a Bevy plugin that integrates the Chromium Embedded Framework (CEF) into Bevy applications, allowing webviews to be rendered as 3D objects in the game world or as UI overlays.
## Architecture
### 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`)
- 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
### Plugin Architecture
The main `CefPlugin` orchestrates several sub-plugins:
- `LocalHostPlugin`: Serves local assets via custom scheme
- `MessageLoopPlugin`: CEF message loop integration
- `WebviewCoreComponentsPlugin`: Core component registration
- `WebviewPlugin`: Main webview management
- `IpcPlugin`: Inter-process communication
- `KeyboardPlugin`, `NavigationPlugin`, `ZoomPlugin`, `AudioMutePlugin`: Feature-specific functionality
### 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
## 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`
### 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
## Local Asset Loading
Local HTML/assets are served via the custom `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`
## 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
## 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

6276
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

56
Cargo.toml Normal file
View File

@@ -0,0 +1,56 @@
[package]
name = "bevy_cef"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[workspace]
resolver = "2"
members = [
"crates/*",
"examples/demo",
]
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "Apache-2.0 OR MIT"
authors = ["notelm"]
repository = "https://github.com/not-elm/bevy_cef"
[workspace.dependencies]
bevy = { version = "0.16" }
bevy_remote = "0.16"
cef = { version = "138" }
cef-dll-sys = { version = "138", features = ["sandbox"] }
download-cef = { version = "2" }
bevy_cef = { path = "." }
bevy_cef_core = { path = "crates/bevy_cef_core" }
async-channel = { version = "2.5" }
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }
raw-window-handle = "0.6"
[dependencies]
bevy = { workspace = true }
bevy_remote = { workspace = true }
cef = { workspace = true }
bevy_cef_core = { workspace = true }
async-channel = { version = "2.5" }
serde = { workspace = true }
serde_json = { workspace = true }
raw-window-handle = "0.6"
[dev-dependencies]
bevy = { workspace = true, features = ["file_watcher"]}
bevy_cef = { workspace = true, features = ["debug"] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = { version = "0.2" }
[features]
default = []
serialize = ["bevy/serialize"]
debug = ["bevy_cef_core/debug"]

6
Makefile Normal file
View File

@@ -0,0 +1,6 @@
fix:
cargo clippy --fix --allow-dirty --allow-staged --workspace --all --all-features
cargo fmt --all
install:
cargo install --path ./crates/bevy_cef_debug_render_process --force

35
assets/brp.html Normal file
View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body style="color: white">
<h1>HTML Path: assets/brp.html</h1>
<p></p>
<input id="name" type="text" placeholder="Enter your name" />
<button id="greet" >Greet</button>
<p id="reply"></p>
<script>
const nameInput = document.getElementById("name");
const greetButton = document.getElementById("greet");
const reply = document.getElementById("reply");
greetButton.addEventListener("click", async () => {
const name = nameInput.value;
if (name) {
try{
reply.innerText = await window.cef.brp({
jsonrpc: "2.0",
method: "greet",
params: name,
});
}catch (e){
console.error("Error calling greet method:", e);
reply.innerText = "Error: " + e.message;
}
}
});
</script>
</body>
</html>

23
assets/host_emit.html Normal file
View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Host Emit</title>
</head>
<body style="color: white">
<h1>HTML Path: assets/host_emit.html</h1>
<p>
This example demonstrates how to receive events from the application.
</p>
<p>
The application emits a count value every second, which is then displayed in the HTML.
</p>
<h1 id="count" style="color: aqua">0</h1>
<script>
const count = document.getElementById("count");
window.cef.listen("count", (payload) => {
count.innerText = payload;
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

28
assets/js_emit.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JS Emit</title>
</head>
<body style=" color: white">
<h1>HTML Path: assets/js_emit.html</h1>
<p>The example sends events from JavaScript to the application.</p>
<p>
It emits a count value every second, which is then sent to the application and logged in the console.
</p>
<h1 id="count" style="color: aqua">0</h1>
<script>
const countElement = document.getElementById("count");
let count = 0;
window.setInterval(() => {
console.log("Emitting count:", count);
window.cef.emit({
count,
});
countElement.innerText = count;
count += 1;
}, 1000)
</script>
</body>
</html>

View File

@@ -0,0 +1,20 @@
#import bevy_pbr::{
forward_io::VertexOutput,
}
#import webview::util::{
surface_color,
}
@group(2) @binding(0) var mask_texture: texture_2d<f32>;
@group(2) @binding(1) var mask_sampler: sampler;
@fragment
fn fragment(
in: VertexOutput,
) -> @location(0) vec4<f32> {
// You can obtain the surface color.
var color = surface_color(in.uv);
// Blend the color with the mask texture.
color *= (textureSample(mask_texture, mask_sampler, in.uv) * vec4(vec3(1.0), 0.3));
return color;
}

59
build.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::env::home_dir;
use std::process::Command;
fn main() -> std::io::Result<()> {
println!("cargo::rerun-if-changed=build.rs");
#[cfg(all(target_os = "macos", feature = "debug"))]
{
install_bevy_cef_debug_render_process()?;
install_export_cef_dir()?;
export_cef_dir()?;
}
Ok(())
}
fn install_bevy_cef_debug_render_process() -> std::io::Result<()> {
let bevy_cef_render_process_path = home_dir()
.unwrap()
.join(".cargo")
.join("bin")
.join("bevy_cef_debug_render_process");
if !bevy_cef_render_process_path.exists() {
Command::new("cargo")
.args(["install", "bevy_cef_debug_render_process"])
.spawn()?;
}
Ok(())
}
fn install_export_cef_dir() -> std::io::Result<()> {
let export_cef_dir_path = home_dir()
.unwrap()
.join(".cargo")
.join("bin")
.join("export-cef-dir");
if !export_cef_dir_path.exists() {
Command::new("cargo")
.args(["install", "export-cef-dir"])
.spawn()?;
}
Ok(())
}
fn export_cef_dir() -> std::io::Result<()> {
let cef_dir = home_dir().unwrap().join(".local").join("share").join("cef");
if cef_dir.exists() {
return Ok(());
}
let export_cef_dir_path = home_dir()
.unwrap()
.join(".cargo")
.join("bin")
.join("export-cef-dir");
Command::new(export_cef_dir_path)
.arg("--force")
.arg(cef_dir)
.spawn()?
.wait()?;
Ok(())
}

View File

@@ -0,0 +1,22 @@
[package]
name = "bevy_cef_core"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
bevy = { workspace = true }
bevy_remote = { workspace = true }
uuid = { version = "1" }
cef = { workspace = true }
cef-dll-sys = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
async-channel = { workspace = true }
raw-window-handle = { workspace = true }
[features]
default = []
debug = []

View File

@@ -0,0 +1,18 @@
mod app;
mod browser_process_handler;
mod browsers;
mod client_handler;
mod context_menu_handler;
mod display_handler;
mod localhost;
mod renderer_handler;
mod request_context_handler;
pub use app::*;
pub use browser_process_handler::*;
pub use browsers::*;
pub use client_handler::*;
pub use context_menu_handler::*;
pub use localhost::*;
pub use renderer_handler::*;
pub use request_context_handler::*;

View File

@@ -0,0 +1,85 @@
use crate::browser_process::browser_process_handler::BrowserProcessHandlerBuilder;
use crate::util::{SCHEME_CEF, cef_scheme_flags};
use cef::rc::{Rc, RcImpl};
use cef::{
BrowserProcessHandler, CefString, CommandLine, ImplApp, ImplCommandLine, ImplSchemeRegistrar,
SchemeRegistrar, WrapApp,
};
use cef_dll_sys::{_cef_app_t, cef_base_ref_counted_t};
/// ## Reference
///
/// - [`CefApp Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefApp.html)
#[derive(Default)]
pub struct BrowserProcessAppBuilder {
object: *mut RcImpl<_cef_app_t, Self>,
}
impl BrowserProcessAppBuilder {
pub fn build() -> cef::App {
cef::App::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Clone for BrowserProcessAppBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
self.object
};
Self { object }
}
}
impl Rc for BrowserProcessAppBuilder {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplApp for BrowserProcessAppBuilder {
fn on_before_command_line_processing(
&self,
_: Option<&CefString>,
command_line: Option<&mut CommandLine>,
) {
let Some(command_line) = command_line else {
return;
};
//TODO: フラグで切り替えるようにする
command_line.append_switch(Some(&"use-mock-keychain".into()));
#[cfg(feature = "debug")]
{
command_line.append_switch(Some(&"disable-gpu".into()));
command_line.append_switch(Some(&"disable-gpu-compositing".into()));
command_line.append_switch(Some(&"disable-software-rasterizer".into()));
}
}
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(BrowserProcessHandlerBuilder::build())
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
registrar.add_custom_scheme(Some(&SCHEME_CEF.into()), cef_scheme_flags() as _);
}
}
#[inline]
fn get_raw(&self) -> *mut _cef_app_t {
self.object as *mut cef::sys::_cef_app_t
}
}
impl WrapApp for BrowserProcessAppBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,63 @@
use cef::rc::{Rc, RcImpl};
use cef::*;
/// ## Reference
///
/// - [`CefBrowserProcessHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefBrowserProcessHandler.html)
pub struct BrowserProcessHandlerBuilder {
object: *mut RcImpl<cef_dll_sys::cef_browser_process_handler_t, Self>,
}
impl BrowserProcessHandlerBuilder {
pub fn build() -> BrowserProcessHandler {
BrowserProcessHandler::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Rc for BrowserProcessHandlerBuilder {
fn as_base(&self) -> &cef_dll_sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapBrowserProcessHandler for BrowserProcessHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<cef_dll_sys::_cef_browser_process_handler_t, Self>) {
self.object = object;
}
}
impl Clone for BrowserProcessHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplBrowserProcessHandler for BrowserProcessHandlerBuilder {
fn on_before_child_process_launch(&self, command_line: Option<&mut CommandLine>) {
let Some(command_line) = command_line else {
return;
};
command_line.append_switch(Some(&"disable-web-security".into()));
command_line.append_switch(Some(&"allow-running-insecure-content".into()));
command_line.append_switch(Some(&"disable-session-crashed-bubble".into()));
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()));
}
#[inline]
fn get_raw(&self) -> *mut cef_dll_sys::_cef_browser_process_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,492 @@
use crate::browser_process::BrpHandler;
use crate::browser_process::ClientHandlerBuilder;
use crate::browser_process::client_handler::{IpcEventRaw, JsEmitEventHandler};
use crate::prelude::IntoString;
use crate::prelude::*;
use async_channel::{Sender, TryRecvError};
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,
};
use cef_dll_sys::{cef_event_flags_t, cef_mouse_button_type_t};
#[allow(deprecated)]
use raw_window_handle::RawWindowHandle;
use std::cell::Cell;
use std::rc::Rc;
mod devtool_render_handler;
mod keyboard;
use crate::browser_process::browsers::devtool_render_handler::DevToolRenderHandlerBuilder;
use crate::browser_process::display_handler::{DisplayHandlerBuilder, SystemCursorIconSenderInner};
pub use keyboard::*;
pub struct WebviewBrowser {
pub client: Browser,
pub host: BrowserHost,
pub size: SharedViewSize,
}
pub struct Browsers {
browsers: HashMap<Entity, WebviewBrowser>,
sender: TextureSender,
receiver: TextureReceiver,
ime_caret: SharedImeCaret,
}
impl Default for Browsers {
fn default() -> Self {
let (sender, receiver) = async_channel::unbounded::<RenderTexture>();
Browsers {
browsers: HashMap::default(),
sender,
receiver,
ime_caret: Rc::new(Cell::new(0)),
}
}
}
impl Browsers {
#[allow(clippy::too_many_arguments)]
pub fn create_browser(
&mut self,
webview: Entity,
uri: &str,
webview_size: Vec2,
requester: Requester,
ipc_event_sender: Sender<IpcEventRaw>,
brp_sender: Sender<BrpMessage>,
system_cursor_icon_sender: SystemCursorIconSenderInner,
window_handle: Option<RawWindowHandle>,
) {
let mut context = Self::request_context(requester);
let size = Rc::new(Cell::new(webview_size));
let browser = browser_host_create_browser_sync(
Some(&WindowInfo {
windowless_rendering_enabled: true as _,
external_begin_frame_enabled: true as _,
parent_view: match window_handle {
Some(RawWindowHandle::AppKit(handle)) => handle.ns_view.as_ptr(),
Some(RawWindowHandle::Win32(handle)) => handle.hwnd.get() as _,
Some(RawWindowHandle::Xlib(handle)) => handle.window as _,
Some(RawWindowHandle::Wayland(handle)) => handle.surface.as_ptr(),
_ => std::ptr::null_mut(),
},
// shared_texture_enabled: true as _,
..Default::default()
}),
Some(&mut self.client_handler(
webview,
size.clone(),
ipc_event_sender,
brp_sender,
system_cursor_icon_sender,
)),
Some(&uri.into()),
Some(&BrowserSettings {
windowless_frame_rate: 60,
..Default::default()
}),
None,
context.as_mut(),
)
.expect("Failed to create browser");
self.browsers.insert(
webview,
WebviewBrowser {
host: browser.host().expect("Failed to get browser host"),
client: browser,
size,
},
);
}
pub fn send_external_begin_frame(&mut self) {
for browser in self.browsers.values_mut() {
browser.host.send_external_begin_frame();
}
}
pub fn send_mouse_move<'a>(
&self,
webview: &Entity,
buttons: impl IntoIterator<Item = &'a MouseButton>,
position: Vec2,
mouse_leave: bool,
) {
if let Some(browser) = self.get_focused_browser(webview) {
let mouse_event = cef::MouseEvent {
x: position.x as i32,
y: position.y as i32,
modifiers: modifiers_from_mouse_buttons(buttons),
};
browser
.host
.send_mouse_move_event(Some(&mouse_event), mouse_leave as _);
}
}
pub fn send_mouse_click(
&self,
webview: &Entity,
position: Vec2,
button: PointerButton,
mouse_up: bool,
) {
if let Some(browser) = self.get_focused_browser(webview) {
let mouse_event = cef::MouseEvent {
x: position.x as i32,
y: position.y as i32,
modifiers: match button {
PointerButton::Primary => cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON,
PointerButton::Secondary => cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON,
PointerButton::Middle => cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON,
} as _, // No modifiers for simplicity
};
let mouse_button = match button {
PointerButton::Secondary => cef_mouse_button_type_t::MBT_RIGHT,
PointerButton::Middle => cef_mouse_button_type_t::MBT_MIDDLE,
_ => cef_mouse_button_type_t::MBT_LEFT,
};
browser.host.set_focus(true as _);
browser.host.send_mouse_click_event(
Some(&mouse_event),
MouseButtonType::from(mouse_button),
mouse_up as _,
1,
);
}
}
/// [`SendMouseWheelEvent`](https://cef-builds.spotifycdn.com/docs/106.1/classCefBrowserHost.html#acd5d057bd5230baa9a94b7853ba755f7)
pub fn send_mouse_wheel(&self, webview: &Entity, position: Vec2, delta: Vec2) {
if let Some(browser) = self.get_focused_browser(webview) {
let mouse_event = cef::MouseEvent {
x: position.x as i32,
y: position.y as i32,
modifiers: 0,
};
browser
.host
.send_mouse_wheel_event(Some(&mouse_event), delta.x as _, delta.y as _);
}
}
#[inline]
pub fn send_key(&self, webview: &Entity, event: cef::KeyEvent) {
if let Some(browser) = self.get_focused_browser(webview) {
browser.host.send_key_event(Some(&event));
}
}
pub fn emit_event(&self, webview: &Entity, id: impl Into<String>, event: &serde_json::Value) {
if let Some(mut process_message) =
process_message_create(Some(&PROCESS_MESSAGE_HOST_EMIT.into()))
&& let Some(argument_list) = process_message.argument_list()
&& let Some(browser) = self.browsers.get(webview)
&& let Some(frame) = browser.client.main_frame()
{
argument_list.set_string(0, Some(&id.into().as_str().into()));
argument_list.set_string(1, Some(&event.to_string().as_str().into()));
frame.send_process_message(
ProcessId::from(cef_dll_sys::cef_process_id_t::PID_RENDERER),
Some(&mut process_message),
);
};
}
pub fn resize(&self, webview: &Entity, size: Vec2) {
if let Some(browser) = self.browsers.get(webview) {
browser.size.set(size);
browser.host.was_resized();
}
}
/// Closes the browser associated with the given webview entity.
///
/// The browser will be removed from the hash map after closing.
pub fn close(&mut self, webview: &Entity) {
if let Some(browser) = self.browsers.remove(webview) {
browser.host.close_browser(true as _);
debug!("Closed browser with webview: {:?}", webview);
}
}
#[inline]
pub fn try_receive_texture(&self) -> core::result::Result<RenderTexture, TryRecvError> {
self.receiver.try_recv()
}
/// Shows the DevTools for the specified webview.
pub fn show_devtool(&self, webview: &Entity) {
let Some(browser) = self.browsers.get(webview) else {
return;
};
browser.host.show_dev_tools(
Some(&WindowInfo::default()),
Some(&mut ClientHandlerBuilder::new(DevToolRenderHandlerBuilder::build()).build()),
Some(&BrowserSettings::default()),
None,
);
}
/// Closes the DevTools for the specified webview.
pub fn close_devtools(&self, webview: &Entity) {
if let Some(browser) = self.browsers.get(webview) {
browser.host.close_dev_tools();
}
}
/// Navigate backwards.
///
/// ## Reference
///
/// - [`GoBack`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowser.html#a85b02760885c070e4ad2a2705cea56cb)
pub fn go_back(&self, webview: &Entity) {
if let Some(browser) = self.browsers.get(webview)
&& browser.client.can_go_back() == 1
{
browser.client.go_back();
}
}
/// Navigate forwards.
///
/// ## Reference
///
/// - [`GoForward`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowser.html#aa8e97fc210ee0e73f16b2d98482419d0)
pub fn go_forward(&self, webview: &Entity) {
if let Some(browser) = self.browsers.get(webview)
&& browser.client.can_go_forward() == 1
{
browser.client.go_forward();
}
}
/// Returns the current zoom level for the specified webview.
///
/// ## Reference
///
/// - [`GetZoomLevel`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a524d4a358287dab284c0dfec6d6d229e)
pub fn zoom_level(&self, webview: &Entity) -> Option<f64> {
self.browsers
.get(webview)
.map(|browser| browser.host.zoom_level())
}
/// Sets the zoom level for the specified webview.
///
/// ## Reference
///
/// - [`SetZoomLevel`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#af2b7bf250ac78345117cd575190f2f7b)
pub fn set_zoom_level(&self, webview: &Entity, zoom_level: f64) {
if let Some(browser) = self.browsers.get(webview) {
browser.host.set_zoom_level(zoom_level);
}
}
/// Sets whether the audio is muted for the specified webview.
///
/// ## Reference
///
/// - [`SetAudioMuted`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a153d179c9ff202c8bb8869d2e9a820a2)
pub fn set_audio_muted(&self, webview: &Entity, muted: bool) {
if let Some(browser) = self.browsers.get(webview) {
browser.host.set_audio_muted(muted as _);
}
}
#[inline]
pub fn reload(&self) {
for browser in self.browsers.values() {
if let Some(frame) = browser.client.main_frame() {
let url = frame.url().into_string();
info!("Reloading browser with URL: {}", url);
frame.load_url(Some(&url.as_str().into()));
}
}
}
/// ## Reference
///
/// - [`ImeSetComposition`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a567b41fb2d3917843ece3b57adc21ebe)
pub fn set_ime_composition(&self, text: &str, cursor_utf16: Option<u32>) {
let underlines = make_underlines_for(text, cursor_utf16.map(|i| (i, i)));
let i = text.encode_utf16().count();
let selection_range = Range {
from: i as _,
to: i as _,
};
let replacement_range = self.ime_caret_range();
for browser in self
.browsers
.values()
.filter(|b| b.client.focused_frame().is_some())
{
browser.host.ime_set_composition(
Some(&text.into()),
underlines.len(),
Some(&underlines[0]),
Some(&replacement_range),
Some(&selection_range),
);
}
}
/// ## Reference
///
/// [`ImeSetComposition`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a567b41fb2d3917843ece3b57adc21ebe)
pub fn ime_finish_composition(&self, keep_selection: bool) {
for browser in self
.browsers
.values()
.filter(|b| b.client.focused_frame().is_some())
{
browser.host.ime_finish_composing_text(keep_selection as _);
}
}
pub fn set_ime_commit_text(&self, text: &str) {
let replacement_range = self.ime_caret_range();
for browser in self
.browsers
.values()
.filter(|b| b.client.focused_frame().is_some())
{
browser
.host
.ime_commit_text(Some(&text.into()), Some(&replacement_range), 0)
}
}
fn request_context(requester: Requester) -> Option<RequestContext> {
let mut context = cef::request_context_create_context(
Some(&RequestContextSettings::default()),
Some(&mut RequestContextHandlerBuilder::build()),
);
if let Some(context) = context.as_mut() {
context.register_scheme_handler_factory(
Some(&SCHEME_CEF.into()),
Some(&HOST_CEF.into()),
Some(&mut LocalSchemaHandlerBuilder::build(requester)),
);
}
context
}
fn client_handler(
&self,
webview: Entity,
size: SharedViewSize,
ipc_event_sender: Sender<IpcEventRaw>,
brp_sender: Sender<BrpMessage>,
system_cursor_icon_sender: SystemCursorIconSenderInner,
) -> Client {
ClientHandlerBuilder::new(RenderHandlerBuilder::build(
webview,
self.sender.clone(),
size.clone(),
self.ime_caret.clone(),
))
.with_display_handler(DisplayHandlerBuilder::build(system_cursor_icon_sender))
.with_message_handler(JsEmitEventHandler::new(webview, ipc_event_sender))
.with_message_handler(BrpHandler::new(brp_sender))
.build()
}
#[inline]
fn ime_caret_range(&self) -> Range {
let caret = self.ime_caret.get();
Range {
from: caret,
to: caret,
}
}
#[inline]
fn get_focused_browser(&self, webview: &Entity) -> Option<&WebviewBrowser> {
self.browsers
.get(webview)
.and_then(|b| b.client.focused_frame().is_some().then_some(b))
}
}
pub fn modifiers_from_mouse_buttons<'a>(buttons: impl IntoIterator<Item = &'a MouseButton>) -> u32 {
let mut modifiers = cef_event_flags_t::EVENTFLAG_NONE as u32;
for button in buttons {
match button {
MouseButton::Left => modifiers |= cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32,
MouseButton::Right => {
modifiers |= cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32
}
MouseButton::Middle => {
modifiers |= cef_event_flags_t::EVENTFLAG_MIDDLE_MOUSE_BUTTON as u32
}
_ => {}
}
}
modifiers
}
pub fn make_underlines_for(
text: &str,
selection_utf16: Option<(u32, u32)>,
) -> Vec<CompositionUnderline> {
let len16 = utf16_len(text);
let base = CompositionUnderline {
size: size_of::<CompositionUnderline>(),
range: Range { from: 0, to: len16 },
color: 0,
background_color: 0,
thick: 0,
style: Default::default(),
};
if let Some((from, to)) = selection_utf16
&& from < to
{
let sel = CompositionUnderline {
size: size_of::<CompositionUnderline>(),
range: Range { from, to },
color: 0,
background_color: 0,
thick: 1,
style: Default::default(),
};
return vec![base, sel];
}
vec![base]
}
#[inline]
fn utf16_len(s: &str) -> u32 {
s.encode_utf16().count() as u32
}
#[allow(dead_code)]
fn utf16_index_from_byte(s: &str, byte_idx: usize) -> u32 {
s[..byte_idx].encode_utf16().count() as u32
}
#[cfg(test)]
mod tests {
use crate::prelude::modifiers_from_mouse_buttons;
use bevy::prelude::*;
#[test]
fn test_modifiers_from_mouse_buttons() {
let buttons = vec![&MouseButton::Left, &MouseButton::Right];
let modifiers = modifiers_from_mouse_buttons(buttons);
assert_eq!(
modifiers,
cef_dll_sys::cef_event_flags_t::EVENTFLAG_LEFT_MOUSE_BUTTON as u32
| cef_dll_sys::cef_event_flags_t::EVENTFLAG_RIGHT_MOUSE_BUTTON as u32
);
}
}

View File

@@ -0,0 +1,57 @@
use cef::rc::{Rc, RcImpl};
use cef::{Browser, ImplRenderHandler, Rect, RenderHandler, WrapRenderHandler, sys};
/// ## Reference
///
/// - [`CefRenderHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRenderHandler.html)
pub struct DevToolRenderHandlerBuilder {
object: *mut RcImpl<sys::cef_render_handler_t, Self>,
}
impl DevToolRenderHandlerBuilder {
pub fn build() -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
})
}
}
impl Rc for DevToolRenderHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRenderHandler for DevToolRenderHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_render_handler_t, Self>) {
self.object = object;
}
}
impl Clone for DevToolRenderHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplRenderHandler for DevToolRenderHandlerBuilder {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect {
rect.width = 800;
rect.height = 800;
}
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_render_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,979 @@
//! ## Reference
//!
//! - [`cef_key_event_t`](https://cef-builds.spotifycdn.com/docs/106.1/structcef__key__event__t.html)
//! - [KeyboardCodes](https://chromium.googlesource.com/external/Webkit/+/safari-4-branch/WebCore/platform/KeyboardCodes.h)
use bevy::input::ButtonState;
use bevy::input::keyboard::KeyboardInput;
use bevy::prelude::{ButtonInput, KeyCode};
use cef_dll_sys::{cef_event_flags_t, cef_key_event_t, cef_key_event_type_t};
pub fn keyboard_modifiers(input: &ButtonInput<KeyCode>) -> u32 {
let mut flags = 0u32;
if input.pressed(KeyCode::ControlLeft) || input.pressed(KeyCode::ControlRight) {
flags |= cef_event_flags_t::EVENTFLAG_CONTROL_DOWN as u32;
}
if input.pressed(KeyCode::AltLeft) || input.pressed(KeyCode::AltRight) {
flags |= cef_event_flags_t::EVENTFLAG_ALT_DOWN as u32;
}
if input.pressed(KeyCode::ShiftLeft) || input.pressed(KeyCode::ShiftRight) {
flags |= cef_event_flags_t::EVENTFLAG_SHIFT_DOWN as u32;
}
if input.pressed(KeyCode::SuperLeft) || input.pressed(KeyCode::SuperRight) {
flags |= cef_event_flags_t::EVENTFLAG_COMMAND_DOWN as u32;
}
if input.pressed(KeyCode::CapsLock) {
flags |= cef_event_flags_t::EVENTFLAG_CAPS_LOCK_ON as u32;
}
if input.pressed(KeyCode::NumLock) {
flags |= cef_event_flags_t::EVENTFLAG_NUM_LOCK_ON as u32;
}
flags
}
pub fn create_cef_key_event(
modifiers: u32,
_input: &ButtonInput<KeyCode>,
key_event: &KeyboardInput,
) -> Option<cef::KeyEvent> {
let key_type = match key_event.state {
// ButtonState::Pressed if input.just_pressed(key_event.key_code) => {
// cef_key_event_type_t::KEYEVENT_RAWKEYDOWN
// }
ButtonState::Pressed => cef_key_event_type_t::KEYEVENT_CHAR,
ButtonState::Released => cef_key_event_type_t::KEYEVENT_KEYUP,
};
let windows_key_code = keycode_to_windows_vk(key_event.key_code);
let character = key_event
.text
.as_ref()
.and_then(|text| text.chars().next())
.unwrap_or('\0') as u16;
Some(cef::KeyEvent::from(cef_key_event_t {
size: core::mem::size_of::<cef_key_event_t>(),
type_: key_type,
modifiers,
windows_key_code,
native_key_code: to_native_key_code(&key_event.key_code) as _,
character,
unmodified_character: character,
is_system_key: false as _,
focus_on_editable_field: false as _,
}))
}
// fn is_not_character_key_code(keycode: &KeyCode) -> bool {
// match keycode {
// // Function keys are not character keys
// KeyCode::F1
// | KeyCode::F2
// | KeyCode::F3
// | KeyCode::F4
// | KeyCode::F5
// | KeyCode::F6
// | KeyCode::F7
// | KeyCode::F8
// | KeyCode::F9
// | KeyCode::F10
// | KeyCode::F11
// | KeyCode::F12 => true,
//
// // Navigation keys are not character keys
// KeyCode::ArrowLeft
// | KeyCode::ArrowUp
// | KeyCode::ArrowRight
// | KeyCode::ArrowDown
// | KeyCode::Home
// | KeyCode::End
// | KeyCode::PageUp
// | KeyCode::PageDown => true,
//
// // Modifier keys are not character keys
// KeyCode::ShiftLeft
// | KeyCode::ShiftRight
// | KeyCode::ControlLeft
// | KeyCode::ControlRight
// | KeyCode::AltLeft
// | KeyCode::AltRight
// | KeyCode::SuperLeft
// | KeyCode::SuperRight => true,
//
// // Lock keys are not character keys
// KeyCode::CapsLock | KeyCode::NumLock | KeyCode::ScrollLock => true,
//
// // Special control keys are not character keys
// KeyCode::Escape
// | KeyCode::Tab
// | KeyCode::Enter
// | KeyCode::Backspace
// | KeyCode::Delete
// | KeyCode::Insert => true,
//
// // All other keys (letters, numbers, punctuation, space, numpad) are character keys
// _ => false,
// }
// }
fn keycode_to_windows_vk(keycode: KeyCode) -> i32 {
match keycode {
// Letters
KeyCode::KeyA => 0x41,
KeyCode::KeyB => 0x42,
KeyCode::KeyC => 0x43,
KeyCode::KeyD => 0x44,
KeyCode::KeyE => 0x45,
KeyCode::KeyF => 0x46,
KeyCode::KeyG => 0x47,
KeyCode::KeyH => 0x48,
KeyCode::KeyI => 0x49,
KeyCode::KeyJ => 0x4A,
KeyCode::KeyK => 0x4B,
KeyCode::KeyL => 0x4C,
KeyCode::KeyM => 0x4D,
KeyCode::KeyN => 0x4E,
KeyCode::KeyO => 0x4F,
KeyCode::KeyP => 0x50,
KeyCode::KeyQ => 0x51,
KeyCode::KeyR => 0x52,
KeyCode::KeyS => 0x53,
KeyCode::KeyT => 0x54,
KeyCode::KeyU => 0x55,
KeyCode::KeyV => 0x56,
KeyCode::KeyW => 0x57,
KeyCode::KeyX => 0x58,
KeyCode::KeyY => 0x59,
KeyCode::KeyZ => 0x5A,
// Numbers
KeyCode::Digit0 => 0x30,
KeyCode::Digit1 => 0x31,
KeyCode::Digit2 => 0x32,
KeyCode::Digit3 => 0x33,
KeyCode::Digit4 => 0x34,
KeyCode::Digit5 => 0x35,
KeyCode::Digit6 => 0x36,
KeyCode::Digit7 => 0x37,
KeyCode::Digit8 => 0x38,
KeyCode::Digit9 => 0x39,
// Function keys
KeyCode::F1 => 0x70,
KeyCode::F2 => 0x71,
KeyCode::F3 => 0x72,
KeyCode::F4 => 0x73,
KeyCode::F5 => 0x74,
KeyCode::F6 => 0x75,
KeyCode::F7 => 0x76,
KeyCode::F8 => 0x77,
KeyCode::F9 => 0x78,
KeyCode::F10 => 0x79,
KeyCode::F11 => 0x7A,
KeyCode::F12 => 0x7B,
// Special keys
KeyCode::Enter => 0x0D,
KeyCode::Space => 0x20,
KeyCode::Backspace => 0x08,
KeyCode::Delete => 0x2E,
KeyCode::Tab => 0x09,
KeyCode::Escape => 0x1B,
KeyCode::Insert => 0x2D,
KeyCode::Home => 0x24,
KeyCode::End => 0x23,
KeyCode::PageUp => 0x21,
KeyCode::PageDown => 0x22,
// Arrow keys
KeyCode::ArrowLeft => 0x25,
KeyCode::ArrowUp => 0x26,
KeyCode::ArrowRight => 0x27,
KeyCode::ArrowDown => 0x28,
// Modifier keys
KeyCode::ShiftLeft | KeyCode::ShiftRight => 0x10,
KeyCode::ControlLeft | KeyCode::ControlRight => 0x11,
KeyCode::AltLeft | KeyCode::AltRight => 0x12,
KeyCode::SuperLeft => 0x5B, // Left Windows key
KeyCode::SuperRight => 0x5C, // Right Windows key
// Lock keys
KeyCode::CapsLock => 0x14,
KeyCode::NumLock => 0x90,
KeyCode::ScrollLock => 0x91,
// Punctuation
KeyCode::Semicolon => 0xBA,
KeyCode::Equal => 0xBB,
KeyCode::Comma => 0xBC,
KeyCode::Minus => 0xBD,
KeyCode::Period => 0xBE,
KeyCode::Slash => 0xBF,
KeyCode::Backquote => 0xC0,
KeyCode::BracketLeft => 0xDB,
KeyCode::Backslash => 0xDC,
KeyCode::BracketRight => 0xDD,
KeyCode::Quote => 0xDE,
// Numpad
KeyCode::Numpad0 => 0x60,
KeyCode::Numpad1 => 0x61,
KeyCode::Numpad2 => 0x62,
KeyCode::Numpad3 => 0x63,
KeyCode::Numpad4 => 0x64,
KeyCode::Numpad5 => 0x65,
KeyCode::Numpad6 => 0x66,
KeyCode::Numpad7 => 0x67,
KeyCode::Numpad8 => 0x68,
KeyCode::Numpad9 => 0x69,
KeyCode::NumpadMultiply => 0x6A,
KeyCode::NumpadAdd => 0x6B,
KeyCode::NumpadSubtract => 0x6D,
KeyCode::NumpadDecimal => 0x6E,
KeyCode::NumpadDivide => 0x6F,
// Default case for unhandled keys
_ => 0,
}
}
// fn is_special_key(keycode: &KeyCode) -> bool {
// matches!(
// keycode,
// KeyCode::Enter
// | KeyCode::Space
// | KeyCode::Backspace
// | KeyCode::Delete
// | KeyCode::Tab
// | KeyCode::Escape
// | KeyCode::Insert
// | KeyCode::Home
// | KeyCode::End
// | KeyCode::PageUp
// | KeyCode::PageDown
// )
// }
/// Native key codes for different platforms based on MDN documentation
/// [`Keyboard_event_key_values`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
fn to_native_key_code(keycode: &KeyCode) -> u32 {
match keycode {
// Letters - Platform specific native codes
KeyCode::KeyA => {
if cfg!(target_os = "macos") {
0x00
} else {
0x41
} // Linux/default
}
KeyCode::KeyB => {
if cfg!(target_os = "macos") {
0x0B
} else {
0x42
}
}
KeyCode::KeyC => {
if cfg!(target_os = "macos") {
0x08
} else {
0x43
}
}
KeyCode::KeyD => {
if cfg!(target_os = "macos") {
0x02
} else {
0x44
}
}
KeyCode::KeyE => {
if cfg!(target_os = "macos") {
0x0E
} else {
0x45
}
}
KeyCode::KeyF => {
if cfg!(target_os = "macos") {
0x03
} else {
0x46
}
}
KeyCode::KeyG => {
if cfg!(target_os = "macos") {
0x05
} else {
0x47
}
}
KeyCode::KeyH => {
if cfg!(target_os = "macos") {
0x04
} else {
0x48
}
}
KeyCode::KeyI => {
if cfg!(target_os = "macos") {
0x22
} else {
0x49
}
}
KeyCode::KeyJ => {
if cfg!(target_os = "macos") {
0x26
} else {
0x4A
}
}
KeyCode::KeyK => {
if cfg!(target_os = "macos") {
0x28
} else {
0x4B
}
}
KeyCode::KeyL => {
if cfg!(target_os = "macos") {
0x25
} else {
0x4C
}
}
KeyCode::KeyM => {
if cfg!(target_os = "macos") {
0x2E
} else {
0x4D
}
}
KeyCode::KeyN => {
if cfg!(target_os = "macos") {
0x2D
} else {
0x4E
}
}
KeyCode::KeyO => {
if cfg!(target_os = "macos") {
0x1F
} else {
0x4F
}
}
KeyCode::KeyP => {
if cfg!(target_os = "macos") {
0x23
} else {
0x50
}
}
KeyCode::KeyQ => {
if cfg!(target_os = "macos") {
0x0C
} else {
0x51
}
}
KeyCode::KeyR => {
if cfg!(target_os = "macos") {
0x0F
} else {
0x52
}
}
KeyCode::KeyS => {
if cfg!(target_os = "macos") {
0x01
} else {
0x53
}
}
KeyCode::KeyT => {
if cfg!(target_os = "macos") {
0x11
} else {
0x54
}
}
KeyCode::KeyU => {
if cfg!(target_os = "macos") {
0x20
} else {
0x55
}
}
KeyCode::KeyV => {
if cfg!(target_os = "macos") {
0x09
} else {
0x56
}
}
KeyCode::KeyW => {
if cfg!(target_os = "macos") {
0x0D
} else {
0x57
}
}
KeyCode::KeyX => {
if cfg!(target_os = "macos") {
0x07
} else {
0x58
}
}
KeyCode::KeyY => {
if cfg!(target_os = "macos") {
0x10
} else {
0x59
}
}
KeyCode::KeyZ => {
if cfg!(target_os = "macos") {
0x06
} else {
0x5A
}
}
// Numbers
KeyCode::Digit0 => {
if cfg!(target_os = "macos") {
0x1D
} else {
0x30
}
}
KeyCode::Digit1 => {
if cfg!(target_os = "macos") {
0x12
} else {
0x31
}
}
KeyCode::Digit2 => {
if cfg!(target_os = "macos") {
0x13
} else {
0x32
}
}
KeyCode::Digit3 => {
if cfg!(target_os = "macos") {
0x14
} else {
0x33
}
}
KeyCode::Digit4 => {
if cfg!(target_os = "macos") {
0x15
} else {
0x34
}
}
KeyCode::Digit5 => {
if cfg!(target_os = "macos") {
0x17
} else {
0x35
}
}
KeyCode::Digit6 => {
if cfg!(target_os = "macos") {
0x16
} else {
0x36
}
}
KeyCode::Digit7 => {
if cfg!(target_os = "macos") {
0x1A
} else {
0x37
}
}
KeyCode::Digit8 => {
if cfg!(target_os = "macos") {
0x1C
} else {
0x38
}
}
KeyCode::Digit9 => {
if cfg!(target_os = "macos") {
0x19
} else {
0x39
}
}
// Function keys
KeyCode::F1 => {
if cfg!(target_os = "macos") {
0x7A
} else {
0x70
}
}
KeyCode::F2 => {
if cfg!(target_os = "macos") {
0x78
} else {
0x71
}
}
KeyCode::F3 => {
if cfg!(target_os = "macos") {
0x63
} else {
0x72
}
}
KeyCode::F4 => {
if cfg!(target_os = "macos") {
0x76
} else {
0x73
}
}
KeyCode::F5 => {
if cfg!(target_os = "macos") {
0x60
} else {
0x74
}
}
KeyCode::F6 => {
if cfg!(target_os = "macos") {
0x61
} else {
0x75
}
}
KeyCode::F7 => {
if cfg!(target_os = "macos") {
0x62
} else {
0x76
}
}
KeyCode::F8 => {
if cfg!(target_os = "macos") {
0x64
} else {
0x77
}
}
KeyCode::F9 => {
if cfg!(target_os = "macos") {
0x65
} else {
0x78
}
}
KeyCode::F10 => {
if cfg!(target_os = "macos") {
0x6D
} else {
0x79
}
}
KeyCode::F11 => {
if cfg!(target_os = "macos") {
0x67
} else {
0x7A
}
}
KeyCode::F12 => {
if cfg!(target_os = "macos") {
0x6F
} else {
0x7B
}
}
// Special keys
KeyCode::Enter => {
if cfg!(target_os = "macos") {
0x24
} else {
0x0D
}
}
KeyCode::Space => {
if cfg!(target_os = "macos") {
0x31
} else {
0x20
}
}
KeyCode::Backspace => {
if cfg!(target_os = "macos") {
0x33
} else {
0x08
}
}
KeyCode::Delete => {
if cfg!(target_os = "macos") {
0x75
} else {
0x2E
}
}
KeyCode::Tab => {
if cfg!(target_os = "macos") {
0x30
} else {
0x09
}
}
KeyCode::Escape => {
if cfg!(target_os = "macos") {
0x35
} else {
0x1B
}
}
KeyCode::Insert => {
if cfg!(target_os = "macos") {
0x72
} else {
0x2D
}
}
KeyCode::Home => {
if cfg!(target_os = "macos") {
0x73
} else {
0x24
}
}
KeyCode::End => {
if cfg!(target_os = "macos") {
0x77
} else {
0x23
}
}
KeyCode::PageUp => {
if cfg!(target_os = "macos") {
0x74
} else {
0x21
}
}
KeyCode::PageDown => {
if cfg!(target_os = "macos") {
0x79
} else {
0x22
}
}
// Arrow keys
KeyCode::ArrowLeft => {
if cfg!(target_os = "macos") {
0x7B
} else {
0x25
}
}
KeyCode::ArrowUp => {
if cfg!(target_os = "macos") {
0x7E
} else {
0x26
}
}
KeyCode::ArrowRight => {
if cfg!(target_os = "macos") {
0x7C
} else {
0x27
}
}
KeyCode::ArrowDown => {
if cfg!(target_os = "macos") {
0x7D
} else {
0x28
}
}
// Modifier keys
KeyCode::ShiftLeft => {
if cfg!(target_os = "macos") {
0x38
} else {
0xA0
}
}
KeyCode::ShiftRight => {
if cfg!(target_os = "macos") {
0x3C
} else {
0xA1
}
}
KeyCode::ControlLeft => {
if cfg!(target_os = "macos") {
0x3B
} else {
0xA2
}
}
KeyCode::ControlRight => {
if cfg!(target_os = "macos") {
0x3E
} else {
0xA3
}
}
KeyCode::AltLeft => {
if cfg!(target_os = "macos") {
0x3A
} else {
0xA4
}
}
KeyCode::AltRight => {
if cfg!(target_os = "macos") {
0x3D
} else {
0xA5
}
}
KeyCode::SuperLeft => {
if cfg!(target_os = "macos") {
0x37
} else {
0x5B
}
}
KeyCode::SuperRight => {
if cfg!(target_os = "macos") {
0x36
} else {
0x5C
}
}
// Lock keys
KeyCode::CapsLock => {
if cfg!(target_os = "macos") {
0x39
} else {
0x14
}
}
KeyCode::NumLock => {
if cfg!(target_os = "macos") {
0x47
} else {
0x90
}
}
KeyCode::ScrollLock => 0x91,
// Punctuation
KeyCode::Semicolon => {
if cfg!(target_os = "macos") {
0x29
} else {
0xBA
}
}
KeyCode::Equal => {
if cfg!(target_os = "macos") {
0x18
} else {
0xBB
}
}
KeyCode::Comma => {
if cfg!(target_os = "macos") {
0x2B
} else {
0xBC
}
}
KeyCode::Minus => {
if cfg!(target_os = "macos") {
0x1B
} else {
0xBD
}
}
KeyCode::Period => {
if cfg!(target_os = "macos") {
0x2F
} else {
0xBE
}
}
KeyCode::Slash => {
if cfg!(target_os = "macos") {
0x2C
} else {
0xBF
}
}
KeyCode::Backquote => {
if cfg!(target_os = "macos") {
0x32
} else {
0xC0
}
}
KeyCode::BracketLeft => {
if cfg!(target_os = "macos") {
0x21
} else {
0xDB
}
}
KeyCode::Backslash => {
if cfg!(target_os = "macos") {
0x2A
} else {
0xDC
}
}
KeyCode::BracketRight => {
if cfg!(target_os = "macos") {
0x1E
} else {
0xDD
}
}
KeyCode::Quote => {
if cfg!(target_os = "macos") {
0x27
} else {
0xDE
}
}
// Numpad
KeyCode::Numpad0 => {
if cfg!(target_os = "macos") {
0x52
} else {
0x60
}
}
KeyCode::Numpad1 => {
if cfg!(target_os = "macos") {
0x53
} else {
0x61
}
}
KeyCode::Numpad2 => {
if cfg!(target_os = "macos") {
0x54
} else {
0x62
}
}
KeyCode::Numpad3 => {
if cfg!(target_os = "macos") {
0x55
} else {
0x63
}
}
KeyCode::Numpad4 => {
if cfg!(target_os = "macos") {
0x56
} else {
0x64
}
}
KeyCode::Numpad5 => {
if cfg!(target_os = "macos") {
0x57
} else {
0x65
}
}
KeyCode::Numpad6 => {
if cfg!(target_os = "macos") {
0x58
} else {
0x66
}
}
KeyCode::Numpad7 => {
if cfg!(target_os = "macos") {
0x59
} else {
0x67
}
}
KeyCode::Numpad8 => {
if cfg!(target_os = "macos") {
0x5B
} else {
0x68
}
}
KeyCode::Numpad9 => {
if cfg!(target_os = "macos") {
0x5C
} else {
0x69
}
}
KeyCode::NumpadMultiply => {
if cfg!(target_os = "macos") {
0x43
} else {
0x6A
}
}
KeyCode::NumpadAdd => {
if cfg!(target_os = "macos") {
0x45
} else {
0x6B
}
}
KeyCode::NumpadSubtract => {
if cfg!(target_os = "macos") {
0x4E
} else {
0x6D
}
}
KeyCode::NumpadDecimal => {
if cfg!(target_os = "macos") {
0x41
} else {
0x6E
}
}
KeyCode::NumpadDivide => {
if cfg!(target_os = "macos") {
0x4B
} else {
0x6F
}
}
// Default case for unhandled keys
_ => 0,
}
}

View File

@@ -0,0 +1,129 @@
mod brp_handler;
mod js_emit_event_handler;
use crate::browser_process::ContextMenuHandlerBuilder;
use crate::prelude::IntoString;
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, Client, ContextMenuHandler, DisplayHandler, Frame, ImplClient, ImplProcessMessage,
ListValue, ProcessId, ProcessMessage, RenderHandler, WrapClient, sys,
};
use std::os::raw::c_int;
pub use brp_handler::BrpHandler;
pub use js_emit_event_handler::{IpcEventRaw, JsEmitEventHandler};
pub trait ProcessMessageHandler {
fn process_name(&self) -> &'static str;
fn handle_message(&self, browser: &mut Browser, frame: &mut Frame, args: Option<ListValue>);
}
/// ## Reference
///
/// - [`CefBrowser Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefBrowser.html)
pub struct ClientHandlerBuilder {
object: *mut RcImpl<sys::cef_client_t, Self>,
render_handler: RenderHandler,
context_menu_handler: ContextMenuHandler,
message_handlers: Vec<std::rc::Rc<dyn ProcessMessageHandler>>,
display_handler: Option<DisplayHandler>,
}
impl ClientHandlerBuilder {
pub fn new(render_handler: RenderHandler) -> Self {
Self {
object: std::ptr::null_mut(),
render_handler,
context_menu_handler: ContextMenuHandlerBuilder::build(),
message_handlers: Vec::new(),
display_handler: None,
}
}
pub fn with_display_handler(mut self, display_handler: DisplayHandler) -> Self {
self.display_handler = Some(display_handler);
self
}
pub fn with_message_handler(mut self, handler: impl ProcessMessageHandler + 'static) -> Self {
self.message_handlers.push(std::rc::Rc::new(handler));
self
}
pub fn build(self) -> Client {
Client::new(self)
}
}
impl Rc for ClientHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapClient for ClientHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::cef_client_t, Self>) {
self.object = object;
}
}
impl Clone for ClientHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
render_handler: self.render_handler.clone(),
context_menu_handler: self.context_menu_handler.clone(),
message_handlers: self.message_handlers.clone(),
display_handler: self.display_handler.clone(),
}
}
}
impl ImplClient for ClientHandlerBuilder {
fn render_handler(&self) -> Option<RenderHandler> {
Some(self.render_handler.clone())
}
fn display_handler(&self) -> Option<DisplayHandler> {
self.display_handler.clone()
}
fn on_process_message_received(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
_: ProcessId,
message: Option<&mut ProcessMessage>,
) -> c_int {
if let Some(message) = message
&& let Some(browser) = browser
&& let Some(frame) = frame
&& let Some(name) = Some(message.name().into_string())
&& let Some(handler) = self
.message_handlers
.iter()
.find(|h| h.process_name() == name.as_str())
{
{
let args = message.argument_list();
handler.handle_message(browser, frame, args);
}
};
1
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_client_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,68 @@
use crate::browser_process::client_handler::ProcessMessageHandler;
use crate::prelude::PROCESS_MESSAGE_BRP;
use crate::util::IntoString;
use async_channel::Sender;
use bevy::tasks::IoTaskPool;
use bevy_remote::{BrpMessage, BrpRequest};
use cef::{
Browser, Frame, ImplFrame, ImplListValue, ImplProcessMessage, ListValue, ProcessId,
process_message_create,
};
use cef_dll_sys::cef_process_id_t;
pub struct BrpHandler {
sender: Sender<BrpMessage>,
}
impl BrpHandler {
pub const fn new(sender: Sender<BrpMessage>) -> Self {
Self { sender }
}
}
impl ProcessMessageHandler for BrpHandler {
fn process_name(&self) -> &'static str {
PROCESS_MESSAGE_BRP
}
fn handle_message(&self, _browser: &mut Browser, frame: &mut Frame, args: Option<ListValue>) {
if let Some(args) = args
&& let Ok(request) = serde_json::from_str::<BrpRequest>(&args.string(1).into_string())
{
let id = args.string(0).into_string();
let frame = frame.clone();
let brp_sender = self.sender.clone();
IoTaskPool::get()
.spawn(async move {
let (tx, rx) = async_channel::unbounded();
if brp_sender
.send(BrpMessage {
method: request.method,
params: request.params,
sender: tx,
})
.await
.is_err()
{
return;
}
if let Ok(result) = rx.recv().await
&& let Some(mut message) =
process_message_create(Some(&PROCESS_MESSAGE_BRP.into()))
&& let Some(argument_list) = message.argument_list()
{
argument_list.set_string(0, Some(&id.as_str().into()));
argument_list.set_string(
1,
Some(&serde_json::to_string(&result).unwrap().as_str().into()),
);
frame.send_process_message(
ProcessId::from(cef_process_id_t::PID_RENDERER),
Some(&mut message),
);
}
})
.detach();
}
}
}

View File

@@ -0,0 +1,39 @@
use crate::browser_process::client_handler::ProcessMessageHandler;
use crate::prelude::{IntoString, PROCESS_MESSAGE_JS_EMIT};
use async_channel::Sender;
use bevy::prelude::Entity;
use cef::{Browser, Frame, ImplListValue, ListValue};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct IpcEventRaw {
pub webview: Entity,
pub payload: String,
}
pub struct JsEmitEventHandler {
webview: Entity,
sender: Sender<IpcEventRaw>,
}
impl JsEmitEventHandler {
pub const fn new(webview: Entity, sender: Sender<IpcEventRaw>) -> Self {
Self { sender, webview }
}
}
impl ProcessMessageHandler for JsEmitEventHandler {
fn process_name(&self) -> &'static str {
PROCESS_MESSAGE_JS_EMIT
}
fn handle_message(&self, _browser: &mut Browser, _frame: &mut Frame, args: Option<ListValue>) {
if let Some(args) = args {
let event = IpcEventRaw {
webview: self.webview, // Placeholder, should be set correctly
payload: args.string(0).into_string(),
};
let _ = self.sender.send_blocking(event);
}
}
}

View File

@@ -0,0 +1,50 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplContextMenuHandler, WrapContextMenuHandler, sys};
/// ## Reference
///
/// - [`CefContextMenuHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefContextMenuHandler.html)
pub struct ContextMenuHandlerBuilder {
object: *mut RcImpl<sys::_cef_context_menu_handler_t, Self>,
}
impl ContextMenuHandlerBuilder {
pub fn build() -> cef::ContextMenuHandler {
cef::ContextMenuHandler::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl WrapContextMenuHandler for ContextMenuHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_context_menu_handler_t, Self>) {
self.object = object;
}
}
impl Rc for ContextMenuHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
core::mem::transmute(&base.cef_object)
}
}
}
impl Clone for ContextMenuHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplContextMenuHandler for ContextMenuHandlerBuilder {
#[inline]
fn get_raw(&self) -> *mut sys::cef_context_menu_handler_t {
self.object as *mut sys::cef_context_menu_handler_t
}
}

View File

@@ -0,0 +1,165 @@
use async_channel::Sender;
use bevy::log::{error, info, trace, warn};
use bevy::window::SystemCursorIcon;
use cef::rc::{ConvertParam, Rc, RcImpl};
use cef::{
Browser, CefString, CursorInfo, CursorType, ImplDisplayHandler, LogSeverity,
WrapDisplayHandler, sys,
};
use cef_dll_sys::{cef_cursor_type_t, cef_log_severity_t};
use std::os::raw::c_int;
pub type SystemCursorIconSenderInner = Sender<SystemCursorIcon>;
/// ## Reference
///
/// - [`CefDisplayHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/112.3/classCefDisplayHandler.html#af1cc8410a0b1a97166923428d3794636)
pub struct DisplayHandlerBuilder {
object: *mut RcImpl<sys::cef_display_handler_t, Self>,
cursor_icon: SystemCursorIconSenderInner,
}
impl DisplayHandlerBuilder {
pub fn build(cursor_icon: SystemCursorIconSenderInner) -> cef::DisplayHandler {
cef::DisplayHandler::new(Self {
object: core::ptr::null_mut(),
cursor_icon,
})
}
}
impl Rc for DisplayHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
core::mem::transmute(&base.cef_object)
}
}
}
impl Clone for DisplayHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
cursor_icon: self.cursor_icon.clone(),
}
}
}
impl WrapDisplayHandler for DisplayHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::cef_display_handler_t, Self>) {
self.object = object;
}
}
impl ImplDisplayHandler for DisplayHandlerBuilder {
fn on_console_message(
&self,
_: Option<&mut Browser>,
level: LogSeverity,
message: Option<&CefString>,
source: Option<&CefString>,
line: c_int,
) -> c_int {
let message = format!(
"{}\nline:{line}\n{}",
source.map(|s| s.to_string()).unwrap_or_default(),
message.map(|m| m.to_string()).unwrap_or_default()
);
match level.into_raw() {
cef_log_severity_t::LOGSEVERITY_ERROR => {
error!("{message}");
}
cef_log_severity_t::LOGSEVERITY_WARNING => {
warn!("{message}");
}
cef_log_severity_t::LOGSEVERITY_VERBOSE => {
trace!("{message}");
}
_ => {
info!("{message}");
}
}
1
}
fn on_cursor_change(
&self,
_browser: Option<&mut Browser>,
_cursor: *mut u8,
type_: CursorType,
_: Option<&CursorInfo>,
) -> c_int {
let _ = self
.cursor_icon
.send_blocking(to_system_cursor_icon(type_.into_raw()));
1
}
#[inline]
fn get_raw(&self) -> *mut sys::cef_display_handler_t {
self.object.cast()
}
}
pub fn to_system_cursor_icon(cursor_type: cef_dll_sys::cef_cursor_type_t) -> SystemCursorIcon {
match cursor_type {
cef_cursor_type_t::CT_POINTER => SystemCursorIcon::Default,
cef_cursor_type_t::CT_CROSS => SystemCursorIcon::Crosshair,
cef_cursor_type_t::CT_HAND => SystemCursorIcon::Pointer,
cef_cursor_type_t::CT_IBEAM => SystemCursorIcon::Text,
cef_cursor_type_t::CT_WAIT => SystemCursorIcon::Wait,
cef_cursor_type_t::CT_HELP => SystemCursorIcon::Help,
cef_cursor_type_t::CT_EASTRESIZE => SystemCursorIcon::EResize,
cef_cursor_type_t::CT_NORTHRESIZE => SystemCursorIcon::NResize,
cef_cursor_type_t::CT_NORTHEASTRESIZE => SystemCursorIcon::NeResize,
cef_cursor_type_t::CT_NORTHWESTRESIZE => SystemCursorIcon::NwResize,
cef_cursor_type_t::CT_SOUTHRESIZE => SystemCursorIcon::SResize,
cef_cursor_type_t::CT_SOUTHEASTRESIZE => SystemCursorIcon::SeResize,
cef_cursor_type_t::CT_SOUTHWESTRESIZE => SystemCursorIcon::SwResize,
cef_cursor_type_t::CT_WESTRESIZE => SystemCursorIcon::WResize,
cef_cursor_type_t::CT_NORTHSOUTHRESIZE => SystemCursorIcon::NsResize,
cef_cursor_type_t::CT_EASTWESTRESIZE => SystemCursorIcon::EwResize,
cef_cursor_type_t::CT_NORTHEASTSOUTHWESTRESIZE => SystemCursorIcon::NeswResize,
cef_cursor_type_t::CT_NORTHWESTSOUTHEASTRESIZE => SystemCursorIcon::NwseResize,
cef_cursor_type_t::CT_COLUMNRESIZE => SystemCursorIcon::ColResize,
cef_cursor_type_t::CT_ROWRESIZE => SystemCursorIcon::RowResize,
cef_cursor_type_t::CT_MIDDLEPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_EASTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_NORTHPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_NORTHEASTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_NORTHWESTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_SOUTHPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_SOUTHEASTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_SOUTHWESTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_WESTPANNING => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_MOVE => SystemCursorIcon::Move,
cef_cursor_type_t::CT_VERTICALTEXT => SystemCursorIcon::VerticalText,
cef_cursor_type_t::CT_CELL => SystemCursorIcon::Cell,
cef_cursor_type_t::CT_CONTEXTMENU => SystemCursorIcon::ContextMenu,
cef_cursor_type_t::CT_ALIAS => SystemCursorIcon::Alias,
cef_cursor_type_t::CT_PROGRESS => SystemCursorIcon::Progress,
cef_cursor_type_t::CT_NODROP => SystemCursorIcon::NoDrop,
cef_cursor_type_t::CT_COPY => SystemCursorIcon::Copy,
cef_cursor_type_t::CT_NONE => SystemCursorIcon::Default,
cef_cursor_type_t::CT_NOTALLOWED => SystemCursorIcon::NotAllowed,
cef_cursor_type_t::CT_ZOOMIN => SystemCursorIcon::ZoomIn,
cef_cursor_type_t::CT_ZOOMOUT => SystemCursorIcon::ZoomOut,
cef_cursor_type_t::CT_GRAB => SystemCursorIcon::Grab,
cef_cursor_type_t::CT_GRABBING => SystemCursorIcon::Grabbing,
cef_cursor_type_t::CT_MIDDLE_PANNING_VERTICAL => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_MIDDLE_PANNING_HORIZONTAL => SystemCursorIcon::AllScroll,
cef_cursor_type_t::CT_CUSTOM => SystemCursorIcon::Default,
cef_cursor_type_t::CT_DND_NONE => SystemCursorIcon::Default,
cef_cursor_type_t::CT_DND_MOVE => SystemCursorIcon::Move,
cef_cursor_type_t::CT_DND_COPY => SystemCursorIcon::Copy,
cef_cursor_type_t::CT_DND_LINK => SystemCursorIcon::Alias,
cef_cursor_type_t::CT_NUM_VALUES => SystemCursorIcon::Default,
_ => SystemCursorIcon::Default,
}
}

View File

@@ -0,0 +1,281 @@
mod data_responser;
mod headers_responser;
use crate::browser_process::localhost::data_responser::{DataResponser, parse_bytes_single_range};
use crate::browser_process::localhost::headers_responser::HeadersResponser;
use crate::prelude::IntoString;
use async_channel::{Receiver, Sender};
use bevy::asset::Asset;
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, Callback, CefString, Frame, ImplCallback, ImplRequest, ImplResourceHandler,
ImplResponse, ImplSchemeHandlerFactory, Request, ResourceHandler, ResourceReadCallback,
Response, SchemeHandlerFactory, WrapResourceHandler, WrapSchemeHandlerFactory, sys,
};
use cef_dll_sys::{_cef_resource_handler_t, cef_base_ref_counted_t};
use serde::{Deserialize, Serialize};
use std::os::raw::c_int;
use std::sync::{Arc, Mutex};
/// `cef://` scheme response asset.
#[derive(Asset, Reflect, Debug, Clone, Serialize, Deserialize)]
#[reflect(Debug, Serialize, Deserialize)]
pub struct CefResponse {
/// The media type.
pub mime_type: String,
/// The status code of the response, e.g., 200 for OK, 404 for Not Found.
pub status_code: u32,
/// The response data, typically HTML or other content.
pub data: Vec<u8>,
}
impl Default for CefResponse {
fn default() -> Self {
Self {
mime_type: "text/html".to_string(),
status_code: 404,
data: b"<!DOCTYPE html><html><body><h1>404 Not Found</h1></body></html>".to_vec(),
}
}
}
#[derive(Debug, Clone, Component)]
pub struct Responser(pub Sender<CefResponse>);
#[derive(Resource, Debug, Clone, Deref)]
pub struct Requester(pub Sender<CefRequest>);
#[derive(Resource, Debug, Clone)]
pub struct RequesterReceiver(pub Receiver<CefRequest>);
#[derive(Debug, Clone)]
pub struct CefRequest {
pub uri: String,
pub responser: Responser,
}
/// Use to register a local schema handler for the CEF browser.
///
/// ## Reference
///
/// - [`CefSchemeHandlerFactory Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefSchemeHandlerFactory.html)
pub struct LocalSchemaHandlerBuilder {
object: *mut RcImpl<sys::_cef_scheme_handler_factory_t, Self>,
requester: Requester,
}
impl LocalSchemaHandlerBuilder {
pub fn build(requester: Requester) -> SchemeHandlerFactory {
SchemeHandlerFactory::new(Self {
object: std::ptr::null_mut(),
requester,
})
}
}
impl Rc for LocalSchemaHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapSchemeHandlerFactory for LocalSchemaHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::cef_scheme_handler_factory_t, Self>) {
self.object = object;
}
}
impl Clone for LocalSchemaHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
requester: self.requester.clone(),
}
}
}
impl ImplSchemeHandlerFactory for LocalSchemaHandlerBuilder {
fn create(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
_scheme_name: Option<&CefString>,
_request: Option<&mut Request>,
) -> Option<ResourceHandler> {
Some(LocalResourceHandlerBuilder::build(self.requester.clone()))
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_scheme_handler_factory_t {
self.object.cast()
}
}
struct LocalResourceHandlerBuilder {
object: *mut RcImpl<_cef_resource_handler_t, Self>,
requester: Requester,
headers: Arc<Mutex<HeadersResponser>>,
data: Arc<Mutex<DataResponser>>,
}
impl LocalResourceHandlerBuilder {
fn build(requester: Requester) -> ResourceHandler {
ResourceHandler::new(Self {
object: std::ptr::null_mut(),
requester,
headers: Arc::new(Mutex::new(HeadersResponser::default())),
data: Arc::new(Mutex::new(DataResponser::default())),
})
}
}
impl WrapResourceHandler for LocalResourceHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_resource_handler_t, Self>) {
self.object = object;
}
}
impl Clone for LocalResourceHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
requester: self.requester.clone(),
headers: self.headers.clone(),
data: self.data.clone(),
}
}
}
impl Rc for LocalResourceHandlerBuilder {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplResourceHandler for LocalResourceHandlerBuilder {
fn open(
&self,
request: Option<&mut Request>,
handle_request: Option<&mut c_int>,
callback: Option<&mut Callback>,
) -> c_int {
let Some(request) = request else {
// Cancel the request if no request is provided
return 0;
};
let range_header_value = request.header_by_name(Some(&"Range".into())).into_string();
let range = parse_bytes_single_range(&range_header_value);
let Some(callback) = callback.cloned() else {
// If no callback is provided, we cannot handle the request
return 0;
};
if let Some(handle_request) = handle_request {
*handle_request = 0;
}
let url = request.url().into_string();
let requester = self.requester.clone();
let headers_responser = self.headers.clone();
let data_responser = self.data.clone();
IoTaskPool::get()
.spawn(async move {
let (tx, rx) = async_channel::bounded(1);
let _ = requester
.send(CefRequest {
uri: url
.strip_prefix("cef://localhost/")
.unwrap_or_default()
.to_string(),
responser: Responser(tx),
})
.await;
let response = rx.recv().await.unwrap_or_default();
headers_responser.lock().unwrap().prepare(&response, &range);
data_responser
.lock()
.unwrap()
.prepare(response.data, &range);
callback.cont();
})
.detach();
1
}
fn response_headers(
&self,
response: Option<&mut Response>,
response_length: Option<&mut i64>,
_redirect_url: Option<&mut CefString>,
) {
let Ok(responser) = self.headers.lock() else {
return;
};
if let Some(response) = response {
response.set_mime_type(Some(&responser.mime_type.as_str().into()));
response.set_status(responser.status_code as _);
for (name, value) in &responser.headers {
response.set_header_by_name(
Some(&name.as_str().into()),
Some(&value.as_str().into()),
false as _,
);
}
}
if let Some(response_length) = response_length {
*response_length = responser.response_length as _;
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn read(
&self,
data_out: *mut u8,
bytes_to_read: c_int,
bytes_read: Option<&mut c_int>,
_: Option<&mut ResourceReadCallback>,
) -> c_int {
let Some(bytes_read) = bytes_read else {
// If no bytes_read is provided, we cannot read data
return 0;
};
let Ok(mut responser) = self.data.lock() else {
return 0;
};
match responser.read(bytes_to_read as _) {
Some(data) if !data.is_empty() => {
let n = data.len();
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), data_out, n);
}
*bytes_read = n as i32;
1
}
_ => {
*bytes_read = 0;
0
}
}
}
#[inline]
fn get_raw(&self) -> *mut _cef_resource_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,236 @@
use bevy::prelude::*;
#[derive(Default, Clone)]
pub struct DataResponser {
data: Vec<u8>,
offset: usize,
end_offset: usize,
}
impl DataResponser {
/// Prepares the data and headers for the response.
///
/// The range header values only support the `bytes` range unit type and single range.
/// TODO: Support multiple ranges.
pub fn prepare(&mut self, data: Vec<u8>, range: &Option<(usize, Option<usize>)>) {
if let Some((start, end)) = range {
self.offset = *start;
self.end_offset = end.unwrap_or(data.len() - 1) + 1;
self.data = data;
} else {
self.offset = 0;
self.end_offset = data.len();
self.data = data;
}
}
pub fn read(&mut self, bytes_to_read: isize) -> Option<&[u8]> {
if self.offset >= self.data.len() {
return None;
}
let start = self.offset;
let end = if bytes_to_read < 0 {
self.data.len()
} else {
(self.offset as isize + bytes_to_read) as usize
};
let end = end.min(self.end_offset);
if start >= end || start >= self.data.len() {
return None;
}
let slice = &self.data[start..end.min(self.data.len())];
self.offset += slice.len();
Some(slice)
}
}
pub fn parse_bytes_single_range(range_header_value: &str) -> Option<(usize, Option<usize>)> {
let ranges = parse_bytes_range(range_header_value)?;
ranges.first().cloned()
}
/// Parses the `Range` header value from a request and returns the start of the range.
///
/// ## Reference
///
/// - [`Range_requests`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests)
fn parse_bytes_range(range_header_value: &str) -> Option<Vec<(usize, Option<usize>)>> {
if !range_header_value.starts_with("bytes=") {
return None;
}
let mut ranges = Vec::new();
let value = range_header_value.trim_start_matches("bytes=");
// bytes=100-200,300-400 => ["100-200", "300-400"]
let byte_ranges = value.split(",");
for range in byte_ranges {
// 100-200 => ["100", "200"]
let mut split = range.split("-");
let start = split.next()?;
let end = split.next();
let start = start.parse::<usize>().ok()?;
let end = end.and_then(|e| e.parse::<usize>().ok());
ranges.push((start, end));
}
Some(ranges)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn range_start_is_none_if_empty() {
assert_eq!(parse_bytes_range(""), None);
}
#[test]
fn range_only_start_offset() {
assert_eq!(parse_bytes_range("bytes=100-"), Some(vec![(100, None)]));
}
#[test]
fn range_one_bytes() {
assert_eq!(
parse_bytes_range("bytes=100-200"),
Some(vec![(100, Some(200))])
);
}
#[test]
fn range_multiple_ranges() {
assert_eq!(
parse_bytes_range("bytes=100-200,300-400"),
Some(vec![(100, Some(200)), (300, Some(400))])
);
}
#[test]
fn data_responser_new_with_start_and_end() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((2, Some(7))));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 2);
assert_eq!(responser.end_offset, 7);
}
#[test]
fn data_responser_new_with_start_only() {
let data = vec![1, 2, 3, 4, 5];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((3, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 3);
assert_eq!(responser.end_offset, 5);
}
#[test]
fn data_responser_new_with_zero_start() {
let data = vec![1, 2, 3];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((0, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 0);
assert_eq!(responser.end_offset, 2);
}
#[test]
fn data_responser_new_with_empty_data() {
let data = vec![];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((0, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 0);
assert_eq!(responser.end_offset, 0);
}
#[test]
fn data_responser_new_with_start_beyond_data_length() {
let data = vec![1, 2, 3];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((5, None)));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 5);
assert_eq!(responser.end_offset, 3);
}
#[test]
fn data_responser_new_with_end_beyond_data_length() {
let data = vec![1, 2, 3];
let mut responser = DataResponser::default();
responser.prepare(data.clone(), &Some((1, Some(10))));
assert_eq!(responser.data, data);
assert_eq!(responser.offset, 1);
assert_eq!(responser.end_offset, 10);
}
#[test]
fn data_responser_read_no_end_data_smaller_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((2, None)));
let result = responser.read(10);
assert_eq!(result, Some(&[3, 4, 5][..]));
}
#[test]
fn data_responser_read_no_end_data_larger_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((2, None)));
let result1 = responser.read(3);
assert_eq!(result1, Some(&[3, 4, 5][..]));
let result2 = responser.read(3);
assert_eq!(result2, Some(&[6, 7, 8][..]));
}
#[test]
fn data_responser_read_with_end_data_smaller_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((2, Some(6))));
let result = responser.read(10);
assert_eq!(result, Some(&[3, 4, 5, 6][..]));
}
#[test]
fn data_responser_read_with_end_data_larger_than_bytes_to_read() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((1, Some(7))));
let result1 = responser.read(3);
assert_eq!(result1, Some(&[2, 3, 4][..]));
let result2 = responser.read(3);
assert_eq!(result2, Some(&[5, 6, 7][..]));
}
#[test]
fn data_responser_read_consecutive_calls_until_end() {
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
let mut responser = DataResponser::default();
responser.prepare(data, &Some((1, Some(6))));
let result1 = responser.read(2);
assert_eq!(result1, Some(&[2, 3][..]));
let result2 = responser.read(2);
assert_eq!(result2, Some(&[4, 5][..]));
let result3 = responser.read(2);
assert_eq!(result3, Some(&[6][..]));
let result4 = responser.read(2);
assert_eq!(result4, None);
}
}

View File

@@ -0,0 +1,236 @@
use crate::prelude::CefResponse;
#[derive(Clone, Default, Debug)]
pub struct HeadersResponser {
pub mime_type: String,
pub status_code: u32,
pub headers: Vec<(String, String)>,
pub response_length: usize,
}
impl HeadersResponser {
pub fn prepare(&mut self, cef_response: &CefResponse, range: &Option<(usize, Option<usize>)>) {
self.mime_type = cef_response.mime_type.clone();
self.status_code = if range.is_some() {
206 // Partial Content
} else {
cef_response.status_code
};
self.headers.clear();
self.response_length = obtain_response_length(&cef_response.data, range);
if let Some(content_range) = content_range_header_value(&cef_response.data, range) {
self.headers
.push(("Content-Range".to_string(), content_range));
self.headers
.push(("Accept-Ranges".to_string(), "bytes".to_string()));
}
}
}
/// Create a `Content-Range` header value based on the provided data and range.
///
/// If the range is `None`, since the request type is not a range request, it returns `None`.
///
/// ## Reference
///
/// - [206 Partial Content](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/206)
fn content_range_header_value(
data: &[u8],
range: &Option<(usize, Option<usize>)>,
) -> Option<String> {
let (start, end) = range.as_ref()?;
Some(format!(
"bytes {}-{}/{}",
start,
end.unwrap_or(data.len() - 1),
data.len()
))
}
fn obtain_response_length(data: &[u8], range: &Option<(usize, Option<usize>)>) -> usize {
match range {
Some((start, end)) => end.unwrap_or(data.len() - 1) - start + 1,
None => data.len(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::utils::default;
#[test]
fn test_obtain_response_length_no_range() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &None);
assert_eq!(result, 13);
}
#[test]
fn test_obtain_response_length_empty_data_no_range() {
let data = b"";
let result = obtain_response_length(data, &None);
assert_eq!(result, 0);
}
#[test]
fn test_obtain_response_length_range_with_end() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((0, Some(5))));
assert_eq!(result, 5);
}
#[test]
fn test_obtain_response_length_range_partial() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((7, Some(12))));
assert_eq!(result, 5);
}
#[test]
fn test_obtain_response_length_range_without_end() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((7, None)));
assert_eq!(result, 6);
}
#[test]
fn test_obtain_response_length_range_from_start() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((0, None)));
assert_eq!(result, 13);
}
#[test]
fn test_obtain_response_length_range_zero_length() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((5, Some(5))));
assert_eq!(result, 0);
}
#[test]
fn test_obtain_response_length_range_end_equals_data_len() {
let data = b"Hello, World!";
let result = obtain_response_length(data, &Some((0, Some(13))));
assert_eq!(result, 13);
}
#[test]
fn test_obtain_response_length_empty_data_with_range() {
let data = b"";
let result = obtain_response_length(data, &Some((0, None)));
assert_eq!(result, 0);
}
#[test]
fn test_obtain_response_length_large_data() {
let data = vec![0u8; 1024];
let result = obtain_response_length(&data, &None);
assert_eq!(result, 1024);
}
#[test]
fn test_obtain_response_length_large_data_with_range() {
let data = vec![0u8; 1024];
let result = obtain_response_length(&data, &Some((100, Some(200))));
assert_eq!(result, 100);
}
#[test]
fn test_content_range_header_value_no_range() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &None);
assert_eq!(result, None);
}
#[test]
fn test_content_range_header_value_range_with_end() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((0, Some(5))));
assert_eq!(result, Some("bytes 0-4/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_without_end() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((7, None)));
assert_eq!(result, Some("bytes 7-12/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_from_start() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((0, None)));
assert_eq!(result, Some("bytes 0-12/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_partial() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((7, Some(12))));
assert_eq!(result, Some("bytes 7-11/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_single_byte() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((5, Some(6))));
assert_eq!(result, Some("bytes 5-5/13".to_string()));
}
#[test]
fn test_content_range_header_value_range_last_byte() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((12, Some(13))));
assert_eq!(result, Some("bytes 12-12/13".to_string()));
}
#[test]
fn test_content_range_header_value_single_byte_data() {
let data = b"a";
let result = content_range_header_value(data, &Some((0, None)));
assert_eq!(result, Some("bytes 0-0/1".to_string()));
}
#[test]
fn test_content_range_header_value_large_data() {
let data = vec![0u8; 1024];
let result = content_range_header_value(&data, &Some((100, Some(200))));
assert_eq!(result, Some("bytes 100-199/1024".to_string()));
}
#[test]
fn test_content_range_header_value_large_data_no_end() {
let data = vec![0u8; 1024];
let result = content_range_header_value(&data, &Some((500, None)));
assert_eq!(result, Some("bytes 500-1023/1024".to_string()));
}
#[test]
fn test_content_range_header_value_zero_start() {
let data = b"test";
let result = content_range_header_value(data, &Some((0, Some(2))));
assert_eq!(result, Some("bytes 0-1/4".to_string()));
}
#[test]
fn test_content_range_header_value_range_end_equals_data_len() {
let data = b"Hello, World!";
let result = content_range_header_value(data, &Some((0, Some(13))));
assert_eq!(result, Some("bytes 0-12/13".to_string()));
}
#[test]
fn status_code_is_206_for_partial_content() {
let data = b"Hello, World!";
let mut headers_responser = HeadersResponser::default();
headers_responser.prepare(
&CefResponse {
data: data.to_vec(),
..default()
},
&Some((0, Some(5))),
);
assert_eq!(headers_responser.status_code, 206);
}
}

View File

@@ -0,0 +1,167 @@
use async_channel::{Receiver, Sender};
use bevy::prelude::{Entity, Event};
use cef::rc::{Rc, RcImpl};
use cef::{
Browser, CefString, ImplRenderHandler, PaintElementType, Range, Rect, RenderHandler,
WrapRenderHandler, sys,
};
use cef_dll_sys::cef_paint_element_type_t;
use std::cell::Cell;
use std::os::raw::c_int;
pub type TextureSender = Sender<RenderTexture>;
pub type TextureReceiver = Receiver<RenderTexture>;
/// The texture structure passed from [`CefRenderHandler::OnPaint`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRenderHandler.html#a6547d5c9dd472e6b84706dc81d3f1741).
#[derive(Debug, Clone, PartialEq, Event)]
pub struct RenderTexture {
/// The entity of target rendering webview.
pub webview: Entity,
/// The type of the paint element.
pub ty: RenderPaintElementType,
/// The width of the texture.
pub width: u32,
/// The height of the texture.
pub height: u32,
/// This buffer will be `width` *`height` * 4 bytes in size and represents a BGRA image with an upper-left origin
pub buffer: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenderPaintElementType {
/// The main frame of the browser.
View,
/// The popup frame of the browser.
Popup,
}
pub type SharedViewSize = std::rc::Rc<Cell<bevy::prelude::Vec2>>;
pub type SharedImeCaret = std::rc::Rc<Cell<u32>>;
/// ## Reference
///
/// - [`CefRenderHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRenderHandler.html)
pub struct RenderHandlerBuilder {
object: *mut RcImpl<sys::cef_render_handler_t, Self>,
webview: Entity,
texture_sender: TextureSender,
size: SharedViewSize,
ime_caret: SharedImeCaret,
}
impl RenderHandlerBuilder {
pub fn build(
webview: Entity,
texture_sender: TextureSender,
size: SharedViewSize,
ime_caret: SharedImeCaret,
) -> RenderHandler {
RenderHandler::new(Self {
object: std::ptr::null_mut(),
webview,
texture_sender,
size,
ime_caret,
})
}
}
impl Rc for RenderHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl WrapRenderHandler for RenderHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_render_handler_t, Self>) {
self.object = object;
}
}
impl Clone for RenderHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self {
object,
webview: self.webview,
texture_sender: self.texture_sender.clone(),
size: self.size.clone(),
ime_caret: self.ime_caret.clone(),
}
}
}
impl ImplRenderHandler for RenderHandlerBuilder {
fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) {
if let Some(rect) = rect {
let size = self.size.get();
rect.width = size.x as _;
rect.height = size.y as _;
}
}
fn on_text_selection_changed(
&self,
_browser: Option<&mut Browser>,
_: Option<&CefString>,
selected_range: Option<&Range>,
) {
if let Some(selected_range) = selected_range {
self.ime_caret.set(selected_range.to);
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn on_paint(
&self,
_browser: Option<&mut Browser>,
type_: PaintElementType,
_dirty_rects_count: usize,
_dirty_rects: Option<&Rect>,
buffer: *const u8,
width: c_int,
height: c_int,
) {
let ty = match type_.as_ref() {
cef_paint_element_type_t::PET_POPUP => RenderPaintElementType::Popup,
_ => RenderPaintElementType::View,
};
let texture = RenderTexture {
webview: self.webview,
ty,
width: width as u32,
height: height as u32,
buffer: unsafe {
std::slice::from_raw_parts(buffer, (width * height * 4) as usize).to_vec()
},
};
let _ = self.texture_sender.send_blocking(texture);
}
/// MEMO: This method only supports on Windows
///
/// In Windows, this method is more performant than `on_paint`?
#[cfg(target_os = "windows")]
fn on_accelerated_paint(
&self,
_browser: Option<&mut Browser>,
_type_: PaintElementType,
_dirty_rects_count: usize,
_dirty_rects: Option<&Rect>,
_: Option<&AcceleratedPaintInfo>,
) {
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_render_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,50 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplRequestContextHandler, WrapRequestContextHandler, sys};
/// ## Reference
///
/// - [`CefRequestContextHandler Class Reference`](https://cef-builds.spotifycdn.com/docs/106.1/classCefRequestContextHandler.html)
pub struct RequestContextHandlerBuilder {
object: *mut RcImpl<sys::cef_request_context_handler_t, Self>,
}
impl RequestContextHandlerBuilder {
pub fn build() -> cef::RequestContextHandler {
cef::RequestContextHandler::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl WrapRequestContextHandler for RequestContextHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_request_context_handler_t, Self>) {
self.object = object;
}
}
impl Rc for RequestContextHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
core::mem::transmute(&base.cef_object)
}
}
}
impl Clone for RequestContextHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplRequestContextHandler for RequestContextHandlerBuilder {
#[inline]
fn get_raw(&self) -> *mut sys::_cef_request_context_handler_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,52 @@
use cef::{load_library, unload_library};
use std::env::home_dir;
/// This loader is a modified version of [LibraryLoader](cef::library_loader::LibraryLoader) that can load the framework located in the home directory.
pub struct DebugLibraryLoader {
path: std::path::PathBuf,
}
impl Default for DebugLibraryLoader {
fn default() -> Self {
Self::new()
}
}
impl DebugLibraryLoader {
const FRAMEWORK_PATH: &'static str =
"Chromium Embedded Framework.framework/Chromium Embedded Framework";
pub fn new() -> Self {
let path = home_dir()
.unwrap()
.join(".local")
.join("share")
.join("cef")
.join(Self::FRAMEWORK_PATH)
.canonicalize()
.unwrap();
Self { path }
}
// See [cef_load_library] for more documentation.
pub fn load(&self) -> bool {
Self::load_library(&self.path)
}
fn load_library(name: &std::path::Path) -> bool {
use std::os::unix::ffi::OsStrExt;
let Ok(name) = std::ffi::CString::new(name.as_os_str().as_bytes()) else {
return false;
};
unsafe { load_library(Some(&*name.as_ptr().cast())) == 1 }
}
}
impl Drop for DebugLibraryLoader {
fn drop(&mut self) {
if unload_library() != 1 {
eprintln!("cannot unload framework {}", self.path.display());
}
}
}

View File

@@ -0,0 +1,15 @@
mod browser_process;
mod debug;
mod render_process;
mod util;
pub mod prelude {
pub use crate::browser_process::*;
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::*;
}

View File

@@ -0,0 +1,28 @@
use crate::prelude::RenderProcessAppBuilder;
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 render_process_handler;
/// Execute the CEF render process.
pub fn execute_render_process() {
let args = Args::new();
#[cfg(target_os = "macos")]
let _loader = {
let loader =
cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true);
assert!(loader.load());
loader
};
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let mut app = RenderProcessAppBuilder::build();
execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
}

View File

@@ -0,0 +1,63 @@
use crate::prelude::RenderProcessHandlerBuilder;
use crate::util::{SCHEME_CEF, cef_scheme_flags};
use cef::rc::{Rc, RcImpl};
use cef::{ImplApp, ImplSchemeRegistrar, RenderProcessHandler, SchemeRegistrar, WrapApp};
use cef_dll_sys::{_cef_app_t, cef_base_ref_counted_t};
#[derive(Default)]
pub struct RenderProcessAppBuilder {
object: *mut RcImpl<_cef_app_t, Self>,
}
impl RenderProcessAppBuilder {
pub fn build() -> cef::App {
cef::App::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Clone for RenderProcessAppBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
self.object
};
Self { object }
}
}
impl Rc for RenderProcessAppBuilder {
fn as_base(&self) -> &cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl ImplApp for RenderProcessAppBuilder {
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
Some(RenderProcessHandler::new(
RenderProcessHandlerBuilder::build(),
))
}
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
registrar.add_custom_scheme(Some(&SCHEME_CEF.into()), cef_scheme_flags() as _);
}
}
#[inline]
fn get_raw(&self) -> *mut _cef_app_t {
self.object as *mut _cef_app_t
}
}
impl WrapApp for RenderProcessAppBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_app_t, Self>) {
self.object = object;
}
}

View File

@@ -0,0 +1,181 @@
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
}

View File

@@ -0,0 +1,82 @@
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()
}
}

View File

@@ -0,0 +1,77 @@
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()
}
}

View File

@@ -0,0 +1,191 @@
use crate::prelude::{EmitBuilder, IntoString};
use crate::render_process::brp::BrpBuilder;
use crate::render_process::listen::ListenBuilder;
use crate::util::json_to_v8;
use crate::util::v8_accessor::V8DefaultAccessorBuilder;
use crate::util::v8_interceptor::V8DefaultInterceptorBuilder;
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,
};
use std::os::raw::c_int;
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());
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";
pub struct RenderProcessHandlerBuilder {
object: *mut RcImpl<sys::_cef_render_process_handler_t, Self>,
}
impl RenderProcessHandlerBuilder {
pub fn build() -> RenderProcessHandlerBuilder {
RenderProcessHandlerBuilder {
object: core::ptr::null_mut(),
}
}
}
impl WrapRenderProcessHandler for RenderProcessHandlerBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_render_process_handler_t, Self>) {
self.object = object;
}
}
impl Rc for RenderProcessHandlerBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl Clone for RenderProcessHandlerBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplRenderProcessHandler for RenderProcessHandlerBuilder {
fn on_context_created(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
) {
if let Some(g) = context.and_then(|c| c.global())
&& 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())),
)
{
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 on_process_message_received(
&self,
_browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
_: ProcessId,
message: Option<&mut ProcessMessage>,
) -> c_int {
if let Some(message) = message
&& let Some(frame) = frame
&& let Some(ctx) = frame.v8_context()
{
match message.name().into_string().as_str() {
PROCESS_MESSAGE_BRP => {
handle_brp_message(message, ctx);
}
PROCESS_MESSAGE_HOST_EMIT => {
handle_listen_message(message, ctx);
}
_ => {}
}
};
1
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_render_process_handler_t {
self.object.cast()
}
}
fn handle_brp_message(message: &ProcessMessage, ctx: V8Context) {
let Some(argument_list) = message.argument_list() else {
return;
};
let id = argument_list.string(0).into_string();
let payload = argument_list.string(1).into_string();
let Ok(Some(promise)) = BRP_PROMISES.lock().map(|mut p| p.remove(&id)) else {
return;
};
if let Ok(brp_result) = serde_json::from_str::<BrpResult>(&payload) {
ctx.enter();
match brp_result {
Ok(v) => {
promise.resolve_promise(json_to_v8(v).as_mut());
}
Err(e) => {
promise.reject_promise(Some(&e.message.as_str().into()));
}
}
ctx.exit();
}
}
fn handle_listen_message(message: &ProcessMessage, mut ctx: V8Context) {
let Some(argument_list) = message.argument_list() else {
return;
};
let id = argument_list.string(0).into_string();
let payload = argument_list.string(1).into_string();
ctx.enter();
if let Ok(value) = serde_json::from_str::<serde_json::Value>(&payload)
&& let Ok(events) = LISTEN_EVENTS.lock()
{
let mut obj = v8_value_create_object(
Some(&mut V8DefaultAccessorBuilder::build()),
Some(&mut V8DefaultInterceptorBuilder::build()),
);
let Some(callback) = events.get(&id) else {
return;
};
callback.execute_function_with_context(
Some(&mut ctx),
obj.as_mut(),
Some(&[json_to_v8(value)]),
);
}
ctx.exit();
}

View File

@@ -0,0 +1,140 @@
pub mod v8_accessor;
mod v8_handler_wrapper;
pub mod v8_interceptor;
use crate::util::v8_accessor::V8DefaultAccessorBuilder;
use crate::util::v8_interceptor::V8DefaultInterceptorBuilder;
use cef::rc::ConvertParam;
use cef::{
CefStringList, CefStringUserfreeUtf16, CefStringUtf16, ImplV8Value, V8Propertyattribute,
v8_value_create_array, v8_value_create_bool, v8_value_create_double, v8_value_create_int,
v8_value_create_null, v8_value_create_object, v8_value_create_string,
};
use cef_dll_sys::_cef_string_utf16_t;
use cef_dll_sys::cef_scheme_options_t::{
CEF_SCHEME_OPTION_CORS_ENABLED, CEF_SCHEME_OPTION_LOCAL, CEF_SCHEME_OPTION_SECURE,
CEF_SCHEME_OPTION_STANDARD,
};
use std::env::home_dir;
use std::path::PathBuf;
pub const SCHEME_CEF: &str = "cef";
pub const HOST_CEF: &str = "localhost";
pub fn cef_scheme_flags() -> u32 {
CEF_SCHEME_OPTION_STANDARD as u32
| CEF_SCHEME_OPTION_SECURE as u32
| CEF_SCHEME_OPTION_LOCAL as u32
| CEF_SCHEME_OPTION_CORS_ENABLED as u32
}
pub fn debug_chromium_libraries_path() -> PathBuf {
debug_chromium_embedded_framework_dir_path().join("Libraries")
}
pub fn debug_chromium_embedded_framework_dir_path() -> PathBuf {
debug_cef_path().join("Chromium Embedded Framework.framework")
}
pub fn debug_cef_path() -> PathBuf {
home_dir().unwrap().join(".local").join("share").join("cef")
}
pub fn debug_render_process_path() -> PathBuf {
cargo_bin_path().join("bevy_cef_debug_render_process")
}
pub fn cargo_bin_path() -> PathBuf {
home_dir().unwrap().join(".cargo").join("bin")
}
pub trait IntoString {
fn into_string(self) -> String;
}
impl IntoString for CefStringUserfreeUtf16 {
fn into_string(self) -> String {
let ptr: *mut _cef_string_utf16_t = self.into_raw();
CefStringUtf16::from(ptr).to_string()
}
}
pub fn v8_value_to_json(v8: &cef::V8Value) -> Option<serde_json::Value> {
if v8.is_bool().is_positive() {
Some(serde_json::Value::Bool(v8.bool_value().is_positive()))
} else if v8.is_int().is_positive() {
Some(serde_json::Value::Number(serde_json::Number::from(
v8.int_value(),
)))
} else if v8.is_double().is_positive() {
Some(serde_json::Value::Number(
serde_json::Number::from_f64(v8.double_value()).unwrap(),
))
} else if v8.is_string().is_positive() {
Some(serde_json::Value::String(v8.string_value().into_string()))
} else if v8.is_null().is_positive() || v8.is_undefined().is_positive() {
Some(serde_json::Value::Null)
} else if v8.is_array().is_positive() {
let mut array = Vec::new();
let mut keys = CefStringList::new();
v8.keys(Some(&mut keys));
for key in keys.into_iter() {
if let Some(v) = v8.value_bykey(Some(&key.as_str().into()))
&& let Some(serialized) = v8_value_to_json(&v)
{
{
array.push(serialized);
}
}
}
Some(serde_json::Value::Array(array))
} else if v8.is_object().is_positive() {
let mut object = serde_json::Map::new();
let mut keys = CefStringList::new();
v8.keys(Some(&mut keys));
for key in keys.into_iter() {
if let Some(v) = v8.value_bykey(Some(&key.as_str().into()))
&& let Some(serialized) = v8_value_to_json(&v)
{
{
object.insert(key, serialized);
}
}
}
Some(serde_json::Value::Object(object))
} else {
None
}
}
pub fn json_to_v8(v: serde_json::Value) -> Option<cef::V8Value> {
match v {
serde_json::Value::Null => v8_value_create_null(),
serde_json::Value::Bool(b) => v8_value_create_bool(b as _),
serde_json::Value::Number(n) if n.is_i64() => v8_value_create_int(n.as_i64()? as i32),
serde_json::Value::Number(n) => v8_value_create_double(n.as_f64()?),
serde_json::Value::String(s) => v8_value_create_string(Some(&s.as_str().into())),
serde_json::Value::Array(arr) => {
let v8_array = v8_value_create_array(arr.len() as _)?;
for (i, item) in arr.into_iter().enumerate() {
v8_array.set_value_byindex(i as _, json_to_v8(item).as_mut());
}
Some(v8_array)
}
serde_json::Value::Object(obj) => {
let v8_object = v8_value_create_object(
Some(&mut V8DefaultAccessorBuilder::build()),
Some(&mut V8DefaultInterceptorBuilder::build()),
)?;
for (key, value) in obj {
v8_object.set_value_bykey(
Some(&key.as_str().into()),
json_to_v8(value).as_mut(),
V8Propertyattribute::default(),
);
}
Some(v8_object)
}
}
}

View File

@@ -0,0 +1,48 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplV8Accessor, V8Accessor, WrapV8Accessor, sys};
use cef_dll_sys::_cef_v8_accessor_t;
pub struct V8DefaultAccessorBuilder {
object: *mut RcImpl<_cef_v8_accessor_t, Self>,
}
impl V8DefaultAccessorBuilder {
pub fn build() -> V8Accessor {
V8Accessor::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl Rc for V8DefaultAccessorBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl Clone for V8DefaultAccessorBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl WrapV8Accessor for V8DefaultAccessorBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_v8_accessor_t, Self>) {
self.object = object;
}
}
impl ImplV8Accessor for V8DefaultAccessorBuilder {
#[inline]
fn get_raw(&self) -> *mut _cef_v8_accessor_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,70 @@
// use std::os::raw::c_int;
// use bevy::ecs::reflect::ReflectCommandExt;
// use cef::rc::{Rc, RcImpl};
// use cef::{sys, CefString, ImplV8Handler, V8Handler, V8Value, WrapV8Handler};
// use cef_dll_sys::{_cef_v8_handler_t, _cef_v8_value_t, cef_string_t};
//
// pub struct V8HandlerWrapBuilder<T> {
// base: T,
// }
//
// impl<T> V8HandlerWrapBuilder<T> {
// pub fn build(base: T) -> V8Handler {
// V8Handler::new(Self {
// base,
// })
// }
// }
//
// impl<T: Rc> Rc for V8HandlerWrapBuilder<T> {
// fn as_base(&self) -> &sys::cef_base_ref_counted_t {
// self.base.as_base()
// }
// }
//
// impl<T: ImplV8Handler> ImplV8Handler for V8HandlerWrapBuilder<T> {
// fn execute(&self, name: Option<&CefString>, object: Option<&mut V8Value>, arguments: Option<&[Option<V8Value>]>, retval: Option<&mut Option<V8Value>>, exception: Option<&mut CefString>) -> c_int {
// self.base.execute(
// name,
// object,
// arguments,
// retval,
// exception,
// )
// }
//
// fn get_raw(&self) -> *mut _cef_v8_handler_t {
// self.base.get_raw()
// }
//
// fn init_methods(object: &mut _cef_v8_handler_t) {
// T::init_methods(object);
//
// }
// }
//
// impl<T: Clone> Clone for V8HandlerWrapBuilder<T> {
// fn clone(&self) -> Self {
// Self{
// base: self.base.clone(),
// }
// }
// }
//
// impl <T: WrapV8Handler> cef::WrapV8Handler for V8HandlerWrapBuilder<T> {
// fn wrap_rc(&mut self, object: *mut RcImpl<sys::_cef_v8_handler_t, Self>) {
// self.base.wrap_rc(object);
// }
// }
//
// // 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,
// // ) -> ::std::os::raw::c_int {
// //
// // }

View File

@@ -0,0 +1,48 @@
use cef::rc::{Rc, RcImpl};
use cef::{ImplV8Interceptor, V8Interceptor, WrapV8Interceptor, sys};
use cef_dll_sys::_cef_v8_interceptor_t;
pub struct V8DefaultInterceptorBuilder {
object: *mut RcImpl<_cef_v8_interceptor_t, Self>,
}
impl V8DefaultInterceptorBuilder {
pub fn build() -> V8Interceptor {
V8Interceptor::new(Self {
object: core::ptr::null_mut(),
})
}
}
impl WrapV8Interceptor for V8DefaultInterceptorBuilder {
fn wrap_rc(&mut self, object: *mut RcImpl<_cef_v8_interceptor_t, Self>) {
self.object = object;
}
}
impl Rc for V8DefaultInterceptorBuilder {
fn as_base(&self) -> &sys::cef_base_ref_counted_t {
unsafe {
let base = &*self.object;
std::mem::transmute(&base.cef_object)
}
}
}
impl Clone for V8DefaultInterceptorBuilder {
fn clone(&self) -> Self {
let object = unsafe {
let rc_impl = &mut *self.object;
rc_impl.interface.add_ref();
rc_impl
};
Self { object }
}
}
impl ImplV8Interceptor for V8DefaultInterceptorBuilder {
#[inline]
fn get_raw(&self) -> *mut _cef_v8_interceptor_t {
self.object.cast()
}
}

View File

@@ -0,0 +1,12 @@
[package]
name = "bevy_cef_debug_render_process"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
[dependencies]
cef = { workspace = true }
bevy_cef_core = { workspace = true }
libloading = { version = "0.8"}

View File

@@ -0,0 +1,12 @@
# bevy_cef_debug_render_process
A debug render process for [bevy_cef](ttps://github.com/not-elm/bevy_cef)
This application is a development tool to build CEF on macOS without requiring an app bundle,
and if you create a release bundle, should create a separate render process.
## Install
```shell
> cargo install bevy_cef_debug_render_process
```

View File

@@ -0,0 +1,19 @@
use bevy_cef_core::prelude::{DebugLibraryLoader, RenderProcessAppBuilder};
use cef::{args::Args, *};
fn main() {
let args = Args::new();
#[cfg(target_os = "macos")]
let _loader = {
let loader = DebugLibraryLoader::new();
assert!(loader.load());
loader
};
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let mut app = RenderProcessAppBuilder::build();
execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
}

5
docs/book.toml Normal file
View File

@@ -0,0 +1,5 @@
[book]
authors = ["not-elm"]
language = "en"
src = "src"
title = "bevy_cef"

91
docs/src/README.md Normal file
View File

@@ -0,0 +1,91 @@
# Introduction to bevy_cef
**bevy_cef** is a powerful Bevy plugin that integrates the Chromium Embedded Framework (CEF) into Bevy applications, enabling you to render web content as 3D objects in your game world or as UI overlays.
## What is bevy_cef?
bevy_cef bridges the gap between modern web technologies and Bevy's 3D engine by:
- **Embedding webviews** as textures on 3D meshes or 2D sprites
- **Supporting bidirectional communication** between JavaScript and Bevy systems
- **Providing a multi-process architecture** for stability and performance
- **Offering local asset serving** through a custom URL scheme
- **Enabling developer tools integration** for debugging web content
## Key Features
### 🌐 Web Content Rendering
- Render any web page as a texture on 3D objects
- Support for HTML5, CSS3, and modern JavaScript
- Local file serving via the `cef://localhost/` scheme
- Remote web page loading with full browser compatibility
### 🔄 Inter-Process Communication (IPC)
- **JS Emit**: Send events from JavaScript to Bevy systems
- **Host Emit**: Send events from Bevy to JavaScript
- **Bevy Remote Protocol (BRP)**: Bidirectional RPC communication
### 🎮 Interactive Controls
- Keyboard input forwarding to webviews
- Mouse interaction support
- Navigation controls (back, forward, refresh)
- Zoom level management
- Audio muting capabilities
### 🔧 Developer Experience
- Chrome DevTools integration for debugging
- Hot-reload support for local assets
- Comprehensive error handling and logging
- Extensive customization options
## Architecture Overview
bevy_cef uses a multi-process architecture similar to modern web browsers:
- **Browser Process**: The main Bevy application process
- **Render Process**: Separate CEF process for web content rendering
- **IPC Communication**: Secure inter-process communication channels
This design ensures stability - if a web page crashes, it won't bring down your entire application.
## Use Cases
### Game UI
Create rich, responsive game interfaces using familiar web technologies:
```rust
commands.spawn((
CefWebviewUri::local("ui/main-menu.html"),
// Render as 2D sprite overlay
));
```
### In-World Displays
Embed interactive web content directly in your 3D world:
```rust
commands.spawn((
CefWebviewUri::new("https://example.com"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
```
### Data Visualization
Display real-time data using web-based charting libraries:
```rust
// Load a local HTML file with Chart.js or D3.js
commands.spawn((
CefWebviewUri::local("charts/dashboard.html"),
WebviewSize(Vec2::new(1920.0, 1080.0)),
));
```
### Development Tools
Integrate web-based development and debugging interfaces directly into your game editor or development build.
## Getting Started
Ready to integrate web content into your Bevy application? Check out the [Quick Start](quick-start.md) guide to get up and running in minutes, or dive into [Basic Concepts](basic-concepts.md) to understand the fundamental components and systems.
## Platform Support
Currently, bevy_cef focuses on macOS development with plans for expanded platform support. The plugin automatically handles CEF framework installation and configuration on supported platforms.

12
docs/src/SUMMARY.md Normal file
View File

@@ -0,0 +1,12 @@
# Summary
[Introduction](README.md)
# Getting Started
- [Installation](installation.md)
- [Quick Start](quick-start.md)
- [Basic Concepts](basic-concepts.md)
# User Guide
- [Core Components](core-components.md)

274
docs/src/basic-concepts.md Normal file
View File

@@ -0,0 +1,274 @@
# Basic Concepts
Understanding these fundamental concepts will help you make the most of bevy_cef in your projects.
## Multi-Process Architecture
bevy_cef follows a multi-process architecture similar to modern web browsers:
### Browser Process
- **Main Application**: Your Bevy game/app runs here
- **CEF Management**: Handles browser creation and management
- **IPC Coordination**: Manages communication with render processes
### Render Process
- **Web Content**: Each webview runs in a separate process
- **Isolation**: Crashes in web content don't affect your main application
- **Sandboxing**: Enhanced security through process separation
### Benefits
- **Stability**: Web content crashes won't crash your game
- **Performance**: CPU-intensive web content won't block your game loop
- **Security**: Sandboxed execution of untrusted web content
## Core Components
bevy_cef provides several key components that work together:
### CefWebviewUri
The primary component that defines what web content to display:
```rust
// Remote web page
CefWebviewUri::new("https://example.com")
// Local HTML file from assets/
CefWebviewUri::local("ui/menu.html")
// Equivalent to above
CefWebviewUri::new("cef://localhost/ui/menu.html")
```
### WebviewSize
Controls the rendering resolution of the webview (not the 3D object size):
```rust
WebviewSize(Vec2::new(1920.0, 1080.0)) // High resolution
WebviewSize(Vec2::new(800.0, 600.0)) // Standard resolution
WebviewSize::default() // 800x800 pixels
```
### Material Integration
Webviews integrate with Bevy's material system:
```rust
// Standard material with webview texture
WebviewExtendStandardMaterial::default()
// Custom material properties
WebviewExtendStandardMaterial {
base: StandardMaterial {
unlit: true,
emissive: Color::WHITE.into(),
..default()
},
..default()
}
```
## Component Requirements
When you add a `CefWebviewUri` component, bevy_cef automatically requires several other components:
```rust
#[require(WebviewSize, ZoomLevel, AudioMuted)]
pub struct CefWebviewUri(pub String);
```
This means every webview entity automatically gets:
- **WebviewSize**: Default 800x800 resolution
- **ZoomLevel**: Default zoom (0.0 = browser default)
- **AudioMuted**: Default unmuted (false)
## Rendering Modes
bevy_cef supports different rendering approaches:
### 3D Mesh Rendering
Render web content on 3D objects in your world:
```rust
commands.spawn((
CefWebviewUri::local("interface.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
```
### 2D Sprite Rendering
Render web content as UI overlays:
```rust
commands.spawn((
CefWebviewUri::local("hud.html"),
Sprite::default(),
WebviewSpriteMaterial::default(),
));
```
## Local Asset Serving
bevy_cef includes a built-in web server that serves files from your `assets/` directory:
### Custom Scheme
- **Scheme**: `cef://localhost/`
- **Path Mapping**: `assets/` directory maps to the root
- **Example**: `assets/ui/menu.html``cef://localhost/ui/menu.html`
### Supported File Types
- HTML, CSS, JavaScript
- Images (PNG, JPG, SVG, etc.)
- Fonts (WOFF, TTF, etc.)
- JSON, XML, and other data files
### Asset Organization
```
assets/
├── ui/
│ ├── menu.html
│ ├── styles.css
│ └── script.js
├── images/
│ └── logo.png
└── data/
└── config.json
```
## Inter-Process Communication (IPC)
bevy_cef provides three communication patterns:
### 1. JavaScript to Bevy (JS Emit)
Send events from web content to Bevy systems:
```javascript
// In your HTML/JavaScript
window.cef.emit('player_action', { action: 'jump', power: 10 });
```
```rust
fn main(){
App::new()
.add_plugins(JsEmitEventPlugin::<PlayerAction>::default())
// ...
}
// In your Bevy system
#[derive(Event, Deserialize)]
struct PlayerAction {
action: String,
power: i32,
}
fn handle_player_action(trigger: Trigger<PlayerAction>) {
let action = trigger.event();
info!("Player action: {} with power {}", action.action, action.power);
}
```
### 2. Bevy to JavaScript (Host Emit)
Send events from Bevy to web content:
```rust
// In your Bevy system
commands.entity(webview_entity).trigger(HostEmitEvent {
event_name: "score_update".to_string(),
data: json!({ "score": 1000, "level": 3 }),
});
```
```javascript
// In your HTML/JavaScript
window.cef.listen('score_update', (data) => {
document.getElementById('score').textContent = data.score;
document.getElementById('level').textContent = data.level;
});
```
### 3. Bevy Remote Protocol (BRP)
Please see [here](https://gist.github.com/coreh/1baf6f255d7e86e4be29874d00137d1d) for about the Bevy Remote Protocol (BRP).
Bidirectional RPC calls for complex interactions:
```rust
// Register BRP method in Bevy
app.add_plugins(RemotePlugin::default().with_method("get_player_stats", get_stats));
```
```javascript
// Call from JavaScript
const stats = await window.cef.brp({
method: 'get_player_stats',
params: { playerId: 42 }
});
```
## User Interaction
### Input Handling
Webviews automatically receive keyboard and mouse input when focused:
- **Keyboard**: All keyboard events are forwarded
- **Mouse**: Click, scroll, and hover events work naturally
- **Focus**: Multiple webviews can coexist; input goes to the focused one
### Navigation
Control web navigation programmatically:
```rust
commands.entity(webview).trigger(RequestGoBack);
commands.entity(webview).trigger(ReqeustGoForward);
```
### Zoom Control
Manage zoom levels per webview:
```rust
// Set zoom level (0.0 = default, positive = zoom in, negative = zoom out)
commands.entity(webview).insert(ZoomLevel(1.2));
// Reset to default zoom
commands.entity(webview).insert(ZoomLevel(0.0));
```
## Developer Experience
### Developer Tools
Access Chrome DevTools for debugging:
```rust
// Show developer tools for a webview
commands.entity(webview).trigger(RequestShowDevTool);
// Close developer tools
commands.entity(webview).trigger(RequestCloseDevtool);
```
### Hot Reload
Local assets automatically reload when changed during development.
## Best Practices
### Component Organization
```rust
// Good: Group related components
commands.spawn((
// The uri convert to `cef://localhost/index.html`
CefWebviewUri::local("index.html"),
WebviewSize(Vec2::new(1920.0, 200.0)),
ZoomLevel(0.8),
AudioMuted(true),
Transform::from_translation(Vec3::new(0.0, 5.0, 0.0)),
Mesh3d(meshes.add(Quad::new(Vec2::new(4.0, 1.0)))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
```
## Next Steps
Now that you understand the basic concepts:
- Explore [Core Components](core-components.md) in detail
- Learn about [Webview Rendering](webview-rendering.md) techniques
- Dive into [Inter-Process Communication](ipc.md) patterns
- Check out practical [Examples](examples/simple.md)

407
docs/src/core-components.md Normal file
View File

@@ -0,0 +1,407 @@
# Core Components
bevy_cef provides several essential components that control webview behavior. Understanding these components is crucial
for effective use of the library.
## Component Overview
| Component | Purpose | Default Value | Required |
|-----------------|-------------------------------|-----------------|--------------|
| `CefWebviewUri` | Specifies web content URL | None | ✅ Primary |
| `WebviewSize` | Controls rendering resolution | 800×800 | ✅ Auto-added |
| `ZoomLevel` | Controls webview zoom | 0.0 (default) | ✅ Auto-added |
| `AudioMuted` | Controls audio output | false (unmuted) | ✅ Auto-added |
| `HostWindow` | Parent window specification | Primary window | ❌ Optional |
## CefWebviewUri
The primary component that defines what web content to display.
### Usage
```rust
use bevy_cef::prelude::*;
// Remote web page
let webview = CefWebviewUri::new("https://example.com");
// Local HTML file from assets/ directory
let webview = CefWebviewUri::local("ui/menu.html");
// Equivalent to local() method
let webview = CefWebviewUri::new("cef://localhost/ui/menu.html");
```
### Implementation Details
```rust
#[derive(Component, Debug, Clone, PartialEq, Eq, Hash, Reflect)]
#[require(WebviewSize, ZoomLevel, AudioMuted)]
pub struct CefWebviewUri(pub String);
```
The `#[require(...)]` attribute ensures that every webview automatically gets the essential supporting components.
### Methods
- **`new(uri)`**: Create with any valid URL (http, https, cef://localhost/)
- **`local(path)`**: Create with local file path, automatically prefixed with `cef://localhost/`
### Local File Serving
When using local files, bevy_cef serves them through a custom scheme:
- **Scheme**: `cef://localhost/`
- **Root Directory**: Your project's `assets/` folder
- **Path Resolution**: Relative paths from assets/ root
**Example File Structure:**
```
assets/
├── index.html → cef://localhost/index.html
├── ui/
│ ├── menu.html → cef://localhost/ui/menu.html
│ └── styles.css → cef://localhost/ui/styles.css
└── js/
└── app.js → cef://localhost/js/app.js
```
## WebviewSize
Controls the internal rendering resolution of the webview, independent of the 3D object size.
### Usage
```rust
use bevy::math::Vec2;
// High resolution webview
WebviewSize(Vec2::new(1920.0, 1080.0))
// Standard resolution
WebviewSize(Vec2::new(800.0, 600.0))
// Square webview
WebviewSize(Vec2::splat(512.0))
// Default size
WebviewSize::default () // 800×800
```
### Performance Considerations
- **Higher Resolution**: Better quality, more memory usage
- **Lower Resolution**: Better performance, potential pixelation
- **Aspect Ratio**: Match your 3D mesh proportions for best results
```rust
// Example: Widescreen webview for cinematic content
commands.spawn((
CefWebviewUri::local("video-player.html"),
WebviewSize(Vec2::new(1920.0, 800.0)), // 21:9 aspect ratio
Mesh3d(meshes.add(Quad::new(Vec2::new(4.8, 2.0)))), // Match aspect in 3D
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default ())),
));
```
### Dynamic Resizing
You can change webview size at runtime:
```rust
fn resize_webview(
mut webviews: Query<&mut WebviewSize>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
if keyboard.just_pressed(KeyCode::KeyR) {
for mut size in webviews.iter_mut() {
size.0 *= 1.5; // Increase resolution by 50%
}
}
}
```
## ZoomLevel
Controls the zoom level of web content within the webview.
### Usage
```rust
// Default zoom (browser default)
ZoomLevel(0.0)
// Zoom in 20%
ZoomLevel(1.2)
// Zoom out 20%
ZoomLevel(0.8)
// Maximum zoom in
ZoomLevel(3.0)
// Maximum zoom out
ZoomLevel(0.25)
```
### Zoom Behavior
- **0.0**: Browser default zoom level
- **Positive values**: Zoom in (1.2 = 120% of default)
- **Negative values**: Zoom out (0.8 = 80% of default)
- **Range**: Typically 0.25 to 3.0 (25% to 300%)
### Dynamic Zoom Control
```rust
fn zoom_control(
mut webviews: Query<&mut ZoomLevel>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
for mut zoom in webviews.iter_mut() {
if keyboard.just_pressed(KeyCode::Equal) {
zoom.0 = (zoom.0 + 0.1).min(3.0); // Zoom in
}
if keyboard.just_pressed(KeyCode::Minus) {
zoom.0 = (zoom.0 - 0.1).max(0.25); // Zoom out
}
if keyboard.just_pressed(KeyCode::Digit0) {
zoom.0 = 0.0; // Reset zoom
}
}
}
```
### Use Cases
- **Accessibility**: Larger text for readability
- **Dense Content**: Fit more information in limited space
- **Responsive Design**: Adapt to different screen sizes
- **User Preference**: Allow users to adjust comfortable viewing size
## AudioMuted
Controls whether audio from the webview is muted.
### Usage
```rust
// Audio enabled (default)
AudioMuted(false)
// Audio muted
AudioMuted(true)
```
### Dynamic Audio Control
```rust
fn toggle_audio(
mut webviews: Query<&mut AudioMuted>,
keyboard: Res<ButtonInput<KeyCode>>,
) {
if keyboard.just_pressed(KeyCode::KeyM) {
for mut muted in webviews.iter_mut() {
muted.0 = !muted.0; // Toggle mute state
}
}
}
```
### Use Cases
- **Background Content**: Mute decorative webviews
- **Multiple Webviews**: Prevent audio conflicts
- **User Control**: Provide mute/unmute functionality
- **Game State**: Mute during pause or cutscenes
## HostWindow (Optional)
Specifies which Bevy window should be the parent of the webview. If not provided, the primary window is used.
### Usage
```rust
// Use primary window (default behavior)
commands.spawn((
CefWebviewUri::local("ui.html"),
// No HostWindow component needed
));
// Specify a particular window
commands.spawn((
CefWebviewUri::local("ui.html"),
HostWindow(secondary_window_entity),
));
```
### Multi-Window Applications
```rust
fn setup_multi_window(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
// Create secondary window
let secondary_window = commands.spawn(Window {
title: "Secondary Display".to_string(),
resolution: (800.0, 600.0).into(),
..default()
}).id();
// Main webview in primary window
commands.spawn((
CefWebviewUri::local("main-ui.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
// Secondary webview in secondary window
commands.spawn((
CefWebviewUri::local("secondary-ui.html"),
HostWindow(secondary_window),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}
```
## Component Combinations
### Common Patterns
**High-Resolution Interactive Display:**
```rust
commands.spawn((
CefWebviewUri::local("dashboard.html"),
WebviewSize(Vec2::new(2560.0, 1440.0)),
ZoomLevel(0.0),
AudioMuted(false),
));
```
**Compact Information Panel:**
```rust
commands.spawn((
CefWebviewUri::local("info-panel.html"),
WebviewSize(Vec2::new(400.0, 300.0)),
ZoomLevel(0.8),
AudioMuted(true),
));
```
**Video Player:**
```rust
commands.spawn((
CefWebviewUri::new("https://player.example.com"),
WebviewSize(Vec2::new(1920.0, 1080.0)),
ZoomLevel(0.0),
AudioMuted(false), // Keep audio for video
));
```
**Background Decoration:**
```rust
commands.spawn((
CefWebviewUri::local("animated-bg.html"),
WebviewSize(Vec2::new(1024.0, 1024.0)),
ZoomLevel(0.0),
AudioMuted(true), // No audio for decoration
));
```
## Component Lifecycle
### Automatic Requirements
When you add `CefWebviewUri`, the required components are automatically added with default values:
```rust
// You only need to specify this:
commands.spawn(CefWebviewUri::local("page.html"));
// But the entity automatically gets:
// - WebviewSize(Vec2::splat(800.0))
// - ZoomLevel(0.0)
// - AudioMuted(false)
```
### Manual Override
You can override the defaults by adding components explicitly:
```rust
commands.spawn((
CefWebviewUri::local("page.html"),
WebviewSize(Vec2::new(1024.0, 768.0)), // Override default
ZoomLevel(1.2), // Override default
AudioMuted(true), // Override default
));
```
### Runtime Modification
All components can be modified at runtime through standard Bevy systems:
```rust
fn modify_webview_properties(
mut query: Query<(&mut WebviewSize, &mut ZoomLevel, &mut AudioMuted)>,
time: Res<Time>,
) {
for (mut size, mut zoom, mut muted) in query.iter_mut() {
// Dynamic effects based on time, input, game state, etc.
let scale = (time.elapsed_secs().sin() + 1.0) / 2.0;
zoom.0 = 0.8 + scale * 0.4; // Oscillate between 0.8 and 1.2
}
}
```
## Best Practices
### Performance Optimization
```rust
// Good: Appropriate resolution for use case
WebviewSize(Vec2::new(800.0, 600.0)) // Standard UI
// Avoid: Excessive resolution unless needed
WebviewSize(Vec2::new(4096.0, 4096.0)) // Only for high-detail content
```
### Memory Management
```rust
// Good: Mute background content
commands.spawn((
CefWebviewUri::local("ambient-display.html"),
AudioMuted(true), // Prevent memory usage for audio processing
));
```
### User Experience
```rust
// Good: Consistent zoom across related webviews
let ui_zoom = ZoomLevel(1.1);
for ui_component in ["menu.html", "inventory.html", "settings.html"] {
commands.spawn((
CefWebviewUri::local(ui_component),
ui_zoom,
));
}
```
## Next Steps
- Learn about [Webview Rendering](webview-rendering.md) techniques
- Explore [Inter-Process Communication](ipc.md) patterns
- Check component-specific guides:
- [CefWebviewUri Details](core-components/webview-uri.md)
- [WebviewSize Details](core-components/webview-size.md)
- [HostWindow Details](core-components/host-window.md)

137
docs/src/installation.md Normal file
View File

@@ -0,0 +1,137 @@
# Installation
This guide will help you set up bevy_cef in your Bevy project.
## Prerequisites
### System Requirements
- **macOS**: Currently the primary supported platform
- **Rust**: 1.70 or later
- **Bevy**: 0.16 or later
## Adding bevy_cef to Your Project
### 1. Add the Dependency
Add bevy_cef to your `Cargo.toml`:
```toml
[dependencies]
bevy = "0.16"
bevy_cef = { version = "0.1", features = ["debug"] }
```
### 2. Enable the Plugin
Add the `CefPlugin` to your Bevy app:
```rust
use bevy::prelude::*;
use bevy_cef::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CefPlugin))
.run();
}
```
## Automatic Setup with Debug Feature
When you enable the `debug` feature (recommended for development), the build system automatically handles everything for you:
**Downloads** and extracts CEF framework to `$HOME/.local/share/cef`
**Installs** the `bevy_cef_debug_render_process` tool
**Installs** the `export-cef-dir` tool
**Configures** all necessary environment variables
Simply run your project:
```bash
cargo run
```
That's it! No manual installation required.
## Production Builds
For production builds where you want to minimize dependencies, you can omit the debug feature:
```toml
[dependencies]
bevy = "0.16"
bevy_cef = "0.1" # No debug feature
```
However, you'll need to ensure the CEF framework is available at runtime. The debug feature is recommended for most development scenarios.
## Verification
To verify your installation is working correctly, try running this simple example:
```rust
use bevy::prelude::*;
use bevy_cef::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CefPlugin))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
// Camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// Light
commands.spawn((
DirectionalLight::default(),
Transform::from_xyz(1.0, 1.0, 1.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// Webview
commands.spawn((
CefWebviewUri::new("https://bevy.org"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}
```
If you see the Bevy website rendered on a plane in your 3D scene, your installation is successful!
## Troubleshooting
### Common Installation Issues
#### Build Fails with Debug Feature
- Ensure you have internet connection for CEF download
- Check available disk space in `$HOME/.local/share/`
- Try cleaning and rebuilding: `cargo clean && cargo build`
#### CEF Framework Issues
- The debug feature handles CEF installation automatically
- If issues persist, delete `$HOME/.local/share/cef` and rebuild
#### Linker Errors on macOS
- Verify Xcode Command Line Tools are installed: `xcode-select --install`
- The debug feature should handle DYLD environment variables automatically
For more troubleshooting information, see the [Troubleshooting](troubleshooting/common-issues.md) section.
## Next Steps
Now that you have bevy_cef installed, check out:
- [Quick Start](quick-start.md) - Build your first webview in minutes
- [Basic Concepts](basic-concepts.md) - Understand the core components
- [Examples](examples/simple.md) - Explore practical examples

220
docs/src/quick-start.md Normal file
View File

@@ -0,0 +1,220 @@
# Quick Start
Get up and running with bevy_cef in just a few minutes! This guide will walk you through creating your first webview-enabled Bevy application.
## Create a New Project
Start by creating a new Bevy project:
```bash
cargo new my_webview_app
cd my_webview_app
```
## Add Dependencies
Add bevy_cef to your `Cargo.toml`:
```toml
[dependencies]
bevy = "0.16"
bevy_cef = { version = "0.1", features = ["debug"] }
```
## Your First Webview
Replace the contents of `src/main.rs` with:
```rust
use bevy::prelude::*;
use bevy_cef::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CefPlugin))
.add_systems(Startup, setup)
.run();
}
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
// Spawn a camera
commands.spawn((
Camera3d::default(),
Transform::from_translation(Vec3::new(0.0, 0.0, 3.0))
.looking_at(Vec3::ZERO, Vec3::Y),
));
// Spawn a light
commands.spawn((
DirectionalLight::default(),
Transform::from_translation(Vec3::new(1.0, 1.0, 1.0))
.looking_at(Vec3::ZERO, Vec3::Y),
));
// Spawn a webview on a 3D plane
commands.spawn((
CefWebviewUri::new("https://bevy.org"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}
```
## Run Your Application
```bash
cargo run
```
That's it! You should see the Bevy website rendered on a 3D plane in your application. The first run might take a moment as bevy_cef downloads and sets up the CEF framework automatically.
## What Just Happened?
Let's break down the key components:
### 1. CefPlugin
```rust
.add_plugins((DefaultPlugins, CefPlugin))
```
The `CefPlugin` initializes the CEF framework and sets up all necessary systems for webview rendering.
### 2. CefWebviewUri
```rust
CefWebviewUri::new("https://bevy.org")
```
This component specifies what web content to load. You can use:
- **Remote URLs**: `"https://example.com"`
- **Local files**: `CefWebviewUri::local("index.html")`
### 3. WebviewExtendStandardMaterial
```rust
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default()))
```
This special material renders the webview content as a texture on your 3D mesh.
## Try Local Content
Create an `assets/` directory and add a simple HTML file:
```bash
mkdir assets
```
Create `assets/hello.html`:
```html
<!DOCTYPE html>
<html>
<head>
<title>Hello bevy_cef!</title>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
color: white;
text-align: center;
padding: 50px;
}
</style>
</head>
<body>
<h1>Hello from bevy_cef! 🎮</h1>
<p>This HTML is rendered directly on a 3D mesh!</p>
<p>Current time: <span id="time"></span></p>
<script>
function updateTime() {
document.getElementById('time').textContent = new Date().toLocaleTimeString();
}
setInterval(updateTime, 1000);
updateTime();
</script>
</body>
</html>
```
Update your webview URI in `main.rs`:
```rust
CefWebviewUri::local("hello.html"), // Load local file
```
Run again:
```bash
cargo run
```
You'll see your custom HTML content with a live-updating clock!
## Next Steps
### Explore More Features
- **[JavaScript Communication](ipc/js-emit.md)**: Send data from web pages to Bevy
- **[Host Communication](ipc/host-emit.md)**: Send data from Bevy to web pages
- **[2D Sprites](webview-rendering/2d-sprite.md)**: Render webviews as UI elements
- **[Developer Tools](developer-tools.md)**: Debug your web content
### Try More Examples
- **Navigation**: Add back/forward buttons
- **Zoom Controls**: Implement zoom in/out functionality
- **Multiple Webviews**: Render different content on multiple objects
### Learn the Architecture
- **[Basic Concepts](basic-concepts.md)**: Understand core components
- **[Multi-Process Design](architecture/multi-process.md)**: How CEF integration works
- **[Plugin System](architecture/plugin-system.md)**: Deep dive into the plugin architecture
## Common First Steps
### Adding Interaction
Make your webview interactive by enabling input:
```rust
// In your setup system, also spawn input handling
commands.spawn((
CefWebviewUri::local("interactive.html"),
WebviewSize(Vec2::new(800.0, 600.0)),
// Webview will automatically receive keyboard and mouse input
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
```
### Sizing Your Webview
Control the resolution of your webview:
```rust
WebviewSize(Vec2::new(1920.0, 1080.0)), // High resolution
WebviewSize(Vec2::new(800.0, 600.0)), // Standard resolution
```
### Multiple Webviews
You can have multiple webviews in the same scene:
```rust
// First webview
commands.spawn((
CefWebviewUri::new("https://news.ycombinator.com"),
Transform::from_translation(Vec3::new(-2.0, 0.0, 0.0)),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
// Second webview
commands.spawn((
CefWebviewUri::local("dashboard.html"),
Transform::from_translation(Vec3::new(2.0, 0.0, 0.0)),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
```
You're now ready to build amazing applications that blend 3D graphics with modern web technologies!

BIN
examples/.DS_Store vendored Normal file

Binary file not shown.

88
examples/brp.rs Normal file
View File

@@ -0,0 +1,88 @@
//! Shows how to use Bevy Remote Protocol (BRP) with the webview.
//!
//! Please see [here](https://gist.github.com/coreh/1baf6f255d7e86e4be29874d00137d1d) for more about BRP.
use bevy::prelude::*;
use bevy::time::common_conditions::on_timer;
use bevy::window::PrimaryWindow;
use bevy_cef::prelude::*;
use bevy_remote::{BrpResult, RemotePlugin};
use cef::Window;
use serde::Deserialize;
use std::time::Duration;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
RemotePlugin::default().with_method("greet", greet),
CefPlugin,
))
.add_systems(
Startup,
(ime, spawn_camera, spawn_directional_light, spawn_webview),
)
.add_systems(
Update,
show_devtool.run_if(on_timer(Duration::from_secs(1))),
)
.run();
}
fn greet(In(name): In<Option<serde_json::Value>>) -> BrpResult {
let name = name.unwrap_or_default();
Ok(serde_json::Value::String(format!("Hello, {name}!")))
}
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("brp.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial {
base: StandardMaterial {
unlit: true,
emissive: Color::WHITE.into(),
..default()
},
..default()
})),
));
}
fn ime(mut windows: Query<&mut bevy::prelude::Window>) {
for mut window in windows.iter_mut() {
window.ime_enabled = true;
}
}
fn show_devtool(
mut commands: Commands,
webviews: Query<Entity, With<CefWebviewUri>>,
mut initialized: Local<bool>,
) {
if *initialized {
return;
}
*initialized = true;
for webview in webviews.iter() {
commands.entity(webview).trigger(RequestShowDevTool);
}
}

View File

@@ -0,0 +1,57 @@
//! You can create a custom material based on [`WebviewMaterial`].
//!
//! This example creates a custom material that blends an image.
use bevy::pbr::MaterialExtension;
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, ShaderRef};
use bevy_cef::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
CefPlugin,
WebviewExtendMaterialPlugin::<CustomExtension>::default(),
))
.add_systems(Startup, (spawn_camera, 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_webview(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendedMaterial<CustomExtension>>>,
asset_server: Res<AssetServer>,
) {
commands.spawn((
CefWebviewUri("https://bevy.org/".to_string()),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendedMaterial {
extension: CustomExtension {
mask: asset_server.load("images/rustacean-flat-gesture.png"),
},
..default()
})),
));
}
#[derive(Asset, Reflect, Clone, Debug, AsBindGroup, Default)]
struct CustomExtension {
#[texture(0)]
#[sampler(1)]
mask: Handle<Image>,
}
impl MaterialExtension for CustomExtension {
fn fragment_shader() -> ShaderRef {
"shaders/custom_material.wgsl".into()
}
}

5937
examples/demo/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
examples/demo/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "demo"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
publish = false
[dependencies]
bevy = { workspace = true, features = ["file_watcher"]}
bevy_remote = { workspace = true }
bevy_cef = { workspace = true, features = ["debug"] }

126
examples/demo/src/main.rs Normal file
View File

@@ -0,0 +1,126 @@
use bevy::input::common_conditions::input_pressed;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy_cef::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CefPlugin))
.add_systems(
Startup,
(
spawn_camera,
spawn_directional_light,
spawn_github_webview,
spawn_google_search_webview,
spawn_ground,
enable_ime,
),
)
.insert_resource(AmbientLight::default())
.add_systems(
Update,
(
walk::<1, 0>.run_if(input_pressed(KeyCode::ArrowRight)),
walk::<-1, 0>.run_if(input_pressed(KeyCode::ArrowLeft)),
walk::<0, 1>.run_if(input_pressed(KeyCode::ArrowUp)),
walk::<0, -1>.run_if(input_pressed(KeyCode::ArrowDown)),
rotate_camera::<1>.run_if(input_pressed(KeyCode::Digit1)),
rotate_camera::<-1>.run_if(input_pressed(KeyCode::Digit2)),
),
)
.run();
}
fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera3d::default());
}
fn spawn_directional_light(mut commands: Commands) {
commands.spawn((
DirectionalLight {
shadows_enabled: true,
..default()
},
Transform::from_translation(Vec3::new(1., 1., 1.)).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn enable_ime(mut primary_window: Query<&mut Window, With<PrimaryWindow>>) {
primary_window.single_mut().unwrap().ime_enabled = true;
}
fn spawn_github_webview(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri("https://github.com/not-elm".to_string()),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
WebviewSize(Vec2::splat(800.0)),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
Transform::from_translation(Vec3::new(1.5, 0., -4.0)),
));
}
fn spawn_google_search_webview(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri("https://www.youtube.com/".to_string()),
WebviewSize(Vec2::splat(800.0)),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
Transform::from_translation(Vec3::new(-1.5, 0., -4.0)), // .with_rotation(Quat::from_rotation_y(-90f32.to_radians())),
));
}
fn spawn_ground(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::new(10., 10.)))),
MeshMaterial3d(materials.add(StandardMaterial {
base_color: Color::srgba(0.8, 0.8, 0.8, 1.0),
..default()
})),
Transform::from_translation(Vec3::new(0., -2., 0.)),
));
}
fn walk<const X: isize, const Z: isize>(
mut q: Query<&mut Transform, With<Camera3d>>,
time: Res<Time>,
) {
for mut t in &mut q {
const SPEED: f32 = 1.5; // 調整可
let dt = time.delta_secs();
let up = Vec3::Y;
let mut f = t.forward().as_vec3();
f = (f - up * f.dot(up)).normalize_or_zero();
let r = f.cross(up).normalize_or_zero();
let input = Vec2::new(X as f32, Z as f32);
if input.length_squared() > 0.0 {
let dir = (r * input.x + f * input.y).normalize_or_zero();
t.translation += dir * SPEED * dt;
}
}
}
fn rotate_camera<const X: isize>(
mut transforms: Query<&mut Transform, With<Camera3d>>,
time: Res<Time>,
) {
for mut transform in transforms.iter_mut() {
const SPEED: f32 = 1.0;
let rotation = Quat::from_rotation_y(SPEED * time.delta_secs() * X as f32);
transform.rotation *= rotation;
}
}

75
examples/devtool.rs Normal file
View File

@@ -0,0 +1,75 @@
//! Shows how to use devtools.
//!
//! ## Key Bindings
//! - `Q`: Show DevTool
//! - `E`: Close DevTool
use bevy::input::common_conditions::input_just_pressed;
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),
)
.add_systems(
Update,
(
show_devtool.run_if(input_just_pressed(KeyCode::KeyQ)),
close_devtool.run_if(input_just_pressed(KeyCode::KeyE)),
),
)
.run();
}
#[derive(Component)]
struct DebugWebview;
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((
DebugWebview,
CefWebviewUri("https://bevy.org/".to_string()),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial {
base: StandardMaterial {
unlit: true,
emissive: LinearRgba::WHITE,
..default()
},
..default()
})),
));
}
fn show_devtool(mut commands: Commands, webviews: Query<Entity, With<DebugWebview>>) {
commands
.entity(webviews.single().unwrap())
.trigger(RequestShowDevTool);
}
fn close_devtool(mut commands: Commands, webviews: Query<Entity, With<DebugWebview>>) {
commands
.entity(webviews.single().unwrap())
.trigger(RequestCloseDevtool);
}

58
examples/host_emit.rs Normal file
View File

@@ -0,0 +1,58 @@
//! Shows how to emit an event from the host to the webview.
use bevy::prelude::*;
use bevy::time::common_conditions::on_timer;
use bevy_cef::prelude::*;
use std::time::Duration;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CefPlugin))
.add_systems(
Startup,
(spawn_camera, spawn_directional_light, spawn_webview),
)
.add_systems(Update, emit_count.run_if(on_timer(Duration::from_secs(1))))
.run();
}
#[derive(Component)]
struct DebugWebview;
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((
DebugWebview,
CefWebviewUri::local("host_emit.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}
fn emit_count(
mut commands: Commands,
mut count: Local<usize>,
webviews: Query<Entity, With<DebugWebview>>,
) {
*count += 1;
commands
.entity(webviews.single().unwrap())
.trigger(HostEmitEvent::new("count", &*count));
}

55
examples/js_emit.rs Normal file
View File

@@ -0,0 +1,55 @@
//! Shows how to emit a message from the webview to the application.
use bevy::prelude::*;
use bevy_cef::prelude::*;
use serde::Deserialize;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
CefPlugin,
JsEmitEventPlugin::<Message>::default(),
))
.add_systems(
Startup,
(spawn_camera, spawn_directional_light, spawn_webview),
)
.add_observer(apply_receive_message)
.run();
}
#[derive(Event, Deserialize)]
struct Message {
count: u32,
}
fn apply_receive_message(trigger: Trigger<Message>) {
info!("Received: {:?}", trigger.count);
}
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("js_emit.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}

69
examples/navigation.rs Normal file
View File

@@ -0,0 +1,69 @@
//! Shows how to navigate a webview using keyboard input.
//!
//! ## Keyboard Controls
//!
//! - Press `Z` to go back in history.
//! - Press `X` to go forward in history.
use bevy::input::common_conditions::input_just_pressed;
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),
)
.add_systems(
Update,
(
request_go_back.run_if(input_just_pressed(KeyCode::KeyZ)),
request_go_forward.run_if(input_just_pressed(KeyCode::KeyX)),
),
)
.run();
}
#[derive(Component)]
struct DebugWebview;
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((
DebugWebview,
CefWebviewUri("https://bevy.org/".to_string()),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}
fn request_go_back(mut commands: Commands, webviews: Query<Entity, With<DebugWebview>>) {
for webview in webviews.iter() {
commands.entity(webview).trigger(RequestGoBack);
}
}
fn request_go_forward(mut commands: Commands, webviews: Query<Entity, With<DebugWebview>>) {
for webview in webviews.iter() {
commands.entity(webview).trigger(RequestGoForward);
}
}

40
examples/simple.rs Normal file
View File

@@ -0,0 +1,40 @@
//! A simple example that shows how to spawn a webview in world space.
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("https://bevy.org/".to_string()),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}

27
examples/sprite.rs Normal file
View File

@@ -0,0 +1,27 @@
//! You can create a webview as a sprite in your scene.
use bevy::prelude::*;
use bevy_cef::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CefPlugin))
.add_systems(Startup, (spawn_camera_2d, spawn_sprite_webview))
.run();
}
fn spawn_camera_2d(mut commands: Commands) {
commands.spawn(Camera2d);
}
fn spawn_sprite_webview(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
commands.spawn((
CefWebviewUri::new("https://bevyengine.org/"),
Pickable::default(),
Sprite {
image: images.add(Image::default()),
custom_size: Some(Vec2::splat(500.0)),
..default()
},
));
}

54
examples/zoom_level.rs Normal file
View File

@@ -0,0 +1,54 @@
//! Shows how to change the zoom level of a webview.
use bevy::input::mouse::MouseWheel;
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),
)
.add_systems(Update, change_zoom_level)
.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("https://bevy.org/".to_string()),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}
fn change_zoom_level(mut er: EventReader<MouseWheel>, mut webviews: Query<&mut ZoomLevel>) {
for event in er.read() {
webviews.par_iter_mut().for_each(|mut level| {
if event.y > 0.0 {
level.0 += 0.1; // Zoom in
} else if event.y < 0.0 {
level.0 -= 0.1; // Zoom out
}
});
}
}

9
src/common.rs Normal file
View File

@@ -0,0 +1,9 @@
mod components;
mod ipc;
mod localhost;
mod message_loop;
pub use components::*;
pub use ipc::*;
pub(crate) use localhost::*;
pub use message_loop::*;

80
src/common/components.rs Normal file
View File

@@ -0,0 +1,80 @@
use bevy::prelude::*;
use bevy_cef_core::prelude::{HOST_CEF, SCHEME_CEF};
use serde::{Deserialize, Serialize};
pub(crate) struct WebviewCoreComponentsPlugin;
impl Plugin for WebviewCoreComponentsPlugin {
fn build(&self, app: &mut App) {
app.register_type::<WebviewSize>()
.register_type::<CefWebviewUri>()
.register_type::<HostWindow>()
.register_type::<ZoomLevel>();
}
}
/// A component that specifies the URI of the webview.
///
/// When opening a remote web page, specify the URI with the http(s) schema.
///
/// When opening a local file, use the custom schema `cef://localhost/` to specify the file path.
/// Alternatively, you can also use [`CefWebviewUri::local`].
#[derive(Component, Debug, Clone, PartialEq, Eq, Hash, Reflect)]
#[reflect(Component, Debug)]
#[require(WebviewSize, ZoomLevel, AudioMuted)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
pub struct CefWebviewUri(pub String);
impl CefWebviewUri {
/// Creates a new `CefWebviewUri` with the given URI.
///
/// If you want to specify a local file path, use [`CefWebviewUri::local`] instead.
pub fn new(uri: impl Into<String>) -> Self {
Self(uri.into())
}
/// Creates a new `CefWebviewUri` with the given file path.
///
/// It interprets the given path as a file path in the format `cef://localhost/<file_path>`.
pub fn local(uri: impl Into<String>) -> Self {
Self(format!("{SCHEME_CEF}://{HOST_CEF}/{}", uri.into()))
}
}
/// Specifies the view size of the webview.
///
/// This does not affect the actual object size.
#[derive(Reflect, Component, Debug, Copy, Clone, PartialEq)]
#[reflect(Component, Debug, Default)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
pub struct WebviewSize(pub Vec2);
impl Default for WebviewSize {
fn default() -> Self {
Self(Vec2::splat(800.0))
}
}
/// An optional component to specify the parent window of the webview.
/// The window handle of [Window] specified by this component is passed to `parent_view` of [`WindowInfo`](cef::WindowInfo).
///
/// If this component is not inserted, the handle of [PrimaryWindow](bevy::window::PrimaryWindow) is passed instead.
#[derive(Reflect, Component, Debug, Copy, Clone, PartialEq)]
#[reflect(Component, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
pub struct HostWindow(pub Entity);
/// This component is used to specify the zoom level of the webview.
///
/// Specify 0.0 to reset the zoom level to the default.
#[derive(Reflect, Component, Debug, Copy, Clone, PartialEq, Serialize, Deserialize, Default)]
#[reflect(Component, Debug, Serialize, Deserialize, Default)]
pub struct ZoomLevel(pub f64);
/// This component is used to specify whether the audio of the webview is muted or not.
#[derive(Reflect, Component, Debug, Copy, Clone, PartialEq, Default, Serialize, Deserialize)]
#[reflect(Component, Debug, Default, Serialize, Deserialize)]
pub struct AudioMuted(pub bool);

17
src/common/ipc.rs Normal file
View File

@@ -0,0 +1,17 @@
mod host_emit;
mod js_emit;
use crate::common::ipc::js_emit::IpcRawEventPlugin;
use bevy::prelude::*;
use crate::common::ipc::host_emit::HostEmitPlugin;
pub use host_emit::*;
pub use js_emit::*;
pub struct IpcPlugin;
impl Plugin for IpcPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((IpcRawEventPlugin, HostEmitPlugin));
}
}

View File

@@ -0,0 +1,37 @@
use bevy::prelude::*;
use bevy_cef_core::prelude::*;
use serde::{Deserialize, Serialize};
/// A trigger event to emit an event from the host to the webview.
///
/// You need to subscribe to this event on the webview side by calling `window.cef.listen("event-id", (e) => {})` beforehand.
#[derive(Default, Reflect, Debug, Clone, Serialize, Deserialize, Event)]
#[reflect(Default, Serialize, Deserialize)]
pub struct HostEmitEvent {
pub id: String,
pub payload: String,
}
impl HostEmitEvent {
/// Creates a new `HostEmitEvent` with the given id and payload.
pub fn new(id: impl Into<String>, payload: &impl Serialize) -> Self {
Self {
id: id.into(),
payload: serde_json::to_string(payload).unwrap_or_default(),
}
}
}
pub(super) struct HostEmitPlugin;
impl Plugin for HostEmitPlugin {
fn build(&self, app: &mut App) {
app.register_type::<HostEmitEvent>().add_observer(host_emit);
}
}
fn host_emit(trigger: Trigger<HostEmitEvent>, browsers: NonSend<Browsers>) {
if let Ok(v) = serde_json::to_value(&trigger.payload) {
browsers.emit_event(&trigger.target(), trigger.id.clone(), &v);
}
}

46
src/common/ipc/js_emit.rs Normal file
View File

@@ -0,0 +1,46 @@
use async_channel::{Receiver, Sender};
use bevy::prelude::*;
use bevy_cef_core::prelude::*;
use serde::de::DeserializeOwned;
use std::marker::PhantomData;
pub struct JsEmitEventPlugin<E: Event + DeserializeOwned>(PhantomData<E>);
impl<E: Event + DeserializeOwned> Plugin for JsEmitEventPlugin<E> {
fn build(&self, app: &mut App) {
app.add_systems(Update, receive_events::<E>);
}
}
impl<E: Event + DeserializeOwned> Default for JsEmitEventPlugin<E> {
fn default() -> Self {
Self(PhantomData)
}
}
fn receive_events<E: Event + DeserializeOwned>(
mut commands: Commands,
receiver: ResMut<IpcEventRawReceiver>,
) {
while let Ok(event) = receiver.0.try_recv() {
if let Ok(payload) = serde_json::from_str::<E>(&event.payload) {
commands.entity(event.webview).trigger(payload);
}
}
}
pub(crate) struct IpcRawEventPlugin;
impl Plugin for IpcRawEventPlugin {
fn build(&self, app: &mut App) {
let (tx, rx) = async_channel::unbounded();
app.insert_resource(IpcEventRawSender(tx))
.insert_resource(IpcEventRawReceiver(rx));
}
}
#[derive(Resource)]
pub(crate) struct IpcEventRawSender(pub Sender<IpcEventRaw>);
#[derive(Resource)]
pub(crate) struct IpcEventRawReceiver(pub Receiver<IpcEventRaw>);

15
src/common/localhost.rs Normal file
View File

@@ -0,0 +1,15 @@
use bevy::prelude::*;
mod asset_loader;
pub(crate) mod responser;
use crate::common::localhost::asset_loader::LocalSchemeAssetLoaderPlugin;
/// A plugin that adds support for handling local scheme requests in Bevy applications.
pub(crate) struct LocalHostPlugin;
impl Plugin for LocalHostPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((responser::ResponserPlugin, LocalSchemeAssetLoaderPlugin));
}
}

View File

@@ -0,0 +1,156 @@
use bevy::asset::io::Reader;
use bevy::asset::{AssetLoader, LoadContext};
use bevy::prelude::*;
use bevy_cef_core::prelude::CefResponse;
use std::path::Path;
use std::sync::LazyLock;
pub struct LocalSchemeAssetLoaderPlugin;
impl Plugin for LocalSchemeAssetLoaderPlugin {
fn build(&self, app: &mut App) {
app.register_type::<CefResponse>()
.register_type::<CefResponseHandle>()
.init_asset::<CefResponse>()
.init_asset_loader::<CefResponseAssetLoader>();
}
}
#[derive(Component, Reflect, Debug, Clone)]
#[reflect(Component, Debug)]
pub struct CefResponseHandle(pub Handle<CefResponse>);
#[derive(Default)]
pub struct CefResponseAssetLoader;
impl AssetLoader for CefResponseAssetLoader {
type Asset = CefResponse;
type Settings = ();
type Error = std::io::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> std::result::Result<Self::Asset, Self::Error> {
let mut body = Vec::new();
let mime_type = get_mime_type(load_context.path())
.unwrap_or("text/html")
.to_string();
reader.read_to_end(&mut body).await?;
Ok(CefResponse {
mime_type,
status_code: 200,
data: body,
})
}
fn extensions(&self) -> &[&str] {
&EXTENSIONS
}
}
const EXTENSION_MAP: &[(&[&str], &str)] = &[
(&["htm", "html"], "text/html"),
(&["txt"], "text/plain"),
(&["css"], "text/css"),
(&["csv"], "text/csv"),
(&["js"], "text/javascript"),
(&["jpeg", "jpg"], "image/jpeg"),
(&["png"], "image/png"),
(&["gif"], "image/gif"),
(&["bmp"], "image/bmp"),
(&["svg"], "image/svg+xml"),
(&["json"], "application/json"),
(&["pdf"], "application/pdf"),
(&["zip"], "application/zip"),
(&["lzh"], "application/x-lzh"),
(&["tar"], "application/x-tar"),
(&["wasm"], "application/wasm"),
(&["mp3"], "audio/mp3"),
(&["mp4"], "video/mp4"),
(&["mpeg"], "video/mpeg"),
(&["ogg"], "audio/ogg"),
(&["opus"], "audio/opus"),
(&["webm"], "video/webm"),
(&["flac"], "audio/flac"),
(&["wav"], "audio/wav"),
(&["m4a"], "audio/mp4"),
(&["mov"], "video/quicktime"),
(&["wmv"], "video/x-ms-wmv"),
(&["mpg", "mpeg"], "video/mpeg"),
(&["mpeg"], "video/mpeg"),
(&["aac"], "audio/aac"),
(&["abw"], "application/x-abiword"),
(&["arc"], "application/x-freearc"),
(&["avi"], "video/m-msvideo"),
(&["azw"], "application/vnd.amazon.ebook"),
(&["bin"], "application/octet-stream"),
(&["bz"], "application/x-bzip"),
(&["bz2"], "application/x-bzip2"),
(&["csh"], "application/x-csh"),
(&["doc"], "application/msword"),
(
&["docx"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
),
(&["eot"], "application/vnd.ms-fontobject"),
(&["epub"], "application/epub+zip"),
(&["gz"], "application/gzip"),
(&["ico"], "image/vnd.microsoft.icon"),
(&["ics"], "text/calendar"),
(&["jar"], "application/java-archive"),
(&["jpeg", "jpg"], "image/jpeg"),
(&["mid", "midi"], "audio/midi"),
(&["mpkg"], "application/vnd.apple.installer+xml"),
(&["odp"], "application/vnd.oasis.opendocument.presentation"),
(&["ods"], "application/vnd.oasis.opendocument.spreadsheet"),
(&["odt"], "application/vnd.oasis.opendocument.text"),
(&["oga"], "audio/ogg"),
(&["ogv"], "video/ogg"),
(&["ogx"], "application/ogg"),
(&["otf"], "font/otf"),
(&["ppt"], "application/vnd.ms-powerpoint"),
(
&["pptx"],
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
),
(&["rar"], "application/vnd.rar"),
(&["rtf"], "application/rtf"),
(&["sh"], "application/x-sh"),
(&["swf"], "application/x-shockwave-flash"),
(&["tif", "tiff"], "image/tiff"),
(&["ttf"], "font/ttf"),
(&["vsd"], "application/vnd.visio"),
(&["wav"], "audio/wav"),
(&["weba"], "audio/webm"),
(&["webm"], "video/web"),
(&["woff"], "font/woff"),
(&["woff2"], "font/woff2"),
(&["xhtml"], "application/xhtml+xml"),
(&["xls"], "application/vnd.ms-excel"),
(
&["xlsx"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
),
(&["xml"], "application/xml"),
(&["xul"], "application/vnd.mozilla.xul+xml"),
(&["7z"], "application/x-7z-compressed"),
];
static EXTENSIONS: LazyLock<Vec<&str>> = LazyLock::new(|| {
EXTENSION_MAP
.iter()
.flat_map(|(extensions, _)| *extensions)
.copied()
.collect::<Vec<&str>>()
});
fn get_mime_type(path: &Path) -> Option<&str> {
let ext = path.extension()?.to_str()?;
EXTENSION_MAP
.iter()
.find(|(extensions, _)| extensions.iter().any(|e| e == &ext))
.map(|(_, mime)| *mime)
}

View File

@@ -0,0 +1,59 @@
use crate::common::localhost::asset_loader::CefResponseHandle;
use bevy::platform::collections::HashSet;
use bevy::prelude::*;
use bevy_cef_core::prelude::*;
pub struct ResponserPlugin;
impl Plugin for ResponserPlugin {
fn build(&self, app: &mut App) {
let (tx, rx) = async_channel::unbounded();
app.insert_resource(Requester(tx))
.insert_resource(RequesterReceiver(rx))
.add_systems(
Update,
(
coming_request,
responser,
hot_reload.run_if(any_changed_assets),
),
);
}
}
fn any_changed_assets(mut er: EventReader<AssetEvent<CefResponse>>) -> bool {
er.read()
.any(|event| matches!(event, AssetEvent::Modified { .. }))
}
fn coming_request(
mut commands: Commands,
requester_receiver: Res<RequesterReceiver>,
asset_server: Res<AssetServer>,
) {
while let Ok(request) = requester_receiver.0.try_recv() {
commands.spawn((
CefResponseHandle(asset_server.load(request.uri)),
request.responser,
));
}
}
fn responser(
mut commands: Commands,
mut handle_stores: Local<HashSet<Handle<CefResponse>>>,
responses: Res<Assets<CefResponse>>,
handles: Query<(Entity, &CefResponseHandle, &Responser)>,
) {
for (entity, handle, responser) in handles.iter() {
if let Some(response) = responses.get(&handle.0) {
let _ = responser.0.send_blocking(response.clone());
commands.entity(entity).despawn();
handle_stores.insert(handle.0.clone());
}
}
}
fn hot_reload(browsers: NonSend<Browsers>) {
browsers.reload();
}

150
src/common/message_loop.rs Normal file
View File

@@ -0,0 +1,150 @@
use crate::RunOnMainThread;
use bevy::prelude::*;
use bevy_cef_core::prelude::*;
use cef::args::Args;
use cef::{Settings, api_hash, do_message_loop_work, execute_process, initialize, shutdown, sys};
/// Controls the CEF message loop.
///
/// - Windows and Linux: Support [`multi_threaded_message_loop`](https://cef-builds.spotifycdn.com/docs/106.1/structcef__settings__t.html#a518ac90db93ca5133a888faa876c08e0), so it is used.
/// - macOS: Calls [`CefDoMessageLoopWork`](https://cef-builds.spotifycdn.com/docs/106.1/cef__app_8h.html#a830ae43dcdffcf4e719540204cefdb61) every frame.
pub struct MessageLoopPlugin {
_app: Box<cef::App>,
#[cfg(all(target_os = "macos", not(feature = "debug")))]
_loader: Box<cef::library_loader::LibraryLoader>,
#[cfg(all(target_os = "macos", feature = "debug"))]
_loader: Box<DebugLibraryLoader>,
}
impl Plugin for MessageLoopPlugin {
fn build(&self, app: &mut App) {
app.insert_non_send_resource(RunOnMainThread)
.add_systems(Update, cef_shutdown.run_if(on_event::<AppExit>));
#[cfg(target_os = "macos")]
app.add_systems(Main, cef_do_message_loop_work);
}
}
impl Default for MessageLoopPlugin {
fn default() -> Self {
let _loader = {
macos::install_cef_app_protocol();
#[cfg(all(target_os = "macos", feature = "debug"))]
let loader = DebugLibraryLoader::new();
#[cfg(all(target_os = "macos", not(feature = "debug")))]
let loader =
cef::library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false);
assert!(loader.load());
loader
};
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let args = Args::new();
let mut app = BrowserProcessAppBuilder::build();
let ret = execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
assert_eq!(ret, -1, "cannot execute browser process");
let settings = Settings {
#[cfg(all(target_os = "macos", feature = "debug"))]
framework_dir_path: debug_chromium_embedded_framework_dir_path()
.to_str()
.unwrap()
.into(),
#[cfg(all(target_os = "macos", feature = "debug"))]
browser_subprocess_path: debug_render_process_path().to_str().unwrap().into(),
#[cfg(all(target_os = "macos", feature = "debug"))]
no_sandbox: true as _,
windowless_rendering_enabled: true as _,
#[cfg(any(target_os = "windows", target_os = "linux"))]
multi_threaded_message_loop: true as _,
#[cfg(target_os = "macos")]
external_message_pump: true as _,
..Default::default()
};
assert_eq!(
initialize(
Some(args.as_main_args()),
Some(&settings),
Some(&mut app),
std::ptr::null_mut(),
),
1
);
Self {
_app: Box::new(app),
#[cfg(target_os = "macos")]
_loader: Box::new(_loader),
}
}
}
fn cef_do_message_loop_work(_: NonSend<RunOnMainThread>) {
do_message_loop_work();
}
fn cef_shutdown(_: NonSend<RunOnMainThread>) {
shutdown();
}
#[cfg(target_os = "macos")]
mod macos {
use core::sync::atomic::AtomicBool;
use objc::runtime::{Class, Object, Sel};
use objc::{sel, sel_impl};
use std::os::raw::c_char;
use std::os::raw::c_void;
use std::sync::atomic::Ordering;
unsafe extern "C" {
fn class_addMethod(
cls: *const Class,
name: Sel,
imp: *const c_void,
types: *const c_char,
) -> bool;
}
static IS_HANDLING_SEND_EVENT: AtomicBool = AtomicBool::new(false);
extern "C" fn is_handling_send_event(_: &Object, _: Sel) -> bool {
IS_HANDLING_SEND_EVENT.load(Ordering::Relaxed)
}
extern "C" fn set_handling_send_event(_: &Object, _: Sel, flag: bool) {
IS_HANDLING_SEND_EVENT.swap(flag, Ordering::Relaxed);
}
pub fn install_cef_app_protocol() {
unsafe {
let cls = Class::get("NSApplication").expect("NSApplication クラスが見つかりません");
#[allow(unexpected_cfgs)]
let sel_name = sel!(isHandlingSendEvent);
let success = class_addMethod(
cls as *const _,
sel_name,
is_handling_send_event as *const c_void,
c"c@:".as_ptr() as *const c_char,
);
assert!(success, "メソッド追加に失敗しました");
#[allow(unexpected_cfgs)]
let sel_set = sel!(setHandlingSendEvent:);
let success2 = class_addMethod(
cls as *const _,
sel_set,
set_handling_send_event as *const c_void,
c"v@:c".as_ptr() as *const c_char,
);
assert!(
success2,
"Failed to add setHandlingSendEvent: to NSApplication"
);
}
}
}

38
src/cursor_icon.rs Normal file
View File

@@ -0,0 +1,38 @@
use async_channel::{Receiver, Sender};
use bevy::prelude::*;
use bevy::window::SystemCursorIcon;
use bevy::winit::cursor::CursorIcon;
/// This plugin manages the system cursor icon by receiving updates from CEF and applying them to the application window's cursor icon.
pub(super) struct SystemCursorIconPlugin;
impl Plugin for SystemCursorIconPlugin {
fn build(&self, app: &mut App) {
let (tx, rx) = async_channel::unbounded();
app.insert_resource(SystemCursorIconSender(tx))
.insert_resource(SystemCursorIconReceiver(rx))
.add_systems(Update, update_cursor_icon);
}
}
#[derive(Resource, Debug, Deref)]
pub(crate) struct SystemCursorIconSender(Sender<SystemCursorIcon>);
#[derive(Resource, Debug)]
pub(crate) struct SystemCursorIconReceiver(pub(crate) Receiver<SystemCursorIcon>);
fn update_cursor_icon(
par_commands: ParallelCommands,
cursor_icon_receiver: Res<SystemCursorIconReceiver>,
windows: Query<Entity>,
) {
while let Ok(cursor_icon) = cursor_icon_receiver.0.try_recv() {
windows.par_iter().for_each(|window| {
par_commands.command_scope(|mut commands| {
commands
.entity(window)
.insert(CursorIcon::System(cursor_icon));
});
});
}
}

73
src/keyboard.rs Normal file
View File

@@ -0,0 +1,73 @@
use crate::common::CefWebviewUri;
use bevy::input::keyboard::KeyboardInput;
use bevy::prelude::*;
use bevy_cef_core::prelude::{Browsers, create_cef_key_event, keyboard_modifiers};
use serde::{Deserialize, Serialize};
/// The plugin to handle keyboard inputs.
///
/// To use IME, you need to set [`Window::ime_enabled`](bevy::prelude::Window) to `true`.
pub(super) struct KeyboardPlugin;
impl Plugin for KeyboardPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<IsImeCommiting>().add_systems(
Update,
(
ime_event.run_if(on_event::<Ime>),
send_key_event.run_if(on_event::<KeyboardInput>),
)
.chain(),
);
}
}
#[derive(Resource, Default, Serialize, Deserialize, Reflect)]
#[reflect(Default, Serialize, Deserialize)]
struct IsImeCommiting(bool);
fn send_key_event(
mut er: EventReader<KeyboardInput>,
mut is_ime_commiting: ResMut<IsImeCommiting>,
input: Res<ButtonInput<KeyCode>>,
browsers: NonSend<Browsers>,
webviews: Query<Entity, With<CefWebviewUri>>,
) {
let modifiers = keyboard_modifiers(&input);
for event in er.read() {
if event.key_code == KeyCode::Enter && is_ime_commiting.0 {
// If the IME is committing, we don't want to send the Enter key event.
// This is to prevent sending the Enter key event when the IME is committing.
is_ime_commiting.0 = false;
continue;
}
let Some(key_event) = create_cef_key_event(modifiers, &input, event) else {
continue;
};
for webview in webviews.iter() {
browsers.send_key(&webview, key_event.clone());
}
}
}
fn ime_event(
mut er: EventReader<Ime>,
mut is_ime_commiting: ResMut<IsImeCommiting>,
browsers: NonSend<Browsers>,
) {
for event in er.read() {
match event {
Ime::Preedit { value, cursor, .. } => {
browsers.set_ime_composition(value, cursor.map(|(_, e)| e as u32))
}
Ime::Commit { value, .. } => {
browsers.set_ime_commit_text(value);
is_ime_commiting.0 = true;
}
Ime::Disabled { .. } => {
browsers.ime_finish_composition(false);
}
_ => {}
}
}
}

47
src/lib.rs Normal file
View File

@@ -0,0 +1,47 @@
#![allow(clippy::type_complexity)]
mod common;
mod cursor_icon;
mod keyboard;
mod mute;
mod navigation;
mod system_param;
mod webview;
mod zoom;
use crate::common::{LocalHostPlugin, MessageLoopPlugin, WebviewCoreComponentsPlugin};
use crate::cursor_icon::SystemCursorIconPlugin;
use crate::keyboard::KeyboardPlugin;
use crate::mute::AudioMutePlugin;
use crate::prelude::{IpcPlugin, NavigationPlugin, WebviewPlugin};
use crate::zoom::ZoomPlugin;
use bevy::prelude::*;
use bevy_remote::RemotePlugin;
pub mod prelude {
pub use crate::{CefPlugin, RunOnMainThread, common::*, navigation::*, webview::prelude::*};
}
pub struct RunOnMainThread;
pub struct CefPlugin;
impl Plugin for CefPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
LocalHostPlugin,
MessageLoopPlugin::default(),
WebviewCoreComponentsPlugin,
WebviewPlugin,
IpcPlugin,
KeyboardPlugin,
SystemCursorIconPlugin,
NavigationPlugin,
ZoomPlugin,
AudioMutePlugin,
));
if !app.is_plugin_added::<RemotePlugin>() {
app.add_plugins(RemotePlugin::default());
}
}
}

23
src/mute.rs Normal file
View File

@@ -0,0 +1,23 @@
use crate::prelude::AudioMuted;
use bevy::prelude::*;
pub(super) struct AudioMutePlugin;
impl Plugin for AudioMutePlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, sync_audio_mute.run_if(any_changed_audio_mute));
}
}
fn any_changed_audio_mute(audio_mute: Query<&AudioMuted, Changed<AudioMuted>>) -> bool {
!audio_mute.is_empty()
}
fn sync_audio_mute(
browsers: NonSend<bevy_cef_core::prelude::Browsers>,
audio_mute: Query<(Entity, &AudioMuted), Changed<AudioMuted>>,
) {
for (entity, mute) in audio_mute.iter() {
browsers.set_audio_muted(&entity, mute.0);
}
}

30
src/navigation.rs Normal file
View File

@@ -0,0 +1,30 @@
use bevy::prelude::*;
use bevy_cef_core::prelude::Browsers;
use serde::{Deserialize, Serialize};
pub(super) struct NavigationPlugin;
impl Plugin for NavigationPlugin {
fn build(&self, app: &mut App) {
app.register_type::<RequestGoBack>()
.register_type::<RequestGoForward>()
.add_observer(apply_request_go_back)
.add_observer(apply_request_go_forward);
}
}
/// A trigger event to navigate backwards.
#[derive(Default, Debug, Event, Copy, Clone, Reflect, Serialize, Deserialize)]
pub struct RequestGoBack;
/// A trigger event to navigate forwards.
#[derive(Default, Debug, Event, Copy, Clone, Reflect, Serialize, Deserialize)]
pub struct RequestGoForward;
fn apply_request_go_back(trigger: Trigger<RequestGoBack>, browsers: NonSend<Browsers>) {
browsers.go_back(&trigger.target());
}
fn apply_request_go_forward(trigger: Trigger<RequestGoForward>, browsers: NonSend<Browsers>) {
browsers.go_forward(&trigger.target());
}

2
src/system_param.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod mesh_aabb;
pub mod pointer;

View File

@@ -0,0 +1,53 @@
use bevy::ecs::system::SystemParam;
use bevy::math::Vec3;
use bevy::prelude::{Children, Entity, GlobalTransform, Query};
use bevy::render::primitives::Aabb;
#[derive(SystemParam)]
pub struct MeshAabb<'w, 's> {
meshes: Query<
'w,
's,
(
&'static GlobalTransform,
Option<&'static Aabb>,
Option<&'static Children>,
),
>,
}
impl MeshAabb<'_, '_> {
pub fn calculate(&self, mesh_root: Entity) -> (Vec3, Vec3) {
calculate_aabb(&[mesh_root], true, &self.meshes)
}
}
fn calculate_aabb(
entities: &[Entity],
include_children: bool,
entities_query: &Query<(&GlobalTransform, Option<&Aabb>, Option<&Children>)>,
) -> (Vec3, Vec3) {
let combine_bounds = |(a_min, a_max): (Vec3, Vec3), (b_min, b_max): (Vec3, Vec3)| {
(a_min.min(b_min), a_max.max(b_max))
};
let default_bounds = (Vec3::splat(f32::INFINITY), Vec3::splat(f32::NEG_INFINITY));
entities
.iter()
.filter_map(|&entity| {
entities_query
.get(entity)
.map(|(&tf, bounds, children)| {
let mut entity_bounds = bounds.map_or(default_bounds, |bounds| {
(tf * Vec3::from(bounds.min()), tf * Vec3::from(bounds.max()))
});
if include_children && let Some(children) = children {
let children_bounds =
calculate_aabb(children, include_children, entities_query);
entity_bounds = combine_bounds(entity_bounds, children_bounds);
}
entity_bounds
})
.ok()
})
.fold(default_bounds, combine_bounds)
}

View File

@@ -0,0 +1,84 @@
use crate::prelude::{CefWebviewUri, WebviewSize};
use crate::system_param::mesh_aabb::MeshAabb;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
use std::fmt::Debug;
#[derive(SystemParam)]
pub struct WebviewPointer<'w, 's, C: Component = Camera3d> {
aabb: MeshAabb<'w, 's>,
cameras: Query<'w, 's, (&'static Camera, &'static GlobalTransform), With<C>>,
webviews: Query<
'w,
's,
(&'static GlobalTransform, &'static WebviewSize),
(With<CefWebviewUri>, Without<Camera>),
>,
parents: Query<'w, 's, &'static ChildOf>,
}
impl<C: Component> WebviewPointer<'_, '_, C> {
pub fn pos_from_trigger<P>(&self, trigger: &Trigger<Pointer<P>>) -> Option<(Entity, Vec2)>
where
P: Clone + Reflect + Debug,
{
let webview = self.parents.root_ancestor(trigger.target);
let pos = self.pointer_pos(webview, trigger.pointer_location.position)?;
Some((webview, pos))
}
pub fn pointer_pos(&self, webview: Entity, viewport_pos: Vec2) -> Option<Vec2> {
let (min, max) = self.aabb.calculate(webview);
let aabb_size = Vec2::new(max.x - min.x, max.y - min.y);
let (webview_gtf, webview_size) = self.webviews.get(webview).ok()?;
self.cameras.iter().find_map(|(camera, camera_gtf)| {
pointer_to_webview_uv(
viewport_pos,
camera,
camera_gtf,
webview_gtf,
aabb_size,
webview_size.0,
)
})
}
}
fn pointer_to_webview_uv(
cursor_pos: Vec2,
camera: &Camera,
cam_tf: &GlobalTransform,
plane_tf: &GlobalTransform,
plane_size: Vec2,
tex_size: Vec2,
) -> Option<Vec2> {
let ray = camera.viewport_to_world(cam_tf, cursor_pos).ok()?;
let n = plane_tf.forward().as_vec3();
let t = ray.intersect_plane(
plane_tf.translation(),
InfinitePlane3d::new(plane_tf.forward()),
)?;
let hit_world = ray.origin + ray.direction * t;
let local_hit = plane_tf.affine().inverse().transform_point(hit_world);
let local_normal = plane_tf.affine().inverse().transform_vector3(n).normalize();
let abs_normal = local_normal.abs();
let (u_coord, v_coord) = if abs_normal.z > abs_normal.x && abs_normal.z > abs_normal.y {
(local_hit.x, local_hit.y)
} else if abs_normal.y > abs_normal.x {
(local_hit.x, local_hit.z)
} else {
(local_hit.y, local_hit.z)
};
let w = plane_size.x;
let h = plane_size.y;
let u = (u_coord + w * 0.5) / w;
let v = (v_coord + h * 0.5) / h;
if !(0.0..=1.0).contains(&u) || !(0.0..=1.0).contains(&v) {
// outside plane bounds
return None;
}
let px = u * tex_size.x;
let py = (1.0 - v) * tex_size.y;
Some(Vec2::new(px, py))
}

148
src/webview.rs Normal file
View File

@@ -0,0 +1,148 @@
use crate::common::{CefWebviewUri, HostWindow, IpcEventRawSender, WebviewSize};
use crate::cursor_icon::SystemCursorIconSender;
use crate::webview::mesh::MeshWebviewPlugin;
use bevy::ecs::component::HookContext;
use bevy::ecs::world::DeferredWorld;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy::winit::WinitWindows;
use bevy_cef_core::prelude::*;
use bevy_remote::BrpSender;
#[allow(deprecated)]
use raw_window_handle::HasRawWindowHandle;
use serde::{Deserialize, Serialize};
mod mesh;
mod webview_sprite;
pub mod prelude {
pub use crate::webview::{RequestCloseDevtool, RequestShowDevTool, WebviewPlugin, mesh::*};
}
/// A Trigger event to request showing the developer tools in a webview.
///
/// When you want to close the developer tools, use [`RequestCloseDevtool`].
///
/// ```rust
/// use bevy::prelude::*;
/// use bevy_cef::prelude::*;
///
/// #[derive(Component)]
/// struct DebugWebview;
///
/// fn show_devtool_system(mut commands: Commands, webviews: Query<Entity, With<DebugWebview>>) {
/// commands.entity(webviews.single().unwrap()).trigger(RequestShowDevTool);
/// }
/// ```
#[derive(Reflect, Debug, Default, Copy, Clone, Serialize, Deserialize, Event)]
#[reflect(Default, Serialize, Deserialize)]
pub struct RequestShowDevTool;
/// A Trigger event to request closing the developer tools in a webview.
///
/// When showing the devtool, use [`RequestShowDevTool`] instead.
///
/// ```rust
/// use bevy::prelude::*;
/// use bevy_cef::prelude::*;
///
/// #[derive(Component)]
/// struct DebugWebview;
///
/// fn close_devtool_system(mut commands: Commands, webviews: Query<Entity, With<DebugWebview>>) {
/// commands.entity(webviews.single().unwrap()).trigger(RequestCloseDevtool);
/// }
/// ```
#[derive(Reflect, Debug, Default, Copy, Clone, Serialize, Deserialize, Event)]
#[reflect(Default, Serialize, Deserialize)]
pub struct RequestCloseDevtool;
pub struct WebviewPlugin;
impl Plugin for WebviewPlugin {
fn build(&self, app: &mut App) {
app.register_type::<RequestShowDevTool>()
.init_non_send_resource::<Browsers>()
.add_plugins((MeshWebviewPlugin,))
.add_systems(Main, send_external_begin_frame)
.add_systems(
Update,
(
resize.run_if(any_resized),
create_webview.run_if(added_webview),
),
)
.add_observer(apply_request_show_devtool)
.add_observer(apply_request_close_devtool);
app.world_mut()
.register_component_hooks::<CefWebviewUri>()
.on_despawn(|mut world: DeferredWorld, ctx: HookContext| {
world.non_send_resource_mut::<Browsers>().close(&ctx.entity);
});
}
}
fn any_resized(webviews: Query<Entity, Changed<WebviewSize>>) -> bool {
!webviews.is_empty()
}
fn added_webview(webviews: Query<Entity, Added<CefWebviewUri>>) -> bool {
!webviews.is_empty()
}
fn send_external_begin_frame(mut hosts: NonSendMut<Browsers>) {
hosts.send_external_begin_frame();
}
#[allow(clippy::too_many_arguments)]
fn create_webview(
mut browsers: NonSendMut<Browsers>,
requester: Res<Requester>,
ipc_event_sender: Res<IpcEventRawSender>,
brp_sender: Res<BrpSender>,
cursor_icon_sender: Res<SystemCursorIconSender>,
winit_windows: NonSend<WinitWindows>,
webviews: Query<
(Entity, &CefWebviewUri, &WebviewSize, Option<&HostWindow>),
Added<CefWebviewUri>,
>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
for (entity, uri, size, parent) in webviews.iter() {
let host_window = parent
.and_then(|w| winit_windows.get_window(w.0))
.or_else(|| winit_windows.get_window(primary_window.single().ok()?))
.and_then(|w| {
#[allow(deprecated)]
w.raw_window_handle().ok()
});
browsers.create_browser(
entity,
&uri.0,
size.0,
requester.clone(),
ipc_event_sender.0.clone(),
brp_sender.clone(),
cursor_icon_sender.clone(),
host_window,
);
}
}
fn resize(
browsers: NonSend<Browsers>,
webviews: Query<(Entity, &WebviewSize), Changed<WebviewSize>>,
) {
for (webview, size) in webviews.iter() {
browsers.resize(&webview, size.0);
}
}
fn apply_request_show_devtool(trigger: Trigger<RequestShowDevTool>, browsers: NonSend<Browsers>) {
browsers.show_devtool(&trigger.target());
}
fn apply_request_close_devtool(trigger: Trigger<RequestCloseDevtool>, browsers: NonSend<Browsers>) {
browsers.close_devtools(&trigger.target());
}

104
src/webview/mesh.rs Normal file
View File

@@ -0,0 +1,104 @@
mod webview_extend_material;
mod webview_extend_standard_material;
mod webview_material;
pub use crate::common::*;
use crate::system_param::pointer::WebviewPointer;
use crate::webview::webview_sprite::WebviewSpritePlugin;
use bevy::input::mouse::MouseWheel;
use bevy::prelude::*;
use bevy_cef_core::prelude::*;
pub use webview_extend_material::*;
pub use webview_extend_standard_material::*;
pub use webview_material::*;
pub struct MeshWebviewPlugin;
impl Plugin for MeshWebviewPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<MeshPickingPlugin>() {
app.add_plugins(MeshPickingPlugin);
}
app.add_plugins((
WebviewMaterialPlugin,
WebviewExtendStandardMaterialPlugin,
WebviewSpritePlugin,
))
.add_systems(
Update,
(
setup_observers,
on_mouse_wheel.run_if(on_event::<MouseWheel>),
),
);
}
}
fn setup_observers(
mut commands: Commands,
webviews: Query<Entity, (Added<CefWebviewUri>, Or<(With<Mesh3d>, With<Mesh2d>)>)>,
) {
for entity in webviews.iter() {
commands
.entity(entity)
.observe(on_pointer_move)
.observe(on_pointer_pressed)
.observe(on_pointer_released);
}
}
fn on_pointer_move(
trigger: Trigger<Pointer<Move>>,
input: Res<ButtonInput<MouseButton>>,
pointer: WebviewPointer,
browsers: NonSend<Browsers>,
) {
let Some((webview, pos)) = pointer.pos_from_trigger(&trigger) else {
return;
};
browsers.send_mouse_move(&webview, input.get_pressed(), pos, false);
}
fn on_pointer_pressed(
trigger: Trigger<Pointer<Pressed>>,
browsers: NonSend<Browsers>,
pointer: WebviewPointer,
) {
let Some((webview, pos)) = pointer.pos_from_trigger(&trigger) else {
return;
};
browsers.send_mouse_click(&webview, pos, trigger.button, false);
}
fn on_pointer_released(
trigger: Trigger<Pointer<Released>>,
browsers: NonSend<Browsers>,
pointer: WebviewPointer,
) {
let Some((webview, pos)) = pointer.pos_from_trigger(&trigger) else {
return;
};
browsers.send_mouse_click(&webview, pos, trigger.button, true);
}
fn on_mouse_wheel(
mut er: EventReader<MouseWheel>,
browsers: NonSend<Browsers>,
pointer: WebviewPointer,
windows: Query<&Window>,
webviews: Query<Entity, With<CefWebviewUri>>,
) {
let Some(cursor_pos) = windows.iter().find_map(|window| window.cursor_position()) else {
return;
};
for event in er.read() {
for webview in webviews.iter() {
let Some(pos) = pointer.pointer_pos(webview, cursor_pos) else {
continue;
};
browsers.send_mouse_wheel(&webview, pos, Vec2::new(event.x, event.y));
}
}
}

View File

@@ -0,0 +1,58 @@
use crate::prelude::{WebviewMaterial, update_webview_image};
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
use bevy::render::render_resource::AsBindGroup;
use bevy_cef_core::prelude::*;
use std::hash::Hash;
use std::marker::PhantomData;
pub type WebviewExtendedMaterial<E> = ExtendedMaterial<WebviewMaterial, E>;
/// A plugin that extends the [`WebviewMaterial`] with a custom material extension.
///
/// This plugin allows you to create custom materials that can be used with webviews.
pub struct WebviewExtendMaterialPlugin<E>(PhantomData<E>);
impl<E> Default for WebviewExtendMaterialPlugin<E>
where
E: MaterialExtension + Default,
<E as AsBindGroup>::Data: PartialEq + Eq + Hash + Clone,
{
fn default() -> Self {
Self(PhantomData)
}
}
impl<E> Plugin for WebviewExtendMaterialPlugin<E>
where
E: MaterialExtension + Default,
<E as AsBindGroup>::Data: PartialEq + Eq + Hash + Clone,
{
fn build(&self, app: &mut App) {
app.add_plugins(MaterialPlugin::<WebviewExtendedMaterial<E>>::default())
.add_systems(PostUpdate, render::<E>);
}
}
fn render<E: MaterialExtension>(
mut er: EventReader<RenderTexture>,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<WebviewExtendedMaterial<E>>>,
webviews: Query<&MeshMaterial3d<WebviewExtendedMaterial<E>>>,
) {
for texture in er.read() {
if let Ok(handle) = webviews.get(texture.webview)
&& let Some(material) = materials.get_mut(handle.id())
&& let Some(image) = {
let handle = material
.base
.surface
.get_or_insert_with(|| images.add(Image::default()));
images.get_mut(handle.id())
}
{
//OPTIMIZE: Avoid cloning the texture.
update_webview_image(texture.clone(), image);
}
}
}

View File

@@ -0,0 +1,54 @@
use crate::prelude::{WebviewMaterial, update_webview_image};
use bevy::asset::{load_internal_asset, weak_handle};
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
use bevy::render::render_resource::ShaderRef;
use bevy_cef_core::prelude::*;
const FRAGMENT_SHADER_HANDLE: Handle<Shader> = weak_handle!("b231681f-9c17-4df6-89c9-9dc353e85a08");
pub(super) struct WebviewExtendStandardMaterialPlugin;
impl Plugin for WebviewExtendStandardMaterialPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(MaterialPlugin::<WebviewExtendStandardMaterial>::default())
.add_systems(PostUpdate, render_standard_materials);
load_internal_asset!(
app,
FRAGMENT_SHADER_HANDLE,
"./webview_extend_standard_material.wgsl",
Shader::from_wgsl
);
}
}
impl MaterialExtension for WebviewMaterial {
fn fragment_shader() -> ShaderRef {
FRAGMENT_SHADER_HANDLE.into()
}
}
pub type WebviewExtendStandardMaterial = ExtendedMaterial<StandardMaterial, WebviewMaterial>;
fn render_standard_materials(
mut er: EventReader<RenderTexture>,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
webviews: Query<&MeshMaterial3d<WebviewExtendStandardMaterial>>,
) {
for texture in er.read() {
if let Ok(handle) = webviews.get(texture.webview)
&& let Some(material) = materials.get_mut(handle.id())
&& let Some(image) = {
let handle = material
.extension
.surface
.get_or_insert_with(|| images.add(Image::default()));
images.get_mut(handle.id())
}
{
//OPTIMIZE: Avoid cloning the texture.
update_webview_image(texture.clone(), image);
}
}
}

View File

@@ -0,0 +1,32 @@
#define_import_path webview::standard_material;
#import bevy_pbr::{
pbr_fragment::pbr_input_from_standard_material,
pbr_functions::alpha_discard,
pbr_types::STANDARD_MATERIAL_FLAGS_UNLIT_BIT,
pbr_bindings::material,
forward_io::{VertexOutput, FragmentOutput},
pbr_functions::{apply_pbr_lighting, main_pass_post_lighting_processing},
mesh_view_bindings::view,
}
#import webview::util::{
surface_color,
}
@fragment
fn fragment(
in: VertexOutput,
@builtin(front_facing) is_front: bool,
) -> FragmentOutput {
var pbr_input = pbr_input_from_standard_material(in, is_front);
pbr_input.material.base_color *= surface_color(in.uv);
pbr_input.material.base_color = alpha_discard(pbr_input.material, pbr_input.material.base_color);
var out: FragmentOutput;
if (material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u {
out.color = apply_pbr_lighting(pbr_input);
out.color = main_pass_post_lighting_processing(pbr_input, out.color);
}else{
out.color = pbr_input.material.base_color;
}
return out;
}

View File

@@ -0,0 +1,55 @@
use bevy::asset::{RenderAssetUsages, load_internal_asset, weak_handle};
use bevy::prelude::*;
use bevy::render::render_resource::{AsBindGroup, Extent3d, TextureDimension, TextureFormat};
use bevy_cef_core::prelude::*;
const WEBVIEW_UTIL_SHADER_HANDLE: Handle<Shader> =
weak_handle!("6c7cb871-4208-4407-9c25-306c6f069e2b");
pub(super) struct WebviewMaterialPlugin;
impl Plugin for WebviewMaterialPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(MaterialPlugin::<WebviewMaterial>::default())
.add_event::<RenderTexture>()
.add_systems(Update, send_render_textures);
load_internal_asset!(
app,
WEBVIEW_UTIL_SHADER_HANDLE,
"./webview_util.wgsl",
Shader::from_wgsl
);
}
}
#[derive(Asset, Reflect, Default, Debug, Clone, AsBindGroup)]
pub struct WebviewMaterial {
/// Holds the texture handle for the webview.
///
/// This texture is automatically updated.
#[texture(101)]
#[sampler(102)]
pub surface: Option<Handle<Image>>,
}
impl Material for WebviewMaterial {}
fn send_render_textures(mut ew: EventWriter<RenderTexture>, browsers: NonSend<Browsers>) {
while let Ok(texture) = browsers.try_receive_texture() {
ew.write(texture);
}
}
pub(crate) fn update_webview_image(texture: RenderTexture, image: &mut Image) {
*image = Image::new(
Extent3d {
width: texture.width,
height: texture.height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
texture.buffer,
TextureFormat::Bgra8UnormSrgb,
RenderAssetUsages::all(),
);
}

View File

@@ -0,0 +1,13 @@
#define_import_path webview::util
#import bevy_pbr::{
mesh_view_bindings::view,
}
@group(2) @binding(101) var surface_texture: texture_2d<f32>;
@group(2) @binding(102) var surface_sampler: sampler;
fn surface_color(uv: vec2<f32>) -> vec4<f32> {
return textureSampleBias(surface_texture, surface_sampler, uv, view.mip_bias);
}

View File

@@ -0,0 +1,143 @@
use crate::common::{CefWebviewUri, WebviewSize};
use crate::prelude::update_webview_image;
use bevy::input::mouse::MouseWheel;
use bevy::prelude::*;
use bevy_cef_core::prelude::{Browsers, RenderTexture};
use std::fmt::Debug;
pub(in crate::webview) struct WebviewSpritePlugin;
impl Plugin for WebviewSpritePlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<SpritePickingPlugin>() {
app.add_plugins(SpritePickingPlugin);
}
app.add_systems(
Update,
(
setup_observers,
on_mouse_wheel.run_if(on_event::<MouseWheel>),
),
)
.add_systems(PostUpdate, render.run_if(on_event::<RenderTexture>));
}
}
fn render(
mut er: EventReader<RenderTexture>,
mut images: ResMut<Assets<bevy::prelude::Image>>,
webviews: Query<&Sprite, With<CefWebviewUri>>,
) {
for texture in er.read() {
if let Ok(sprite) = webviews.get(texture.webview)
&& let Some(image) = images.get_mut(sprite.image.id())
{
update_webview_image(texture.clone(), image);
}
}
}
fn setup_observers(
mut commands: Commands,
webviews: Query<Entity, (Added<CefWebviewUri>, With<Sprite>)>,
) {
for entity in webviews.iter() {
commands
.entity(entity)
.observe(apply_on_pointer_move)
.observe(apply_on_pointer_pressed)
.observe(apply_on_pointer_released);
}
}
fn apply_on_pointer_move(
trigger: Trigger<Pointer<Move>>,
input: Res<ButtonInput<MouseButton>>,
browsers: NonSend<Browsers>,
cameras: Query<(&Camera, &GlobalTransform)>,
webviews: Query<(&Sprite, &WebviewSize, &GlobalTransform)>,
) {
let Some(pos) = obtain_relative_pos_from_trigger(&trigger, &webviews, &cameras) else {
return;
};
browsers.send_mouse_move(&trigger.target, input.get_pressed(), pos, false);
}
fn apply_on_pointer_pressed(
trigger: Trigger<Pointer<Pressed>>,
browsers: NonSend<Browsers>,
cameras: Query<(&Camera, &GlobalTransform)>,
webviews: Query<(&Sprite, &WebviewSize, &GlobalTransform)>,
) {
let Some(pos) = obtain_relative_pos_from_trigger(&trigger, &webviews, &cameras) else {
return;
};
browsers.send_mouse_click(&trigger.target, pos, trigger.button, false);
}
fn apply_on_pointer_released(
trigger: Trigger<Pointer<Released>>,
browsers: NonSend<Browsers>,
cameras: Query<(&Camera, &GlobalTransform)>,
webviews: Query<(&Sprite, &WebviewSize, &GlobalTransform)>,
) {
let Some(pos) = obtain_relative_pos_from_trigger(&trigger, &webviews, &cameras) else {
return;
};
browsers.send_mouse_click(&trigger.target, pos, trigger.button, true);
}
fn on_mouse_wheel(
mut er: EventReader<MouseWheel>,
browsers: NonSend<Browsers>,
webviews: Query<(Entity, &Sprite, &WebviewSize, &GlobalTransform)>,
cameras: Query<(&Camera, &GlobalTransform)>,
windows: Query<&Window>,
) {
let Some(cursor_pos) = windows.iter().find_map(|window| window.cursor_position()) else {
return;
};
for event in er.read() {
for (webview, sprite, webview_size, gtf) in webviews.iter() {
let Some(pos) = obtain_relative_pos(sprite, webview_size, gtf, &cameras, cursor_pos)
else {
continue;
};
browsers.send_mouse_wheel(&webview, pos, Vec2::new(event.x, event.y));
}
}
}
fn obtain_relative_pos_from_trigger<E: Debug + Clone + Reflect>(
trigger: &Trigger<Pointer<E>>,
webviews: &Query<(&Sprite, &WebviewSize, &GlobalTransform)>,
cameras: &Query<(&Camera, &GlobalTransform)>,
) -> Option<Vec2> {
let (sprite, webview_size, gtf) = webviews.get(trigger.target()).ok()?;
obtain_relative_pos(
sprite,
webview_size,
gtf,
cameras,
trigger.pointer_location.position,
)
}
fn obtain_relative_pos(
sprite: &Sprite,
webview_size: &WebviewSize,
transform: &GlobalTransform,
cameras: &Query<(&Camera, &GlobalTransform)>,
cursor_pos: Vec2,
) -> Option<Vec2> {
let size = sprite.custom_size?;
let viewport_pos = cameras.iter().find_map(|(camera, camera_gtf)| {
camera
.world_to_viewport(camera_gtf, transform.translation())
.ok()
})?;
let relative_pos = (cursor_pos - viewport_pos + size / 2.0) / size;
Some(relative_pos * webview_size.0)
}

21
src/zoom.rs Normal file
View File

@@ -0,0 +1,21 @@
use crate::common::ZoomLevel;
use bevy::prelude::*;
use bevy_cef_core::prelude::Browsers;
pub(crate) struct ZoomPlugin;
impl Plugin for ZoomPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, sync_zoom.run_if(any_changed_zoom));
}
}
fn any_changed_zoom(zoom: Query<&ZoomLevel, Changed<ZoomLevel>>) -> bool {
!zoom.is_empty()
}
fn sync_zoom(browsers: NonSend<Browsers>, zoom: Query<(Entity, &ZoomLevel), Changed<ZoomLevel>>) {
for (entity, zoom_level) in zoom.iter() {
browsers.set_zoom_level(&entity, zoom_level.0);
}
}