Menu
Public repository / 5aea767e0114a6b39d51aa068dc13de51fc610a5
AkurAI-Framework / crates/ui/src/host.js
Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson
// akurai-ui browser host — the DOM side of the Rust UI runtime.
// Loads a wasm app built with `akurai_ui::app!`, owns the node handle table,
// and forwards events, fetches and timers back into Rust. No dependencies.
//
// Usage: <script type="module">import { boot } from "./akurai-ui.js"; boot("./app.wasm");</script>
const enc = new TextEncoder();
const dec = new TextDecoder();
const NONE = 0xffffffff;
export async function boot(url) {
const nodes = new Map([[1, document], [2, document.body], [3, window]]);
const events = new Map();
let nextNode = 100;
let nextEvent = 1;
let parked = new Uint8Array(0);
let wasm;
const mem = () => new Uint8Array(wasm.exports.memory.buffer);
const str = (p, l) => dec.decode(mem().subarray(p, p + l));
const park = (s) => {
if (s === null || s === undefined) return NONE;
parked = enc.encode(String(s));
return parked.length;
};
const handle = (n) => {
if (!n) return 0;
if (n.__ak) return n.__ak;
const h = nextNode++;
n.__ak = h;
nodes.set(h, n);
return h;
};
const release = (n) => {
if (n.__ak) nodes.delete(n.__ak);
for (const c of n.childNodes) release(c);
};
const toRust = (s) => {
const bytes = enc.encode(s);
const ptr = wasm.exports.akurai_alloc(bytes.length);
mem().set(bytes, ptr);
return [ptr, bytes.length];
};
const FOCUSABLE =
'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
const akurai = {
ak_create_element: (p, l) => handle(document.createElement(str(p, l))),
ak_create_element_ns: (np, nl, p, l) => handle(document.createElementNS(str(np, nl), str(p, l))),
ak_create_text: (p, l) => handle(document.createTextNode(str(p, l))),
ak_create_comment: (p, l) => handle(document.createComment(str(p, l))),
ak_set_attr: (n, np, nl, vp, vl) => nodes.get(n)?.setAttribute(str(np, nl), str(vp, vl)),
ak_remove_attr: (n, np, nl) => nodes.get(n)?.removeAttribute(str(np, nl)),
ak_set_prop_str: (n, np, nl, vp, vl) => {
const el = nodes.get(n), k = str(np, nl), v = str(vp, vl);
if (el && el[k] !== v) el[k] = v;
},
ak_set_prop_bool: (n, np, nl, v) => { const el = nodes.get(n); if (el) el[str(np, nl)] = !!v; },
ak_get_prop_str: (n, np, nl) => park(nodes.get(n)?.[str(np, nl)] ?? ""),
ak_insert_before: (p, c, b) => nodes.get(p)?.insertBefore(nodes.get(c), b ? nodes.get(b) : null),
ak_remove: (n) => { const el = nodes.get(n); if (el) { el.remove(); release(el); } },
ak_clear_between: (s, e) => {
const start = nodes.get(s), end = nodes.get(e);
if (!start || !end) return;
while (start.nextSibling && start.nextSibling !== end) {
const x = start.nextSibling;
x.remove();
release(x);
}
},
ak_move_range: (s, e, b) => {
const start = nodes.get(s), end = nodes.get(e), before = nodes.get(b);
if (!start || !end || !before) return;
const parent = before.parentNode;
let cur = start;
for (;;) {
const next = cur.nextSibling;
parent.insertBefore(cur, before);
if (cur === end || !next) break;
cur = next;
}
},
ak_parent: (n) => handle(nodes.get(n)?.parentNode),
ak_next_sibling: (n) => handle(nodes.get(n)?.nextSibling),
ak_listen: (n, ep, el, cb) => {
nodes.get(n)?.addEventListener(str(ep, el), (e) => {
const id = nextEvent++;
events.set(id, e);
try { wasm.exports.akurai_dispatch(cb, id); } finally { events.delete(id); }
});
},
ak_get_by_id: (p, l) => handle(document.getElementById(str(p, l))),
ak_focus: (n) => nodes.get(n)?.focus(),
ak_focus_first: (n) => {
const el = nodes.get(n)?.querySelector?.(FOCUSABLE);
if (!el) return 0;
el.focus();
return 1;
},
ak_active_element: () => handle(document.activeElement),
ak_contains: (a, n) => (nodes.get(a)?.contains?.(nodes.get(n)) ? 1 : 0),
ak_event_value: (ev) => park(events.get(ev)?.target?.value ?? ""),
ak_event_checked: (ev) => (events.get(ev)?.target?.checked ? 1 : 0),
ak_event_key: (ev) => park(events.get(ev)?.key ?? ""),
ak_event_target: (ev) => handle(events.get(ev)?.target),
ak_event_shift: (ev) => (events.get(ev)?.shiftKey ? 1 : 0),
ak_prevent_default: (ev) => events.get(ev)?.preventDefault(),
ak_stop_propagation: (ev) => events.get(ev)?.stopPropagation(),
ak_fetch: (mp, ml, up, ul, bp, bl, hasBody, cb) => {
const init = { method: str(mp, ml), credentials: "same-origin", headers: {} };
if (hasBody) {
init.body = str(bp, bl);
init.headers["content-type"] = "application/json";
}
fetch(str(up, ul), init)
.then(async (r) => [r.status, await r.text()])
.catch((e) => [0, String(e)])
.then(([status, text]) => {
const [ptr, len] = toRust(text);
wasm.exports.akurai_complete(cb, status, ptr, len);
});
},
ak_set_timeout: (ms, cb) => setTimeout(() => {
const [ptr, len] = toRust("");
wasm.exports.akurai_complete(cb, 0, ptr, len);
}, ms),
ak_location_hash: () => park(location.hash.replace(/^#/, "")),
ak_set_location_hash: (p, l) => { location.hash = str(p, l); },
ak_now_ms: () => Date.now(),
ak_tz_offset_minutes: () => -new Date().getTimezoneOffset(),
ak_storage_get: (p, l) => { try { return park(localStorage.getItem(str(p, l))); } catch { return NONE; } },
ak_storage_set: (kp, kl, vp, vl) => { try { localStorage.setItem(str(kp, kl), str(vp, vl)); } catch {} },
ak_set_title: (p, l) => { document.title = str(p, l); },
ak_set_root_attr: (np, nl, vp, vl) => document.documentElement.setAttribute(str(np, nl), str(vp, vl)),
ak_log: (p, l) => console.log(str(p, l)),
ak_take: (dst) => { mem().set(parked, dst); parked = new Uint8Array(0); },
};
const { instance } = await WebAssembly.instantiateStreaming(fetch(url), { akurai });
wasm = instance;
wasm.exports.akurai_start();
}