INIT
This commit is contained in:
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