AkurAI Build
Menu

Public repository / a0501370bec01f30f21f48fbebd95284b81aca77

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();
}