use crate::common::localhost::responser::{InlineHtmlId, InlineHtmlStore};
use crate::common::{
CefShutDown, HostWindow, IpcEventRawSender, ResolvedWebviewUri, WebviewDpr, WebviewSize,
WebviewSource, ZoomLevel,
};
use crate::cursor_icon::SystemCursorIconSender;
use crate::drag::DraggableRegionSender;
use crate::navigation::{AddressChangedSender, LoadHandlerSender};
use crate::prelude::{PreloadScripts, WebviewProfile};
use crate::title::TitleChangedSender;
use crate::webview::mesh::MeshWebviewPlugin;
use crate::webview::ui::UiWebviewPlugin;
use bevy::ecs::lifecycle::HookContext;
use bevy::ecs::system::SystemParam;
use bevy::ecs::world::DeferredWorld;
use bevy::input::mouse::MouseScrollUnit;
use bevy::prelude::*;
use bevy::shader::load_shader_library;
use bevy::window::PrimaryWindow;
use bevy::winit::WINIT_WINDOWS;
use bevy_cef_core::prelude::*;
use bevy_remote::BrpSender;
#[allow(deprecated)]
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use serde::{Deserialize, Serialize};
use std::time::Duration;
pub(crate) mod alpha;
#[cfg(target_os = "macos")]
pub(crate) mod gpu_surface;
mod mesh;
pub mod texture_target;
mod ui;
pub(crate) mod webview_sprite;
pub mod prelude {
pub use crate::webview::{
BeginFrameInterval, RequestCloseDevtool, RequestShowDevTool, WebviewPlugin, mesh::*,
texture_target::*, ui::WebviewUiMaterial,
};
}
#[derive(Reflect, Debug, Copy, Clone, Serialize, Deserialize, EntityEvent)]
#[reflect(Serialize, Deserialize)]
pub struct RequestShowDevTool {
#[event_target]
pub webview: Entity,
}
#[derive(Reflect, Debug, Copy, Clone, Serialize, Deserialize, EntityEvent)]
#[reflect(Serialize, Deserialize)]
pub struct RequestCloseDevtool {
#[event_target]
pub webview: Entity,
}
#[derive(Resource)]
pub struct BeginFrameInterval(pub Duration);
impl Default for BeginFrameInterval {
fn default() -> Self {
Self(Duration::from_millis(1000 / 30))
}
}
#[derive(SystemSet, Clone, Debug, Hash, PartialEq, Eq)]
pub enum WebviewSet {
ResizeInteraction,
DpiSeed,
DerivePipeline,
CreateBrowser,
CommitResize,
}
pub struct WebviewPlugin;
impl Plugin for WebviewPlugin {
fn build(&self, app: &mut App) {
load_shader_library!(app, "webview/util.wesl");
app.register_type::<RequestShowDevTool>();
app.configure_sets(
Update,
(
WebviewSet::ResizeInteraction,
WebviewSet::DpiSeed,
WebviewSet::DerivePipeline,
WebviewSet::CreateBrowser,
WebviewSet::CommitResize,
)
.chain(),
);
app.init_non_send::<Browsers>()
.init_resource::<BeginFrameInterval>()
.add_plugins((MeshWebviewPlugin, UiWebviewPlugin))
.add_systems(
Update,
(
resize.run_if(any_resized).in_set(WebviewSet::CommitResize),
create_webview
.run_if(added_webview)
.run_if(not(resource_exists::<CefShutDown>))
.in_set(WebviewSet::CreateBrowser),
navigate_on_source_change.before(WebviewSet::CreateBrowser),
),
)
.add_observer(apply_request_show_devtool)
.add_observer(apply_request_close_devtool);
if EXTERNAL_BEGIN_FRAME {
app.add_systems(Main, send_external_begin_frame);
}
#[cfg(target_os = "macos")]
app.add_plugins(crate::webview::gpu_surface::WebviewGpuInjectPlugin);
app.world_mut()
.register_component_hooks::<WebviewSource>()
.on_despawn(|mut world: DeferredWorld, ctx: HookContext| {
world.non_send_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);
});
}
}
pub(crate) type MeshWebviewFilter = (Or<(With<Mesh3d>, With<Mesh2d>)>, Without<Sprite>);
pub(crate) fn touch_asset<A: Asset>(assets: &mut Assets<A>, id: impl Into<AssetId<A>>) {
if let Some(asset) = assets.get_mut(id) {
asset.into_inner();
}
}
pub(crate) fn scroll_delta(unit: MouseScrollUnit, x: f32, y: f32) -> Vec2 {
match unit {
MouseScrollUnit::Line => Vec2::new(x * 120.0, y * 120.0),
MouseScrollUnit::Pixel => Vec2::new(x, y),
}
}
#[derive(SystemParam)]
pub(crate) struct BrowserCreation<'w, 's> {
requester: Res<'w, Requester>,
ipc_event_sender: Res<'w, IpcEventRawSender>,
brp_sender: Res<'w, BrpSender>,
cursor_icon_sender: Res<'w, SystemCursorIconSender>,
drag_regions_sender: Res<'w, DraggableRegionSender>,
load_handler_sender: Res<'w, LoadHandlerSender>,
address_changed_sender: Res<'w, AddressChangedSender>,
title_changed_sender: Res<'w, TitleChangedSender>,
primary_window: Query<'w, 's, Entity, With<PrimaryWindow>>,
}
pub(crate) struct CreateArgs<'a> {
pub(crate) uri: &'a str,
pub(crate) size: Vec2,
pub(crate) dpr: f32,
pub(crate) preload: &'a [String],
pub(crate) zoom: f64,
pub(crate) host_window: Option<Entity>,
}
impl BrowserCreation<'_, '_> {
pub(crate) fn create(
&self,
browsers: &mut Browsers,
webview: Entity,
args: CreateArgs,
profile: Option<&str>,
) -> CefCoreResult {
browsers.create_browser(
webview,
args.uri,
args.size,
args.dpr,
self.requester.clone(),
self.ipc_event_sender.0.clone(),
self.brp_sender.clone(),
self.cursor_icon_sender.clone(),
self.drag_regions_sender.0.clone(),
self.load_handler_sender.0.clone(),
self.address_changed_sender.0.clone(),
self.title_changed_sender.0.clone(),
args.preload,
self.window_handle(args.host_window),
args.zoom,
profile,
)
}
fn window_handle(&self, host_window: Option<Entity>) -> Option<RawWindowHandle> {
WINIT_WINDOWS.with(|winit_windows| {
let winit_windows = winit_windows.borrow();
host_window
.and_then(|window| winit_windows.get_window(window))
.or_else(|| winit_windows.get_window(self.primary_window.single().ok()?))
.and_then(|window| {
#[allow(deprecated)]
window.raw_window_handle().ok()
})
})
}
}
fn any_resized(webviews: Query<Entity, Changed<WebviewSize>>) -> bool {
!webviews.is_empty()
}
fn added_webview(
webviews: Query<Entity, (Added<ResolvedWebviewUri>, Without<WebviewProfile>)>,
) -> bool {
!webviews.is_empty()
}
fn send_external_begin_frame(
browsers: NonSend<Browsers>,
time: Res<Time>,
interval: Res<BeginFrameInterval>,
mut timer: Local<Option<Timer>>,
) {
if interval.is_changed() || timer.is_none() {
*timer = Some(Timer::new(interval.0, TimerMode::Repeating));
}
let timer = timer.as_mut().unwrap();
timer.tick(time.delta());
if timer.just_finished() {
browsers.send_external_begin_frame();
}
}
fn create_webview(
mut browsers: NonSendMut<Browsers>,
creation: BrowserCreation,
webviews: Query<
(
Entity,
&ResolvedWebviewUri,
&WebviewSize,
&WebviewDpr,
&PreloadScripts,
Option<&HostWindow>,
Option<&ZoomLevel>,
),
(Added<ResolvedWebviewUri>, Without<WebviewProfile>),
>,
) {
for (webview, uri, size, dpr, preload, host_window, zoom) in webviews.iter() {
let args = CreateArgs {
uri: &uri.0,
size: size.0,
dpr: dpr.0,
preload: &preload.0,
zoom: zoom.map_or(0.0, |zoom| zoom.0),
host_window: host_window.map(|window| window.0),
};
if let Err(error) = creation.create(&mut browsers, webview, args, None) {
error!("bevy_cef: failed to create the browser of webview {webview}: {error}");
}
}
}
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>>,
) {
for (webview, size) in webviews.iter() {
browsers.resize(&webview, size.0);
}
}
fn apply_request_show_devtool(trigger: On<RequestShowDevTool>, browsers: NonSend<Browsers>) {
browsers.show_devtool(&trigger.webview);
}
fn apply_request_close_devtool(trigger: On<RequestCloseDevtool>, browsers: NonSend<Browsers>) {
browsers.close_devtools(&trigger.webview);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sprite_with_mesh2d_is_not_a_mesh_webview() {
let mut world = World::new();
let sprite = world
.spawn((Sprite::default(), Mesh2d(Handle::default())))
.id();
let mut webviews = world.query_filtered::<Entity, MeshWebviewFilter>();
assert!(webviews.get(&world, sprite).is_err());
}
#[test]
fn meshes_without_sprite_are_mesh_webviews() {
let mut world = World::new();
let mesh3d = world.spawn(Mesh3d(Handle::default())).id();
let mesh2d = world.spawn(Mesh2d(Handle::default())).id();
let mut webviews = world.query_filtered::<Entity, MeshWebviewFilter>();
assert!(webviews.get(&world, mesh3d).is_ok());
assert!(webviews.get(&world, mesh2d).is_ok());
}
}