AkurAI Build
Menu

Public repository / ef2b6f39a839842d6f1fa9f1d7d401dee73d9e15

AkurAI-Framework / crates/ui/src/host/wasm.rs

Latest change c51ade7a9466ad00c3c364890a6ae2a5dae7e3fb - akurai-ui: microphone recording and audio playback through the host (MediaRecorder bridge, native fake, testing helpers) by Ólafur Búi Ólafsson

//! Browser host: raw imports from `host.js` (module `"akurai"`).
//!
//! Strings go Rust→JS as `(ptr, len)` into linear memory. Strings coming back
//! synchronously use a two-step read: the import returns the byte length (or
//! `u32::MAX` for "none") and parks the bytes in JS, then [`take`] copies them
//! out. This module is the crate's only `unsafe`; every call is a plain FFI
//! call with pointers into memory we own for the duration of the call.

use super::{EventRef, Node};

#[link(wasm_import_module = "akurai")]
extern "C" {
    fn ak_create_element(p: *const u8, l: usize) -> u32;
    fn ak_create_element_ns(np: *const u8, nl: usize, p: *const u8, l: usize) -> u32;
    fn ak_create_text(p: *const u8, l: usize) -> u32;
    fn ak_create_comment(p: *const u8, l: usize) -> u32;
    fn ak_set_attr(n: u32, np: *const u8, nl: usize, vp: *const u8, vl: usize);
    fn ak_remove_attr(n: u32, np: *const u8, nl: usize);
    fn ak_set_prop_str(n: u32, np: *const u8, nl: usize, vp: *const u8, vl: usize);
    fn ak_set_prop_bool(n: u32, np: *const u8, nl: usize, v: u32);
    fn ak_get_prop_str(n: u32, np: *const u8, nl: usize) -> u32;
    fn ak_insert_before(parent: u32, child: u32, before: u32);
    fn ak_remove(n: u32);
    fn ak_clear_between(start: u32, end: u32);
    fn ak_move_range(start: u32, end: u32, before: u32);
    fn ak_parent(n: u32) -> u32;
    fn ak_next_sibling(n: u32) -> u32;
    fn ak_listen(n: u32, ep: *const u8, el: usize, cb: u32);
    fn ak_get_by_id(p: *const u8, l: usize) -> u32;
    fn ak_focus(n: u32);
    fn ak_focus_first(n: u32) -> u32;
    fn ak_active_element() -> u32;
    fn ak_contains(a: u32, n: u32) -> u32;
    fn ak_event_value(ev: u32) -> u32;
    fn ak_event_checked(ev: u32) -> u32;
    fn ak_event_key(ev: u32) -> u32;
    fn ak_event_target(ev: u32) -> u32;
    fn ak_event_shift(ev: u32) -> u32;
    fn ak_prevent_default(ev: u32);
    fn ak_stop_propagation(ev: u32);
    fn ak_fetch(
        mp: *const u8,
        ml: usize,
        up: *const u8,
        ul: usize,
        bp: *const u8,
        bl: usize,
        has_body: u32,
        cb: u32,
    );
    fn ak_set_timeout(ms: u32, cb: u32);
    fn ak_record_start() -> u32;
    fn ak_record_stop(cb: u32);
    fn ak_play_audio(p: *const u8, l: usize);
    fn ak_event_source(up: *const u8, ul: usize, np: *const u8, nl: usize, cb: u32) -> u32;
    fn ak_event_source_close(id: u32);
    fn ak_location_hash() -> u32;
    fn ak_set_location_hash(p: *const u8, l: usize);
    fn ak_now_ms() -> f64;
    fn ak_tz_offset_minutes() -> i32;
    fn ak_storage_get(p: *const u8, l: usize) -> u32;
    fn ak_storage_set(kp: *const u8, kl: usize, vp: *const u8, vl: usize);
    fn ak_set_title(p: *const u8, l: usize);
    fn ak_set_root_attr(np: *const u8, nl: usize, vp: *const u8, vl: usize);
    fn ak_log(p: *const u8, l: usize);
    fn ak_take(dst: *mut u8);
}

const NONE: u32 = u32::MAX;

fn take(len: u32) -> Option<String> {
    if len == NONE {
        return None;
    }
    let mut buf = vec![0u8; len as usize];
    // SAFETY: `buf` holds exactly `len` writable bytes; JS copies the parked
    // string of that length into it.
    unsafe { ak_take(buf.as_mut_ptr()) };
    Some(String::from_utf8(buf).unwrap_or_default())
}

fn node(id: u32) -> Option<Node> {
    (id != 0).then_some(Node(id))
}

// SAFETY (all fns below): FFI calls passing pointers/lengths of live `&str`s.
pub(crate) fn create_element(tag: &str) -> Node {
    Node(unsafe { ak_create_element(tag.as_ptr(), tag.len()) })
}
pub(crate) fn create_element_ns(ns: &str, tag: &str) -> Node {
    Node(unsafe { ak_create_element_ns(ns.as_ptr(), ns.len(), tag.as_ptr(), tag.len()) })
}
pub(crate) fn create_text(text: &str) -> Node {
    Node(unsafe { ak_create_text(text.as_ptr(), text.len()) })
}
pub(crate) fn create_comment(text: &str) -> Node {
    Node(unsafe { ak_create_comment(text.as_ptr(), text.len()) })
}
pub(crate) fn set_attr(n: Node, name: &str, value: &str) {
    unsafe { ak_set_attr(n.0, name.as_ptr(), name.len(), value.as_ptr(), value.len()) }
}
pub(crate) fn remove_attr(n: Node, name: &str) {
    unsafe { ak_remove_attr(n.0, name.as_ptr(), name.len()) }
}
pub(crate) fn set_prop_str(n: Node, name: &str, value: &str) {
    unsafe { ak_set_prop_str(n.0, name.as_ptr(), name.len(), value.as_ptr(), value.len()) }
}
pub(crate) fn set_prop_bool(n: Node, name: &str, value: bool) {
    unsafe { ak_set_prop_bool(n.0, name.as_ptr(), name.len(), value as u32) }
}
pub(crate) fn get_prop_str(n: Node, name: &str) -> String {
    take(unsafe { ak_get_prop_str(n.0, name.as_ptr(), name.len()) }).unwrap_or_default()
}
pub(crate) fn insert_before(parent: Node, child: Node, before: Option<Node>) {
    unsafe { ak_insert_before(parent.0, child.0, before.map_or(0, |b| b.0)) }
}
pub(crate) fn remove(n: Node) {
    unsafe { ak_remove(n.0) }
}
pub(crate) fn clear_between(start: Node, end: Node) {
    unsafe { ak_clear_between(start.0, end.0) }
}
pub(crate) fn move_range(start: Node, end: Node, before: Node) {
    unsafe { ak_move_range(start.0, end.0, before.0) }
}
pub(crate) fn parent(n: Node) -> Option<Node> {
    node(unsafe { ak_parent(n.0) })
}
pub(crate) fn next_sibling(n: Node) -> Option<Node> {
    node(unsafe { ak_next_sibling(n.0) })
}
pub(crate) fn listen(n: Node, event: &str, cb: u32) {
    unsafe { ak_listen(n.0, event.as_ptr(), event.len(), cb) }
}
pub(crate) fn get_by_id(id: &str) -> Option<Node> {
    node(unsafe { ak_get_by_id(id.as_ptr(), id.len()) })
}
pub(crate) fn focus(n: Node) {
    unsafe { ak_focus(n.0) }
}
pub(crate) fn focus_first(n: Node) -> bool {
    unsafe { ak_focus_first(n.0) != 0 }
}
pub(crate) fn active_element() -> Option<Node> {
    node(unsafe { ak_active_element() })
}
pub(crate) fn contains(a: Node, n: Node) -> bool {
    unsafe { ak_contains(a.0, n.0) != 0 }
}
pub(crate) fn event_value(ev: EventRef) -> String {
    take(unsafe { ak_event_value(ev.0) }).unwrap_or_default()
}
pub(crate) fn event_checked(ev: EventRef) -> bool {
    unsafe { ak_event_checked(ev.0) != 0 }
}
pub(crate) fn event_key(ev: EventRef) -> String {
    take(unsafe { ak_event_key(ev.0) }).unwrap_or_default()
}
pub(crate) fn event_target(ev: EventRef) -> Option<Node> {
    node(unsafe { ak_event_target(ev.0) })
}
pub(crate) fn event_shift(ev: EventRef) -> bool {
    unsafe { ak_event_shift(ev.0) != 0 }
}
pub(crate) fn prevent_default(ev: EventRef) {
    unsafe { ak_prevent_default(ev.0) }
}
pub(crate) fn stop_propagation(ev: EventRef) {
    unsafe { ak_stop_propagation(ev.0) }
}
pub(crate) fn fetch(method: &str, url: &str, body: Option<&str>, cb: u32) {
    let b = body.unwrap_or("");
    unsafe {
        ak_fetch(
            method.as_ptr(),
            method.len(),
            url.as_ptr(),
            url.len(),
            b.as_ptr(),
            b.len(),
            body.is_some() as u32,
            cb,
        )
    }
}
pub(crate) fn set_timeout(ms: u32, cb: u32) {
    unsafe { ak_set_timeout(ms, cb) }
}
pub(crate) fn record_start() -> bool {
    unsafe { ak_record_start() == 1 }
}
pub(crate) fn record_stop(cb: u32) {
    unsafe { ak_record_stop(cb) }
}
pub(crate) fn play_audio(src: &str) {
    unsafe { ak_play_audio(src.as_ptr(), src.len()) }
}
pub(crate) fn event_source(url: &str, names: &str, cb: u32) -> u32 {
    unsafe { ak_event_source(url.as_ptr(), url.len(), names.as_ptr(), names.len(), cb) }
}
pub(crate) fn event_source_close(id: u32) {
    unsafe { ak_event_source_close(id) }
}
pub(crate) fn location_hash() -> String {
    take(unsafe { ak_location_hash() }).unwrap_or_default()
}
pub(crate) fn set_location_hash(hash: &str) {
    unsafe { ak_set_location_hash(hash.as_ptr(), hash.len()) }
}
pub(crate) fn now_ms() -> f64 {
    unsafe { ak_now_ms() }
}
pub(crate) fn tz_offset_minutes() -> i32 {
    unsafe { ak_tz_offset_minutes() }
}
pub(crate) fn storage_get(key: &str) -> Option<String> {
    take(unsafe { ak_storage_get(key.as_ptr(), key.len()) })
}
pub(crate) fn storage_set(key: &str, value: &str) {
    unsafe { ak_storage_set(key.as_ptr(), key.len(), value.as_ptr(), value.len()) }
}
pub(crate) fn set_title(title: &str) {
    unsafe { ak_set_title(title.as_ptr(), title.len()) }
}
pub(crate) fn set_root_attr(name: &str, value: &str) {
    unsafe { ak_set_root_attr(name.as_ptr(), name.len(), value.as_ptr(), value.len()) }
}
pub(crate) fn log(message: &str) {
    unsafe { ak_log(message.as_ptr(), message.len()) }
}