kael 0.2.0

GPU-accelerated native UI framework for Rust — build desktop apps with Metal, DirectX, and Vulkan rendering
Documentation
use super::super::webview_common::{
    bridge_script, create_web_context, css_script, to_wry_rect, webview_command_id,
};
use super::{WindowsWindow, WindowsWindowInner};
use crate::{
    AsyncWindowContext, Bounds, Pixels, SharedString,
    webview::{
        NavigationPolicy, PlatformWebView, PlatformWebViewCommand, WebViewNavigationHandler,
    },
};
use anyhow::{Context as _, Result};
use std::{collections::HashSet, rc::Rc};
use util::ResultExt;
use wry::{NewWindowResponse, WebContext, WebView, WebViewBuilder};

pub(crate) struct WindowsWebViewHost {
    desired: PlatformWebView,
    webview: WebView,
    _context: Option<WebContext>,
    current_url: SharedString,
}

#[derive(Clone, Eq, PartialEq)]
struct WindowsWebViewSignature {
    storage_key: Option<SharedString>,
    user_agent: Option<SharedString>,
    injected_css: Vec<SharedString>,
    injected_javascript: Vec<SharedString>,
}

pub(crate) fn sync_webviews(window: &Rc<WindowsWindowInner>, webviews: &[PlatformWebView]) {
    let mut active_ids: HashSet<SharedString> = HashSet::default();
    let mut state = window.state.borrow_mut();

    for webview in webviews {
        let webview_id = webview.id.clone();
        active_ids.insert(webview_id.clone());

        let needs_recreate = state
            .webviews
            .get(&webview_id)
            .is_some_and(|host| host.needs_recreate(webview));
        if needs_recreate {
            state.webviews.remove(&webview_id);
        }

        let scale_factor = state.scale_factor;
        if let Some(host) = state.webviews.get_mut(&webview_id) {
            host.update_desired(webview.clone(), scale_factor);
        } else {
            match WindowsWebViewHost::new(window, webview.clone(), scale_factor) {
                Ok(host) => {
                    state.webviews.insert(webview_id, host);
                }
                Err(error) => {
                    log::error!("failed to create Windows WebView {}: {error:#}", webview.id);
                }
            }
        }
    }

    let stale_ids = state
        .webviews
        .keys()
        .filter(|webview_id| !active_ids.contains(*webview_id))
        .cloned()
        .collect::<Vec<_>>();
    for webview_id in stale_ids {
        state.webviews.remove(&webview_id);
    }
}

pub(crate) fn dispatch_webview_command(
    window: &Rc<WindowsWindowInner>,
    command: PlatformWebViewCommand,
) -> Result<()> {
    let webview_id = webview_command_id(&command);
    let mut state = window.state.borrow_mut();
    let Some(host) = state.webviews.get_mut(&webview_id) else {
        anyhow::bail!("unknown webview: {}", webview_id);
    };
    host.apply_command(command);
    Ok(())
}

impl WindowsWebViewHost {
    fn new(
        window: &Rc<WindowsWindowInner>,
        desired: PlatformWebView,
        scale_factor: f32,
    ) -> Result<Self> {
        let mut context = create_web_context(&desired)?;
        let builder = configure_webview_builder(
            if let Some(context) = context.as_mut() {
                WebViewBuilder::new_with_web_context(context)
            } else {
                WebViewBuilder::new()
            },
            &desired,
            desired.bounds,
        );

        let webview = builder
            .build_as_child(&WindowsWindow(window.clone()))
            .context("building Windows child webview")?;
        webview.set_visible(desired.visible).log_err();

        let current_url = desired.url.clone();
        let mut host = Self {
            desired,
            webview,
            _context: context,
            current_url,
        };
        host.apply(scale_factor);
        Ok(host)
    }

    fn needs_recreate(&self, webview: &PlatformWebView) -> bool {
        WindowsWebViewSignature::from(&self.desired) != WindowsWebViewSignature::from(webview)
    }

    fn update_desired(&mut self, desired: PlatformWebView, scale_factor: f32) {
        self.desired = desired;
        self.apply(scale_factor);
    }

    fn apply(&mut self, _scale_factor: f32) {
        self.webview
            .set_bounds(to_wry_rect(self.desired.bounds))
            .log_err();
        self.webview.set_visible(self.desired.visible).log_err();

        if !self.desired.url.is_empty() && self.current_url != self.desired.url {
            self.webview.load_url(self.desired.url.as_ref()).log_err();
            self.current_url = self.desired.url.clone();
        }
    }

    pub(crate) fn apply_command(&mut self, command: PlatformWebViewCommand) {
        match command {
            PlatformWebViewCommand::Navigate { url, .. } => {
                self.webview.load_url(url.as_ref()).log_err();
                self.current_url = url;
            }
            PlatformWebViewCommand::EvaluateJavaScript { script, .. } => {
                self.webview.evaluate_script(script.as_ref()).log_err();
            }
            PlatformWebViewCommand::PostMessage { message, .. } => {
                let payload = serde_json::to_string(&message).unwrap_or_else(|_| "null".into());
                let script = format!(
                    "(() => {{ const payload = {payload}; if (window.dispatchEvent) {{ window.dispatchEvent(new MessageEvent('message', {{ data: payload }})); }} if (typeof window.onmessage === 'function') {{ window.onmessage({{ data: payload }}); }} }})();"
                );
                self.webview.evaluate_script(&script).log_err();
            }
            PlatformWebViewCommand::Reload { .. } => {
                self.webview.reload().log_err();
            }
            PlatformWebViewCommand::GoBack { .. } => {
                self.webview.evaluate_script("history.back()").log_err();
            }
            PlatformWebViewCommand::GoForward { .. } => {
                self.webview.evaluate_script("history.forward()").log_err();
            }
        }
    }
}

impl From<&PlatformWebView> for WindowsWebViewSignature {
    fn from(webview: &PlatformWebView) -> Self {
        Self {
            storage_key: webview.storage_key.clone(),
            user_agent: webview.user_agent.clone(),
            injected_css: webview.injected_css.clone(),
            injected_javascript: webview.injected_javascript.clone(),
        }
    }
}

fn configure_webview_builder<'a>(
    mut builder: WebViewBuilder<'a>,
    desired: &PlatformWebView,
    bounds: Bounds<Pixels>,
) -> WebViewBuilder<'a> {
    builder = builder.with_bounds(to_wry_rect(bounds));

    if !desired.url.is_empty() {
        builder = builder.with_url(desired.url.as_ref());
    }

    if let Some(user_agent) = &desired.user_agent {
        builder = builder.with_user_agent(user_agent.as_ref());
    }

    let message_handler = desired.message_handler.clone();
    let ipc_async_window = desired.async_window.clone();
    builder = builder.with_ipc_handler(move |request| {
        let Some(handler) = message_handler.clone() else {
            return;
        };

        let body = request.body().to_string();
        let payload =
            serde_json::from_str(&body).unwrap_or_else(|_| serde_json::Value::String(body));
        let mut async_window = ipc_async_window.clone();
        let _ = async_window.update(|window, cx| handler(payload, window, cx));
    });

    let navigation_handler = desired.navigation_handler.clone();
    let navigation_async_window = desired.async_window.clone();
    builder = builder.with_navigation_handler(move |url| {
        handle_navigation_request(
            &url,
            navigation_handler.clone(),
            navigation_async_window.clone(),
        )
    });

    let new_window_async_window = desired.async_window.clone();
    let new_window_id = desired.id.clone();
    let new_window_navigation_handler = desired.navigation_handler.clone();
    builder = builder.with_new_window_req_handler(move |url, _features| {
        if handle_navigation_request(
            &url,
            new_window_navigation_handler.clone(),
            new_window_async_window.clone(),
        ) {
            let mut async_window = new_window_async_window.clone();
            let webview_id = new_window_id.clone();
            let _ = async_window.update(|window, _| {
                let _ = window.navigate_webview(webview_id.clone(), url.clone());
            });
        }

        NewWindowResponse::Deny
    });

    builder = builder.with_initialization_script(bridge_script(desired.storage_key.as_ref()));
    for css in &desired.injected_css {
        builder = builder.with_initialization_script(css_script(css.as_ref()));
    }
    for javascript in &desired.injected_javascript {
        builder = builder.with_initialization_script(javascript.as_ref());
    }

    builder
}

fn handle_navigation_request(
    url: &str,
    navigation_handler: Option<WebViewNavigationHandler>,
    async_window: AsyncWindowContext,
) -> bool {
    let url = url.to_string();
    let allow = if let Some(handler) = navigation_handler {
        let mut async_window = async_window.clone();
        async_window
            .update(|window, cx| handler(url.clone().into(), window, cx))
            .unwrap_or(NavigationPolicy::Deny)
            == NavigationPolicy::Allow
    } else {
        true
    };

    if allow && is_external_scheme(&url) {
        let mut async_window = async_window;
        let _ = async_window.update(|_, cx| {
            cx.open_url(&url).log_err();
        });
        return false;
    }

    allow
}

fn is_external_scheme(url: &str) -> bool {
    let Some((scheme, _)) = url.split_once(':') else {
        return false;
    };

    !matches!(
        scheme.to_ascii_lowercase().as_str(),
        "http" | "https" | "file" | "about" | "data" | "javascript" | "blob"
    )
}