Compare commits

...

10 Commits

Author SHA1 Message Date
not-elm
d422e14cf8 fix: restrict on_mouse_wheel to mesh webviews only
Some checks failed
book / deploy (push) Has been cancelled
CI / test (macos-latest) (push) Has been cancelled
CI / test (ubuntu-latest) (push) Has been cancelled
CI / test (windows-latest) (push) Has been cancelled
CI / all-doc-tests (push) Has been cancelled
CI / lint (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:25:15 +09:00
not-elm
19f7628a53 feat: add alpha hit-test to WebviewPointer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:24:51 +09:00
not-elm
255dc27e1a feat: sync WebviewSurface in render systems
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:11:17 +09:00
not-elm
c35ca1b920 feat: add WebviewSurface component for alpha hit-testing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 23:11:14 +09:00
elm
1b1da6665c feat: unify webview source into WebviewSource enum with dynamic reload (#22)
Unify CefWebviewUri and InlineHtml into a single WebviewSource enum component, eliminating the dual-component inconsistency where both existed on the same entity
Add ResolvedWebviewUri internal component as a resolution layer between user-facing source and CEF
Support dynamic reload: mutating WebviewSource at runtime automatically navigates the existing browser via browsers.navigate() without recreation
Inline HTML is served via cef://localhost/__inline__/{id} with automatic cleanup on entity despawn
2026-02-12 15:26:47 +09:00
elm
a6fec33006 feat: per-entity navigate and reload requests (#21)
* feat: add per-entity navigate and reload methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* clippy

---------

Co-authored-by: not-elm <elmgameinfo@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 18:23:40 +09:00
not-elm
a26b8e9648 fmt 2026-02-07 11:58:22 +09:00
not-elm
9da992fdd6 update: bump CEF version to 144.4.0 and adjust CHANGELOG 2026-02-07 11:56:58 +09:00
elm
40a3455f55 Fix IME input handling (#20)
bevy_winit does not call `set_ime_allowed()` during initial window
creation when `Window::ime_enabled` is `true`. The `changed_windows`
system only fires when the value differs from cache, but the cache
is initialized from the window itself so they start equal. Since
winit 0.27+ requires explicit `set_ime_allowed(true)`, no Ime events
were ever generated.
2026-02-06 23:52:05 +09:00
elm
2afc7bb0de fix: enable IME input by working around bevy_winit bug (#19)
* fix: enable IME input by working around bevy_winit initialization bug
2026-02-06 00:43:49 +09:00
30 changed files with 413 additions and 97 deletions

View File

@@ -3,7 +3,7 @@
### Breaking Changes
- Support Bevy 0.18
- Update CEF version to 144.2.0+144.0.11
- Update CEF version to 144.4.0
- Improve message loop handling
- We can now specify command-line switches when creating the `CefPlugin`.
- As a result, `CefPlugin` is no longer a unit struct.
@@ -34,6 +34,7 @@
- Fixed so that the webview can detect pointers correctly even if it is not the root entity.
- Avoid a crash when updating the cursor icon
- Fixed IME input not working due to `bevy_winit` not calling `set_ime_allowed()` on initial window creation
## v0.1.0

8
Cargo.lock generated
View File

@@ -1930,9 +1930,9 @@ dependencies = [
[[package]]
name = "cef"
version = "144.2.0+144.0.11"
version = "144.4.0+144.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8743b7a17569f5a0b27a1ef72a2275dc096ae32263bf35fc5541f46e73084ec5"
checksum = "42320c2356f3fe4ed3f875b8fc93cbbabd1be3c59fcc0a8eb1e5b5c25616ac28"
dependencies = [
"anyhow",
"cargo_metadata",
@@ -1950,9 +1950,9 @@ dependencies = [
[[package]]
name = "cef-dll-sys"
version = "144.2.0+144.0.11"
version = "144.4.0+144.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf6882e1e10fe7ff0c9b1815e8a2943c0975be5bb48f15ca3b4cbb9dc4329d0e"
checksum = "6d802abd4ac0b8c12be11b6af24100c6e21543c265867ad53067268048bc7e3a"
dependencies = [
"anyhow",
"cmake",

View File

@@ -38,8 +38,8 @@ bevy = { version = "0.18", default-features = false, features = [
"picking",
] }
bevy_remote = "0.18"
cef = { version = "144.2.0" }
cef-dll-sys = { version = "144.2.0", features = ["sandbox"] }
cef = { version = "144.4.0" }
cef-dll-sys = { version = "144.4.0", features = ["sandbox"] }
bevy_cef = { path = "." , version = "0.2.0-dev" }
bevy_cef_core = { path = "crates/bevy_cef_core", version = "0.2.0-dev" }
async-channel = { version = "2.5" }
@@ -64,6 +64,9 @@ bevy_cef = { workspace = true, features = ["debug"] }
[target.'cfg(target_os = "macos")'.dependencies]
objc = { version = "0.2" }
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("cargo-clippy"))'] }
[features]
default = []
serialize = ["bevy/serialize"]

View File

@@ -68,6 +68,7 @@ impl ImplBrowserProcessHandler for BrowserProcessHandlerBuilder {
command_line.append_switch(Some(&"ignore-certificate-errors".into()));
command_line.append_switch(Some(&"ignore-ssl-errors".into()));
command_line.append_switch(Some(&"enable-logging=stderr".into()));
command_line.append_switch(Some(&"disable-web-security".into()));
// Pass extensions to render process via command line
if !self.extensions.is_empty()
&& let Ok(json) = serde_json::to_string(&self.extensions.0)

View File

@@ -31,7 +31,6 @@ pub struct WebviewBrowser {
pub client: Browser,
pub host: BrowserHost,
pub size: SharedViewSize,
pub ime_caret: SharedImeCaret,
}
pub struct Browsers {
@@ -67,7 +66,6 @@ impl Browsers {
) {
let mut context = Self::request_context(requester);
let size = Rc::new(Cell::new(webview_size));
let ime_caret = Rc::new(Cell::new(0));
let browser = browser_host_create_browser_sync(
Some(&WindowInfo {
windowless_rendering_enabled: true as _,
@@ -86,7 +84,6 @@ impl Browsers {
Some(&mut self.client_handler(
webview,
size.clone(),
ime_caret.clone(),
ipc_event_sender,
brp_sender,
system_cursor_icon_sender,
@@ -105,7 +102,6 @@ impl Browsers {
host,
client: browser,
size,
ime_caret,
};
self.browsers.insert(webview, webview_browser);
@@ -273,6 +269,25 @@ impl Browsers {
}
}
/// Navigate a specific webview to a new URL.
pub fn navigate(&self, webview: &Entity, url: &str) {
if let Some(browser) = self.browsers.get(webview)
&& let Some(frame) = browser.client.main_frame()
{
frame.load_url(Some(&url.into()));
}
}
/// Reload a specific webview's current page.
pub fn reload_webview(&self, webview: &Entity) {
if let Some(browser) = self.browsers.get(webview)
&& let Some(frame) = browser.client.main_frame()
{
let url = frame.url().into_string();
frame.load_url(Some(&url.as_str().into()));
}
}
/// Returns the current zoom level for the specified webview.
///
/// ## Reference
@@ -332,7 +347,7 @@ impl Browsers {
.values()
.filter(|b| b.client.focused_frame().is_some())
{
let replacement_range = Self::ime_caret_range_for(browser);
let replacement_range = Self::ime_caret_range_for();
browser.host.ime_set_composition(
Some(&text.into()),
Some(&underlines),
@@ -342,6 +357,19 @@ impl Browsers {
}
}
/// ## Reference
///
/// [`ImeCancelComposition`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#ac12a8076859d0c1e58e55080f698e7a9)
pub fn ime_cancel_composition(&self) {
for browser in self
.browsers
.values()
.filter(|b| b.client.focused_frame().is_some())
{
browser.host.ime_cancel_composition();
}
}
/// ## Reference
///
/// [`ImeSetComposition`](https://cef-builds.spotifycdn.com/docs/122.0/classCefBrowserHost.html#a567b41fb2d3917843ece3b57adc21ebe)
@@ -361,10 +389,10 @@ impl Browsers {
.values()
.filter(|b| b.client.focused_frame().is_some())
{
let replacement_range = Self::ime_caret_range_for(browser);
let replacement_range = Self::ime_caret_range_for();
browser
.host
.ime_commit_text(Some(&text.into()), Some(&replacement_range), 0)
.ime_commit_text(Some(&text.into()), Some(&replacement_range), 0);
}
}
@@ -387,7 +415,6 @@ impl Browsers {
&self,
webview: Entity,
size: SharedViewSize,
ime_caret: SharedImeCaret,
ipc_event_sender: Sender<IpcEventRaw>,
brp_sender: Sender<BrpMessage>,
system_cursor_icon_sender: SystemCursorIconSenderInner,
@@ -396,7 +423,6 @@ impl Browsers {
webview,
self.sender.clone(),
size.clone(),
ime_caret,
))
.with_display_handler(DisplayHandlerBuilder::build(system_cursor_icon_sender))
.with_message_handler(JsEmitEventHandler::new(webview, ipc_event_sender))
@@ -405,11 +431,11 @@ impl Browsers {
}
#[inline]
fn ime_caret_range_for(browser: &WebviewBrowser) -> Range {
let caret = browser.ime_caret.get();
fn ime_caret_range_for() -> Range {
// Use sentinel replacement range to indicate caret position
Range {
from: caret,
to: caret,
from: u32::MAX,
to: u32::MAX,
}
}

View File

@@ -33,8 +33,7 @@ pub enum RenderPaintElementType {
Popup,
}
pub type SharedViewSize = std::rc::Rc<Cell<bevy::prelude::Vec2>>;
pub type SharedImeCaret = std::rc::Rc<Cell<u32>>;
pub type SharedViewSize = std::rc::Rc<Cell<Vec2>>;
/// ## Reference
///
@@ -44,7 +43,6 @@ pub struct RenderHandlerBuilder {
webview: Entity,
texture_sender: TextureSender,
size: SharedViewSize,
ime_caret: SharedImeCaret,
}
impl RenderHandlerBuilder {
@@ -52,14 +50,12 @@ impl RenderHandlerBuilder {
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,
})
}
}
@@ -91,7 +87,6 @@ impl Clone for RenderHandlerBuilder {
webview: self.webview,
texture_sender: self.texture_sender.clone(),
size: self.size.clone(),
ime_caret: self.ime_caret.clone(),
}
}
}
@@ -131,17 +126,6 @@ impl ImplRenderHandler for RenderHandlerBuilder {
let _ = self.texture_sender.send_blocking(texture);
}
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);
}
}
#[inline]
fn get_raw(&self) -> *mut sys::_cef_render_handler_t {
self.object.cast()

View File

@@ -51,7 +51,7 @@ fn spawn_webview(
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri::local("brp.html"),
WebviewSource::local("brp.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial {
base: StandardMaterial {
@@ -72,7 +72,7 @@ fn ime(mut windows: Query<&mut bevy::prelude::Window>) {
fn show_devtool(
mut commands: Commands,
webviews: Query<Entity, With<CefWebviewUri>>,
webviews: Query<Entity, With<WebviewSource>>,
mut initialized: Local<bool>,
) {
if *initialized {

View File

@@ -33,7 +33,7 @@ fn spawn_webview(
asset_server: Res<AssetServer>,
) {
commands.spawn((
CefWebviewUri::new("https://github.com/not-elm/bevy_cef"),
WebviewSource::new("https://github.com/not-elm/bevy_cef"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendedMaterial {
extension: CustomExtension {

View File

@@ -49,7 +49,7 @@ fn spawn_webview(
) {
commands.spawn((
DebugWebview,
CefWebviewUri::new("https://github.com/not-elm/bevy_cef"),
WebviewSource::new("https://github.com/not-elm/bevy_cef"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial {
base: StandardMaterial {

View File

@@ -64,7 +64,7 @@ fn spawn_webview(
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri::local("extensions.html"),
WebviewSource::local("extensions.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));

View File

@@ -40,7 +40,7 @@ fn spawn_webview(
) {
commands.spawn((
DebugWebview,
CefWebviewUri::local("host_emit.html"),
WebviewSource::local("host_emit.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));

67
examples/inline_html.rs Normal file
View File

@@ -0,0 +1,67 @@
//! Demonstrates rendering inline HTML content directly without an external URL or asset file.
use bevy::prelude::*;
use bevy_cef::prelude::*;
fn main() {
App::new()
.add_plugins((DefaultPlugins, CefPlugin::default()))
.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((
WebviewSource::inline(
r#"<!DOCTYPE html>
<html>
<head>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: system-ui, sans-serif;
color: white;
}
.container { text-align: center; }
h1 { font-size: 3rem; margin-bottom: 0.5rem; }
p { font-size: 1.2rem; opacity: 0.8; }
</style>
</head>
<body>
<div class="container">
<h1>Inline HTML</h1>
<p>This content is rendered from a Rust string.</p>
</div>
</body>
</html>"#,
),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));
}

View File

@@ -48,7 +48,7 @@ fn spawn_webview(
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri::local("js_emit.html"),
WebviewSource::local("js_emit.html"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));

View File

@@ -50,7 +50,7 @@ fn spawn_webview(
) {
commands.spawn((
DebugWebview,
CefWebviewUri::new("https://github.com/not-elm/bevy_cef"),
WebviewSource::new("https://github.com/not-elm/bevy_cef"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));

View File

@@ -35,7 +35,7 @@ fn spawn_webview(
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri::new("https://github.com/not-elm/bevy_cef"),
WebviewSource::new("https://github.com/not-elm/bevy_cef"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
// Here, we add a simple script to show an alert.

View File

@@ -33,7 +33,7 @@ fn spawn_webview(
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri::new("https://github.com/not-elm/bevy_cef"),
WebviewSource::new("https://github.com/not-elm/bevy_cef"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));

View File

@@ -16,7 +16,7 @@ fn spawn_camera_2d(mut commands: Commands) {
fn spawn_sprite_webview(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
commands.spawn((
CefWebviewUri::new("https://github.com/not-elm/bevy_cef"),
WebviewSource::new("https://github.com/not-elm/bevy_cef"),
Pickable::default(),
Sprite {
image: images.add(Image::default()),

View File

@@ -35,7 +35,7 @@ fn spawn_webview(
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
) {
commands.spawn((
CefWebviewUri("https://bevy.org/".to_string()),
WebviewSource::new("https://bevy.org/"),
Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))),
MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())),
));

View File

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

View File

@@ -7,7 +7,7 @@ pub(crate) struct WebviewCoreComponentsPlugin;
impl Plugin for WebviewCoreComponentsPlugin {
fn build(&self, app: &mut App) {
app.register_type::<WebviewSize>()
.register_type::<CefWebviewUri>()
.register_type::<WebviewSource>()
.register_type::<HostWindow>()
.register_type::<ZoomLevel>()
.register_type::<AudioMuted>()
@@ -15,35 +15,54 @@ impl Plugin for WebviewCoreComponentsPlugin {
}
}
/// A component that specifies the URI of the webview.
/// A component that specifies the content source of a webview.
///
/// When opening a remote web page, specify the URI with the http(s) schema.
/// Use [`WebviewSource::new`] for remote URLs, [`WebviewSource::local`] for local files
/// served via `cef://localhost/`, or [`WebviewSource::inline`] for raw HTML content.
///
/// 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)]
/// When the value of this component is changed at runtime, the existing browser
/// automatically navigates to the new source without being recreated.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Debug)]
#[require(WebviewSize, ZoomLevel, AudioMuted, PreloadScripts)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
pub struct CefWebviewUri(pub String);
pub enum WebviewSource {
/// A remote or local URL (e.g. `"https://..."` or `"cef://localhost/file.html"`).
Url(String),
/// Raw HTML content served via an internal `cef://localhost/__inline__/{id}` scheme.
InlineHtml(String),
}
impl CefWebviewUri {
/// Creates a new `CefWebviewUri` with the given URI.
impl WebviewSource {
/// Creates a URL source.
///
/// If you want to specify a local file path, use [`CefWebviewUri::local`] instead.
pub fn new(uri: impl Into<String>) -> Self {
Self(uri.into())
/// To specify a local file path, use [`WebviewSource::local`] instead.
pub fn new(url: impl Into<String>) -> Self {
Self::Url(url.into())
}
/// Creates a new `CefWebviewUri` with the given file path.
/// Creates a local file source.
///
/// 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()))
/// The given path is interpreted as `cef://localhost/<path>`.
pub fn local(path: impl Into<String>) -> Self {
Self::Url(format!("{SCHEME_CEF}://{HOST_CEF}/{}", path.into()))
}
/// Creates an inline HTML source.
///
/// The HTML content is served through the internal `cef://localhost/__inline__/{id}` scheme,
/// so IPC (`window.cef.emit/listen/brp`) and [`PreloadScripts`] work as expected.
pub fn inline(html: impl Into<String>) -> Self {
Self::InlineHtml(html.into())
}
}
/// Internal component holding the resolved URL string passed to CEF.
///
/// This is automatically managed by the resolver system and should not be
/// inserted manually.
#[derive(Component, Debug, Clone)]
pub(crate) struct ResolvedWebviewUri(pub(crate) String);
/// Specifies the view size of the webview.
///
/// This does not affect the actual object size.
@@ -97,3 +116,10 @@ where
Self(scripts.into_iter().map(Into::into).collect())
}
}
/// Holds the webview surface texture handle for alpha hit-testing.
///
/// This component is automatically inserted and updated by the render systems.
/// It provides material-type-agnostic access to the webview texture.
#[derive(Component, Debug, Clone)]
pub(crate) struct WebviewSurface(pub(crate) Handle<Image>);

View File

@@ -1,7 +1,30 @@
use crate::common::localhost::asset_loader::CefResponseHandle;
use bevy::platform::collections::HashSet;
use crate::common::{ResolvedWebviewUri, WebviewSource};
use bevy::platform::collections::{HashMap, HashSet};
use bevy::prelude::*;
use bevy_cef_core::prelude::*;
use std::sync::atomic::{AtomicU64, Ordering};
static INLINE_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
/// Prefix for inline HTML URIs within the `cef://localhost/` scheme.
const INLINE_PREFIX: &str = "__inline__/";
/// Cleanup marker that stays on the entity. Removed on despawn to clean up the store.
#[derive(Component)]
pub(crate) struct InlineHtmlId(pub(crate) String);
/// In-memory store for inline HTML content.
#[derive(Resource, Default)]
pub(crate) struct InlineHtmlStore {
by_id: HashMap<String, Vec<u8>>,
}
impl InlineHtmlStore {
pub(crate) fn remove(&mut self, id: &str) {
self.by_id.remove(id);
}
}
pub struct ResponserPlugin;
@@ -10,6 +33,8 @@ impl Plugin for ResponserPlugin {
let (tx, rx) = async_channel::unbounded();
app.insert_resource(Requester(tx))
.insert_resource(RequesterReceiver(rx))
.init_resource::<InlineHtmlStore>()
.add_systems(PreUpdate, resolve_webview_source)
.add_systems(
Update,
(
@@ -26,19 +51,78 @@ fn any_changed_assets(mut er: MessageReader<AssetEvent<CefResponse>>) -> bool {
.any(|event| matches!(event, AssetEvent::Modified { .. }))
}
fn resolve_webview_source(
mut commands: Commands,
mut store: ResMut<InlineHtmlStore>,
query: Query<(Entity, &WebviewSource, Option<&InlineHtmlId>), Changed<WebviewSource>>,
) {
for (entity, source, existing_id) in query.iter() {
// Clean up old inline entry if switching away or updating
if let Some(old_id) = existing_id {
store.by_id.remove(&old_id.0);
}
match source {
WebviewSource::Url(url) => {
let mut entity_commands = commands.entity(entity);
entity_commands.insert(ResolvedWebviewUri(url.clone()));
if existing_id.is_some() {
entity_commands.remove::<InlineHtmlId>();
}
}
WebviewSource::InlineHtml(html) => {
let id = INLINE_ID_COUNTER
.fetch_add(1, Ordering::Relaxed)
.to_string();
store.by_id.insert(id.clone(), html.as_bytes().to_vec());
let url = format!("{SCHEME_CEF}://{HOST_CEF}/{INLINE_PREFIX}{id}");
commands
.entity(entity)
.insert((ResolvedWebviewUri(url), InlineHtmlId(id)));
}
}
}
}
fn coming_request(
mut commands: Commands,
requester_receiver: Res<RequesterReceiver>,
asset_server: Res<AssetServer>,
store: Res<InlineHtmlStore>,
) {
while let Ok(request) = requester_receiver.0.try_recv() {
commands.spawn((
CefResponseHandle(asset_server.load(request.uri)),
request.responser,
));
if let Some(id) = extract_inline_id(&request.uri) {
let response = match store.by_id.get(id) {
Some(data) => CefResponse {
mime_type: "text/html".to_string(),
status_code: 200,
data: data.clone(),
},
None => CefResponse {
mime_type: "text/plain".to_string(),
status_code: 404,
data: b"Not Found".to_vec(),
},
};
let _ = request.responser.0.send_blocking(response);
} else {
commands.spawn((
CefResponseHandle(asset_server.load(request.uri)),
request.responser,
));
}
}
}
/// Extracts the inline ID from a URI like `__inline__/123` or `__inline__/123?query#fragment`.
fn extract_inline_id(uri: &str) -> Option<&str> {
let rest = uri.strip_prefix(INLINE_PREFIX)?;
// Strip query string and fragment
let id = rest.split(['?', '#']).next().unwrap_or(rest);
Some(id)
}
fn responser(
mut commands: Commands,
mut handle_stores: Local<HashSet<Handle<CefResponse>>>,

View File

@@ -131,7 +131,6 @@ mod macos {
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 _,
@@ -141,7 +140,6 @@ mod macos {
);
assert!(success, "メソッド追加に失敗しました");
#[allow(unexpected_cfgs)]
let sel_set = sel!(setHandlingSendEvent:);
let success2 = class_addMethod(
cls as *const _,

View File

@@ -1,4 +1,4 @@
use crate::common::CefWebviewUri;
use crate::common::WebviewSource;
use bevy::input::keyboard::KeyboardInput;
use bevy::prelude::*;
use bevy_cef_core::prelude::{Browsers, create_cef_key_event, keyboard_modifiers};
@@ -14,6 +14,9 @@ impl Plugin for KeyboardPlugin {
app.init_resource::<IsImeCommiting>().add_systems(
Update,
(
// Workaround for bevy_winit not calling `set_ime_allowed` on initial window
// creation when `Window::ime_enabled` is `true` from the start.
activate_ime,
ime_event.run_if(on_message::<Ime>),
send_key_event.run_if(on_message::<KeyboardInput>),
)
@@ -22,6 +25,42 @@ impl Plugin for KeyboardPlugin {
}
}
/// Workaround: bevy_winit does not call `winit::Window::set_ime_allowed()` during initial window
/// creation when `Window::ime_enabled` is `true`. This means `Ime` events are never generated.
///
/// To trigger bevy_winit's own `changed_windows` system, we temporarily toggle `ime_enabled` off
/// then back on over two frames, which causes the change detection to fire and call
/// `set_ime_allowed(true)` internally.
fn activate_ime(mut windows: Query<&mut Window>, mut state: Local<ImeActivationState>) {
match *state {
ImeActivationState::Pending => {
for mut window in windows.iter_mut() {
if window.ime_enabled {
window.ime_enabled = false;
*state = ImeActivationState::Toggled;
}
}
}
ImeActivationState::Toggled => {
for mut window in windows.iter_mut() {
if !window.ime_enabled {
window.ime_enabled = true;
*state = ImeActivationState::Done;
}
}
}
ImeActivationState::Done => {}
}
}
#[derive(Default)]
enum ImeActivationState {
#[default]
Pending,
Toggled,
Done,
}
#[derive(Resource, Default, Serialize, Deserialize, Reflect)]
#[reflect(Default, Serialize, Deserialize)]
struct IsImeCommiting(bool);
@@ -31,7 +70,7 @@ fn send_key_event(
mut is_ime_commiting: ResMut<IsImeCommiting>,
input: Res<ButtonInput<KeyCode>>,
browsers: NonSend<Browsers>,
webviews: Query<Entity, With<CefWebviewUri>>,
webviews: Query<Entity, With<WebviewSource>>,
) {
let modifiers = keyboard_modifiers(&input);
for event in er.read() {
@@ -65,7 +104,7 @@ fn ime_event(
is_ime_commiting.0 = true;
}
Ime::Disabled { .. } => {
browsers.ime_finish_composition(false);
browsers.ime_cancel_composition();
}
_ => {}
}

View File

@@ -8,8 +8,12 @@ impl Plugin for NavigationPlugin {
fn build(&self, app: &mut App) {
app.register_type::<RequestGoBack>()
.register_type::<RequestGoForward>()
.register_type::<RequestNavigate>()
.register_type::<RequestReload>()
.add_observer(apply_request_go_back)
.add_observer(apply_request_go_forward);
.add_observer(apply_request_go_forward)
.add_observer(apply_request_navigate)
.add_observer(apply_request_reload);
}
}
@@ -27,6 +31,21 @@ pub struct RequestGoForward {
pub webview: Entity,
}
/// A trigger event to navigate to a new URL.
#[derive(Debug, EntityEvent, Clone, Reflect, Serialize, Deserialize)]
pub struct RequestNavigate {
#[event_target]
pub webview: Entity,
pub url: String,
}
/// A trigger event to reload the current page.
#[derive(Debug, EntityEvent, Copy, Clone, Reflect, Serialize, Deserialize)]
pub struct RequestReload {
#[event_target]
pub webview: Entity,
}
fn apply_request_go_back(trigger: On<RequestGoBack>, browsers: NonSend<Browsers>) {
browsers.go_back(&trigger.webview);
}
@@ -34,3 +53,11 @@ fn apply_request_go_back(trigger: On<RequestGoBack>, browsers: NonSend<Browsers>
fn apply_request_go_forward(trigger: On<RequestGoForward>, browsers: NonSend<Browsers>) {
browsers.go_forward(&trigger.webview);
}
fn apply_request_navigate(trigger: On<RequestNavigate>, browsers: NonSend<Browsers>) {
browsers.navigate(&trigger.webview, &trigger.url);
}
fn apply_request_reload(trigger: On<RequestReload>, browsers: NonSend<Browsers>) {
browsers.reload_webview(&trigger.webview);
}

View File

@@ -1,4 +1,5 @@
use crate::prelude::{CefWebviewUri, WebviewSize};
use crate::common::WebviewSurface;
use crate::prelude::{WebviewSize, WebviewSource};
use crate::system_param::mesh_aabb::MeshAabb;
use bevy::ecs::system::SystemParam;
use bevy::prelude::*;
@@ -12,9 +13,11 @@ pub struct WebviewPointer<'w, 's, C: Component = Camera3d> {
'w,
's,
(&'static GlobalTransform, &'static WebviewSize),
(With<CefWebviewUri>, Without<Camera>),
(With<WebviewSource>, Without<Camera>),
>,
parents: Query<'w, 's, (Option<&'static ChildOf>, Has<CefWebviewUri>)>,
parents: Query<'w, 's, (Option<&'static ChildOf>, Has<WebviewSource>)>,
surfaces: Query<'w, 's, &'static WebviewSurface>,
images: Res<'w, Assets<Image>>,
}
impl<C: Component> WebviewPointer<'_, '_, C> {
@@ -31,7 +34,7 @@ impl<C: Component> WebviewPointer<'_, '_, C> {
let (min, max) = self.aabb.calculate_local(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)| {
let pos = self.cameras.iter().find_map(|(camera, camera_gtf)| {
pointer_to_webview_uv(
viewport_pos,
camera,
@@ -40,13 +43,38 @@ impl<C: Component> WebviewPointer<'_, '_, C> {
aabb_size,
webview_size.0,
)
})
})?;
if self.is_transparent_at(webview, pos) {
return None;
}
Some(pos)
}
fn is_transparent_at(&self, webview: Entity, pos: Vec2) -> bool {
let Ok(surface) = self.surfaces.get(webview) else {
return false;
};
let Some(image) = self.images.get(surface.0.id()) else {
return false;
};
let width = image.width();
let height = image.height();
if width == 0 || height == 0 {
return false;
}
let x = (pos.x.floor() as u32).min(width - 1);
let y = (pos.y.floor() as u32).min(height - 1);
let offset = ((y * width + x) * 4 + 3) as usize;
let Some(data) = image.data.as_ref() else {
return false;
};
data.len() > offset && data[offset] == 0
}
}
fn find_webview_entity(
entity: Entity,
parents: &Query<(Option<&ChildOf>, Has<CefWebviewUri>)>,
parents: &Query<(Option<&ChildOf>, Has<WebviewSource>)>,
) -> Option<Entity> {
let (child_of, has_webview) = parents.get(entity).ok()?;
if has_webview {

View File

@@ -1,4 +1,7 @@
use crate::common::{CefWebviewUri, HostWindow, IpcEventRawSender, WebviewSize};
use crate::common::localhost::responser::{InlineHtmlId, InlineHtmlStore};
use crate::common::{
HostWindow, IpcEventRawSender, ResolvedWebviewUri, WebviewSize, WebviewSource,
};
use crate::cursor_icon::SystemCursorIconSender;
use crate::prelude::PreloadScripts;
use crate::webview::mesh::MeshWebviewPlugin;
@@ -79,16 +82,24 @@ impl Plugin for WebviewPlugin {
(
resize.run_if(any_resized),
create_webview.run_if(added_webview),
navigate_on_source_change,
),
)
.add_observer(apply_request_show_devtool)
.add_observer(apply_request_close_devtool);
app.world_mut()
.register_component_hooks::<CefWebviewUri>()
.register_component_hooks::<WebviewSource>()
.on_despawn(|mut world: DeferredWorld, ctx: HookContext| {
world.non_send_resource_mut::<Browsers>().close(&ctx.entity);
});
app.world_mut()
.register_component_hooks::<InlineHtmlId>()
.on_remove(|mut world: DeferredWorld, ctx: HookContext| {
let id = world.get::<InlineHtmlId>(ctx.entity).unwrap().0.clone();
world.resource_mut::<InlineHtmlStore>().remove(&id);
});
}
}
@@ -96,7 +107,7 @@ fn any_resized(webviews: Query<Entity, Changed<WebviewSize>>) -> bool {
!webviews.is_empty()
}
fn added_webview(webviews: Query<Entity, Added<CefWebviewUri>>) -> bool {
fn added_webview(webviews: Query<Entity, Added<ResolvedWebviewUri>>) -> bool {
!webviews.is_empty()
}
@@ -114,12 +125,12 @@ fn create_webview(
webviews: Query<
(
Entity,
&CefWebviewUri,
&ResolvedWebviewUri,
&WebviewSize,
&PreloadScripts,
Option<&HostWindow>,
),
Added<CefWebviewUri>,
Added<ResolvedWebviewUri>,
>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) {
@@ -148,6 +159,19 @@ fn create_webview(
});
}
fn navigate_on_source_change(
browsers: NonSend<Browsers>,
webviews: Query<(Entity, &ResolvedWebviewUri), Changed<ResolvedWebviewUri>>,
added: Query<Entity, Added<ResolvedWebviewUri>>,
) {
for (entity, uri) in webviews.iter() {
if added.contains(entity) {
continue;
}
browsers.navigate(&entity, &uri.0);
}
}
fn resize(
browsers: NonSend<Browsers>,
webviews: Query<(Entity, &WebviewSize), Changed<WebviewSize>>,

View File

@@ -37,7 +37,7 @@ impl Plugin for MeshWebviewPlugin {
fn setup_observers(
mut commands: Commands,
webviews: Query<Entity, (Added<CefWebviewUri>, Or<(With<Mesh3d>, With<Mesh2d>)>)>,
webviews: Query<Entity, (Added<WebviewSource>, Or<(With<Mesh3d>, With<Mesh2d>)>)>,
) {
for entity in webviews.iter() {
commands
@@ -88,7 +88,7 @@ fn on_mouse_wheel(
browsers: NonSend<Browsers>,
pointer: WebviewPointer,
windows: Query<&Window>,
webviews: Query<Entity, With<CefWebviewUri>>,
webviews: Query<Entity, (With<WebviewSource>, Or<(With<Mesh3d>, With<Mesh2d>)>)>,
) {
let Some(cursor_pos) = windows.iter().find_map(|window| window.cursor_position()) else {
return;

View File

@@ -1,4 +1,4 @@
use crate::prelude::{WebviewMaterial, update_webview_image};
use crate::prelude::{WebviewMaterial, WebviewSurface, update_webview_image};
use bevy::app::Plugin;
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
@@ -35,6 +35,7 @@ where
}
fn render<E: MaterialExtension>(
mut commands: Commands,
mut er: MessageReader<RenderTextureMessage>,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<WebviewExtendedMaterial<E>>>,
@@ -48,6 +49,9 @@ fn render<E: MaterialExtension>(
.base
.surface
.get_or_insert_with(|| images.add(Image::default()));
commands
.entity(texture.webview)
.insert(WebviewSurface(handle.clone()));
images.get_mut(handle.id())
}
{

View File

@@ -1,4 +1,4 @@
use crate::prelude::{WebviewMaterial, update_webview_image};
use crate::prelude::{WebviewMaterial, WebviewSurface, update_webview_image};
use bevy::asset::*;
use bevy::pbr::{ExtendedMaterial, MaterialExtension};
use bevy::prelude::*;
@@ -31,6 +31,7 @@ impl MaterialExtension for WebviewMaterial {
pub type WebviewExtendStandardMaterial = ExtendedMaterial<StandardMaterial, WebviewMaterial>;
fn render_standard_materials(
mut commands: Commands,
mut er: MessageReader<RenderTextureMessage>,
mut images: ResMut<Assets<Image>>,
mut materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
@@ -44,6 +45,9 @@ fn render_standard_materials(
.extension
.surface
.get_or_insert_with(|| images.add(Image::default()));
commands
.entity(texture.webview)
.insert(WebviewSurface(handle.clone()));
images.get_mut(handle.id())
}
{

View File

@@ -1,4 +1,4 @@
use crate::common::{CefWebviewUri, WebviewSize};
use crate::common::{WebviewSize, WebviewSource};
use crate::prelude::update_webview_image;
use bevy::input::mouse::MouseWheel;
use bevy::prelude::*;
@@ -30,7 +30,7 @@ impl Plugin for WebviewSpritePlugin {
fn render(
mut er: MessageReader<RenderTextureMessage>,
mut images: ResMut<Assets<bevy::prelude::Image>>,
webviews: Query<&Sprite, With<CefWebviewUri>>,
webviews: Query<&Sprite, With<WebviewSource>>,
) {
for texture in er.read() {
if let Ok(sprite) = webviews.get(texture.webview)
@@ -43,7 +43,7 @@ fn render(
fn setup_observers(
mut commands: Commands,
webviews: Query<Entity, (Added<CefWebviewUri>, With<Sprite>)>,
webviews: Query<Entity, (Added<WebviewSource>, With<Sprite>)>,
) {
for entity in webviews.iter() {
commands