use std::rc::Rc;
use std::sync::Arc;
use teksilo_canvas::Rect;
use teksilo_core::AppEventPoster;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::window::TeksiloWindowId;
use dpi::PhysicalSize;
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use servo::{
EventLoopWaker, LoadStatus, RenderingContext, Servo, ServoBuilder, WebView as ServoWebView,
WebViewBuilder, WebViewDelegate, WindowRenderingContext,
};
use url::Url;
use crate::backend::{
ConsoleLevel, NoopWebViewHandle, WebSource, WebViewAttributes, WebViewBackend, WebViewEvent,
WebViewHandle, WebViewId, js_string, post_event,
};
#[derive(Debug, Default)]
pub struct ServoBackend {
_private: (),
}
impl ServoBackend {
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 ServoBackend {
fn open(
&mut self,
web_view_id: WebViewId,
window_id: TeksiloWindowId,
parent: Option<ParentHandle>,
attrs: WebViewAttributes,
poster: Option<Arc<dyn AppEventPoster>>,
) -> Box<dyn WebViewHandle> {
let Some(parent) = parent else {
return fail(
&poster,
window_id,
web_view_id,
"ServoBackend: no parent window handle available; webview not created".to_string(),
);
};
let (Ok(display_handle), Ok(window_handle)) =
(parent.display_handle(), parent.window_handle())
else {
return fail(
&poster,
window_id,
web_view_id,
"ServoBackend: could not resolve raw window/display handles".to_string(),
);
};
let initial_size = PhysicalSize::new(800u32, 600u32);
let rendering_context =
match WindowRenderingContext::new(display_handle, window_handle, initial_size) {
Ok(ctx) => Rc::new(ctx),
Err(e) => {
return fail(
&poster,
window_id,
web_view_id,
format!("ServoBackend: rendering context init failed: {e:?}"),
);
}
};
let _ = rendering_context.make_current();
let servo = ServoBuilder::default()
.event_loop_waker(Box::new(NoopWaker))
.build();
let delegate: Rc<dyn WebViewDelegate> = Rc::new(ServoEventDelegate {
poster: poster.clone(),
window_id,
web_view_id,
});
let mut builder = WebViewBuilder::new(&servo, rendering_context.clone()).delegate(delegate);
if let Some(WebSource::Url(url)) = &attrs.source
&& let Ok(parsed) = Url::parse(url)
{
builder = builder.url(parsed);
}
let webview = builder.build();
post_event(
&poster,
window_id,
web_view_id,
WebViewEvent::ConsoleMessage {
level: ConsoleLevel::Warn,
text: "ServoBackend: webview constructed but not yet frame-driven (Phase 4 — \
spin_event_loop / paint / present not wired into the render loop)."
.to_string(),
},
);
Box::new(ServoHandle {
_servo: servo,
_rendering_context: rendering_context,
webview,
})
}
}
struct ServoEventDelegate {
poster: Option<Arc<dyn AppEventPoster>>,
window_id: TeksiloWindowId,
web_view_id: WebViewId,
}
impl WebViewDelegate for ServoEventDelegate {
fn notify_load_status_changed(&self, webview: ServoWebView, status: LoadStatus) {
match status {
LoadStatus::Started => {
post_event(
&self.poster,
self.window_id,
self.web_view_id,
WebViewEvent::PageLoadStarted,
);
}
LoadStatus::HeadParsed => {}
LoadStatus::Complete => {
post_event(
&self.poster,
self.window_id,
self.web_view_id,
WebViewEvent::PageLoadFinished,
);
post_event(
&self.poster,
self.window_id,
self.web_view_id,
WebViewEvent::NavigationFinished {
url: webview.url().map(|u| u.to_string()).unwrap_or_default(),
success: true,
},
);
}
}
}
fn notify_page_title_changed(&self, _webview: ServoWebView, title: Option<String>) {
post_event(
&self.poster,
self.window_id,
self.web_view_id,
WebViewEvent::TitleChanged(title.unwrap_or_default()),
);
}
}
struct ServoHandle {
_servo: Servo,
_rendering_context: Rc<WindowRenderingContext>,
webview: ServoWebView,
}
impl WebViewHandle for ServoHandle {
fn set_bounds(&self, bounds: Rect, scale_factor: f32) {
self.webview.resize(PhysicalSize::new(
(bounds.width * scale_factor).max(1.0) as u32,
(bounds.height * scale_factor).max(1.0) as u32,
));
}
fn load_url(&self, url: &str) {
if let Ok(parsed) = Url::parse(url) {
self.webview.load(parsed);
}
}
fn load_html(&self, _html: &str, _base_url: Option<&str>) {
}
fn eval(&self, script: &str) {
self.webview
.evaluate_javascript(script.to_string(), |_result| {});
}
fn post_message(&self, msg: &str) {
self.webview.evaluate_javascript(
format!(
"window.dispatchEvent(new MessageEvent('teksilo-message',{{data:{}}}))",
js_string(msg)
),
|_result| {},
);
}
fn reload(&self) {
self.webview.reload();
}
fn go_back(&self) {
self.webview.go_back(1);
}
fn go_forward(&self) {
self.webview.go_forward(1);
}
fn stop(&self) {
self.webview
.evaluate_javascript("window.stop()".to_string(), |_r| {});
}
fn set_visible(&self, visible: bool) {
if visible {
self.webview.show();
} else {
self.webview.hide();
}
}
fn set_focus(&self) {
self.webview.focus();
}
fn set_input_passthrough(&self, passthrough: bool) {
let _ = passthrough;
}
}
#[derive(Clone)]
struct NoopWaker;
impl EventLoopWaker for NoopWaker {
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
Box::new(NoopWaker)
}
fn wake(&self) {}
}