use std::sync::Arc;
use teksilo_canvas::Rect;
use teksilo_core::AppEventPoster;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::window::TeksiloWindowId;
use wry::{PageLoadEvent, WebView, WebViewBuilder};
#[cfg(not(target_os = "linux"))]
use wry::dpi::{LogicalPosition, LogicalSize};
use crate::backend::{
ConsoleLevel, NoopWebViewHandle, WebSource, WebViewAttributes, WebViewBackend, WebViewEvent,
WebViewHandle, WebViewId, js_string, post_event,
};
pub const FOCUS_IPC_PREFIX: &str = "__teksilo_webview_focus:";
fn focus_bridge_script() -> String {
format!(
"(function(){{\
var p={prefix};\
var send=function(v){{try{{window.ipc.postMessage(p+v)}}catch(e){{}}}};\
window.addEventListener('focus',function(){{send('1')}});\
window.addEventListener('blur',function(){{send('0')}});\
}})()",
prefix = js_string(FOCUS_IPC_PREFIX)
)
}
#[derive(Debug, Default)]
pub struct WryBackend {
_private: (),
}
impl WryBackend {
pub fn new() -> Self {
Self { _private: () }
}
}
fn fail(
poster: &Option<Arc<dyn AppEventPoster>>,
window_id: TeksiloWindowId,
web_view_id: WebViewId,
text: String,
) -> Box<dyn WebViewHandle> {
post_event(
poster,
window_id,
web_view_id,
WebViewEvent::ConsoleMessage {
level: ConsoleLevel::Error,
text,
},
);
post_event(
poster,
window_id,
web_view_id,
WebViewEvent::NavigationFinished {
url: String::new(),
success: false,
},
);
Box::new(NoopWebViewHandle)
}
impl WebViewBackend for WryBackend {
fn open(
&mut self,
web_view_id: WebViewId,
window_id: TeksiloWindowId,
parent: Option<ParentHandle>,
attrs: WebViewAttributes,
poster: Option<Arc<dyn AppEventPoster>>,
) -> Box<dyn WebViewHandle> {
#[cfg(target_os = "linux")]
if gtk::init().is_err() {
return fail(
&poster,
window_id,
web_view_id,
"WryBackend: gtk::init() failed — WebKitGTK needs GTK initialised".to_string(),
);
}
let Some(parent) = parent else {
return fail(
&poster,
window_id,
web_view_id,
"WryBackend: no parent window handle available; webview not created".to_string(),
);
};
let mut builder = WebViewBuilder::new();
match &attrs.source {
Some(WebSource::Url(url)) => builder = builder.with_url(url.clone()),
Some(WebSource::Html { html, .. }) => builder = builder.with_html(html.clone()),
None => {}
}
if let Some(ua) = &attrs.user_agent {
builder = builder.with_user_agent(ua.clone());
}
builder = builder
.with_transparent(attrs.transparent)
.with_devtools(attrs.devtools);
builder = builder.with_initialization_script(focus_bridge_script());
{
let poster = poster.clone();
builder = builder.with_ipc_handler(move |req| {
let body = req.body();
let event = match body.strip_prefix(FOCUS_IPC_PREFIX) {
Some(flag) => WebViewEvent::EngineFocusChanged(flag == "1"),
None => WebViewEvent::Message(body.clone()),
};
post_event(&poster, window_id, web_view_id, event);
});
}
{
let poster = poster.clone();
builder = builder.with_on_page_load_handler(move |event, url| {
let ev = match event {
PageLoadEvent::Started => WebViewEvent::PageLoadStarted,
PageLoadEvent::Finished => WebViewEvent::PageLoadFinished,
};
post_event(&poster, window_id, web_view_id, ev);
if matches!(event, PageLoadEvent::Finished) {
post_event(
&poster,
window_id,
web_view_id,
WebViewEvent::NavigationFinished { url, success: true },
);
}
});
}
{
let poster = poster.clone();
builder = builder.with_navigation_handler(move |url| {
post_event(
&poster,
window_id,
web_view_id,
WebViewEvent::NavigationStarted {
url,
can_cancel: false,
},
);
true });
}
{
let poster = poster.clone();
builder = builder.with_document_title_changed_handler(move |title| {
post_event(
&poster,
window_id,
web_view_id,
WebViewEvent::TitleChanged(title),
);
});
}
{
let poster = poster.clone();
builder = builder.with_download_started_handler(move |url, path| {
post_event(
&poster,
window_id,
web_view_id,
WebViewEvent::DownloadStarted {
url,
suggested_path: path.clone(),
},
);
true
});
}
{
let poster = poster.clone();
builder = builder.with_download_completed_handler(move |_url, path, success| {
post_event(
&poster,
window_id,
web_view_id,
WebViewEvent::DownloadFinished {
path: path.unwrap_or_default(),
success,
},
);
});
}
match builder.build_as_child(&parent) {
Ok(webview) => Box::new(WryHandle {
webview,
poster: poster.clone(),
window_id,
web_view_id,
}),
Err(e) => fail(
&poster,
window_id,
web_view_id,
format!("WryBackend: failed to create webview: {e}"),
),
}
}
}
struct WryHandle {
webview: WebView,
poster: Option<Arc<dyn AppEventPoster>>,
window_id: TeksiloWindowId,
web_view_id: WebViewId,
}
impl WebViewHandle for WryHandle {
fn set_bounds(&self, bounds: Rect, scale_factor: f32) {
#[cfg(target_os = "linux")]
let rect = wry::Rect {
position: wry::dpi::PhysicalPosition::new(
(bounds.x * scale_factor) as f64,
(bounds.y * scale_factor) as f64,
)
.into(),
size: wry::dpi::PhysicalSize::new(
(bounds.width * scale_factor) as f64,
(bounds.height * scale_factor) as f64,
)
.into(),
};
#[cfg(not(target_os = "linux"))]
let rect = {
let _ = scale_factor;
wry::Rect {
position: LogicalPosition::new(bounds.x, bounds.y).into(),
size: LogicalSize::new(bounds.width, bounds.height).into(),
}
};
let _ = self.webview.set_bounds(rect);
}
fn load_url(&self, url: &str) {
let _ = self.webview.load_url(url);
}
fn load_html(&self, html: &str, _base_url: Option<&str>) {
let _ = self.webview.evaluate_script(&format!(
"document.open();document.write({});document.close();",
js_string(html)
));
}
fn eval(&self, script: &str) {
let _ = self.webview.evaluate_script(script);
}
fn post_message(&self, msg: &str) {
let _ = self.webview.evaluate_script(&format!(
"window.dispatchEvent(new MessageEvent('teksilo-message',{{data:{}}}))",
js_string(msg)
));
}
fn reload(&self) {
let _ = self.webview.reload();
}
fn go_back(&self) {
let _ = self.webview.evaluate_script("history.back()");
}
fn go_forward(&self) {
let _ = self.webview.evaluate_script("history.forward()");
}
fn stop(&self) {
let _ = self.webview.evaluate_script("window.stop()");
}
fn set_visible(&self, visible: bool) {
let _ = self.webview.set_visible(visible);
}
fn set_focus(&self) {
let _ = self.webview.focus();
}
fn set_input_passthrough(&self, passthrough: bool) {
if !passthrough {
return;
}
post_event(
&self.poster,
self.window_id,
self.web_view_id,
WebViewEvent::ConsoleMessage {
level: ConsoleLevel::Warn,
text: "WryBackend: input pass-through is unsupported — this engine \
exposes no control over its surface's hit region, so the page \
keeps taking presses over its own rectangle"
.to_string(),
},
);
}
fn open_devtools(&self) {
self.webview.open_devtools();
}
fn close_devtools(&self) {
self.webview.close_devtools();
}
}