nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
use std::marker::PhantomData;
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread;

use include_dir::Dir;
use tiny_http::{Header, Response, Server};
use wry::dpi::{LogicalPosition, LogicalSize};
use wry::{Rect, WebView, WebViewBuilder};

use crate::prelude::{egui, window};
use crate::webview::{FromBase64, HANDLER_NAME, ToBase64};

fn rect(x: f64, y: f64, width: f64, height: f64) -> Rect {
    Rect {
        position: LogicalPosition::new(x, y).into(),
        size: LogicalSize::new(width, height).into(),
    }
}

pub fn serve_embedded_dir(dir: &'static Dir<'static>) -> u16 {
    let server = Server::http("127.0.0.1:0").expect("server");
    let port = server.server_addr().to_ip().unwrap().port();
    thread::spawn(move || {
        for request in server.incoming_requests() {
            let path = request.url().trim_start_matches('/');
            let path = if path.is_empty() { "index.html" } else { path };
            let file = dir
                .get_file(path)
                .or_else(|| dir.get_file("index.html"))
                .unwrap();
            let mime = match path.rsplit('.').next() {
                Some("html") => "text/html",
                Some("js") => "application/javascript",
                Some("wasm") => "application/wasm",
                Some("css") => "text/css",
                _ => "application/octet-stream",
            };
            let _ = request.respond(
                Response::from_data(file.contents())
                    .with_header(Header::from_bytes("Content-Type", mime).unwrap()),
            );
        }
    });
    port
}

pub struct WebviewContext<Cmd, Evt> {
    webview: Option<WebView>,
    bounds: (f64, f64, f64, f64),
    tx: Sender<Cmd>,
    rx: Receiver<Cmd>,
    _marker: PhantomData<Evt>,
}

impl<Cmd, Evt> Default for WebviewContext<Cmd, Evt> {
    fn default() -> Self {
        let (tx, rx) = channel();
        Self {
            webview: None,
            bounds: (0.0, 0.0, 0.0, 0.0),
            tx,
            rx,
            _marker: PhantomData,
        }
    }
}

impl<Cmd, Evt> WebviewContext<Cmd, Evt>
where
    Cmd: FromBase64 + Send + 'static,
    Evt: ToBase64,
{
    pub fn ensure_webview(
        &mut self,
        window: Arc<window::Window>,
        port: u16,
        r: egui::Rect,
    ) -> bool {
        let b = (
            r.min.x as f64,
            r.min.y as f64,
            r.width() as f64,
            r.height() as f64,
        );

        if let Some(wv) = &self.webview {
            if self.bounds != b {
                let _ = wv.set_bounds(rect(b.0, b.1, b.2, b.3));
                self.bounds = b;
            }
            return false;
        }

        let tx = self.tx.clone();
        let init_script = format!(
            "window.onBackendMessage=function(d){{window.{HANDLER_NAME}&&window.{HANDLER_NAME}(d)}};"
        );
        if let Ok(wv) = WebViewBuilder::new()
            .with_url(format!("http://127.0.0.1:{port}"))
            .with_bounds(rect(b.0, b.1, b.2, b.3))
            .with_navigation_handler(|url| {
                url.starts_with("http://127.0.0.1") || url.starts_with("https://127.0.0.1")
            })
            .with_initialization_script(&init_script)
            .with_ipc_handler(move |request| {
                if let Some(command) = Cmd::from_base64(request.body()) {
                    let _ = tx.send(command);
                }
            })
            .build_as_child(window.as_ref())
        {
            let _ = wv.set_visible(true);
            let _ = wv.focus();
            self.bounds = b;
            self.webview = Some(wv);
            return true;
        }
        false
    }

    pub fn send(&self, event: Evt) {
        if let (Some(wv), Some(data)) = (&self.webview, event.to_base64()) {
            let _ = wv.evaluate_script(&format!("window.onBackendMessage('{data}')"));
        }
    }

    pub fn drain_messages(&self) -> impl Iterator<Item = Cmd> + '_ {
        self.rx.try_iter()
    }
}