bevy_cef 0.11.0

Bevy CEF integration for web rendering
use bevy::prelude::*;
#[cfg(target_os = "macos")]
use bevy_cef_core::prelude::RetainedIoSurface;
use bevy_cef_core::prelude::{HOST_CEF, SCHEME_CEF};
use serde::{Deserialize, Serialize};

use crate::title::WebviewTitle;

pub(crate) struct WebviewCoreComponentsPlugin;

impl Plugin for WebviewCoreComponentsPlugin {
    fn build(&self, app: &mut App) {
        app.register_type::<WebviewSize>()
            .register_type::<WebviewSource>()
            .register_type::<HostWindow>()
            .register_type::<ZoomLevel>()
            .register_type::<AudioMuted>()
            .register_type::<PreloadScripts>()
            .register_type::<WebviewDpr>()
            .register_type::<WebviewTextureTarget>();
    }
}

/// A component that specifies the content source of a webview.
///
/// Use [`WebviewSource::new`] for remote URLs, [`WebviewSource::local`] for local files
/// served via `cef://localhost/`, or [`WebviewSource::inline`] for raw HTML content.
///
/// 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,
    WebviewDpr,
    WebviewTitle
)]
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 WebviewSource {
    /// Creates a URL source.
    ///
    /// To specify a local file path, use [`WebviewSource::local`] instead.
    pub fn new(url: impl Into<String>) -> Self {
        Self::Url(url.into())
    }

    /// Creates a local file source.
    ///
    /// 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.
#[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))
    }
}

/// Device pixel ratio (DPR) for the webview's backing CEF render buffer.
///
/// Automatically set and kept up-to-date by `WebviewDpiPlugin`: seeded from
/// the host window's `scale_factor()` at spawn, refreshed on
/// `WindowScaleFactorChanged`. User code normally does not need to write this
/// component, but may override it (e.g. to force 2× rendering for screenshots).
///
/// `WebviewSize` is interpreted in logical pixels (DIP). The actual GPU
/// texture CEF allocates is `WebviewSize × WebviewDpr` physical pixels.
#[derive(Reflect, Component, Debug, Copy, Clone, PartialEq, Deref, DerefMut)]
#[reflect(Component, Debug, Default)]
pub struct WebviewDpr(pub f32);

impl Default for WebviewDpr {
    fn default() -> Self {
        Self(1.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);

/// This component is used to preload scripts in the webview.
///
/// Scripts specified in this component are executed before the scripts in the HTML.
#[derive(Reflect, Component, Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[reflect(Component, Debug, Default, Serialize, Deserialize)]
pub struct PreloadScripts(pub Vec<String>);

impl<L, S> From<L> for PreloadScripts
where
    L: IntoIterator<Item = S>,
    S: Into<String>,
{
    fn from(scripts: L) -> Self {
        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>);

/// [macOS GPU OSR] Per-webview holder of the latest retained IOSurface, used for
/// on-demand transparent-pixel hit-testing.
///
/// The hit-test code (`is_pixel_transparent_surface`) reads a single alpha byte
/// from this surface only when a pointer is over the webview — replacing the old
/// per-frame full-plane alpha extraction. Updated in place on each new
/// accelerated-paint frame by `collect_webview_iosurfaces` (a `Clone` =
/// independent `CFRetain`). The retain keeps the surface object alive (memory
/// safe), but CEF's frame pool may recycle the buffer, so a read between paints
/// can observe a newer frame than the one displayed — acceptable for hit-testing.
#[cfg(target_os = "macos")]
#[derive(Component)]
pub(crate) struct WebviewIoSurface(pub(crate) RetainedIoSurface);

/// The render target for a *headless* webview — one that has none of the
/// display components (mesh material / `MaterialNode<WebviewUiMaterial>` /
/// `Sprite`). bevy_cef renders the page into the referenced `Image` asset so
/// third-party materials can sample it (e.g. a terminal shader compositing an
/// inline webview).
///
/// The asset's contents and format (`Bgra8UnormSrgb`) are MANAGED BY bevy_cef:
/// anything the user wrote into the image is overwritten with a placeholder on
/// allocation, and the GPU path injects the live page texture for this asset
/// id each frame. Create the handle with `images.add(Image::default())`. Do
/// NOT pass `Handle::default()` (shared by every defaulted handle; skipped
/// with a warning), a handle the `AssetServer` is still loading into (the
/// finished load would clobber the placeholder), or one handle shared between
/// two webviews (last blit wins; a warning is logged).
///
/// Platform: the texture is only written on macOS (GPU IOSurface path). On
/// Linux/Windows the component is inert — note the browser itself is still
/// created and keeps painting CPU frames that nothing consumes.
///
/// Rebind contract: when the injected GPU texture is (re)created — first
/// frame, resize, handle swap — bevy_cef touches this `Image` asset so
/// `AssetEvent::Modified { id }` fires. A consumer material must rebuild its
/// bind group then: either implement `WebviewTextureSlot` and register
/// `WebviewTargetUiMaterialPlugin` (turnkey), or listen for the event and
/// `get_mut` your own material asset.
///
/// After the webview despawns, the texture freezes on the last frame until the
/// image asset is next modified; drop the last handle to release it.
#[derive(Component, Reflect, Debug, Clone, PartialEq)]
#[reflect(Component, Debug)]
pub struct WebviewTextureTarget(pub Handle<Image>);

impl From<Handle<Image>> for WebviewTextureTarget {
    fn from(handle: Handle<Image>) -> Self {
        Self(handle)
    }
}

#[cfg(test)]
mod texture_target_tests {
    use super::*;

    #[test]
    fn from_handle_preserves_id() {
        let handle = Handle::<Image>::default();
        let target = WebviewTextureTarget::from(handle.clone());
        assert_eq!(target.0.id(), handle.id());
    }
}