Skip to main content

oxide_sdk/
lib.rs

1//! # Oxide SDK
2//!
3//! Guest-side SDK for building WebAssembly applications that run inside the
4//! [Oxide browser](https://github.com/niklabh/oxide). This crate provides
5//! safe Rust wrappers around the raw host-imported functions exposed by the
6//! `"oxide"` wasm import module.
7//!
8//! ## Quick Start
9//!
10//! Add `oxide-sdk` to your `Cargo.toml` and set `crate-type = ["cdylib"]`:
11//!
12//! ```toml
13//! [lib]
14//! crate-type = ["cdylib"]
15//!
16//! [dependencies]
17//! oxide-sdk = "0.2"
18//! ```
19//!
20//! Then write your app:
21//!
22//! ```rust,ignore
23//! use oxide_sdk::*;
24//!
25//! #[no_mangle]
26//! pub extern "C" fn start_app() {
27//!     log("Hello from Oxide!");
28//!     canvas_clear(30, 30, 46, 255);
29//!     canvas_text(20.0, 40.0, 28.0, 255, 255, 255, "Welcome to Oxide");
30//! }
31//! ```
32//!
33//! Build with `cargo build --target wasm32-unknown-unknown --release`.
34//!
35//! ## Interactive Apps
36//!
37//! For apps that need a render loop, export `on_frame`:
38//!
39//! ```rust,ignore
40//! use oxide_sdk::*;
41//!
42//! #[no_mangle]
43//! pub extern "C" fn start_app() {
44//!     log("Interactive app started");
45//! }
46//!
47//! #[no_mangle]
48//! pub extern "C" fn on_frame(_dt_ms: u32) {
49//!     canvas_clear(30, 30, 46, 255);
50//!     let (mx, my) = mouse_position();
51//!     canvas_circle(mx, my, 20.0, 255, 100, 100, 255);
52//!
53//!     if ui_button(1, 20.0, 20.0, 100.0, 30.0, "Click me!") {
54//!         log("Button was clicked!");
55//!     }
56//! }
57//! ```
58//!
59//! ## API Categories
60//!
61//! | Category | Functions |
62//! |----------|-----------|
63//! | **Canvas** | [`canvas_clear`], [`canvas_rect`], [`canvas_circle`], [`canvas_text`], [`canvas_line`], [`canvas_image`], [`canvas_dimensions`] |
64//! | **Console** | [`log`], [`warn`], [`error`] |
65//! | **HTTP** | [`fetch`], [`fetch_get`], [`fetch_post`], [`fetch_post_proto`], [`fetch_put`], [`fetch_delete`] |
66//! | **Protobuf** | [`proto::ProtoEncoder`], [`proto::ProtoDecoder`] |
67//! | **Storage** | [`storage_set`], [`storage_get`], [`storage_remove`], [`kv_store_set`], [`kv_store_get`], [`kv_store_delete`] |
68//! | **Audio** | [`audio_play`], [`audio_play_url`], [`audio_pause`], [`audio_resume`], [`audio_stop`], [`audio_set_volume`], [`audio_channel_play`] |
69//! | **Timers** | [`set_timeout`], [`set_interval`], [`clear_timer`], [`time_now_ms`] |
70//! | **Navigation** | [`navigate`], [`push_state`], [`replace_state`], [`get_url`], [`history_back`], [`history_forward`] |
71//! | **Input** | [`mouse_position`], [`mouse_button_down`], [`key_down`], [`key_pressed`], [`scroll_delta`] |
72//! | **Widgets** | [`ui_button`], [`ui_checkbox`], [`ui_slider`], [`ui_text_input`] |
73//! | **Crypto** | [`hash_sha256`], [`hash_sha256_hex`], [`base64_encode`], [`base64_decode`] |
74//! | **Other** | [`clipboard_write`], [`clipboard_read`], [`random_u64`], [`random_f64`], [`notify`], [`upload_file`], [`load_module`] |
75//!
76//! ## Full API Documentation
77//!
78//! See <https://docs.oxide.foundation/oxide_sdk/> for the complete API
79//! reference, or browse the individual function documentation below.
80
81pub mod proto;
82
83// ─── Raw FFI imports from the host ──────────────────────────────────────────
84
85#[link(wasm_import_module = "oxide")]
86extern "C" {
87    #[link_name = "api_log"]
88    fn _api_log(ptr: u32, len: u32);
89
90    #[link_name = "api_warn"]
91    fn _api_warn(ptr: u32, len: u32);
92
93    #[link_name = "api_error"]
94    fn _api_error(ptr: u32, len: u32);
95
96    #[link_name = "api_get_location"]
97    fn _api_get_location(out_ptr: u32, out_cap: u32) -> u32;
98
99    #[link_name = "api_upload_file"]
100    fn _api_upload_file(name_ptr: u32, name_cap: u32, data_ptr: u32, data_cap: u32) -> u64;
101
102    #[link_name = "api_canvas_clear"]
103    fn _api_canvas_clear(r: u32, g: u32, b: u32, a: u32);
104
105    #[link_name = "api_canvas_rect"]
106    fn _api_canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u32, g: u32, b: u32, a: u32);
107
108    #[link_name = "api_canvas_circle"]
109    fn _api_canvas_circle(cx: f32, cy: f32, radius: f32, r: u32, g: u32, b: u32, a: u32);
110
111    #[link_name = "api_canvas_text"]
112    fn _api_canvas_text(x: f32, y: f32, size: f32, r: u32, g: u32, b: u32, ptr: u32, len: u32);
113
114    #[link_name = "api_canvas_line"]
115    fn _api_canvas_line(x1: f32, y1: f32, x2: f32, y2: f32, r: u32, g: u32, b: u32, thickness: f32);
116
117    #[link_name = "api_canvas_dimensions"]
118    fn _api_canvas_dimensions() -> u64;
119
120    #[link_name = "api_canvas_image"]
121    fn _api_canvas_image(x: f32, y: f32, w: f32, h: f32, data_ptr: u32, data_len: u32);
122
123    #[link_name = "api_storage_set"]
124    fn _api_storage_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32);
125
126    #[link_name = "api_storage_get"]
127    fn _api_storage_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> u32;
128
129    #[link_name = "api_storage_remove"]
130    fn _api_storage_remove(key_ptr: u32, key_len: u32);
131
132    #[link_name = "api_clipboard_write"]
133    fn _api_clipboard_write(ptr: u32, len: u32);
134
135    #[link_name = "api_clipboard_read"]
136    fn _api_clipboard_read(out_ptr: u32, out_cap: u32) -> u32;
137
138    #[link_name = "api_time_now_ms"]
139    fn _api_time_now_ms() -> u64;
140
141    #[link_name = "api_set_timeout"]
142    fn _api_set_timeout(callback_id: u32, delay_ms: u32) -> u32;
143
144    #[link_name = "api_set_interval"]
145    fn _api_set_interval(callback_id: u32, interval_ms: u32) -> u32;
146
147    #[link_name = "api_clear_timer"]
148    fn _api_clear_timer(timer_id: u32);
149
150    #[link_name = "api_random"]
151    fn _api_random() -> u64;
152
153    #[link_name = "api_notify"]
154    fn _api_notify(title_ptr: u32, title_len: u32, body_ptr: u32, body_len: u32);
155
156    #[link_name = "api_fetch"]
157    fn _api_fetch(
158        method_ptr: u32,
159        method_len: u32,
160        url_ptr: u32,
161        url_len: u32,
162        ct_ptr: u32,
163        ct_len: u32,
164        body_ptr: u32,
165        body_len: u32,
166        out_ptr: u32,
167        out_cap: u32,
168    ) -> i64;
169
170    #[link_name = "api_load_module"]
171    fn _api_load_module(url_ptr: u32, url_len: u32) -> i32;
172
173    #[link_name = "api_hash_sha256"]
174    fn _api_hash_sha256(data_ptr: u32, data_len: u32, out_ptr: u32) -> u32;
175
176    #[link_name = "api_base64_encode"]
177    fn _api_base64_encode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
178
179    #[link_name = "api_base64_decode"]
180    fn _api_base64_decode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
181
182    #[link_name = "api_kv_store_set"]
183    fn _api_kv_store_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32) -> i32;
184
185    #[link_name = "api_kv_store_get"]
186    fn _api_kv_store_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> i32;
187
188    #[link_name = "api_kv_store_delete"]
189    fn _api_kv_store_delete(key_ptr: u32, key_len: u32) -> i32;
190
191    // ── Navigation ──────────────────────────────────────────────────
192
193    #[link_name = "api_navigate"]
194    fn _api_navigate(url_ptr: u32, url_len: u32) -> i32;
195
196    #[link_name = "api_push_state"]
197    fn _api_push_state(
198        state_ptr: u32,
199        state_len: u32,
200        title_ptr: u32,
201        title_len: u32,
202        url_ptr: u32,
203        url_len: u32,
204    );
205
206    #[link_name = "api_replace_state"]
207    fn _api_replace_state(
208        state_ptr: u32,
209        state_len: u32,
210        title_ptr: u32,
211        title_len: u32,
212        url_ptr: u32,
213        url_len: u32,
214    );
215
216    #[link_name = "api_get_url"]
217    fn _api_get_url(out_ptr: u32, out_cap: u32) -> u32;
218
219    #[link_name = "api_get_state"]
220    fn _api_get_state(out_ptr: u32, out_cap: u32) -> i32;
221
222    #[link_name = "api_history_length"]
223    fn _api_history_length() -> u32;
224
225    #[link_name = "api_history_back"]
226    fn _api_history_back() -> i32;
227
228    #[link_name = "api_history_forward"]
229    fn _api_history_forward() -> i32;
230
231    // ── Hyperlinks ──────────────────────────────────────────────────
232
233    #[link_name = "api_register_hyperlink"]
234    fn _api_register_hyperlink(x: f32, y: f32, w: f32, h: f32, url_ptr: u32, url_len: u32) -> i32;
235
236    #[link_name = "api_clear_hyperlinks"]
237    fn _api_clear_hyperlinks();
238
239    // ── Input Polling ────────────────────────────────────────────────
240
241    #[link_name = "api_mouse_position"]
242    fn _api_mouse_position() -> u64;
243
244    #[link_name = "api_mouse_button_down"]
245    fn _api_mouse_button_down(button: u32) -> u32;
246
247    #[link_name = "api_mouse_button_clicked"]
248    fn _api_mouse_button_clicked(button: u32) -> u32;
249
250    #[link_name = "api_key_down"]
251    fn _api_key_down(key: u32) -> u32;
252
253    #[link_name = "api_key_pressed"]
254    fn _api_key_pressed(key: u32) -> u32;
255
256    #[link_name = "api_scroll_delta"]
257    fn _api_scroll_delta() -> u64;
258
259    #[link_name = "api_modifiers"]
260    fn _api_modifiers() -> u32;
261
262    // ── Interactive Widgets ─────────────────────────────────────────
263
264    #[link_name = "api_ui_button"]
265    fn _api_ui_button(
266        id: u32,
267        x: f32,
268        y: f32,
269        w: f32,
270        h: f32,
271        label_ptr: u32,
272        label_len: u32,
273    ) -> u32;
274
275    #[link_name = "api_ui_checkbox"]
276    fn _api_ui_checkbox(
277        id: u32,
278        x: f32,
279        y: f32,
280        label_ptr: u32,
281        label_len: u32,
282        initial: u32,
283    ) -> u32;
284
285    #[link_name = "api_ui_slider"]
286    fn _api_ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32;
287
288    #[link_name = "api_ui_text_input"]
289    fn _api_ui_text_input(
290        id: u32,
291        x: f32,
292        y: f32,
293        w: f32,
294        init_ptr: u32,
295        init_len: u32,
296        out_ptr: u32,
297        out_cap: u32,
298    ) -> u32;
299
300    // ── Audio Playback ──────────────────────────────────────────────
301
302    #[link_name = "api_audio_play"]
303    fn _api_audio_play(data_ptr: u32, data_len: u32) -> i32;
304
305    #[link_name = "api_audio_play_url"]
306    fn _api_audio_play_url(url_ptr: u32, url_len: u32) -> i32;
307
308    #[link_name = "api_audio_pause"]
309    fn _api_audio_pause();
310
311    #[link_name = "api_audio_resume"]
312    fn _api_audio_resume();
313
314    #[link_name = "api_audio_stop"]
315    fn _api_audio_stop();
316
317    #[link_name = "api_audio_set_volume"]
318    fn _api_audio_set_volume(level: f32);
319
320    #[link_name = "api_audio_get_volume"]
321    fn _api_audio_get_volume() -> f32;
322
323    #[link_name = "api_audio_is_playing"]
324    fn _api_audio_is_playing() -> u32;
325
326    #[link_name = "api_audio_position"]
327    fn _api_audio_position() -> u64;
328
329    #[link_name = "api_audio_seek"]
330    fn _api_audio_seek(position_ms: u64) -> i32;
331
332    #[link_name = "api_audio_duration"]
333    fn _api_audio_duration() -> u64;
334
335    #[link_name = "api_audio_set_loop"]
336    fn _api_audio_set_loop(enabled: u32);
337
338    #[link_name = "api_audio_channel_play"]
339    fn _api_audio_channel_play(channel: u32, data_ptr: u32, data_len: u32) -> i32;
340
341    #[link_name = "api_audio_channel_stop"]
342    fn _api_audio_channel_stop(channel: u32);
343
344    #[link_name = "api_audio_channel_set_volume"]
345    fn _api_audio_channel_set_volume(channel: u32, level: f32);
346
347    // ── URL Utilities ───────────────────────────────────────────────
348
349    #[link_name = "api_url_resolve"]
350    fn _api_url_resolve(
351        base_ptr: u32,
352        base_len: u32,
353        rel_ptr: u32,
354        rel_len: u32,
355        out_ptr: u32,
356        out_cap: u32,
357    ) -> i32;
358
359    #[link_name = "api_url_encode"]
360    fn _api_url_encode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
361
362    #[link_name = "api_url_decode"]
363    fn _api_url_decode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
364}
365
366// ─── Console API ────────────────────────────────────────────────────────────
367
368/// Print a message to the browser console (log level).
369pub fn log(msg: &str) {
370    unsafe { _api_log(msg.as_ptr() as u32, msg.len() as u32) }
371}
372
373/// Print a warning to the browser console.
374pub fn warn(msg: &str) {
375    unsafe { _api_warn(msg.as_ptr() as u32, msg.len() as u32) }
376}
377
378/// Print an error to the browser console.
379pub fn error(msg: &str) {
380    unsafe { _api_error(msg.as_ptr() as u32, msg.len() as u32) }
381}
382
383// ─── Geolocation API ────────────────────────────────────────────────────────
384
385/// Get the device's mock geolocation as a `"lat,lon"` string.
386pub fn get_location() -> String {
387    let mut buf = [0u8; 128];
388    let len = unsafe { _api_get_location(buf.as_mut_ptr() as u32, buf.len() as u32) };
389    String::from_utf8_lossy(&buf[..len as usize]).to_string()
390}
391
392// ─── File Upload API ────────────────────────────────────────────────────────
393
394/// File returned from the native file picker.
395pub struct UploadedFile {
396    pub name: String,
397    pub data: Vec<u8>,
398}
399
400/// Opens the native OS file picker and returns the selected file.
401/// Returns `None` if the user cancels.
402pub fn upload_file() -> Option<UploadedFile> {
403    let mut name_buf = [0u8; 256];
404    let mut data_buf = vec![0u8; 1024 * 1024]; // 1MB max
405
406    let result = unsafe {
407        _api_upload_file(
408            name_buf.as_mut_ptr() as u32,
409            name_buf.len() as u32,
410            data_buf.as_mut_ptr() as u32,
411            data_buf.len() as u32,
412        )
413    };
414
415    if result == 0 {
416        return None;
417    }
418
419    let name_len = (result >> 32) as usize;
420    let data_len = (result & 0xFFFF_FFFF) as usize;
421
422    Some(UploadedFile {
423        name: String::from_utf8_lossy(&name_buf[..name_len]).to_string(),
424        data: data_buf[..data_len].to_vec(),
425    })
426}
427
428// ─── Canvas API ─────────────────────────────────────────────────────────────
429
430/// Clear the canvas with a solid RGBA color.
431pub fn canvas_clear(r: u8, g: u8, b: u8, a: u8) {
432    unsafe { _api_canvas_clear(r as u32, g as u32, b as u32, a as u32) }
433}
434
435/// Draw a filled rectangle.
436pub fn canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u8, g: u8, b: u8, a: u8) {
437    unsafe { _api_canvas_rect(x, y, w, h, r as u32, g as u32, b as u32, a as u32) }
438}
439
440/// Draw a filled circle.
441pub fn canvas_circle(cx: f32, cy: f32, radius: f32, r: u8, g: u8, b: u8, a: u8) {
442    unsafe { _api_canvas_circle(cx, cy, radius, r as u32, g as u32, b as u32, a as u32) }
443}
444
445/// Draw text on the canvas.
446pub fn canvas_text(x: f32, y: f32, size: f32, r: u8, g: u8, b: u8, text: &str) {
447    unsafe {
448        _api_canvas_text(
449            x,
450            y,
451            size,
452            r as u32,
453            g as u32,
454            b as u32,
455            text.as_ptr() as u32,
456            text.len() as u32,
457        )
458    }
459}
460
461/// Draw a line between two points.
462pub fn canvas_line(x1: f32, y1: f32, x2: f32, y2: f32, r: u8, g: u8, b: u8, thickness: f32) {
463    unsafe { _api_canvas_line(x1, y1, x2, y2, r as u32, g as u32, b as u32, thickness) }
464}
465
466/// Returns `(width, height)` of the canvas in pixels.
467pub fn canvas_dimensions() -> (u32, u32) {
468    let packed = unsafe { _api_canvas_dimensions() };
469    ((packed >> 32) as u32, (packed & 0xFFFF_FFFF) as u32)
470}
471
472/// Draw an image on the canvas from encoded image bytes (PNG, JPEG, GIF, WebP).
473/// The browser decodes the image and renders it at the given rectangle.
474pub fn canvas_image(x: f32, y: f32, w: f32, h: f32, data: &[u8]) {
475    unsafe { _api_canvas_image(x, y, w, h, data.as_ptr() as u32, data.len() as u32) }
476}
477
478// ─── Local Storage API ──────────────────────────────────────────────────────
479
480/// Store a key-value pair in sandboxed local storage.
481pub fn storage_set(key: &str, value: &str) {
482    unsafe {
483        _api_storage_set(
484            key.as_ptr() as u32,
485            key.len() as u32,
486            value.as_ptr() as u32,
487            value.len() as u32,
488        )
489    }
490}
491
492/// Retrieve a value from local storage. Returns empty string if not found.
493pub fn storage_get(key: &str) -> String {
494    let mut buf = [0u8; 4096];
495    let len = unsafe {
496        _api_storage_get(
497            key.as_ptr() as u32,
498            key.len() as u32,
499            buf.as_mut_ptr() as u32,
500            buf.len() as u32,
501        )
502    };
503    String::from_utf8_lossy(&buf[..len as usize]).to_string()
504}
505
506/// Remove a key from local storage.
507pub fn storage_remove(key: &str) {
508    unsafe { _api_storage_remove(key.as_ptr() as u32, key.len() as u32) }
509}
510
511// ─── Clipboard API ──────────────────────────────────────────────────────────
512
513/// Copy text to the system clipboard.
514pub fn clipboard_write(text: &str) {
515    unsafe { _api_clipboard_write(text.as_ptr() as u32, text.len() as u32) }
516}
517
518/// Read text from the system clipboard.
519pub fn clipboard_read() -> String {
520    let mut buf = [0u8; 4096];
521    let len = unsafe { _api_clipboard_read(buf.as_mut_ptr() as u32, buf.len() as u32) };
522    String::from_utf8_lossy(&buf[..len as usize]).to_string()
523}
524
525// ─── Timer / Clock API ─────────────────────────────────────────────────────
526
527/// Get the current time in milliseconds since the UNIX epoch.
528pub fn time_now_ms() -> u64 {
529    unsafe { _api_time_now_ms() }
530}
531
532/// Schedule a one-shot timer that fires after `delay_ms` milliseconds.
533/// When it fires the host calls your exported `on_timer(callback_id)`.
534/// Returns a timer ID that can be passed to [`clear_timer`].
535pub fn set_timeout(callback_id: u32, delay_ms: u32) -> u32 {
536    unsafe { _api_set_timeout(callback_id, delay_ms) }
537}
538
539/// Schedule a repeating timer that fires every `interval_ms` milliseconds.
540/// When it fires the host calls your exported `on_timer(callback_id)`.
541/// Returns a timer ID that can be passed to [`clear_timer`].
542pub fn set_interval(callback_id: u32, interval_ms: u32) -> u32 {
543    unsafe { _api_set_interval(callback_id, interval_ms) }
544}
545
546/// Cancel a timer previously created with [`set_timeout`] or [`set_interval`].
547pub fn clear_timer(timer_id: u32) {
548    unsafe { _api_clear_timer(timer_id) }
549}
550
551// ─── Random API ─────────────────────────────────────────────────────────────
552
553/// Get a random u64 from the host.
554pub fn random_u64() -> u64 {
555    unsafe { _api_random() }
556}
557
558/// Get a random f64 in [0, 1).
559pub fn random_f64() -> f64 {
560    (random_u64() >> 11) as f64 / (1u64 << 53) as f64
561}
562
563// ─── Notification API ───────────────────────────────────────────────────────
564
565/// Send a notification to the user (rendered in the browser console).
566pub fn notify(title: &str, body: &str) {
567    unsafe {
568        _api_notify(
569            title.as_ptr() as u32,
570            title.len() as u32,
571            body.as_ptr() as u32,
572            body.len() as u32,
573        )
574    }
575}
576
577// ─── Audio Playback API ─────────────────────────────────────────────────────
578
579/// Play audio from encoded bytes (WAV, MP3, OGG, FLAC).
580/// The host decodes and plays the audio. Returns 0 on success, negative on error.
581pub fn audio_play(data: &[u8]) -> i32 {
582    unsafe { _api_audio_play(data.as_ptr() as u32, data.len() as u32) }
583}
584
585/// Fetch audio from a URL and play it.
586/// The host performs the HTTP fetch and decodes the audio.
587/// Returns 0 on success, negative on error.
588pub fn audio_play_url(url: &str) -> i32 {
589    unsafe { _api_audio_play_url(url.as_ptr() as u32, url.len() as u32) }
590}
591
592/// Pause audio playback.
593pub fn audio_pause() {
594    unsafe { _api_audio_pause() }
595}
596
597/// Resume paused audio playback.
598pub fn audio_resume() {
599    unsafe { _api_audio_resume() }
600}
601
602/// Stop audio playback and clear the queue.
603pub fn audio_stop() {
604    unsafe { _api_audio_stop() }
605}
606
607/// Set audio volume. 1.0 is normal, 0.0 is silent, up to 2.0 for boost.
608pub fn audio_set_volume(level: f32) {
609    unsafe { _api_audio_set_volume(level) }
610}
611
612/// Get the current audio volume.
613pub fn audio_get_volume() -> f32 {
614    unsafe { _api_audio_get_volume() }
615}
616
617/// Returns `true` if audio is currently playing (not paused and not empty).
618pub fn audio_is_playing() -> bool {
619    unsafe { _api_audio_is_playing() != 0 }
620}
621
622/// Get the current playback position in milliseconds.
623pub fn audio_position() -> u64 {
624    unsafe { _api_audio_position() }
625}
626
627/// Seek to a position in milliseconds. Returns 0 on success, negative on error.
628pub fn audio_seek(position_ms: u64) -> i32 {
629    unsafe { _api_audio_seek(position_ms) }
630}
631
632/// Get the total duration of the currently loaded track in milliseconds.
633/// Returns 0 if unknown or nothing is loaded.
634pub fn audio_duration() -> u64 {
635    unsafe { _api_audio_duration() }
636}
637
638/// Enable or disable looping on the default channel.
639/// When enabled, subsequent `audio_play` calls will loop indefinitely.
640pub fn audio_set_loop(enabled: bool) {
641    unsafe { _api_audio_set_loop(if enabled { 1 } else { 0 }) }
642}
643
644// ─── Multi-Channel Audio API ────────────────────────────────────────────────
645
646/// Play audio on a specific channel. Multiple channels play simultaneously.
647/// Channel 0 is the default used by `audio_play`. Use channels 1+ for layered
648/// sound effects, background music, etc.
649pub fn audio_channel_play(channel: u32, data: &[u8]) -> i32 {
650    unsafe { _api_audio_channel_play(channel, data.as_ptr() as u32, data.len() as u32) }
651}
652
653/// Stop playback on a specific channel.
654pub fn audio_channel_stop(channel: u32) {
655    unsafe { _api_audio_channel_stop(channel) }
656}
657
658/// Set volume for a specific channel (0.0 silent, 1.0 normal, up to 2.0 boost).
659pub fn audio_channel_set_volume(channel: u32, level: f32) {
660    unsafe { _api_audio_channel_set_volume(channel, level) }
661}
662
663// ─── HTTP Fetch API ─────────────────────────────────────────────────────────
664
665/// Response from an HTTP fetch call.
666pub struct FetchResponse {
667    pub status: u32,
668    pub body: Vec<u8>,
669}
670
671impl FetchResponse {
672    /// Interpret the response body as UTF-8 text.
673    pub fn text(&self) -> String {
674        String::from_utf8_lossy(&self.body).to_string()
675    }
676}
677
678/// Perform an HTTP request.  Returns the status code and response body.
679///
680/// `content_type` sets the `Content-Type` header (pass `""` to omit).
681/// Protobuf is the native format — use `"application/protobuf"` for binary
682/// payloads.
683pub fn fetch(
684    method: &str,
685    url: &str,
686    content_type: &str,
687    body: &[u8],
688) -> Result<FetchResponse, i64> {
689    let mut out_buf = vec![0u8; 4 * 1024 * 1024]; // 4 MB response buffer
690    let result = unsafe {
691        _api_fetch(
692            method.as_ptr() as u32,
693            method.len() as u32,
694            url.as_ptr() as u32,
695            url.len() as u32,
696            content_type.as_ptr() as u32,
697            content_type.len() as u32,
698            body.as_ptr() as u32,
699            body.len() as u32,
700            out_buf.as_mut_ptr() as u32,
701            out_buf.len() as u32,
702        )
703    };
704    if result < 0 {
705        return Err(result);
706    }
707    let status = (result >> 32) as u32;
708    let body_len = (result & 0xFFFF_FFFF) as usize;
709    Ok(FetchResponse {
710        status,
711        body: out_buf[..body_len].to_vec(),
712    })
713}
714
715/// HTTP GET request.
716pub fn fetch_get(url: &str) -> Result<FetchResponse, i64> {
717    fetch("GET", url, "", &[])
718}
719
720/// HTTP POST with raw bytes.
721pub fn fetch_post(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
722    fetch("POST", url, content_type, body)
723}
724
725/// HTTP POST with protobuf body (sets `Content-Type: application/protobuf`).
726pub fn fetch_post_proto(url: &str, msg: &proto::ProtoEncoder) -> Result<FetchResponse, i64> {
727    fetch("POST", url, "application/protobuf", msg.as_bytes())
728}
729
730/// HTTP PUT with raw bytes.
731pub fn fetch_put(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
732    fetch("PUT", url, content_type, body)
733}
734
735/// HTTP DELETE.
736pub fn fetch_delete(url: &str) -> Result<FetchResponse, i64> {
737    fetch("DELETE", url, "", &[])
738}
739
740// ─── Dynamic Module Loading ─────────────────────────────────────────────────
741
742/// Fetch and execute another `.wasm` module from a URL.
743/// The loaded module shares the same canvas, console, and storage context.
744/// Returns 0 on success, negative error code on failure.
745pub fn load_module(url: &str) -> i32 {
746    unsafe { _api_load_module(url.as_ptr() as u32, url.len() as u32) }
747}
748
749// ─── Crypto / Hash API ─────────────────────────────────────────────────────
750
751/// Compute the SHA-256 hash of the given data. Returns 32 bytes.
752pub fn hash_sha256(data: &[u8]) -> [u8; 32] {
753    let mut out = [0u8; 32];
754    unsafe {
755        _api_hash_sha256(
756            data.as_ptr() as u32,
757            data.len() as u32,
758            out.as_mut_ptr() as u32,
759        );
760    }
761    out
762}
763
764/// Return SHA-256 hash as a lowercase hex string.
765pub fn hash_sha256_hex(data: &[u8]) -> String {
766    let hash = hash_sha256(data);
767    let mut hex = String::with_capacity(64);
768    for byte in &hash {
769        hex.push(HEX_CHARS[(*byte >> 4) as usize]);
770        hex.push(HEX_CHARS[(*byte & 0x0F) as usize]);
771    }
772    hex
773}
774
775const HEX_CHARS: [char; 16] = [
776    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
777];
778
779// ─── Base64 API ─────────────────────────────────────────────────────────────
780
781/// Base64-encode arbitrary bytes.
782pub fn base64_encode(data: &[u8]) -> String {
783    let mut buf = vec![0u8; data.len() * 4 / 3 + 8];
784    let len = unsafe {
785        _api_base64_encode(
786            data.as_ptr() as u32,
787            data.len() as u32,
788            buf.as_mut_ptr() as u32,
789            buf.len() as u32,
790        )
791    };
792    String::from_utf8_lossy(&buf[..len as usize]).to_string()
793}
794
795/// Decode a base64-encoded string back to bytes.
796pub fn base64_decode(encoded: &str) -> Vec<u8> {
797    let mut buf = vec![0u8; encoded.len()];
798    let len = unsafe {
799        _api_base64_decode(
800            encoded.as_ptr() as u32,
801            encoded.len() as u32,
802            buf.as_mut_ptr() as u32,
803            buf.len() as u32,
804        )
805    };
806    buf[..len as usize].to_vec()
807}
808
809// ─── Persistent Key-Value Store API ─────────────────────────────────────────
810
811/// Store a key-value pair in the persistent on-disk KV store.
812/// Returns `true` on success.
813pub fn kv_store_set(key: &str, value: &[u8]) -> bool {
814    let rc = unsafe {
815        _api_kv_store_set(
816            key.as_ptr() as u32,
817            key.len() as u32,
818            value.as_ptr() as u32,
819            value.len() as u32,
820        )
821    };
822    rc == 0
823}
824
825/// Convenience wrapper: store a UTF-8 string value.
826pub fn kv_store_set_str(key: &str, value: &str) -> bool {
827    kv_store_set(key, value.as_bytes())
828}
829
830/// Retrieve a value from the persistent KV store.
831/// Returns `None` if the key does not exist.
832pub fn kv_store_get(key: &str) -> Option<Vec<u8>> {
833    let mut buf = vec![0u8; 64 * 1024]; // 64 KB read buffer
834    let rc = unsafe {
835        _api_kv_store_get(
836            key.as_ptr() as u32,
837            key.len() as u32,
838            buf.as_mut_ptr() as u32,
839            buf.len() as u32,
840        )
841    };
842    if rc < 0 {
843        return None;
844    }
845    Some(buf[..rc as usize].to_vec())
846}
847
848/// Convenience wrapper: retrieve a UTF-8 string value.
849pub fn kv_store_get_str(key: &str) -> Option<String> {
850    kv_store_get(key).map(|v| String::from_utf8_lossy(&v).into_owned())
851}
852
853/// Delete a key from the persistent KV store. Returns `true` on success.
854pub fn kv_store_delete(key: &str) -> bool {
855    let rc = unsafe { _api_kv_store_delete(key.as_ptr() as u32, key.len() as u32) };
856    rc == 0
857}
858
859// ─── Navigation API ─────────────────────────────────────────────────────────
860
861/// Navigate to a new URL.  The URL can be absolute or relative to the current
862/// page.  Navigation happens asynchronously after the current `start_app`
863/// returns.  Returns 0 on success, negative on invalid URL.
864pub fn navigate(url: &str) -> i32 {
865    unsafe { _api_navigate(url.as_ptr() as u32, url.len() as u32) }
866}
867
868/// Push a new entry onto the browser's history stack without triggering a
869/// module reload.  This is analogous to `history.pushState()` in web browsers.
870///
871/// - `state`:  Opaque binary data retrievable later via [`get_state`].
872/// - `title`:  Human-readable title for the history entry.
873/// - `url`:    The URL to display in the address bar (relative or absolute).
874///             Pass `""` to keep the current URL.
875pub fn push_state(state: &[u8], title: &str, url: &str) {
876    unsafe {
877        _api_push_state(
878            state.as_ptr() as u32,
879            state.len() as u32,
880            title.as_ptr() as u32,
881            title.len() as u32,
882            url.as_ptr() as u32,
883            url.len() as u32,
884        )
885    }
886}
887
888/// Replace the current history entry (no new entry is pushed).
889/// Analogous to `history.replaceState()`.
890pub fn replace_state(state: &[u8], title: &str, url: &str) {
891    unsafe {
892        _api_replace_state(
893            state.as_ptr() as u32,
894            state.len() as u32,
895            title.as_ptr() as u32,
896            title.len() as u32,
897            url.as_ptr() as u32,
898            url.len() as u32,
899        )
900    }
901}
902
903/// Get the URL of the currently loaded page.
904pub fn get_url() -> String {
905    let mut buf = [0u8; 4096];
906    let len = unsafe { _api_get_url(buf.as_mut_ptr() as u32, buf.len() as u32) };
907    String::from_utf8_lossy(&buf[..len as usize]).to_string()
908}
909
910/// Retrieve the opaque state bytes attached to the current history entry.
911/// Returns `None` if no state has been set.
912pub fn get_state() -> Option<Vec<u8>> {
913    let mut buf = vec![0u8; 64 * 1024]; // 64 KB
914    let rc = unsafe { _api_get_state(buf.as_mut_ptr() as u32, buf.len() as u32) };
915    if rc < 0 {
916        return None;
917    }
918    Some(buf[..rc as usize].to_vec())
919}
920
921/// Return the total number of entries in the history stack.
922pub fn history_length() -> u32 {
923    unsafe { _api_history_length() }
924}
925
926/// Navigate backward in history.  Returns `true` if a navigation was queued.
927pub fn history_back() -> bool {
928    unsafe { _api_history_back() == 1 }
929}
930
931/// Navigate forward in history.  Returns `true` if a navigation was queued.
932pub fn history_forward() -> bool {
933    unsafe { _api_history_forward() == 1 }
934}
935
936// ─── Hyperlink API ──────────────────────────────────────────────────────────
937
938/// Register a rectangular region on the canvas as a clickable hyperlink.
939///
940/// When the user clicks inside the rectangle the browser navigates to `url`.
941/// Coordinates are in the same canvas-local space used by the drawing APIs.
942/// Returns 0 on success.
943pub fn register_hyperlink(x: f32, y: f32, w: f32, h: f32, url: &str) -> i32 {
944    unsafe { _api_register_hyperlink(x, y, w, h, url.as_ptr() as u32, url.len() as u32) }
945}
946
947/// Remove all previously registered hyperlinks.
948pub fn clear_hyperlinks() {
949    unsafe { _api_clear_hyperlinks() }
950}
951
952// ─── URL Utility API ────────────────────────────────────────────────────────
953
954/// Resolve a relative URL against a base URL (WHATWG algorithm).
955/// Returns `None` if either URL is invalid.
956pub fn url_resolve(base: &str, relative: &str) -> Option<String> {
957    let mut buf = [0u8; 4096];
958    let rc = unsafe {
959        _api_url_resolve(
960            base.as_ptr() as u32,
961            base.len() as u32,
962            relative.as_ptr() as u32,
963            relative.len() as u32,
964            buf.as_mut_ptr() as u32,
965            buf.len() as u32,
966        )
967    };
968    if rc < 0 {
969        return None;
970    }
971    Some(String::from_utf8_lossy(&buf[..rc as usize]).to_string())
972}
973
974/// Percent-encode a string for safe inclusion in URL components.
975pub fn url_encode(input: &str) -> String {
976    let mut buf = vec![0u8; input.len() * 3 + 4];
977    let len = unsafe {
978        _api_url_encode(
979            input.as_ptr() as u32,
980            input.len() as u32,
981            buf.as_mut_ptr() as u32,
982            buf.len() as u32,
983        )
984    };
985    String::from_utf8_lossy(&buf[..len as usize]).to_string()
986}
987
988/// Decode a percent-encoded string.
989pub fn url_decode(input: &str) -> String {
990    let mut buf = vec![0u8; input.len() + 4];
991    let len = unsafe {
992        _api_url_decode(
993            input.as_ptr() as u32,
994            input.len() as u32,
995            buf.as_mut_ptr() as u32,
996            buf.len() as u32,
997        )
998    };
999    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1000}
1001
1002// ─── Input Polling API ──────────────────────────────────────────────────────
1003
1004/// Get the mouse position in canvas-local coordinates.
1005pub fn mouse_position() -> (f32, f32) {
1006    let packed = unsafe { _api_mouse_position() };
1007    let x = f32::from_bits((packed >> 32) as u32);
1008    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
1009    (x, y)
1010}
1011
1012/// Returns `true` if the given mouse button is currently held down.
1013/// Button 0 = primary (left), 1 = secondary (right), 2 = middle.
1014pub fn mouse_button_down(button: u32) -> bool {
1015    unsafe { _api_mouse_button_down(button) != 0 }
1016}
1017
1018/// Returns `true` if the given mouse button was clicked this frame.
1019pub fn mouse_button_clicked(button: u32) -> bool {
1020    unsafe { _api_mouse_button_clicked(button) != 0 }
1021}
1022
1023/// Returns `true` if the given key is currently held down.
1024/// See `KEY_*` constants for key codes.
1025pub fn key_down(key: u32) -> bool {
1026    unsafe { _api_key_down(key) != 0 }
1027}
1028
1029/// Returns `true` if the given key was pressed this frame.
1030pub fn key_pressed(key: u32) -> bool {
1031    unsafe { _api_key_pressed(key) != 0 }
1032}
1033
1034/// Get the scroll wheel delta for this frame.
1035pub fn scroll_delta() -> (f32, f32) {
1036    let packed = unsafe { _api_scroll_delta() };
1037    let x = f32::from_bits((packed >> 32) as u32);
1038    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
1039    (x, y)
1040}
1041
1042/// Returns modifier key state as a bitmask: bit 0 = Shift, bit 1 = Ctrl, bit 2 = Alt.
1043pub fn modifiers() -> u32 {
1044    unsafe { _api_modifiers() }
1045}
1046
1047/// Returns `true` if Shift is held.
1048pub fn shift_held() -> bool {
1049    modifiers() & 1 != 0
1050}
1051
1052/// Returns `true` if Ctrl (or Cmd on macOS) is held.
1053pub fn ctrl_held() -> bool {
1054    modifiers() & 2 != 0
1055}
1056
1057/// Returns `true` if Alt is held.
1058pub fn alt_held() -> bool {
1059    modifiers() & 4 != 0
1060}
1061
1062// ─── Key Constants ──────────────────────────────────────────────────────────
1063
1064pub const KEY_A: u32 = 0;
1065pub const KEY_B: u32 = 1;
1066pub const KEY_C: u32 = 2;
1067pub const KEY_D: u32 = 3;
1068pub const KEY_E: u32 = 4;
1069pub const KEY_F: u32 = 5;
1070pub const KEY_G: u32 = 6;
1071pub const KEY_H: u32 = 7;
1072pub const KEY_I: u32 = 8;
1073pub const KEY_J: u32 = 9;
1074pub const KEY_K: u32 = 10;
1075pub const KEY_L: u32 = 11;
1076pub const KEY_M: u32 = 12;
1077pub const KEY_N: u32 = 13;
1078pub const KEY_O: u32 = 14;
1079pub const KEY_P: u32 = 15;
1080pub const KEY_Q: u32 = 16;
1081pub const KEY_R: u32 = 17;
1082pub const KEY_S: u32 = 18;
1083pub const KEY_T: u32 = 19;
1084pub const KEY_U: u32 = 20;
1085pub const KEY_V: u32 = 21;
1086pub const KEY_W: u32 = 22;
1087pub const KEY_X: u32 = 23;
1088pub const KEY_Y: u32 = 24;
1089pub const KEY_Z: u32 = 25;
1090pub const KEY_0: u32 = 26;
1091pub const KEY_1: u32 = 27;
1092pub const KEY_2: u32 = 28;
1093pub const KEY_3: u32 = 29;
1094pub const KEY_4: u32 = 30;
1095pub const KEY_5: u32 = 31;
1096pub const KEY_6: u32 = 32;
1097pub const KEY_7: u32 = 33;
1098pub const KEY_8: u32 = 34;
1099pub const KEY_9: u32 = 35;
1100pub const KEY_ENTER: u32 = 36;
1101pub const KEY_ESCAPE: u32 = 37;
1102pub const KEY_TAB: u32 = 38;
1103pub const KEY_BACKSPACE: u32 = 39;
1104pub const KEY_DELETE: u32 = 40;
1105pub const KEY_SPACE: u32 = 41;
1106pub const KEY_UP: u32 = 42;
1107pub const KEY_DOWN: u32 = 43;
1108pub const KEY_LEFT: u32 = 44;
1109pub const KEY_RIGHT: u32 = 45;
1110pub const KEY_HOME: u32 = 46;
1111pub const KEY_END: u32 = 47;
1112pub const KEY_PAGE_UP: u32 = 48;
1113pub const KEY_PAGE_DOWN: u32 = 49;
1114
1115// ─── Interactive Widget API ─────────────────────────────────────────────────
1116
1117/// Render a button at the given position. Returns `true` if it was clicked
1118/// on the previous frame.
1119///
1120/// Must be called from `on_frame()` — widgets are only rendered for
1121/// interactive applications that export a frame loop.
1122pub fn ui_button(id: u32, x: f32, y: f32, w: f32, h: f32, label: &str) -> bool {
1123    unsafe { _api_ui_button(id, x, y, w, h, label.as_ptr() as u32, label.len() as u32) != 0 }
1124}
1125
1126/// Render a checkbox. Returns the current checked state.
1127///
1128/// `initial` sets the value the first time this ID is seen.
1129pub fn ui_checkbox(id: u32, x: f32, y: f32, label: &str, initial: bool) -> bool {
1130    unsafe {
1131        _api_ui_checkbox(
1132            id,
1133            x,
1134            y,
1135            label.as_ptr() as u32,
1136            label.len() as u32,
1137            if initial { 1 } else { 0 },
1138        ) != 0
1139    }
1140}
1141
1142/// Render a slider. Returns the current value.
1143///
1144/// `initial` sets the value the first time this ID is seen.
1145pub fn ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32 {
1146    unsafe { _api_ui_slider(id, x, y, w, min, max, initial) }
1147}
1148
1149/// Render a single-line text input. Returns the current text content.
1150///
1151/// `initial` sets the text the first time this ID is seen.
1152pub fn ui_text_input(id: u32, x: f32, y: f32, w: f32, initial: &str) -> String {
1153    let mut buf = [0u8; 4096];
1154    let len = unsafe {
1155        _api_ui_text_input(
1156            id,
1157            x,
1158            y,
1159            w,
1160            initial.as_ptr() as u32,
1161            initial.len() as u32,
1162            buf.as_mut_ptr() as u32,
1163            buf.len() as u32,
1164        )
1165    };
1166    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1167}