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()
}
}