use std::{
cell::{Cell, RefCell},
rc::Rc,
sync::{Arc, Mutex},
time::{Duration, Instant},
};
use dpi::PhysicalSize;
use euclid::{Box2D, Point2D, Scale};
use futures::{
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
channel::oneshot,
stream::{BoxStream, StreamExt},
};
use iced::{Subscription, Task};
use servo::{
EventLoopWaker, JSValue, JavaScriptEvaluationError, RenderingContext, Servo, ServoBuilder,
SoftwareRenderingContext, WebView, WebViewBuilder,
};
use tracing::error;
use url::Url;
use iced_frame::{Frame, FrameSource, SizeRequestSlot};
use crate::delegate::{DelegateState, ServoBridge, WebViewBridge};
const RESIZE_DEBOUNCE: Duration = Duration::from_millis(100);
#[derive(Clone)]
pub enum Content {
Url(String),
Html(String),
}
impl Default for Content {
fn default() -> Self {
Self::Url("about:blank".into())
}
}
#[derive(Default)]
pub struct WebViewConfig {
content: Content,
}
impl WebViewConfig {
pub fn url(mut self, url: impl Into<String>) -> Self {
self.content = Content::Url(url.into());
self
}
pub fn html(mut self, html: impl Into<String>) -> Self {
self.content = Content::Html(html.into());
self
}
}
#[derive(Clone)]
pub struct ServoRuntime {
inner: Rc<ServoRuntimeInner>,
}
struct ServoRuntimeInner {
servo: Servo,
rendering_context: Rc<SoftwareRenderingContext>,
rendering_context_dyn: Rc<dyn RenderingContext>,
wake_rx: RefCell<Option<UnboundedReceiver<()>>>,
}
impl ServoRuntime {
pub fn new(initial_size: PhysicalSize<u32>) -> Result<Self, String> {
let w = initial_size.width.max(1);
let h = initial_size.height.max(1);
let initial_size = PhysicalSize::new(w, h);
let rendering_context = SoftwareRenderingContext::new(initial_size)
.map_err(|e| format!("SoftwareRenderingContext::new failed: {e:?}"))?;
let rendering_context = Rc::new(rendering_context);
rendering_context
.make_current()
.map_err(|e| format!("SoftwareRenderingContext::make_current failed: {e:?}"))?;
let (wake_tx, wake_rx) = unbounded::<()>();
let waker: Box<dyn EventLoopWaker> = Box::new(ChannelWaker { tx: wake_tx });
let servo = ServoBuilder::default().event_loop_waker(waker).build();
servo.set_delegate(Rc::new(ServoBridge));
let rendering_context_dyn: Rc<dyn RenderingContext> = Rc::clone(&rendering_context) as _;
Ok(Self {
inner: Rc::new(ServoRuntimeInner {
servo,
rendering_context,
rendering_context_dyn,
wake_rx: RefCell::new(Some(wake_rx)),
}),
})
}
fn servo(&self) -> &Servo {
&self.inner.servo
}
fn rendering_context(&self) -> &Rc<SoftwareRenderingContext> {
&self.inner.rendering_context
}
fn rendering_context_dyn(&self) -> Rc<dyn RenderingContext> {
Rc::clone(&self.inner.rendering_context_dyn)
}
pub fn set_preference(&self, name: &str, value: servo::PrefValue) {
self.inner.servo.set_preference(name, value);
}
pub fn default_user_agent(&self) -> String {
servo::UserAgentPlatform::default().to_user_agent_string()
}
pub fn subscription(&self) -> Subscription<()> {
let wake_rx = self.inner.wake_rx.borrow_mut().take();
let data = WakeSubData {
wake_rx: Arc::new(Mutex::new(wake_rx)),
};
iced::Subscription::run_with(data, build_wake_drain)
}
}
struct Inner {
runtime: ServoRuntime,
webview: WebView,
delegate_state: Rc<DelegateState>,
pending_resize: Cell<Option<(PhysicalSize<u32>, Instant)>>,
scale_factor: Cell<f32>,
size_request: SizeRequestSlot,
}
#[derive(Clone)]
pub struct ServoWebViewController {
inner: Rc<Inner>,
}
impl ServoWebViewController {
pub fn new(
runtime: &ServoRuntime,
config: WebViewConfig,
scale_factor: f32,
) -> Result<Self, String> {
let delegate_state = Rc::new(DelegateState {
webview: RefCell::new(None),
rendering_context: RefCell::new(Some(runtime.rendering_context_dyn())),
pending_popup_webview: RefCell::new(None),
new_webview_handler: RefCell::new(None),
needs_paint: Cell::new(false),
current_cursor: Cell::new(servo::Cursor::Default),
latest_frame: Arc::new(Mutex::new(None)),
current_url: RefCell::new(None),
current_title: RefCell::new(None),
status_text: RefCell::new(None),
load_status: Cell::new(servo::LoadStatus::Started),
});
let delegate = Rc::new(WebViewBridge {
state: Rc::clone(&delegate_state),
});
let initial_url = match config.content {
Content::Url(s) => Url::parse(&s).ok(),
Content::Html(html) => Url::parse(&format!("data:text/html;charset=utf-8,{html}")).ok(),
};
let mut builder = WebViewBuilder::new(runtime.servo(), runtime.rendering_context_dyn())
.delegate(delegate)
.hidpi_scale_factor(Scale::new(scale_factor));
if let Some(url) = initial_url {
builder = builder.url(url);
}
let webview = builder.build();
*delegate_state.webview.borrow_mut() = Some(webview.clone());
Ok(Self {
inner: Rc::new(Inner {
runtime: runtime.clone(),
webview,
delegate_state,
pending_resize: Cell::new(None),
scale_factor: Cell::new(scale_factor),
size_request: SizeRequestSlot::new(),
}),
})
}
pub fn evaluate_javascript(
&self,
script: impl ToString,
callback: impl FnOnce(Result<JSValue, JavaScriptEvaluationError>) + 'static,
) {
self.inner.webview.evaluate_javascript(script, callback);
}
pub fn evaluate_javascript_task(
&self,
script: impl ToString,
) -> Task<Result<JSValue, JavaScriptEvaluationError>> {
let (tx, rx) = oneshot::channel();
self.inner
.webview
.evaluate_javascript(script, move |result| {
let _ = tx.send(result);
});
Task::future(async move {
rx.await
.unwrap_or(Err(JavaScriptEvaluationError::InternalError))
})
}
pub fn navigate(&self, url: &str) -> Result<(), url::ParseError> {
let parsed = Url::parse(url)?;
self.inner.webview.load(parsed);
Ok(())
}
pub fn go_back(&self) {
let _ = self.inner.webview.go_back(1);
}
pub fn go_forward(&self) {
let _ = self.inner.webview.go_forward(1);
}
pub fn reload(&self) {
self.inner.webview.reload();
}
pub fn can_go_back(&self) -> bool {
self.inner.webview.can_go_back()
}
pub fn can_go_forward(&self) -> bool {
self.inner.webview.can_go_forward()
}
pub fn activate(&self) {
self.inner.webview.show();
self.inner.webview.focus();
self.inner.delegate_state.needs_paint.set(true);
}
pub fn deactivate(&self) {
self.inner.webview.blur();
self.inner.webview.hide();
}
pub fn on_new_webview_requested(&self, handler: impl Fn(Url) + 'static) {
*self.inner.delegate_state.new_webview_handler.borrow_mut() = Some(Rc::new(handler));
}
pub fn title(&self) -> Option<String> {
self.inner.delegate_state.current_title.borrow().clone()
}
pub fn url(&self) -> Option<String> {
self.inner
.delegate_state
.current_url
.borrow()
.as_ref()
.map(|u| u.to_string())
}
pub fn status_text(&self) -> Option<String> {
self.inner.delegate_state.status_text.borrow().clone()
}
pub fn load_status(&self) -> servo::LoadStatus {
self.inner.delegate_state.load_status.get()
}
pub(crate) fn scale_factor(&self) -> f32 {
self.inner.scale_factor.get()
}
pub(crate) fn webview(&self) -> &WebView {
&self.inner.webview
}
pub fn tick(&self) {
let inner = &self.inner;
if let Some(wb) = inner.size_request.bounds() {
let size = wb.size();
let scale = wb.scale_factor;
if (inner.scale_factor.get() - scale).abs() > f32::EPSILON {
inner.scale_factor.set(scale);
inner.webview.set_hidpi_scale_factor(Scale::new(scale));
}
match inner.pending_resize.get() {
Some((existing, _)) if existing == size => {}
_ => inner.pending_resize.set(Some((size, Instant::now()))),
}
}
let rendering_context = inner.runtime.rendering_context();
if let Some((new_size, requested_at)) = inner.pending_resize.get() {
let current = rendering_context.size();
if new_size != current && requested_at.elapsed() >= RESIZE_DEBOUNCE {
inner.pending_resize.set(None);
inner.webview.resize(new_size);
} else if new_size == current {
inner.pending_resize.set(None);
}
}
inner.runtime.servo().spin_event_loop();
if inner.delegate_state.needs_paint.replace(false) {
if let Err(e) = rendering_context.make_current() {
error!("SoftwareRenderingContext::make_current failed: {e:?}");
return;
}
inner.webview.paint();
let size = rendering_context.size();
let rect = Box2D::new(
Point2D::new(0, 0),
Point2D::new(size.width as i32, size.height as i32),
);
if let Some(rgba) = rendering_context.read_to_image(rect) {
let frame = Frame::new(rgba.into_raw(), size.width, size.height);
*inner.delegate_state.latest_frame.lock().unwrap() = Some(frame);
}
rendering_context.present();
}
}
}
impl FrameSource for ServoWebViewController {
fn frame_slot(&self) -> Arc<Mutex<Option<Frame>>> {
Arc::clone(&self.inner.delegate_state.latest_frame)
}
fn size_request_slot(&self) -> SizeRequestSlot {
self.inner.size_request.clone()
}
fn cursor(&self) -> iced::mouse::Interaction {
crate::input::cursor_to_interaction(self.inner.delegate_state.current_cursor.get())
}
fn handle_event(
&self,
event: &iced::Event,
bounds: iced::Rectangle,
cursor: iced::mouse::Cursor,
focused: bool,
) -> bool {
crate::input::translate_event(event, bounds, cursor, focused, self)
}
}
struct WakeSubData {
wake_rx: Arc<Mutex<Option<UnboundedReceiver<()>>>>,
}
impl std::hash::Hash for WakeSubData {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
(Arc::as_ptr(&self.wake_rx) as usize).hash(state);
}
}
fn build_wake_drain(data: &WakeSubData) -> BoxStream<'static, ()> {
let rx = data.wake_rx.lock().unwrap().take();
match rx {
Some(rx) => Box::pin(rx.map(|()| ())),
None => Box::pin(futures::stream::pending()),
}
}
struct ChannelWaker {
tx: UnboundedSender<()>,
}
impl EventLoopWaker for ChannelWaker {
fn clone_box(&self) -> Box<dyn EventLoopWaker> {
Box::new(ChannelWaker {
tx: self.tx.clone(),
})
}
fn wake(&self) {
let _ = self.tx.unbounded_send(());
}
}