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