day_bridge::bridge! {
#[day_bridge::declare]
extern "day" {
fn attach_browser(id: i32, base: &str);
fn observe_browser_load(id: i32);
fn attach_tabs(id: i32, foreground: &str, background: &str);
fn resource_namespace() -> Result<f64, day_bridge::Error>;
fn attach_resources(id: i32, base: &str, start: &str);
fn resource_reply(token: i32, status: i32, headers: &str, body: &[u8]);
fn eval_browser(id: i32, req: f64, script: &str);
}
#[day_bridge::impl(js, platforms = [web])]
js!(r#"
const randomId = crypto.getRandomValues(new Uint32Array(2));
const namespaceId = String(randomId[0] * 2097152 + (randomId[1] >>> 11));
let resourceToken=0;
const resourcePorts=new Map();
function resource_namespace() { return Number(namespaceId); }
navigator.serviceWorker?.addEventListener('message',event=>{
const r=event.data;
if(r?.type!=='day-resource' || r.namespace!==namespaceId)return;
const token=++resourceToken;
resourcePorts.set(token,event.ports[0]);
try {dayHost.exports().day_web_resource_browser(token,r.provider,...dayHost.intoWasm(JSON.stringify(r)));}
catch(error){resource_reply(token,500,'[]',new Uint8Array());console.error(error);}
});
function resource_reply(token,status,headers,body) {
const port=resourcePorts.get(token);resourcePorts.delete(token);
if(!port)return;
const bytes=body.slice();
port.postMessage({status,headers:JSON.parse(headers),body:bytes},[bytes.buffer]);port.close();
}
async function attach_resources(id,base,start) {
const frame=dayHost.dom.element(id);
let alive=true;dayHost.dom.onRelease(id,()=>{alive=false});
try {
const root=new URL('assets/data/day-piece-webview/',document.baseURI);
const reg=await navigator.serviceWorker.register(new URL('resource-worker.js',root),{scope:root.pathname});
const worker=reg.installing||reg.waiting||reg.active;
if(worker.state!=='activated') await new Promise((resolve,reject)=>{
const changed=()=>{
if(worker.state==='activated'||worker.state==='redundant'){
worker.removeEventListener('statechange',changed);
worker.state==='activated'?resolve():reject(new Error('Resource worker failed'));
}
};
worker.addEventListener('statechange',changed);
changed();
});
if(alive)frame.src=new URL(start,document.baseURI).href;
}catch(error){if(alive)dayHost.dom.emit(id,-2,String(error));}
}
function eval_browser(id, req, script) {
let reply;
try {
const frame = dayHost.dom.element(id);
if (!frame?.contentDocument) throw new Error('Frame is unavailable or cross-origin');
reply = frame.contentWindow.eval(script);
if (typeof reply !== 'string') throw new Error('Non-string evaluation reply');
} catch (error) {
reply = '0\u001f' + (error.name || 'Error') + '\u001f' + error.message;
}
dayHost.dom.emit(id, req, reply);
}
function observe_browser_load(id) {
const frame = dayHost.dom.element(id);
function loaded() {
let url = frame.src || 'about:blank';
try { url = frame.contentWindow.location.href; } catch {}
// Resource-backed frames initially load an internal blank document
// while their worker starts. It is not the requested page's load.
if (url === 'about:blank' && frame.src !== 'about:blank') return;
dayHost.dom.emit(id, 0, url);
}
frame.addEventListener('load', loaded);
dayHost.dom.onRelease(id, () => frame.removeEventListener('load', loaded));
}
function attach_tabs(id, foreground, background) {
const frame=dayHost.dom.element(id);
const open=(url,background)=>dayHost.dom.emit(id,-4,JSON.stringify({url,background}));
let current, cleanup=()=>{};
function loaded() {
cleanup();
try { current=frame.contentDocument; if(!current)return; } catch {return;}
let menu=null;
const dismiss=()=>{menu?.remove();menu=null;};
const link=e=>e.composedPath().find(n=>n?.tagName==='A'&&n.href);
const click=e=>{const a=link(e);if(!a||!(e.metaKey||e.ctrlKey||e.button===1||a.target==='_blank'))return;e.preventDefault();e.stopImmediatePropagation();open(a.href,!e.shiftKey&&(e.metaKey||e.ctrlKey||e.button===1));};
const context=e=>{
const a=link(e);if(!a)return;e.preventDefault();e.stopImmediatePropagation();dismiss();
menu=current.createElement('div');menu.setAttribute('role','menu');
Object.assign(menu.style,{position:'fixed',left:e.clientX+'px',top:e.clientY+'px',zIndex:2147483647,background:'Canvas',color:'CanvasText',border:'1px solid GrayText',padding:'4px',boxShadow:'0 2px 8px #0005'});
for(const [label,backgroundTab] of [[foreground,false],[background,true]]) {
const item=current.createElement('button');item.textContent=label;item.setAttribute('role','menuitem');item.style.display='block';item.onclick=()=>{open(a.href,backgroundTab);dismiss();};menu.append(item);
}
current.body.append(menu);menu.firstChild.focus();
};
const key=e=>{if(e.key==='Escape')dismiss();};
current.addEventListener('click',click,true);current.addEventListener('auxclick',click,true);current.addEventListener('contextmenu',context,true);current.addEventListener('keydown',key,true);
cleanup=()=>{dismiss();current.removeEventListener('click',click,true);current.removeEventListener('auxclick',click,true);current.removeEventListener('contextmenu',context,true);current.removeEventListener('keydown',key,true);};
}
frame.addEventListener('load',loaded);
dayHost.dom.onRelease(id,()=>{cleanup();frame.removeEventListener('load',loaded);});
}
function attach_browser(id, base) {
const frame = dayHost.dom.element(id);
const site = new URL(base, document.baseURI);
let current = null;
function clicked(event) {
const anchor = event.target?.closest?.('a[href]');
if (!anchor) return;
let url;
try { url = new URL(anchor.getAttribute('href'), current.baseURI); }
catch { return; }
if ((url.origin === site.origin && url.pathname.startsWith(site.pathname))
|| url.href === 'about:blank') return;
event.preventDefault();
dayHost.dom.emit(id, -1, url.href);
}
function detachDocument() {
current?.removeEventListener('click', clicked, { capture: true });
current = null;
}
function loaded() {
detachDocument();
try { current = frame.contentDocument; } catch { return; }
current?.addEventListener('click', clicked, { capture: true });
}
frame.addEventListener('load', loaded);
dayHost.dom.onRelease(id, () => {
frame.removeEventListener('load', loaded);
detachDocument();
});
}
"#);
#[day_bridge::impl(rust, platforms = [other])]
fn resource_namespace() -> Result<f64, day_bridge::Error> { Ok(0.0) }
#[day_bridge::impl(rust, platforms = [other])]
fn attach_resources(id:i32,base:&str,start:&str) {let _=(id,base,start);}
#[day_bridge::impl(rust, platforms = [other])]
fn resource_reply(token:i32,status:i32,headers:&str,body:&[u8]) {let _=(token,status,headers,body);}
#[day_bridge::impl(rust, platforms = [other])]
fn observe_browser_load(id: i32) { let _ = id; }
#[day_bridge::impl(rust, platforms = [other])]
fn attach_tabs(id:i32,foreground:&str,background:&str) {let _=(id,foreground,background);}
#[day_bridge::impl(rust, platforms = [other])]
fn attach_browser(id: i32, base: &str) { let _ = (id, base); }
#[day_bridge::impl(rust, platforms = [other])]
fn eval_browser(id: i32, req: f64, script: &str) { let _ = (id, req, script); }
}
pub(crate) fn observe_load(id: i32) {
observe_browser_load(id);
}
pub(crate) fn tabs(id: i32, labels: &crate::TabLinkLabels) {
attach_tabs(id, &labels.foreground, &labels.background);
}
pub(crate) fn attach(id: i32, base: &str) {
attach_browser(id, base);
}
pub(crate) fn eval(id: i32, req: f64, script: &str) {
eval_browser(id, req, script);
}
pub(crate) fn namespace() -> String {
resource_namespace()
.expect("resource namespace")
.to_string()
}
pub(crate) fn resources(id: i32, base: &str, start: &str) {
attach_browser(id, base);
attach_resources(id, base, start);
}
#[cfg(target_arch = "wasm32")]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn day_web_resource_browser(
token: i32,
provider: u32,
ptr: *mut u8,
len: usize,
) {
let bytes = unsafe { day_bridge::__take_wasm(ptr, len) };
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap_or_default();
let r = crate::resources::respond(
provider as u64,
value["url"].as_str().unwrap_or(""),
value["method"].as_str().unwrap_or(""),
value["range"].as_str().unwrap_or(""),
);
let mut headers = r.headers.clone();
headers.push(("Content-Type".into(), r.mime.clone()));
resource_reply(
token,
r.status as i32,
&serde_json::to_string(&headers).unwrap(),
&r.body,
);
}