use crate::{ResourceResponse, WebRenderer};
use day_spec::{Event, Support};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Capabilities {
pub private_browsing: Support,
pub profiles: Support,
pub clear_data: Support,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RegistrationError {
Unsupported,
WrongThread,
AlreadyRegistered,
ReservedRenderer,
}
impl std::fmt::Display for RegistrationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Unsupported => "renderer host is unavailable in this build",
Self::WrongThread => "register the renderer on the UI thread",
Self::AlreadyRegistered => "renderer already registered by another adapter",
Self::ReservedRenderer => "the native renderer cannot be replaced",
})
}
}
impl std::error::Error for RegistrationError {}
pub(crate) fn capabilities(renderer: WebRenderer) -> Option<Capabilities> {
if let Some(host) = surface::get(renderer) {
return Some(host.capabilities);
}
#[cfg(all(feature = "appkit", target_os = "macos"))]
return appkit::get(renderer).map(|host| host.capabilities);
#[cfg(not(all(feature = "appkit", target_os = "macos")))]
{
let _ = renderer;
None
}
}
pub mod surface {
use super::{Capabilities, RegistrationError, WebRenderer};
use crate::{WebPatch, WebProps};
use day_core::{BuildCx, RNode};
use std::{cell::RefCell, collections::HashSet, sync::OnceLock};
pub const KIND: day_spec::PieceKind = day_spec::kinds::CANVAS;
pub struct Host {
pub capabilities: Capabilities,
pub attach: fn(&mut BuildCx, RNode, &WebProps),
pub update: fn(RNode, &WebPatch),
pub release: fn(RNode),
}
static SERVO: OnceLock<&'static Host> = OnceLock::new();
thread_local! {
static NODES: RefCell<HashSet<RNode>> = RefCell::new(HashSet::new());
}
pub fn register(renderer: WebRenderer, host: &'static Host) -> Result<(), RegistrationError> {
if !day_core::has_tree() {
return Err(RegistrationError::WrongThread);
}
if renderer == WebRenderer::WebView {
return Err(RegistrationError::ReservedRenderer);
}
let installed = SERVO.get_or_init(|| host);
if std::ptr::eq(*installed, host) {
Ok(())
} else {
Err(RegistrationError::AlreadyRegistered)
}
}
pub(crate) fn get(renderer: WebRenderer) -> Option<&'static Host> {
match renderer {
WebRenderer::WebView => None,
WebRenderer::Servo => SERVO.get().copied(),
}
}
pub(crate) fn attach(host: &'static Host, cx: &mut BuildCx, node: RNode, props: &WebProps) {
NODES.with(|nodes| nodes.borrow_mut().insert(node));
day_reactive::Scope::current().on_cleanup(move || {
NODES.with(|nodes| nodes.borrow_mut().remove(&node));
(host.release)(node);
});
(host.attach)(cx, node, props);
}
pub(crate) fn owns(node: RNode) -> bool {
NODES.with(|nodes| nodes.borrow().contains(&node))
}
pub(crate) fn send(node: RNode, patch: &WebPatch) -> bool {
if !owns(node) {
return false;
}
if let Some(host) = SERVO.get() {
(host.update)(node, patch);
}
true
}
}
pub(crate) fn send_patch(node: day_core::RNode, patch: crate::WebPatch) {
if !surface::send(node, &patch) {
day_core::with_tree(|tree| tree.patch(node, Box::new(patch), false));
}
}
#[cfg(all(feature = "appkit", target_os = "macos"))]
pub mod appkit {
use super::{Capabilities, RegistrationError, WebRenderer};
use crate::{WebPatch, WebProps};
use day_spec::NodeId;
use objc2::{MainThreadMarker, rc::Retained};
use objc2_app_kit::NSView;
use std::sync::OnceLock;
pub struct Host {
pub capabilities: Capabilities,
pub make: fn(MainThreadMarker, &WebProps, NodeId) -> Retained<NSView>,
pub update: fn(&Retained<NSView>, &WebPatch) -> bool,
pub release: fn(&Retained<NSView>) -> bool,
}
static SERVO: OnceLock<&'static Host> = OnceLock::new();
fn install(
slot: &OnceLock<&'static Host>,
host: &'static Host,
) -> Result<(), RegistrationError> {
let installed = slot.get_or_init(|| host);
if std::ptr::eq(*installed, host) {
Ok(())
} else {
Err(RegistrationError::AlreadyRegistered)
}
}
pub fn register(
_mtm: MainThreadMarker,
renderer: WebRenderer,
host: &'static Host,
) -> Result<(), RegistrationError> {
match renderer {
WebRenderer::WebView => Err(RegistrationError::ReservedRenderer),
WebRenderer::Servo => install(&SERVO, host),
}
}
pub(crate) fn get(renderer: WebRenderer) -> Option<&'static Host> {
match renderer {
WebRenderer::WebView => None,
WebRenderer::Servo => SERVO.get().copied(),
}
}
#[cfg(test)]
mod tests {
use super::*;
const CAPS: Capabilities = Capabilities {
private_browsing: day_spec::Support::Unsupported,
profiles: day_spec::Support::Unsupported,
clear_data: day_spec::Support::Unsupported,
};
fn unused_make(_: MainThreadMarker, _: &WebProps, _: NodeId) -> Retained<NSView> {
panic!("registration must not create views or initialize engines")
}
static FIRST: Host = Host {
capabilities: CAPS,
make: unused_make,
update: |_, _| false,
release: |_| false,
};
static SECOND: Host = Host {
capabilities: Capabilities {
profiles: day_spec::Support::Emulated,
..CAPS
},
..FIRST
};
#[test]
fn registration_is_idempotent_and_preserves_the_original_host() {
let slot = OnceLock::new();
assert!(slot.get().is_none());
assert_eq!(install(&slot, &FIRST), Ok(()));
assert_eq!(install(&slot, &FIRST), Ok(()));
assert_eq!(
install(&slot, &SECOND),
Err(RegistrationError::AlreadyRegistered)
);
assert!(std::ptr::eq(*slot.get().unwrap(), &FIRST));
assert_eq!(slot.get().unwrap().capabilities, CAPS);
}
}
}
pub fn load_event(url: String) -> Event {
event(0.0, url)
}
pub fn external_link_event(url: String) -> Event {
event(crate::LINK_REPORT, url)
}
pub fn reply_event(request: u64, reply: String) -> Event {
event(request as f64, reply)
}
fn event(num: f64, text: String) -> Event {
Event::Custom {
tag: "webview:renderer",
num,
text,
}
}
pub fn new_tab_event(url: String, background: bool) -> Event {
crate::new_tab_event(url, background)
}
pub fn data_request(script: &str) -> Option<serde_json::Value> {
script
.strip_prefix(crate::DATA_REQUEST)
.map(|request| serde_json::from_str(request).unwrap_or_default())
}
pub fn success_json(value: serde_json::Value) -> String {
format!("1{}{value}", crate::SEP)
}
pub fn engine_error(name: &str, message: &str) -> String {
crate::engine_error(name, message)
}
pub fn resource_response(id: u64, url: &str, method: &str, range: &str) -> ResourceResponse {
crate::resources::respond(id, url, method, range)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn background(work: impl FnOnce() + Send + 'static) {
crate::resources::background(work);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adapter_replies_decode_through_the_shared_evaluation_protocol() {
let value = serde_json::json!({"title": "fixture\u{1f}title", "loading": false});
let Event::Custom { num, text, .. } = reply_event(42, success_json(value.clone())) else {
panic!("expected evaluation event")
};
assert_eq!(num, 42.0);
let decoded: serde_json::Value =
serde_json::from_str(&crate::decode(&text).unwrap()).unwrap();
assert_eq!(decoded, value);
assert_eq!(
crate::decode(&engine_error("FixtureError", "fixture failure")),
Err(crate::EvalError::Threw {
name: "FixtureError".into(),
message: "fixture failure".into()
})
);
assert!(data_request(&crate::wrap_script("42")).is_none());
let script = format!(
"{}{}",
crate::DATA_REQUEST,
serde_json::json!({"operation":"navigation"})
);
assert_eq!(data_request(&script).unwrap()["operation"], "navigation");
}
}