diff --git a/Cargo.toml b/Cargo.toml index 928df56..8ee31cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/examples/brp.rs b/examples/brp.rs index ef264c9..7b22b71 100644 --- a/examples/brp.rs +++ b/examples/brp.rs @@ -51,7 +51,7 @@ fn spawn_webview( mut materials: ResMut>, ) { 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>, + webviews: Query>, mut initialized: Local, ) { if *initialized { diff --git a/examples/custom_material.rs b/examples/custom_material.rs index 9cfb503..9a3aad1 100644 --- a/examples/custom_material.rs +++ b/examples/custom_material.rs @@ -33,7 +33,7 @@ fn spawn_webview( asset_server: Res, ) { 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 { diff --git a/examples/devtool.rs b/examples/devtool.rs index de5ea5b..17ceeea 100644 --- a/examples/devtool.rs +++ b/examples/devtool.rs @@ -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 { diff --git a/examples/extensions.rs b/examples/extensions.rs index 5bb69ef..7ed39ab 100644 --- a/examples/extensions.rs +++ b/examples/extensions.rs @@ -64,7 +64,7 @@ fn spawn_webview( mut materials: ResMut>, ) { commands.spawn(( - CefWebviewUri::local("extensions.html"), + WebviewSource::local("extensions.html"), Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))), MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())), )); diff --git a/examples/host_emit.rs b/examples/host_emit.rs index de2c407..b7ac9f6 100644 --- a/examples/host_emit.rs +++ b/examples/host_emit.rs @@ -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())), )); diff --git a/examples/inline_html.rs b/examples/inline_html.rs new file mode 100644 index 0000000..6b78021 --- /dev/null +++ b/examples/inline_html.rs @@ -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>, + mut materials: ResMut>, +) { + commands.spawn(( + WebviewSource::inline( + r#" + + + + + +
+

Inline HTML

+

This content is rendered from a Rust string.

+
+ +"#, + ), + Mesh3d(meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE))), + MeshMaterial3d(materials.add(WebviewExtendStandardMaterial::default())), + )); +} diff --git a/examples/js_emit.rs b/examples/js_emit.rs index 6df888a..e178dab 100644 --- a/examples/js_emit.rs +++ b/examples/js_emit.rs @@ -48,7 +48,7 @@ fn spawn_webview( mut materials: ResMut>, ) { 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())), )); diff --git a/examples/navigation.rs b/examples/navigation.rs index fc20289..5418ce1 100644 --- a/examples/navigation.rs +++ b/examples/navigation.rs @@ -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())), )); diff --git a/examples/preload_scripts.rs b/examples/preload_scripts.rs index 84f87c7..55adde0 100644 --- a/examples/preload_scripts.rs +++ b/examples/preload_scripts.rs @@ -35,7 +35,7 @@ fn spawn_webview( mut materials: ResMut>, ) { 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. diff --git a/examples/simple.rs b/examples/simple.rs index 3cfd51d..e7d397b 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -33,7 +33,7 @@ fn spawn_webview( mut materials: ResMut>, ) { 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())), )); diff --git a/examples/sprite.rs b/examples/sprite.rs index bd72f14..2484c4e 100644 --- a/examples/sprite.rs +++ b/examples/sprite.rs @@ -16,7 +16,7 @@ fn spawn_camera_2d(mut commands: Commands) { fn spawn_sprite_webview(mut commands: Commands, mut images: ResMut>) { 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()), diff --git a/examples/zoom_level.rs b/examples/zoom_level.rs index f0ba217..dcd3a70 100644 --- a/examples/zoom_level.rs +++ b/examples/zoom_level.rs @@ -35,7 +35,7 @@ fn spawn_webview( mut materials: ResMut>, ) { 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())), )); diff --git a/src/common.rs b/src/common.rs index b829ca6..0e09e25 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,6 +1,6 @@ mod components; mod ipc; -mod localhost; +pub(crate) mod localhost; mod message_loop; pub use components::*; diff --git a/src/common/components.rs b/src/common/components.rs index dabbbdc..9882306 100644 --- a/src/common/components.rs +++ b/src/common/components.rs @@ -7,7 +7,7 @@ pub(crate) struct WebviewCoreComponentsPlugin; impl Plugin for WebviewCoreComponentsPlugin { fn build(&self, app: &mut App) { app.register_type::() - .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -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) -> Self { - Self(uri.into()) + /// To specify a local file path, use [`WebviewSource::local`] instead. + pub fn new(url: impl Into) -> 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/`. - pub fn local(uri: impl Into) -> Self { - Self(format!("{SCHEME_CEF}://{HOST_CEF}/{}", uri.into())) + /// The given path is interpreted as `cef://localhost/`. + pub fn local(path: impl Into) -> 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) -> 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. diff --git a/src/common/localhost/responser.rs b/src/common/localhost/responser.rs index c883057..5fd956e 100644 --- a/src/common/localhost/responser.rs +++ b/src/common/localhost/responser.rs @@ -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>, +} + +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::() + .add_systems(PreUpdate, resolve_webview_source) .add_systems( Update, ( @@ -26,19 +51,78 @@ fn any_changed_assets(mut er: MessageReader>) -> bool { .any(|event| matches!(event, AssetEvent::Modified { .. })) } +fn resolve_webview_source( + mut commands: Commands, + mut store: ResMut, + query: Query<(Entity, &WebviewSource, Option<&InlineHtmlId>), Changed>, +) { + 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::(); + } + } + 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, asset_server: Res, + store: Res, ) { 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>>, diff --git a/src/common/message_loop.rs b/src/common/message_loop.rs index 5fbaa26..94d8068 100644 --- a/src/common/message_loop.rs +++ b/src/common/message_loop.rs @@ -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 _, diff --git a/src/keyboard.rs b/src/keyboard.rs index 793dc9e..4392946 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -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}; @@ -70,7 +70,7 @@ fn send_key_event( mut is_ime_commiting: ResMut, input: Res>, browsers: NonSend, - webviews: Query>, + webviews: Query>, ) { let modifiers = keyboard_modifiers(&input); for event in er.read() { diff --git a/src/system_param/pointer.rs b/src/system_param/pointer.rs index 8b605b6..dd478b8 100644 --- a/src/system_param/pointer.rs +++ b/src/system_param/pointer.rs @@ -1,4 +1,4 @@ -use crate::prelude::{CefWebviewUri, WebviewSize}; +use crate::prelude::{WebviewSize, WebviewSource}; use crate::system_param::mesh_aabb::MeshAabb; use bevy::ecs::system::SystemParam; use bevy::prelude::*; @@ -12,9 +12,9 @@ pub struct WebviewPointer<'w, 's, C: Component = Camera3d> { 'w, 's, (&'static GlobalTransform, &'static WebviewSize), - (With, Without), + (With, Without), >, - parents: Query<'w, 's, (Option<&'static ChildOf>, Has)>, + parents: Query<'w, 's, (Option<&'static ChildOf>, Has)>, } impl WebviewPointer<'_, '_, C> { @@ -46,7 +46,7 @@ impl WebviewPointer<'_, '_, C> { fn find_webview_entity( entity: Entity, - parents: &Query<(Option<&ChildOf>, Has)>, + parents: &Query<(Option<&ChildOf>, Has)>, ) -> Option { let (child_of, has_webview) = parents.get(entity).ok()?; if has_webview { diff --git a/src/webview.rs b/src/webview.rs index 72f3a95..1d1f061 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -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::() + .register_component_hooks::() .on_despawn(|mut world: DeferredWorld, ctx: HookContext| { world.non_send_resource_mut::().close(&ctx.entity); }); + + app.world_mut() + .register_component_hooks::() + .on_remove(|mut world: DeferredWorld, ctx: HookContext| { + let id = world.get::(ctx.entity).unwrap().0.clone(); + world.resource_mut::().remove(&id); + }); } } @@ -96,7 +107,7 @@ fn any_resized(webviews: Query>) -> bool { !webviews.is_empty() } -fn added_webview(webviews: Query>) -> bool { +fn added_webview(webviews: Query>) -> bool { !webviews.is_empty() } @@ -114,12 +125,12 @@ fn create_webview( webviews: Query< ( Entity, - &CefWebviewUri, + &ResolvedWebviewUri, &WebviewSize, &PreloadScripts, Option<&HostWindow>, ), - Added, + Added, >, primary_window: Query>, ) { @@ -148,6 +159,19 @@ fn create_webview( }); } +fn navigate_on_source_change( + browsers: NonSend, + webviews: Query<(Entity, &ResolvedWebviewUri), Changed>, + added: Query>, +) { + for (entity, uri) in webviews.iter() { + if added.contains(entity) { + continue; + } + browsers.navigate(&entity, &uri.0); + } +} + fn resize( browsers: NonSend, webviews: Query<(Entity, &WebviewSize), Changed>, diff --git a/src/webview/mesh.rs b/src/webview/mesh.rs index e597783..8611098 100644 --- a/src/webview/mesh.rs +++ b/src/webview/mesh.rs @@ -37,7 +37,7 @@ impl Plugin for MeshWebviewPlugin { fn setup_observers( mut commands: Commands, - webviews: Query, Or<(With, With)>)>, + webviews: Query, Or<(With, With)>)>, ) { for entity in webviews.iter() { commands @@ -88,7 +88,7 @@ fn on_mouse_wheel( browsers: NonSend, pointer: WebviewPointer, windows: Query<&Window>, - webviews: Query>, + webviews: Query>, ) { let Some(cursor_pos) = windows.iter().find_map(|window| window.cursor_position()) else { return; diff --git a/src/webview/webview_sprite.rs b/src/webview/webview_sprite.rs index ccdd9ce..8746b9f 100644 --- a/src/webview/webview_sprite.rs +++ b/src/webview/webview_sprite.rs @@ -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, mut images: ResMut>, - webviews: Query<&Sprite, With>, + webviews: Query<&Sprite, With>, ) { 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, With)>, + webviews: Query, With)>, ) { for entity in webviews.iter() { commands