INIT
This commit is contained in:
39
.claude/commands/create-test.md
Normal file
39
.claude/commands/create-test.md
Normal 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メソッドでヘッダーとレスポンスボディが返されることが期待されます。
|
||||
初回と2回目までのreadメソッドは呼ばれるのですが、3回目以降の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メソッドが3回目以降呼ばれない原因を調査する
|
||||
4. 原因を特定し、修正する
|
||||
7
.claude/settings.json
Normal file
7
.claude/settings.json
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["WebFetch", "WebSearch"],
|
||||
"deny": ["Read(./.env)", "Read(./secrets/**)"]
|
||||
}
|
||||
}
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
.idea/
|
||||
book/
|
||||
102
CLAUDE.md
Normal file
102
CLAUDE.md
Normal 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
6276
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
Cargo.toml
Normal file
56
Cargo.toml
Normal 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
6
Makefile
Normal 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
35
assets/brp.html
Normal 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
23
assets/host_emit.html
Normal 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>
|
||||
BIN
assets/images/rustacean-flat-gesture.png
Normal file
BIN
assets/images/rustacean-flat-gesture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
28
assets/js_emit.html
Normal file
28
assets/js_emit.html
Normal 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>
|
||||
20
assets/shaders/custom_material.wgsl
Normal file
20
assets/shaders/custom_material.wgsl
Normal 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
59
build.rs
Normal 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(())
|
||||
}
|
||||
22
crates/bevy_cef_core/Cargo.toml
Normal file
22
crates/bevy_cef_core/Cargo.toml
Normal 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 = []
|
||||
18
crates/bevy_cef_core/src/browser_process.rs
Normal file
18
crates/bevy_cef_core/src/browser_process.rs
Normal 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::*;
|
||||
85
crates/bevy_cef_core/src/browser_process/app.rs
Normal file
85
crates/bevy_cef_core/src/browser_process/app.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
492
crates/bevy_cef_core/src/browser_process/browsers.rs
Normal file
492
crates/bevy_cef_core/src/browser_process/browsers.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
979
crates/bevy_cef_core/src/browser_process/browsers/keyboard.rs
Normal file
979
crates/bevy_cef_core/src/browser_process/browsers/keyboard.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
129
crates/bevy_cef_core/src/browser_process/client_handler.rs
Normal file
129
crates/bevy_cef_core/src/browser_process/client_handler.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
165
crates/bevy_cef_core/src/browser_process/display_handler.rs
Normal file
165
crates/bevy_cef_core/src/browser_process/display_handler.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
281
crates/bevy_cef_core/src/browser_process/localhost.rs
Normal file
281
crates/bevy_cef_core/src/browser_process/localhost.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
167
crates/bevy_cef_core/src/browser_process/renderer_handler.rs
Normal file
167
crates/bevy_cef_core/src/browser_process/renderer_handler.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
52
crates/bevy_cef_core/src/debug.rs
Normal file
52
crates/bevy_cef_core/src/debug.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
15
crates/bevy_cef_core/src/lib.rs
Normal file
15
crates/bevy_cef_core/src/lib.rs
Normal 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::*;
|
||||
}
|
||||
28
crates/bevy_cef_core/src/render_process.rs
Normal file
28
crates/bevy_cef_core/src/render_process.rs
Normal 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(),
|
||||
);
|
||||
}
|
||||
63
crates/bevy_cef_core/src/render_process/app.rs
Normal file
63
crates/bevy_cef_core/src/render_process/app.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
181
crates/bevy_cef_core/src/render_process/brp.rs
Normal file
181
crates/bevy_cef_core/src/render_process/brp.rs
Normal 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
|
||||
}
|
||||
82
crates/bevy_cef_core/src/render_process/emit.rs
Normal file
82
crates/bevy_cef_core/src/render_process/emit.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
77
crates/bevy_cef_core/src/render_process/listen.rs
Normal file
77
crates/bevy_cef_core/src/render_process/listen.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
140
crates/bevy_cef_core/src/util.rs
Normal file
140
crates/bevy_cef_core/src/util.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
crates/bevy_cef_core/src/util/v8_accessor.rs
Normal file
48
crates/bevy_cef_core/src/util/v8_accessor.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
70
crates/bevy_cef_core/src/util/v8_handler_wrapper.rs
Normal file
70
crates/bevy_cef_core/src/util/v8_handler_wrapper.rs
Normal 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 {
|
||||
// //
|
||||
// // }
|
||||
48
crates/bevy_cef_core/src/util/v8_interceptor.rs
Normal file
48
crates/bevy_cef_core/src/util/v8_interceptor.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
12
crates/bevy_cef_debug_render_process/Cargo.toml
Normal file
12
crates/bevy_cef_debug_render_process/Cargo.toml
Normal 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"}
|
||||
12
crates/bevy_cef_debug_render_process/README.md
Normal file
12
crates/bevy_cef_debug_render_process/README.md
Normal 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
|
||||
```
|
||||
19
crates/bevy_cef_debug_render_process/src/main.rs
Normal file
19
crates/bevy_cef_debug_render_process/src/main.rs
Normal 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
5
docs/book.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[book]
|
||||
authors = ["not-elm"]
|
||||
language = "en"
|
||||
src = "src"
|
||||
title = "bevy_cef"
|
||||
91
docs/src/README.md
Normal file
91
docs/src/README.md
Normal 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
12
docs/src/SUMMARY.md
Normal 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
274
docs/src/basic-concepts.md
Normal 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
407
docs/src/core-components.md
Normal 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
137
docs/src/installation.md
Normal 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
220
docs/src/quick-start.md
Normal 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
BIN
examples/.DS_Store
vendored
Normal file
Binary file not shown.
88
examples/brp.rs
Normal file
88
examples/brp.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
57
examples/custom_material.rs
Normal file
57
examples/custom_material.rs
Normal 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
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
14
examples/demo/Cargo.toml
Normal 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
126
examples/demo/src/main.rs
Normal 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
75
examples/devtool.rs
Normal 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
58
examples/host_emit.rs
Normal 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
55
examples/js_emit.rs
Normal 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
69
examples/navigation.rs
Normal 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
40
examples/simple.rs
Normal 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
27
examples/sprite.rs
Normal 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
54
examples/zoom_level.rs
Normal 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
9
src/common.rs
Normal 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
80
src/common/components.rs
Normal 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
17
src/common/ipc.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
37
src/common/ipc/host_emit.rs
Normal file
37
src/common/ipc/host_emit.rs
Normal 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
46
src/common/ipc/js_emit.rs
Normal 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
15
src/common/localhost.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
156
src/common/localhost/asset_loader.rs
Normal file
156
src/common/localhost/asset_loader.rs
Normal 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)
|
||||
}
|
||||
59
src/common/localhost/responser.rs
Normal file
59
src/common/localhost/responser.rs
Normal 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
150
src/common/message_loop.rs
Normal 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
38
src/cursor_icon.rs
Normal 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
73
src/keyboard.rs
Normal 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
47
src/lib.rs
Normal 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
23
src/mute.rs
Normal 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
30
src/navigation.rs
Normal 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
2
src/system_param.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod mesh_aabb;
|
||||
pub mod pointer;
|
||||
53
src/system_param/mesh_aabb.rs
Normal file
53
src/system_param/mesh_aabb.rs
Normal 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)
|
||||
}
|
||||
84
src/system_param/pointer.rs
Normal file
84
src/system_param/pointer.rs
Normal 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
148
src/webview.rs
Normal 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
104
src/webview/mesh.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/webview/mesh/webview_extend_material.rs
Normal file
58
src/webview/mesh/webview_extend_material.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/webview/mesh/webview_extend_standard_material.rs
Normal file
54
src/webview/mesh/webview_extend_standard_material.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/webview/mesh/webview_extend_standard_material.wgsl
Normal file
32
src/webview/mesh/webview_extend_standard_material.wgsl
Normal 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;
|
||||
}
|
||||
55
src/webview/mesh/webview_material.rs
Normal file
55
src/webview/mesh/webview_material.rs
Normal 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(),
|
||||
);
|
||||
}
|
||||
13
src/webview/mesh/webview_util.wgsl
Normal file
13
src/webview/mesh/webview_util.wgsl
Normal 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);
|
||||
}
|
||||
143
src/webview/webview_sprite.rs
Normal file
143
src/webview/webview_sprite.rs
Normal 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
21
src/zoom.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user