use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use teksilo_canvas::Rect;
use teksilo_core::AppEventPoster;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::widget::EventContext;
use teksilo_core::window::TeksiloWindowId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WebViewId(u64);
impl WebViewId {
pub fn next() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(1);
Self(COUNTER.fetch_add(1, Ordering::Relaxed))
}
pub fn raw(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebSource {
Url(String),
Html {
html: String,
base_url: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConsoleLevel {
Log,
Warn,
Error,
}
#[derive(Debug, Clone, Default)]
pub struct WebViewAttributes {
pub source: Option<WebSource>,
pub user_agent: Option<String>,
pub transparent: bool,
pub devtools: bool,
pub custom_protocols: Vec<String>,
}
pub trait WebViewHandle: 'static {
fn set_bounds(&self, bounds: Rect, scale_factor: f32);
fn load_url(&self, url: &str);
fn load_html(&self, html: &str, base_url: Option<&str>);
fn eval(&self, script: &str);
fn post_message(&self, msg: &str);
fn reload(&self);
fn go_back(&self);
fn go_forward(&self);
fn stop(&self);
fn set_visible(&self, visible: bool);
fn set_focus(&self);
fn open_devtools(&self) {}
fn close_devtools(&self) {}
}
#[derive(Debug, Clone)]
pub enum WebViewEvent {
NavigationStarted { url: String, can_cancel: bool },
NavigationFinished { url: String, success: bool },
PageLoadStarted,
PageLoadFinished,
TitleChanged(String),
Message(String),
DownloadStarted {
url: String,
suggested_path: PathBuf,
},
DownloadFinished { path: PathBuf, success: bool },
ConsoleMessage { level: ConsoleLevel, text: String },
}
pub struct WebViewEventPayload {
pub window_id_owner: TeksiloWindowId,
pub web_view_id: WebViewId,
pub event: WebViewEvent,
}
#[allow(dead_code)] pub(crate) fn post_event(
poster: &Option<Arc<dyn AppEventPoster>>,
window_id: TeksiloWindowId,
web_view_id: WebViewId,
event: WebViewEvent,
) {
if let Some(poster) = poster {
let payload = WebViewEventPayload {
window_id_owner: window_id,
web_view_id,
event,
};
poster.post_external(Box::new(payload) as Box<dyn std::any::Any + Send>);
}
}
#[allow(dead_code)] pub(crate) fn js_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{2028}' => out.push_str("\\u2028"),
'\u{2029}' => out.push_str("\\u2029"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
out
}
pub trait WebViewBackend {
fn open(
&mut self,
web_view_id: WebViewId,
window_id: TeksiloWindowId,
parent: Option<ParentHandle>,
attrs: WebViewAttributes,
poster: Option<Arc<dyn AppEventPoster>>,
) -> Box<dyn WebViewHandle>;
}
type EventCallback = Box<dyn FnMut(WebViewEvent, &mut EventContext)>;
struct Registered {
window_id: TeksiloWindowId,
callback: EventCallback,
}
struct RegistryState {
backend: RefCell<Box<dyn WebViewBackend>>,
callbacks: RefCell<HashMap<WebViewId, Registered>>,
open_count: Cell<u64>,
delivering: Cell<Option<(WebViewId, TeksiloWindowId)>>,
delivery_aborted: Cell<bool>,
}
#[derive(Clone)]
pub struct WebViewRegistry {
inner: Rc<RegistryState>,
}
impl WebViewRegistry {
pub fn new<B: WebViewBackend + 'static>(backend: B) -> Self {
Self {
inner: Rc::new(RegistryState {
backend: RefCell::new(Box::new(backend)),
callbacks: RefCell::new(HashMap::new()),
open_count: Cell::new(0),
delivering: Cell::new(None),
delivery_aborted: Cell::new(false),
}),
}
}
pub fn open(
&self,
web_view_id: WebViewId,
window_id: TeksiloWindowId,
parent: Option<ParentHandle>,
attrs: WebViewAttributes,
poster: Option<Arc<dyn AppEventPoster>>,
on_event: impl FnMut(WebViewEvent, &mut EventContext) + 'static,
) -> Box<dyn WebViewHandle> {
self.inner
.open_count
.set(self.inner.open_count.get().wrapping_add(1));
self.inner.callbacks.borrow_mut().insert(
web_view_id,
Registered {
window_id,
callback: Box::new(on_event),
},
);
self.inner
.backend
.borrow_mut()
.open(web_view_id, window_id, parent, attrs, poster)
}
pub fn deliver(&self, payload: WebViewEventPayload, ctx: &mut EventContext) {
let entry = self
.inner
.callbacks
.borrow_mut()
.remove(&payload.web_view_id);
let Some(mut reg) = entry else {
return;
};
if reg.window_id != payload.window_id_owner {
return;
}
self.inner
.delivering
.set(Some((payload.web_view_id, reg.window_id)));
self.inner.delivery_aborted.set(false);
(reg.callback)(payload.event, ctx);
self.inner.delivering.set(None);
if !self.inner.delivery_aborted.get() {
self.inner
.callbacks
.borrow_mut()
.entry(payload.web_view_id)
.or_insert(reg);
}
}
pub fn unregister(&self, web_view_id: WebViewId) {
self.inner.callbacks.borrow_mut().remove(&web_view_id);
if matches!(self.inner.delivering.get(), Some((id, _)) if id == web_view_id) {
self.inner.delivery_aborted.set(true);
}
}
pub fn purge_window(&self, window_id: TeksiloWindowId) {
self.inner
.callbacks
.borrow_mut()
.retain(|_, r| r.window_id != window_id);
if matches!(self.inner.delivering.get(), Some((_, win)) if win == window_id) {
self.inner.delivery_aborted.set(true);
}
}
pub fn registered_count(&self) -> usize {
self.inner.callbacks.borrow().len()
}
}
impl std::fmt::Debug for WebViewRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebViewRegistry")
.field("registered", &self.inner.callbacks.borrow().len())
.field("opens", &self.inner.open_count.get())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum WebViewOp {
Open {
web_view_id: WebViewId,
},
SetBounds {
web_view_id: WebViewId,
bounds: Rect,
},
LoadUrl {
web_view_id: WebViewId,
url: String,
},
LoadHtml {
web_view_id: WebViewId,
},
Eval {
web_view_id: WebViewId,
script: String,
},
PostMessage {
web_view_id: WebViewId,
msg: String,
},
Reload {
web_view_id: WebViewId,
},
GoBack {
web_view_id: WebViewId,
},
GoForward {
web_view_id: WebViewId,
},
Stop {
web_view_id: WebViewId,
},
SetVisible {
web_view_id: WebViewId,
visible: bool,
},
SetFocus {
web_view_id: WebViewId,
},
OpenDevtools {
web_view_id: WebViewId,
},
CloseDevtools {
web_view_id: WebViewId,
},
Dropped {
web_view_id: WebViewId,
},
}
#[derive(Clone, Default)]
pub struct MemoryWebViewRecords {
ops: Rc<RefCell<Vec<WebViewOp>>>,
}
impl MemoryWebViewRecords {
pub fn ops(&self) -> Vec<WebViewOp> {
self.ops.borrow().clone()
}
pub fn ops_for(&self, id: WebViewId) -> Vec<WebViewOp> {
self.ops
.borrow()
.iter()
.filter(|op| op_web_view_id(op) == id)
.cloned()
.collect()
}
pub fn visibility_log(&self, id: WebViewId) -> Vec<bool> {
self.ops
.borrow()
.iter()
.filter_map(|op| match op {
WebViewOp::SetVisible {
web_view_id,
visible,
} if *web_view_id == id => Some(*visible),
_ => None,
})
.collect()
}
fn push(&self, op: WebViewOp) {
self.ops.borrow_mut().push(op);
}
}
fn op_web_view_id(op: &WebViewOp) -> WebViewId {
match op {
WebViewOp::Open { web_view_id }
| WebViewOp::SetBounds { web_view_id, .. }
| WebViewOp::LoadUrl { web_view_id, .. }
| WebViewOp::LoadHtml { web_view_id }
| WebViewOp::Eval { web_view_id, .. }
| WebViewOp::PostMessage { web_view_id, .. }
| WebViewOp::Reload { web_view_id }
| WebViewOp::GoBack { web_view_id }
| WebViewOp::GoForward { web_view_id }
| WebViewOp::Stop { web_view_id }
| WebViewOp::SetVisible { web_view_id, .. }
| WebViewOp::SetFocus { web_view_id }
| WebViewOp::OpenDevtools { web_view_id }
| WebViewOp::CloseDevtools { web_view_id }
| WebViewOp::Dropped { web_view_id } => *web_view_id,
}
}
pub struct MemoryWebViewBackend {
records: MemoryWebViewRecords,
}
impl MemoryWebViewBackend {
pub fn new() -> (Self, MemoryWebViewRecords) {
let records = MemoryWebViewRecords::default();
(
Self {
records: records.clone(),
},
records,
)
}
}
struct MemoryWebViewHandle {
web_view_id: WebViewId,
records: MemoryWebViewRecords,
}
impl WebViewHandle for MemoryWebViewHandle {
fn set_bounds(&self, bounds: Rect, _scale_factor: f32) {
self.records.push(WebViewOp::SetBounds {
web_view_id: self.web_view_id,
bounds,
});
}
fn load_url(&self, url: &str) {
self.records.push(WebViewOp::LoadUrl {
web_view_id: self.web_view_id,
url: url.to_string(),
});
}
fn load_html(&self, _html: &str, _base_url: Option<&str>) {
self.records.push(WebViewOp::LoadHtml {
web_view_id: self.web_view_id,
});
}
fn eval(&self, script: &str) {
self.records.push(WebViewOp::Eval {
web_view_id: self.web_view_id,
script: script.to_string(),
});
}
fn post_message(&self, msg: &str) {
self.records.push(WebViewOp::PostMessage {
web_view_id: self.web_view_id,
msg: msg.to_string(),
});
}
fn reload(&self) {
self.records.push(WebViewOp::Reload {
web_view_id: self.web_view_id,
});
}
fn go_back(&self) {
self.records.push(WebViewOp::GoBack {
web_view_id: self.web_view_id,
});
}
fn go_forward(&self) {
self.records.push(WebViewOp::GoForward {
web_view_id: self.web_view_id,
});
}
fn stop(&self) {
self.records.push(WebViewOp::Stop {
web_view_id: self.web_view_id,
});
}
fn set_visible(&self, visible: bool) {
self.records.push(WebViewOp::SetVisible {
web_view_id: self.web_view_id,
visible,
});
}
fn set_focus(&self) {
self.records.push(WebViewOp::SetFocus {
web_view_id: self.web_view_id,
});
}
fn open_devtools(&self) {
self.records.push(WebViewOp::OpenDevtools {
web_view_id: self.web_view_id,
});
}
fn close_devtools(&self) {
self.records.push(WebViewOp::CloseDevtools {
web_view_id: self.web_view_id,
});
}
}
impl Drop for MemoryWebViewHandle {
fn drop(&mut self) {
self.records.push(WebViewOp::Dropped {
web_view_id: self.web_view_id,
});
}
}
impl WebViewBackend for MemoryWebViewBackend {
fn open(
&mut self,
web_view_id: WebViewId,
_window_id: TeksiloWindowId,
_parent: Option<ParentHandle>,
attrs: WebViewAttributes,
_poster: Option<Arc<dyn AppEventPoster>>,
) -> Box<dyn WebViewHandle> {
self.records.push(WebViewOp::Open { web_view_id });
match attrs.source {
Some(WebSource::Url(url)) => self.records.push(WebViewOp::LoadUrl { web_view_id, url }),
Some(WebSource::Html { .. }) => self.records.push(WebViewOp::LoadHtml { web_view_id }),
None => {}
}
Box::new(MemoryWebViewHandle {
web_view_id,
records: self.records.clone(),
})
}
}
pub fn memory_registry() -> (WebViewRegistry, MemoryWebViewRecords) {
let (backend, records) = MemoryWebViewBackend::new();
(WebViewRegistry::new(backend), records)
}
#[derive(Debug, Default)]
pub struct NoopWebViewBackend;
pub(crate) struct NoopWebViewHandle;
impl WebViewHandle for NoopWebViewHandle {
fn set_bounds(&self, _bounds: Rect, _scale_factor: f32) {}
fn load_url(&self, _url: &str) {}
fn load_html(&self, _html: &str, _base_url: Option<&str>) {}
fn eval(&self, _script: &str) {}
fn post_message(&self, _msg: &str) {}
fn reload(&self) {}
fn go_back(&self) {}
fn go_forward(&self) {}
fn stop(&self) {}
fn set_visible(&self, _visible: bool) {}
fn set_focus(&self) {}
}
impl WebViewBackend for NoopWebViewBackend {
fn open(
&mut self,
_web_view_id: WebViewId,
_window_id: TeksiloWindowId,
_parent: Option<ParentHandle>,
_attrs: WebViewAttributes,
_poster: Option<Arc<dyn AppEventPoster>>,
) -> Box<dyn WebViewHandle> {
Box::new(NoopWebViewHandle)
}
}