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_detect_format`], [`audio_play_with_format`], [`audio_last_url_content_type`], [`audio_pause`], [`audio_channel_play`] |
69//! | **Video** | [`video_load`], [`video_load_url`], [`video_render`], [`video_play`], [`video_hls_open_variant`], [`subtitle_load_srt`] |
70//! | **Media capture** | [`camera_open`], [`camera_capture_frame`], [`microphone_open`], [`microphone_read_samples`], [`screen_capture`], [`media_pipeline_stats`] |
71//! | **Timers** | [`set_timeout`], [`set_interval`], [`clear_timer`], [`time_now_ms`] |
72//! | **Navigation** | [`navigate`], [`push_state`], [`replace_state`], [`get_url`], [`history_back`], [`history_forward`] |
73//! | **Input** | [`mouse_position`], [`mouse_button_down`], [`key_down`], [`key_pressed`], [`scroll_delta`] |
74//! | **Widgets** | [`ui_button`], [`ui_checkbox`], [`ui_slider`], [`ui_text_input`] |
75//! | **Crypto** | [`hash_sha256`], [`hash_sha256_hex`], [`base64_encode`], [`base64_decode`] |
76//! | **Other** | [`clipboard_write`], [`clipboard_read`], [`random_u64`], [`random_f64`], [`notify`], [`upload_file`], [`load_module`] |
77//!
78//! ## Full API Documentation
79//!
80//! See <https://docs.oxide.foundation/oxide_sdk/> for the complete API
81//! reference, or browse the individual function documentation below.
82
83pub mod proto;
84
85// ─── Raw FFI imports from the host ──────────────────────────────────────────
86
87#[link(wasm_import_module = "oxide")]
88extern "C" {
89    #[link_name = "api_log"]
90    fn _api_log(ptr: u32, len: u32);
91
92    #[link_name = "api_warn"]
93    fn _api_warn(ptr: u32, len: u32);
94
95    #[link_name = "api_error"]
96    fn _api_error(ptr: u32, len: u32);
97
98    #[link_name = "api_get_location"]
99    fn _api_get_location(out_ptr: u32, out_cap: u32) -> u32;
100
101    #[link_name = "api_upload_file"]
102    fn _api_upload_file(name_ptr: u32, name_cap: u32, data_ptr: u32, data_cap: u32) -> u64;
103
104    #[link_name = "api_canvas_clear"]
105    fn _api_canvas_clear(r: u32, g: u32, b: u32, a: u32);
106
107    #[link_name = "api_canvas_rect"]
108    fn _api_canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u32, g: u32, b: u32, a: u32);
109
110    #[link_name = "api_canvas_circle"]
111    fn _api_canvas_circle(cx: f32, cy: f32, radius: f32, r: u32, g: u32, b: u32, a: u32);
112
113    #[link_name = "api_canvas_text"]
114    fn _api_canvas_text(x: f32, y: f32, size: f32, r: u32, g: u32, b: u32, ptr: u32, len: u32);
115
116    #[link_name = "api_canvas_line"]
117    fn _api_canvas_line(x1: f32, y1: f32, x2: f32, y2: f32, r: u32, g: u32, b: u32, thickness: f32);
118
119    #[link_name = "api_canvas_dimensions"]
120    fn _api_canvas_dimensions() -> u64;
121
122    #[link_name = "api_canvas_image"]
123    fn _api_canvas_image(x: f32, y: f32, w: f32, h: f32, data_ptr: u32, data_len: u32);
124
125    #[link_name = "api_storage_set"]
126    fn _api_storage_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32);
127
128    #[link_name = "api_storage_get"]
129    fn _api_storage_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> u32;
130
131    #[link_name = "api_storage_remove"]
132    fn _api_storage_remove(key_ptr: u32, key_len: u32);
133
134    #[link_name = "api_clipboard_write"]
135    fn _api_clipboard_write(ptr: u32, len: u32);
136
137    #[link_name = "api_clipboard_read"]
138    fn _api_clipboard_read(out_ptr: u32, out_cap: u32) -> u32;
139
140    #[link_name = "api_time_now_ms"]
141    fn _api_time_now_ms() -> u64;
142
143    #[link_name = "api_set_timeout"]
144    fn _api_set_timeout(callback_id: u32, delay_ms: u32) -> u32;
145
146    #[link_name = "api_set_interval"]
147    fn _api_set_interval(callback_id: u32, interval_ms: u32) -> u32;
148
149    #[link_name = "api_clear_timer"]
150    fn _api_clear_timer(timer_id: u32);
151
152    #[link_name = "api_random"]
153    fn _api_random() -> u64;
154
155    #[link_name = "api_notify"]
156    fn _api_notify(title_ptr: u32, title_len: u32, body_ptr: u32, body_len: u32);
157
158    #[link_name = "api_fetch"]
159    fn _api_fetch(
160        method_ptr: u32,
161        method_len: u32,
162        url_ptr: u32,
163        url_len: u32,
164        ct_ptr: u32,
165        ct_len: u32,
166        body_ptr: u32,
167        body_len: u32,
168        out_ptr: u32,
169        out_cap: u32,
170    ) -> i64;
171
172    #[link_name = "api_load_module"]
173    fn _api_load_module(url_ptr: u32, url_len: u32) -> i32;
174
175    #[link_name = "api_hash_sha256"]
176    fn _api_hash_sha256(data_ptr: u32, data_len: u32, out_ptr: u32) -> u32;
177
178    #[link_name = "api_base64_encode"]
179    fn _api_base64_encode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
180
181    #[link_name = "api_base64_decode"]
182    fn _api_base64_decode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
183
184    #[link_name = "api_kv_store_set"]
185    fn _api_kv_store_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32) -> i32;
186
187    #[link_name = "api_kv_store_get"]
188    fn _api_kv_store_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> i32;
189
190    #[link_name = "api_kv_store_delete"]
191    fn _api_kv_store_delete(key_ptr: u32, key_len: u32) -> i32;
192
193    // ── Navigation ──────────────────────────────────────────────────
194
195    #[link_name = "api_navigate"]
196    fn _api_navigate(url_ptr: u32, url_len: u32) -> i32;
197
198    #[link_name = "api_push_state"]
199    fn _api_push_state(
200        state_ptr: u32,
201        state_len: u32,
202        title_ptr: u32,
203        title_len: u32,
204        url_ptr: u32,
205        url_len: u32,
206    );
207
208    #[link_name = "api_replace_state"]
209    fn _api_replace_state(
210        state_ptr: u32,
211        state_len: u32,
212        title_ptr: u32,
213        title_len: u32,
214        url_ptr: u32,
215        url_len: u32,
216    );
217
218    #[link_name = "api_get_url"]
219    fn _api_get_url(out_ptr: u32, out_cap: u32) -> u32;
220
221    #[link_name = "api_get_state"]
222    fn _api_get_state(out_ptr: u32, out_cap: u32) -> i32;
223
224    #[link_name = "api_history_length"]
225    fn _api_history_length() -> u32;
226
227    #[link_name = "api_history_back"]
228    fn _api_history_back() -> i32;
229
230    #[link_name = "api_history_forward"]
231    fn _api_history_forward() -> i32;
232
233    // ── Hyperlinks ──────────────────────────────────────────────────
234
235    #[link_name = "api_register_hyperlink"]
236    fn _api_register_hyperlink(x: f32, y: f32, w: f32, h: f32, url_ptr: u32, url_len: u32) -> i32;
237
238    #[link_name = "api_clear_hyperlinks"]
239    fn _api_clear_hyperlinks();
240
241    // ── Input Polling ────────────────────────────────────────────────
242
243    #[link_name = "api_mouse_position"]
244    fn _api_mouse_position() -> u64;
245
246    #[link_name = "api_mouse_button_down"]
247    fn _api_mouse_button_down(button: u32) -> u32;
248
249    #[link_name = "api_mouse_button_clicked"]
250    fn _api_mouse_button_clicked(button: u32) -> u32;
251
252    #[link_name = "api_key_down"]
253    fn _api_key_down(key: u32) -> u32;
254
255    #[link_name = "api_key_pressed"]
256    fn _api_key_pressed(key: u32) -> u32;
257
258    #[link_name = "api_scroll_delta"]
259    fn _api_scroll_delta() -> u64;
260
261    #[link_name = "api_modifiers"]
262    fn _api_modifiers() -> u32;
263
264    // ── Interactive Widgets ─────────────────────────────────────────
265
266    #[link_name = "api_ui_button"]
267    fn _api_ui_button(
268        id: u32,
269        x: f32,
270        y: f32,
271        w: f32,
272        h: f32,
273        label_ptr: u32,
274        label_len: u32,
275    ) -> u32;
276
277    #[link_name = "api_ui_checkbox"]
278    fn _api_ui_checkbox(
279        id: u32,
280        x: f32,
281        y: f32,
282        label_ptr: u32,
283        label_len: u32,
284        initial: u32,
285    ) -> u32;
286
287    #[link_name = "api_ui_slider"]
288    fn _api_ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32;
289
290    #[link_name = "api_ui_text_input"]
291    fn _api_ui_text_input(
292        id: u32,
293        x: f32,
294        y: f32,
295        w: f32,
296        init_ptr: u32,
297        init_len: u32,
298        out_ptr: u32,
299        out_cap: u32,
300    ) -> u32;
301
302    // ── Audio Playback ──────────────────────────────────────────────
303
304    #[link_name = "api_audio_play"]
305    fn _api_audio_play(data_ptr: u32, data_len: u32) -> i32;
306
307    #[link_name = "api_audio_play_url"]
308    fn _api_audio_play_url(url_ptr: u32, url_len: u32) -> i32;
309
310    #[link_name = "api_audio_detect_format"]
311    fn _api_audio_detect_format(data_ptr: u32, data_len: u32) -> u32;
312
313    #[link_name = "api_audio_play_with_format"]
314    fn _api_audio_play_with_format(data_ptr: u32, data_len: u32, format_hint: u32) -> i32;
315
316    #[link_name = "api_audio_last_url_content_type"]
317    fn _api_audio_last_url_content_type(out_ptr: u32, out_cap: u32) -> u32;
318
319    #[link_name = "api_audio_pause"]
320    fn _api_audio_pause();
321
322    #[link_name = "api_audio_resume"]
323    fn _api_audio_resume();
324
325    #[link_name = "api_audio_stop"]
326    fn _api_audio_stop();
327
328    #[link_name = "api_audio_set_volume"]
329    fn _api_audio_set_volume(level: f32);
330
331    #[link_name = "api_audio_get_volume"]
332    fn _api_audio_get_volume() -> f32;
333
334    #[link_name = "api_audio_is_playing"]
335    fn _api_audio_is_playing() -> u32;
336
337    #[link_name = "api_audio_position"]
338    fn _api_audio_position() -> u64;
339
340    #[link_name = "api_audio_seek"]
341    fn _api_audio_seek(position_ms: u64) -> i32;
342
343    #[link_name = "api_audio_duration"]
344    fn _api_audio_duration() -> u64;
345
346    #[link_name = "api_audio_set_loop"]
347    fn _api_audio_set_loop(enabled: u32);
348
349    #[link_name = "api_audio_channel_play"]
350    fn _api_audio_channel_play(channel: u32, data_ptr: u32, data_len: u32) -> i32;
351
352    #[link_name = "api_audio_channel_play_with_format"]
353    fn _api_audio_channel_play_with_format(
354        channel: u32,
355        data_ptr: u32,
356        data_len: u32,
357        format_hint: u32,
358    ) -> i32;
359
360    #[link_name = "api_audio_channel_stop"]
361    fn _api_audio_channel_stop(channel: u32);
362
363    #[link_name = "api_audio_channel_set_volume"]
364    fn _api_audio_channel_set_volume(channel: u32, level: f32);
365
366    // ── Video ─────────────────────────────────────────────────────────
367
368    #[link_name = "api_video_detect_format"]
369    fn _api_video_detect_format(data_ptr: u32, data_len: u32) -> u32;
370
371    #[link_name = "api_video_load"]
372    fn _api_video_load(data_ptr: u32, data_len: u32, format_hint: u32) -> i32;
373
374    #[link_name = "api_video_load_url"]
375    fn _api_video_load_url(url_ptr: u32, url_len: u32) -> i32;
376
377    #[link_name = "api_video_last_url_content_type"]
378    fn _api_video_last_url_content_type(out_ptr: u32, out_cap: u32) -> u32;
379
380    #[link_name = "api_video_hls_variant_count"]
381    fn _api_video_hls_variant_count() -> u32;
382
383    #[link_name = "api_video_hls_variant_url"]
384    fn _api_video_hls_variant_url(index: u32, out_ptr: u32, out_cap: u32) -> u32;
385
386    #[link_name = "api_video_hls_open_variant"]
387    fn _api_video_hls_open_variant(index: u32) -> i32;
388
389    #[link_name = "api_video_play"]
390    fn _api_video_play();
391
392    #[link_name = "api_video_pause"]
393    fn _api_video_pause();
394
395    #[link_name = "api_video_stop"]
396    fn _api_video_stop();
397
398    #[link_name = "api_video_seek"]
399    fn _api_video_seek(position_ms: u64) -> i32;
400
401    #[link_name = "api_video_position"]
402    fn _api_video_position() -> u64;
403
404    #[link_name = "api_video_duration"]
405    fn _api_video_duration() -> u64;
406
407    #[link_name = "api_video_render"]
408    fn _api_video_render(x: f32, y: f32, w: f32, h: f32) -> i32;
409
410    #[link_name = "api_video_set_volume"]
411    fn _api_video_set_volume(level: f32);
412
413    #[link_name = "api_video_get_volume"]
414    fn _api_video_get_volume() -> f32;
415
416    #[link_name = "api_video_set_loop"]
417    fn _api_video_set_loop(enabled: u32);
418
419    #[link_name = "api_video_set_pip"]
420    fn _api_video_set_pip(enabled: u32);
421
422    #[link_name = "api_subtitle_load_srt"]
423    fn _api_subtitle_load_srt(ptr: u32, len: u32) -> i32;
424
425    #[link_name = "api_subtitle_load_vtt"]
426    fn _api_subtitle_load_vtt(ptr: u32, len: u32) -> i32;
427
428    #[link_name = "api_subtitle_clear"]
429    fn _api_subtitle_clear();
430
431    // ── Media capture ─────────────────────────────────────────────────
432
433    #[link_name = "api_camera_open"]
434    fn _api_camera_open() -> i32;
435
436    #[link_name = "api_camera_close"]
437    fn _api_camera_close();
438
439    #[link_name = "api_camera_capture_frame"]
440    fn _api_camera_capture_frame(out_ptr: u32, out_cap: u32) -> u32;
441
442    #[link_name = "api_camera_frame_dimensions"]
443    fn _api_camera_frame_dimensions() -> u64;
444
445    #[link_name = "api_microphone_open"]
446    fn _api_microphone_open() -> i32;
447
448    #[link_name = "api_microphone_close"]
449    fn _api_microphone_close();
450
451    #[link_name = "api_microphone_sample_rate"]
452    fn _api_microphone_sample_rate() -> u32;
453
454    #[link_name = "api_microphone_read_samples"]
455    fn _api_microphone_read_samples(out_ptr: u32, max_samples: u32) -> u32;
456
457    #[link_name = "api_screen_capture"]
458    fn _api_screen_capture(out_ptr: u32, out_cap: u32) -> i32;
459
460    #[link_name = "api_screen_capture_dimensions"]
461    fn _api_screen_capture_dimensions() -> u64;
462
463    #[link_name = "api_media_pipeline_stats"]
464    fn _api_media_pipeline_stats() -> u64;
465
466    // ── URL Utilities ───────────────────────────────────────────────
467
468    #[link_name = "api_url_resolve"]
469    fn _api_url_resolve(
470        base_ptr: u32,
471        base_len: u32,
472        rel_ptr: u32,
473        rel_len: u32,
474        out_ptr: u32,
475        out_cap: u32,
476    ) -> i32;
477
478    #[link_name = "api_url_encode"]
479    fn _api_url_encode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
480
481    #[link_name = "api_url_decode"]
482    fn _api_url_decode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
483}
484
485// ─── Console API ────────────────────────────────────────────────────────────
486
487/// Print a message to the browser console (log level).
488pub fn log(msg: &str) {
489    unsafe { _api_log(msg.as_ptr() as u32, msg.len() as u32) }
490}
491
492/// Print a warning to the browser console.
493pub fn warn(msg: &str) {
494    unsafe { _api_warn(msg.as_ptr() as u32, msg.len() as u32) }
495}
496
497/// Print an error to the browser console.
498pub fn error(msg: &str) {
499    unsafe { _api_error(msg.as_ptr() as u32, msg.len() as u32) }
500}
501
502// ─── Geolocation API ────────────────────────────────────────────────────────
503
504/// Get the device's mock geolocation as a `"lat,lon"` string.
505pub fn get_location() -> String {
506    let mut buf = [0u8; 128];
507    let len = unsafe { _api_get_location(buf.as_mut_ptr() as u32, buf.len() as u32) };
508    String::from_utf8_lossy(&buf[..len as usize]).to_string()
509}
510
511// ─── File Upload API ────────────────────────────────────────────────────────
512
513/// File returned from the native file picker.
514pub struct UploadedFile {
515    pub name: String,
516    pub data: Vec<u8>,
517}
518
519/// Opens the native OS file picker and returns the selected file.
520/// Returns `None` if the user cancels.
521pub fn upload_file() -> Option<UploadedFile> {
522    let mut name_buf = [0u8; 256];
523    let mut data_buf = vec![0u8; 1024 * 1024]; // 1MB max
524
525    let result = unsafe {
526        _api_upload_file(
527            name_buf.as_mut_ptr() as u32,
528            name_buf.len() as u32,
529            data_buf.as_mut_ptr() as u32,
530            data_buf.len() as u32,
531        )
532    };
533
534    if result == 0 {
535        return None;
536    }
537
538    let name_len = (result >> 32) as usize;
539    let data_len = (result & 0xFFFF_FFFF) as usize;
540
541    Some(UploadedFile {
542        name: String::from_utf8_lossy(&name_buf[..name_len]).to_string(),
543        data: data_buf[..data_len].to_vec(),
544    })
545}
546
547// ─── Canvas API ─────────────────────────────────────────────────────────────
548
549/// Clear the canvas with a solid RGBA color.
550pub fn canvas_clear(r: u8, g: u8, b: u8, a: u8) {
551    unsafe { _api_canvas_clear(r as u32, g as u32, b as u32, a as u32) }
552}
553
554/// Draw a filled rectangle.
555pub fn canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u8, g: u8, b: u8, a: u8) {
556    unsafe { _api_canvas_rect(x, y, w, h, r as u32, g as u32, b as u32, a as u32) }
557}
558
559/// Draw a filled circle.
560pub fn canvas_circle(cx: f32, cy: f32, radius: f32, r: u8, g: u8, b: u8, a: u8) {
561    unsafe { _api_canvas_circle(cx, cy, radius, r as u32, g as u32, b as u32, a as u32) }
562}
563
564/// Draw text on the canvas.
565pub fn canvas_text(x: f32, y: f32, size: f32, r: u8, g: u8, b: u8, text: &str) {
566    unsafe {
567        _api_canvas_text(
568            x,
569            y,
570            size,
571            r as u32,
572            g as u32,
573            b as u32,
574            text.as_ptr() as u32,
575            text.len() as u32,
576        )
577    }
578}
579
580/// Draw a line between two points.
581pub fn canvas_line(x1: f32, y1: f32, x2: f32, y2: f32, r: u8, g: u8, b: u8, thickness: f32) {
582    unsafe { _api_canvas_line(x1, y1, x2, y2, r as u32, g as u32, b as u32, thickness) }
583}
584
585/// Returns `(width, height)` of the canvas in pixels.
586pub fn canvas_dimensions() -> (u32, u32) {
587    let packed = unsafe { _api_canvas_dimensions() };
588    ((packed >> 32) as u32, (packed & 0xFFFF_FFFF) as u32)
589}
590
591/// Draw an image on the canvas from encoded image bytes (PNG, JPEG, GIF, WebP).
592/// The browser decodes the image and renders it at the given rectangle.
593pub fn canvas_image(x: f32, y: f32, w: f32, h: f32, data: &[u8]) {
594    unsafe { _api_canvas_image(x, y, w, h, data.as_ptr() as u32, data.len() as u32) }
595}
596
597// ─── Local Storage API ──────────────────────────────────────────────────────
598
599/// Store a key-value pair in sandboxed local storage.
600pub fn storage_set(key: &str, value: &str) {
601    unsafe {
602        _api_storage_set(
603            key.as_ptr() as u32,
604            key.len() as u32,
605            value.as_ptr() as u32,
606            value.len() as u32,
607        )
608    }
609}
610
611/// Retrieve a value from local storage. Returns empty string if not found.
612pub fn storage_get(key: &str) -> String {
613    let mut buf = [0u8; 4096];
614    let len = unsafe {
615        _api_storage_get(
616            key.as_ptr() as u32,
617            key.len() as u32,
618            buf.as_mut_ptr() as u32,
619            buf.len() as u32,
620        )
621    };
622    String::from_utf8_lossy(&buf[..len as usize]).to_string()
623}
624
625/// Remove a key from local storage.
626pub fn storage_remove(key: &str) {
627    unsafe { _api_storage_remove(key.as_ptr() as u32, key.len() as u32) }
628}
629
630// ─── Clipboard API ──────────────────────────────────────────────────────────
631
632/// Copy text to the system clipboard.
633pub fn clipboard_write(text: &str) {
634    unsafe { _api_clipboard_write(text.as_ptr() as u32, text.len() as u32) }
635}
636
637/// Read text from the system clipboard.
638pub fn clipboard_read() -> String {
639    let mut buf = [0u8; 4096];
640    let len = unsafe { _api_clipboard_read(buf.as_mut_ptr() as u32, buf.len() as u32) };
641    String::from_utf8_lossy(&buf[..len as usize]).to_string()
642}
643
644// ─── Timer / Clock API ─────────────────────────────────────────────────────
645
646/// Get the current time in milliseconds since the UNIX epoch.
647pub fn time_now_ms() -> u64 {
648    unsafe { _api_time_now_ms() }
649}
650
651/// Schedule a one-shot timer that fires after `delay_ms` milliseconds.
652/// When it fires the host calls your exported `on_timer(callback_id)`.
653/// Returns a timer ID that can be passed to [`clear_timer`].
654pub fn set_timeout(callback_id: u32, delay_ms: u32) -> u32 {
655    unsafe { _api_set_timeout(callback_id, delay_ms) }
656}
657
658/// Schedule a repeating timer that fires every `interval_ms` milliseconds.
659/// When it fires the host calls your exported `on_timer(callback_id)`.
660/// Returns a timer ID that can be passed to [`clear_timer`].
661pub fn set_interval(callback_id: u32, interval_ms: u32) -> u32 {
662    unsafe { _api_set_interval(callback_id, interval_ms) }
663}
664
665/// Cancel a timer previously created with [`set_timeout`] or [`set_interval`].
666pub fn clear_timer(timer_id: u32) {
667    unsafe { _api_clear_timer(timer_id) }
668}
669
670// ─── Random API ─────────────────────────────────────────────────────────────
671
672/// Get a random u64 from the host.
673pub fn random_u64() -> u64 {
674    unsafe { _api_random() }
675}
676
677/// Get a random f64 in [0, 1).
678pub fn random_f64() -> f64 {
679    (random_u64() >> 11) as f64 / (1u64 << 53) as f64
680}
681
682// ─── Notification API ───────────────────────────────────────────────────────
683
684/// Send a notification to the user (rendered in the browser console).
685pub fn notify(title: &str, body: &str) {
686    unsafe {
687        _api_notify(
688            title.as_ptr() as u32,
689            title.len() as u32,
690            body.as_ptr() as u32,
691            body.len() as u32,
692        )
693    }
694}
695
696// ─── Audio Playback API ─────────────────────────────────────────────────────
697
698/// Detected or hinted audio container (host codes: 0 unknown, 1 WAV, 2 MP3, 3 Ogg, 4 FLAC).
699#[repr(u32)]
700#[derive(Clone, Copy, Debug, PartialEq, Eq)]
701pub enum AudioFormat {
702    /// Could not classify from bytes (try decode anyway).
703    Unknown = 0,
704    Wav = 1,
705    Mp3 = 2,
706    Ogg = 3,
707    Flac = 4,
708}
709
710impl From<u32> for AudioFormat {
711    fn from(code: u32) -> Self {
712        match code {
713            1 => AudioFormat::Wav,
714            2 => AudioFormat::Mp3,
715            3 => AudioFormat::Ogg,
716            4 => AudioFormat::Flac,
717            _ => AudioFormat::Unknown,
718        }
719    }
720}
721
722impl From<AudioFormat> for u32 {
723    fn from(f: AudioFormat) -> u32 {
724        f as u32
725    }
726}
727
728/// Play audio from encoded bytes (WAV, MP3, OGG, FLAC).
729/// The host decodes and plays the audio. Returns 0 on success, negative on error.
730pub fn audio_play(data: &[u8]) -> i32 {
731    unsafe { _api_audio_play(data.as_ptr() as u32, data.len() as u32) }
732}
733
734/// Sniff the container/codec from raw bytes (magic bytes / MP3 sync). Does not decode audio.
735pub fn audio_detect_format(data: &[u8]) -> AudioFormat {
736    let code = unsafe { _api_audio_detect_format(data.as_ptr() as u32, data.len() as u32) };
737    AudioFormat::from(code)
738}
739
740/// Play with an optional format hint (`AudioFormat::Unknown` = same as [`audio_play`]).
741/// If the hint disagrees with what the host sniffs from the bytes, the host logs a warning but still decodes.
742pub fn audio_play_with_format(data: &[u8], format: AudioFormat) -> i32 {
743    unsafe {
744        _api_audio_play_with_format(data.as_ptr() as u32, data.len() as u32, u32::from(format))
745    }
746}
747
748/// Fetch audio from a URL and play it.
749/// The host sends an `Accept` header listing supported codecs, records the response `Content-Type`,
750/// and rejects obvious HTML/JSON error bodies when no audio signature is found (`-4`).
751/// Returns 0 on success, negative on error.
752pub fn audio_play_url(url: &str) -> i32 {
753    unsafe { _api_audio_play_url(url.as_ptr() as u32, url.len() as u32) }
754}
755
756/// `Content-Type` header from the last successful [`audio_play_url`] response (may be empty).
757pub fn audio_last_url_content_type() -> String {
758    let mut buf = [0u8; 512];
759    let len =
760        unsafe { _api_audio_last_url_content_type(buf.as_mut_ptr() as u32, buf.len() as u32) };
761    let n = (len as usize).min(buf.len());
762    String::from_utf8_lossy(&buf[..n]).to_string()
763}
764
765/// Pause audio playback.
766pub fn audio_pause() {
767    unsafe { _api_audio_pause() }
768}
769
770/// Resume paused audio playback.
771pub fn audio_resume() {
772    unsafe { _api_audio_resume() }
773}
774
775/// Stop audio playback and clear the queue.
776pub fn audio_stop() {
777    unsafe { _api_audio_stop() }
778}
779
780/// Set audio volume. 1.0 is normal, 0.0 is silent, up to 2.0 for boost.
781pub fn audio_set_volume(level: f32) {
782    unsafe { _api_audio_set_volume(level) }
783}
784
785/// Get the current audio volume.
786pub fn audio_get_volume() -> f32 {
787    unsafe { _api_audio_get_volume() }
788}
789
790/// Returns `true` if audio is currently playing (not paused and not empty).
791pub fn audio_is_playing() -> bool {
792    unsafe { _api_audio_is_playing() != 0 }
793}
794
795/// Get the current playback position in milliseconds.
796pub fn audio_position() -> u64 {
797    unsafe { _api_audio_position() }
798}
799
800/// Seek to a position in milliseconds. Returns 0 on success, negative on error.
801pub fn audio_seek(position_ms: u64) -> i32 {
802    unsafe { _api_audio_seek(position_ms) }
803}
804
805/// Get the total duration of the currently loaded track in milliseconds.
806/// Returns 0 if unknown or nothing is loaded.
807pub fn audio_duration() -> u64 {
808    unsafe { _api_audio_duration() }
809}
810
811/// Enable or disable looping on the default channel.
812/// When enabled, subsequent `audio_play` calls will loop indefinitely.
813pub fn audio_set_loop(enabled: bool) {
814    unsafe { _api_audio_set_loop(if enabled { 1 } else { 0 }) }
815}
816
817// ─── Multi-Channel Audio API ────────────────────────────────────────────────
818
819/// Play audio on a specific channel. Multiple channels play simultaneously.
820/// Channel 0 is the default used by `audio_play`. Use channels 1+ for layered
821/// sound effects, background music, etc.
822pub fn audio_channel_play(channel: u32, data: &[u8]) -> i32 {
823    unsafe { _api_audio_channel_play(channel, data.as_ptr() as u32, data.len() as u32) }
824}
825
826/// Like [`audio_channel_play`] with an optional [`AudioFormat`] hint.
827pub fn audio_channel_play_with_format(channel: u32, data: &[u8], format: AudioFormat) -> i32 {
828    unsafe {
829        _api_audio_channel_play_with_format(
830            channel,
831            data.as_ptr() as u32,
832            data.len() as u32,
833            u32::from(format),
834        )
835    }
836}
837
838/// Stop playback on a specific channel.
839pub fn audio_channel_stop(channel: u32) {
840    unsafe { _api_audio_channel_stop(channel) }
841}
842
843/// Set volume for a specific channel (0.0 silent, 1.0 normal, up to 2.0 boost).
844pub fn audio_channel_set_volume(channel: u32, level: f32) {
845    unsafe { _api_audio_channel_set_volume(channel, level) }
846}
847
848// ─── Video API ─────────────────────────────────────────────────────────────
849
850/// Container or hint for [`video_load_with_format`] (host codes: 0 unknown, 1 MP4, 2 WebM, 3 AV1).
851#[repr(u32)]
852#[derive(Clone, Copy, Debug, PartialEq, Eq)]
853pub enum VideoFormat {
854    Unknown = 0,
855    Mp4 = 1,
856    Webm = 2,
857    Av1 = 3,
858}
859
860impl From<u32> for VideoFormat {
861    fn from(code: u32) -> Self {
862        match code {
863            1 => VideoFormat::Mp4,
864            2 => VideoFormat::Webm,
865            3 => VideoFormat::Av1,
866            _ => VideoFormat::Unknown,
867        }
868    }
869}
870
871impl From<VideoFormat> for u32 {
872    fn from(f: VideoFormat) -> u32 {
873        f as u32
874    }
875}
876
877/// Sniff container from leading bytes (magic only; does not decode).
878pub fn video_detect_format(data: &[u8]) -> VideoFormat {
879    let code = unsafe { _api_video_detect_format(data.as_ptr() as u32, data.len() as u32) };
880    VideoFormat::from(code)
881}
882
883/// Load video from encoded bytes (MP4, WebM, etc.). Requires FFmpeg on the host.
884/// Returns 0 on success, negative on error.
885pub fn video_load(data: &[u8]) -> i32 {
886    unsafe {
887        _api_video_load(
888            data.as_ptr() as u32,
889            data.len() as u32,
890            VideoFormat::Unknown as u32,
891        )
892    }
893}
894
895/// Load with a [`VideoFormat`] hint (unknown = same as [`video_load`]).
896pub fn video_load_with_format(data: &[u8], format: VideoFormat) -> i32 {
897    unsafe { _api_video_load(data.as_ptr() as u32, data.len() as u32, u32::from(format)) }
898}
899
900/// Open a progressive or adaptive (HLS) URL. The host uses FFmpeg; master playlists may list variants.
901pub fn video_load_url(url: &str) -> i32 {
902    unsafe { _api_video_load_url(url.as_ptr() as u32, url.len() as u32) }
903}
904
905/// `Content-Type` from the last successful [`video_load_url`] (may be empty).
906pub fn video_last_url_content_type() -> String {
907    let mut buf = [0u8; 512];
908    let len =
909        unsafe { _api_video_last_url_content_type(buf.as_mut_ptr() as u32, buf.len() as u32) };
910    let n = (len as usize).min(buf.len());
911    String::from_utf8_lossy(&buf[..n]).to_string()
912}
913
914/// Number of variant stream URIs parsed from the last HLS master playlist (0 if not a master).
915pub fn video_hls_variant_count() -> u32 {
916    unsafe { _api_video_hls_variant_count() }
917}
918
919/// Resolved variant URL for `index`, written into `buf`-style API (use fixed buffer).
920pub fn video_hls_variant_url(index: u32) -> String {
921    let mut buf = [0u8; 2048];
922    let len =
923        unsafe { _api_video_hls_variant_url(index, buf.as_mut_ptr() as u32, buf.len() as u32) };
924    let n = (len as usize).min(buf.len());
925    String::from_utf8_lossy(&buf[..n]).to_string()
926}
927
928/// Open a variant playlist by index (after loading a master with [`video_load_url`]).
929pub fn video_hls_open_variant(index: u32) -> i32 {
930    unsafe { _api_video_hls_open_variant(index) }
931}
932
933pub fn video_play() {
934    unsafe { _api_video_play() }
935}
936
937pub fn video_pause() {
938    unsafe { _api_video_pause() }
939}
940
941pub fn video_stop() {
942    unsafe { _api_video_stop() }
943}
944
945pub fn video_seek(position_ms: u64) -> i32 {
946    unsafe { _api_video_seek(position_ms) }
947}
948
949pub fn video_position() -> u64 {
950    unsafe { _api_video_position() }
951}
952
953pub fn video_duration() -> u64 {
954    unsafe { _api_video_duration() }
955}
956
957/// Draw the current video frame into the given rectangle (same coordinate space as canvas).
958pub fn video_render(x: f32, y: f32, w: f32, h: f32) -> i32 {
959    unsafe { _api_video_render(x, y, w, h) }
960}
961
962/// Volume multiplier for the video track (0.0–2.0; embedded audio mixing may follow in future hosts).
963pub fn video_set_volume(level: f32) {
964    unsafe { _api_video_set_volume(level) }
965}
966
967pub fn video_get_volume() -> f32 {
968    unsafe { _api_video_get_volume() }
969}
970
971pub fn video_set_loop(enabled: bool) {
972    unsafe { _api_video_set_loop(if enabled { 1 } else { 0 }) }
973}
974
975/// Floating picture-in-picture preview (host mirrors the last rendered frame).
976pub fn video_set_pip(enabled: bool) {
977    unsafe { _api_video_set_pip(if enabled { 1 } else { 0 }) }
978}
979
980/// Load SubRip subtitles (cues rendered on [`video_render`]).
981pub fn subtitle_load_srt(text: &str) -> i32 {
982    unsafe { _api_subtitle_load_srt(text.as_ptr() as u32, text.len() as u32) }
983}
984
985/// Load WebVTT subtitles.
986pub fn subtitle_load_vtt(text: &str) -> i32 {
987    unsafe { _api_subtitle_load_vtt(text.as_ptr() as u32, text.len() as u32) }
988}
989
990pub fn subtitle_clear() {
991    unsafe { _api_subtitle_clear() }
992}
993
994// ─── Media capture API ─────────────────────────────────────────────────────
995
996/// Opens the default camera after a host permission dialog.
997///
998/// Returns `0` on success. Negative codes: `-1` user denied, `-2` no camera, `-3` open failed.
999pub fn camera_open() -> i32 {
1000    unsafe { _api_camera_open() }
1001}
1002
1003/// Stops the camera stream opened by [`camera_open`].
1004pub fn camera_close() {
1005    unsafe { _api_camera_close() }
1006}
1007
1008/// Captures one RGBA8 frame into `out`. Returns the number of bytes written (`0` if the camera
1009/// is not open or capture failed). Query [`camera_frame_dimensions`] after a successful write.
1010pub fn camera_capture_frame(out: &mut [u8]) -> u32 {
1011    unsafe { _api_camera_capture_frame(out.as_mut_ptr() as u32, out.len() as u32) }
1012}
1013
1014/// Width and height in pixels of the last [`camera_capture_frame`] buffer.
1015pub fn camera_frame_dimensions() -> (u32, u32) {
1016    let packed = unsafe { _api_camera_frame_dimensions() };
1017    let w = (packed >> 32) as u32;
1018    let h = packed as u32;
1019    (w, h)
1020}
1021
1022/// Starts microphone capture (mono `f32` ring buffer) after a host permission dialog.
1023///
1024/// Returns `0` on success. Negative codes: `-1` denied, `-2` no input device, `-3` stream error.
1025pub fn microphone_open() -> i32 {
1026    unsafe { _api_microphone_open() }
1027}
1028
1029pub fn microphone_close() {
1030    unsafe { _api_microphone_close() }
1031}
1032
1033/// Sample rate of the opened input stream in Hz (`0` if the microphone is not open).
1034pub fn microphone_sample_rate() -> u32 {
1035    unsafe { _api_microphone_sample_rate() }
1036}
1037
1038/// Dequeues up to `out.len()` mono `f32` samples from the microphone ring buffer.
1039/// Returns how many samples were written.
1040pub fn microphone_read_samples(out: &mut [f32]) -> u32 {
1041    unsafe { _api_microphone_read_samples(out.as_mut_ptr() as u32, out.len() as u32) }
1042}
1043
1044/// Captures the primary display as RGBA8 after permission dialogs (OS may prompt separately).
1045///
1046/// Returns `Ok(bytes_written)` or an error code: `-1` denied, `-2` no display, `-3` capture failed, `-4` buffer error.
1047pub fn screen_capture(out: &mut [u8]) -> Result<usize, i32> {
1048    let n = unsafe { _api_screen_capture(out.as_mut_ptr() as u32, out.len() as u32) };
1049    if n >= 0 {
1050        Ok(n as usize)
1051    } else {
1052        Err(n)
1053    }
1054}
1055
1056/// Width and height of the last [`screen_capture`] image.
1057pub fn screen_capture_dimensions() -> (u32, u32) {
1058    let packed = unsafe { _api_screen_capture_dimensions() };
1059    let w = (packed >> 32) as u32;
1060    let h = packed as u32;
1061    (w, h)
1062}
1063
1064/// Host-side pipeline counters: total camera frames captured (high 32 bits) and current microphone
1065/// ring depth in samples (low 32 bits).
1066pub fn media_pipeline_stats() -> (u64, u32) {
1067    let packed = unsafe { _api_media_pipeline_stats() };
1068    let camera_frames = packed >> 32;
1069    let mic_ring = packed as u32;
1070    (camera_frames, mic_ring)
1071}
1072
1073// ─── HTTP Fetch API ─────────────────────────────────────────────────────────
1074
1075/// Response from an HTTP fetch call.
1076pub struct FetchResponse {
1077    pub status: u32,
1078    pub body: Vec<u8>,
1079}
1080
1081impl FetchResponse {
1082    /// Interpret the response body as UTF-8 text.
1083    pub fn text(&self) -> String {
1084        String::from_utf8_lossy(&self.body).to_string()
1085    }
1086}
1087
1088/// Perform an HTTP request.  Returns the status code and response body.
1089///
1090/// `content_type` sets the `Content-Type` header (pass `""` to omit).
1091/// Protobuf is the native format — use `"application/protobuf"` for binary
1092/// payloads.
1093pub fn fetch(
1094    method: &str,
1095    url: &str,
1096    content_type: &str,
1097    body: &[u8],
1098) -> Result<FetchResponse, i64> {
1099    let mut out_buf = vec![0u8; 4 * 1024 * 1024]; // 4 MB response buffer
1100    let result = unsafe {
1101        _api_fetch(
1102            method.as_ptr() as u32,
1103            method.len() as u32,
1104            url.as_ptr() as u32,
1105            url.len() as u32,
1106            content_type.as_ptr() as u32,
1107            content_type.len() as u32,
1108            body.as_ptr() as u32,
1109            body.len() as u32,
1110            out_buf.as_mut_ptr() as u32,
1111            out_buf.len() as u32,
1112        )
1113    };
1114    if result < 0 {
1115        return Err(result);
1116    }
1117    let status = (result >> 32) as u32;
1118    let body_len = (result & 0xFFFF_FFFF) as usize;
1119    Ok(FetchResponse {
1120        status,
1121        body: out_buf[..body_len].to_vec(),
1122    })
1123}
1124
1125/// HTTP GET request.
1126pub fn fetch_get(url: &str) -> Result<FetchResponse, i64> {
1127    fetch("GET", url, "", &[])
1128}
1129
1130/// HTTP POST with raw bytes.
1131pub fn fetch_post(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
1132    fetch("POST", url, content_type, body)
1133}
1134
1135/// HTTP POST with protobuf body (sets `Content-Type: application/protobuf`).
1136pub fn fetch_post_proto(url: &str, msg: &proto::ProtoEncoder) -> Result<FetchResponse, i64> {
1137    fetch("POST", url, "application/protobuf", msg.as_bytes())
1138}
1139
1140/// HTTP PUT with raw bytes.
1141pub fn fetch_put(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
1142    fetch("PUT", url, content_type, body)
1143}
1144
1145/// HTTP DELETE.
1146pub fn fetch_delete(url: &str) -> Result<FetchResponse, i64> {
1147    fetch("DELETE", url, "", &[])
1148}
1149
1150// ─── Dynamic Module Loading ─────────────────────────────────────────────────
1151
1152/// Fetch and execute another `.wasm` module from a URL.
1153/// The loaded module shares the same canvas, console, and storage context.
1154/// Returns 0 on success, negative error code on failure.
1155pub fn load_module(url: &str) -> i32 {
1156    unsafe { _api_load_module(url.as_ptr() as u32, url.len() as u32) }
1157}
1158
1159// ─── Crypto / Hash API ─────────────────────────────────────────────────────
1160
1161/// Compute the SHA-256 hash of the given data. Returns 32 bytes.
1162pub fn hash_sha256(data: &[u8]) -> [u8; 32] {
1163    let mut out = [0u8; 32];
1164    unsafe {
1165        _api_hash_sha256(
1166            data.as_ptr() as u32,
1167            data.len() as u32,
1168            out.as_mut_ptr() as u32,
1169        );
1170    }
1171    out
1172}
1173
1174/// Return SHA-256 hash as a lowercase hex string.
1175pub fn hash_sha256_hex(data: &[u8]) -> String {
1176    let hash = hash_sha256(data);
1177    let mut hex = String::with_capacity(64);
1178    for byte in &hash {
1179        hex.push(HEX_CHARS[(*byte >> 4) as usize]);
1180        hex.push(HEX_CHARS[(*byte & 0x0F) as usize]);
1181    }
1182    hex
1183}
1184
1185const HEX_CHARS: [char; 16] = [
1186    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
1187];
1188
1189// ─── Base64 API ─────────────────────────────────────────────────────────────
1190
1191/// Base64-encode arbitrary bytes.
1192pub fn base64_encode(data: &[u8]) -> String {
1193    let mut buf = vec![0u8; data.len() * 4 / 3 + 8];
1194    let len = unsafe {
1195        _api_base64_encode(
1196            data.as_ptr() as u32,
1197            data.len() as u32,
1198            buf.as_mut_ptr() as u32,
1199            buf.len() as u32,
1200        )
1201    };
1202    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1203}
1204
1205/// Decode a base64-encoded string back to bytes.
1206pub fn base64_decode(encoded: &str) -> Vec<u8> {
1207    let mut buf = vec![0u8; encoded.len()];
1208    let len = unsafe {
1209        _api_base64_decode(
1210            encoded.as_ptr() as u32,
1211            encoded.len() as u32,
1212            buf.as_mut_ptr() as u32,
1213            buf.len() as u32,
1214        )
1215    };
1216    buf[..len as usize].to_vec()
1217}
1218
1219// ─── Persistent Key-Value Store API ─────────────────────────────────────────
1220
1221/// Store a key-value pair in the persistent on-disk KV store.
1222/// Returns `true` on success.
1223pub fn kv_store_set(key: &str, value: &[u8]) -> bool {
1224    let rc = unsafe {
1225        _api_kv_store_set(
1226            key.as_ptr() as u32,
1227            key.len() as u32,
1228            value.as_ptr() as u32,
1229            value.len() as u32,
1230        )
1231    };
1232    rc == 0
1233}
1234
1235/// Convenience wrapper: store a UTF-8 string value.
1236pub fn kv_store_set_str(key: &str, value: &str) -> bool {
1237    kv_store_set(key, value.as_bytes())
1238}
1239
1240/// Retrieve a value from the persistent KV store.
1241/// Returns `None` if the key does not exist.
1242pub fn kv_store_get(key: &str) -> Option<Vec<u8>> {
1243    let mut buf = vec![0u8; 64 * 1024]; // 64 KB read buffer
1244    let rc = unsafe {
1245        _api_kv_store_get(
1246            key.as_ptr() as u32,
1247            key.len() as u32,
1248            buf.as_mut_ptr() as u32,
1249            buf.len() as u32,
1250        )
1251    };
1252    if rc < 0 {
1253        return None;
1254    }
1255    Some(buf[..rc as usize].to_vec())
1256}
1257
1258/// Convenience wrapper: retrieve a UTF-8 string value.
1259pub fn kv_store_get_str(key: &str) -> Option<String> {
1260    kv_store_get(key).map(|v| String::from_utf8_lossy(&v).into_owned())
1261}
1262
1263/// Delete a key from the persistent KV store. Returns `true` on success.
1264pub fn kv_store_delete(key: &str) -> bool {
1265    let rc = unsafe { _api_kv_store_delete(key.as_ptr() as u32, key.len() as u32) };
1266    rc == 0
1267}
1268
1269// ─── Navigation API ─────────────────────────────────────────────────────────
1270
1271/// Navigate to a new URL.  The URL can be absolute or relative to the current
1272/// page.  Navigation happens asynchronously after the current `start_app`
1273/// returns.  Returns 0 on success, negative on invalid URL.
1274pub fn navigate(url: &str) -> i32 {
1275    unsafe { _api_navigate(url.as_ptr() as u32, url.len() as u32) }
1276}
1277
1278/// Push a new entry onto the browser's history stack without triggering a
1279/// module reload.  This is analogous to `history.pushState()` in web browsers.
1280///
1281/// - `state`:  Opaque binary data retrievable later via [`get_state`].
1282/// - `title`:  Human-readable title for the history entry.
1283/// - `url`:    The URL to display in the address bar (relative or absolute).
1284///             Pass `""` to keep the current URL.
1285pub fn push_state(state: &[u8], title: &str, url: &str) {
1286    unsafe {
1287        _api_push_state(
1288            state.as_ptr() as u32,
1289            state.len() as u32,
1290            title.as_ptr() as u32,
1291            title.len() as u32,
1292            url.as_ptr() as u32,
1293            url.len() as u32,
1294        )
1295    }
1296}
1297
1298/// Replace the current history entry (no new entry is pushed).
1299/// Analogous to `history.replaceState()`.
1300pub fn replace_state(state: &[u8], title: &str, url: &str) {
1301    unsafe {
1302        _api_replace_state(
1303            state.as_ptr() as u32,
1304            state.len() as u32,
1305            title.as_ptr() as u32,
1306            title.len() as u32,
1307            url.as_ptr() as u32,
1308            url.len() as u32,
1309        )
1310    }
1311}
1312
1313/// Get the URL of the currently loaded page.
1314pub fn get_url() -> String {
1315    let mut buf = [0u8; 4096];
1316    let len = unsafe { _api_get_url(buf.as_mut_ptr() as u32, buf.len() as u32) };
1317    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1318}
1319
1320/// Retrieve the opaque state bytes attached to the current history entry.
1321/// Returns `None` if no state has been set.
1322pub fn get_state() -> Option<Vec<u8>> {
1323    let mut buf = vec![0u8; 64 * 1024]; // 64 KB
1324    let rc = unsafe { _api_get_state(buf.as_mut_ptr() as u32, buf.len() as u32) };
1325    if rc < 0 {
1326        return None;
1327    }
1328    Some(buf[..rc as usize].to_vec())
1329}
1330
1331/// Return the total number of entries in the history stack.
1332pub fn history_length() -> u32 {
1333    unsafe { _api_history_length() }
1334}
1335
1336/// Navigate backward in history.  Returns `true` if a navigation was queued.
1337pub fn history_back() -> bool {
1338    unsafe { _api_history_back() == 1 }
1339}
1340
1341/// Navigate forward in history.  Returns `true` if a navigation was queued.
1342pub fn history_forward() -> bool {
1343    unsafe { _api_history_forward() == 1 }
1344}
1345
1346// ─── Hyperlink API ──────────────────────────────────────────────────────────
1347
1348/// Register a rectangular region on the canvas as a clickable hyperlink.
1349///
1350/// When the user clicks inside the rectangle the browser navigates to `url`.
1351/// Coordinates are in the same canvas-local space used by the drawing APIs.
1352/// Returns 0 on success.
1353pub fn register_hyperlink(x: f32, y: f32, w: f32, h: f32, url: &str) -> i32 {
1354    unsafe { _api_register_hyperlink(x, y, w, h, url.as_ptr() as u32, url.len() as u32) }
1355}
1356
1357/// Remove all previously registered hyperlinks.
1358pub fn clear_hyperlinks() {
1359    unsafe { _api_clear_hyperlinks() }
1360}
1361
1362// ─── URL Utility API ────────────────────────────────────────────────────────
1363
1364/// Resolve a relative URL against a base URL (WHATWG algorithm).
1365/// Returns `None` if either URL is invalid.
1366pub fn url_resolve(base: &str, relative: &str) -> Option<String> {
1367    let mut buf = [0u8; 4096];
1368    let rc = unsafe {
1369        _api_url_resolve(
1370            base.as_ptr() as u32,
1371            base.len() as u32,
1372            relative.as_ptr() as u32,
1373            relative.len() as u32,
1374            buf.as_mut_ptr() as u32,
1375            buf.len() as u32,
1376        )
1377    };
1378    if rc < 0 {
1379        return None;
1380    }
1381    Some(String::from_utf8_lossy(&buf[..rc as usize]).to_string())
1382}
1383
1384/// Percent-encode a string for safe inclusion in URL components.
1385pub fn url_encode(input: &str) -> String {
1386    let mut buf = vec![0u8; input.len() * 3 + 4];
1387    let len = unsafe {
1388        _api_url_encode(
1389            input.as_ptr() as u32,
1390            input.len() as u32,
1391            buf.as_mut_ptr() as u32,
1392            buf.len() as u32,
1393        )
1394    };
1395    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1396}
1397
1398/// Decode a percent-encoded string.
1399pub fn url_decode(input: &str) -> String {
1400    let mut buf = vec![0u8; input.len() + 4];
1401    let len = unsafe {
1402        _api_url_decode(
1403            input.as_ptr() as u32,
1404            input.len() as u32,
1405            buf.as_mut_ptr() as u32,
1406            buf.len() as u32,
1407        )
1408    };
1409    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1410}
1411
1412// ─── Input Polling API ──────────────────────────────────────────────────────
1413
1414/// Get the mouse position in canvas-local coordinates.
1415pub fn mouse_position() -> (f32, f32) {
1416    let packed = unsafe { _api_mouse_position() };
1417    let x = f32::from_bits((packed >> 32) as u32);
1418    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
1419    (x, y)
1420}
1421
1422/// Returns `true` if the given mouse button is currently held down.
1423/// Button 0 = primary (left), 1 = secondary (right), 2 = middle.
1424pub fn mouse_button_down(button: u32) -> bool {
1425    unsafe { _api_mouse_button_down(button) != 0 }
1426}
1427
1428/// Returns `true` if the given mouse button was clicked this frame.
1429pub fn mouse_button_clicked(button: u32) -> bool {
1430    unsafe { _api_mouse_button_clicked(button) != 0 }
1431}
1432
1433/// Returns `true` if the given key is currently held down.
1434/// See `KEY_*` constants for key codes.
1435pub fn key_down(key: u32) -> bool {
1436    unsafe { _api_key_down(key) != 0 }
1437}
1438
1439/// Returns `true` if the given key was pressed this frame.
1440pub fn key_pressed(key: u32) -> bool {
1441    unsafe { _api_key_pressed(key) != 0 }
1442}
1443
1444/// Get the scroll wheel delta for this frame.
1445pub fn scroll_delta() -> (f32, f32) {
1446    let packed = unsafe { _api_scroll_delta() };
1447    let x = f32::from_bits((packed >> 32) as u32);
1448    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
1449    (x, y)
1450}
1451
1452/// Returns modifier key state as a bitmask: bit 0 = Shift, bit 1 = Ctrl, bit 2 = Alt.
1453pub fn modifiers() -> u32 {
1454    unsafe { _api_modifiers() }
1455}
1456
1457/// Returns `true` if Shift is held.
1458pub fn shift_held() -> bool {
1459    modifiers() & 1 != 0
1460}
1461
1462/// Returns `true` if Ctrl (or Cmd on macOS) is held.
1463pub fn ctrl_held() -> bool {
1464    modifiers() & 2 != 0
1465}
1466
1467/// Returns `true` if Alt is held.
1468pub fn alt_held() -> bool {
1469    modifiers() & 4 != 0
1470}
1471
1472// ─── Key Constants ──────────────────────────────────────────────────────────
1473
1474pub const KEY_A: u32 = 0;
1475pub const KEY_B: u32 = 1;
1476pub const KEY_C: u32 = 2;
1477pub const KEY_D: u32 = 3;
1478pub const KEY_E: u32 = 4;
1479pub const KEY_F: u32 = 5;
1480pub const KEY_G: u32 = 6;
1481pub const KEY_H: u32 = 7;
1482pub const KEY_I: u32 = 8;
1483pub const KEY_J: u32 = 9;
1484pub const KEY_K: u32 = 10;
1485pub const KEY_L: u32 = 11;
1486pub const KEY_M: u32 = 12;
1487pub const KEY_N: u32 = 13;
1488pub const KEY_O: u32 = 14;
1489pub const KEY_P: u32 = 15;
1490pub const KEY_Q: u32 = 16;
1491pub const KEY_R: u32 = 17;
1492pub const KEY_S: u32 = 18;
1493pub const KEY_T: u32 = 19;
1494pub const KEY_U: u32 = 20;
1495pub const KEY_V: u32 = 21;
1496pub const KEY_W: u32 = 22;
1497pub const KEY_X: u32 = 23;
1498pub const KEY_Y: u32 = 24;
1499pub const KEY_Z: u32 = 25;
1500pub const KEY_0: u32 = 26;
1501pub const KEY_1: u32 = 27;
1502pub const KEY_2: u32 = 28;
1503pub const KEY_3: u32 = 29;
1504pub const KEY_4: u32 = 30;
1505pub const KEY_5: u32 = 31;
1506pub const KEY_6: u32 = 32;
1507pub const KEY_7: u32 = 33;
1508pub const KEY_8: u32 = 34;
1509pub const KEY_9: u32 = 35;
1510pub const KEY_ENTER: u32 = 36;
1511pub const KEY_ESCAPE: u32 = 37;
1512pub const KEY_TAB: u32 = 38;
1513pub const KEY_BACKSPACE: u32 = 39;
1514pub const KEY_DELETE: u32 = 40;
1515pub const KEY_SPACE: u32 = 41;
1516pub const KEY_UP: u32 = 42;
1517pub const KEY_DOWN: u32 = 43;
1518pub const KEY_LEFT: u32 = 44;
1519pub const KEY_RIGHT: u32 = 45;
1520pub const KEY_HOME: u32 = 46;
1521pub const KEY_END: u32 = 47;
1522pub const KEY_PAGE_UP: u32 = 48;
1523pub const KEY_PAGE_DOWN: u32 = 49;
1524
1525// ─── Interactive Widget API ─────────────────────────────────────────────────
1526
1527/// Render a button at the given position. Returns `true` if it was clicked
1528/// on the previous frame.
1529///
1530/// Must be called from `on_frame()` — widgets are only rendered for
1531/// interactive applications that export a frame loop.
1532pub fn ui_button(id: u32, x: f32, y: f32, w: f32, h: f32, label: &str) -> bool {
1533    unsafe { _api_ui_button(id, x, y, w, h, label.as_ptr() as u32, label.len() as u32) != 0 }
1534}
1535
1536/// Render a checkbox. Returns the current checked state.
1537///
1538/// `initial` sets the value the first time this ID is seen.
1539pub fn ui_checkbox(id: u32, x: f32, y: f32, label: &str, initial: bool) -> bool {
1540    unsafe {
1541        _api_ui_checkbox(
1542            id,
1543            x,
1544            y,
1545            label.as_ptr() as u32,
1546            label.len() as u32,
1547            if initial { 1 } else { 0 },
1548        ) != 0
1549    }
1550}
1551
1552/// Render a slider. Returns the current value.
1553///
1554/// `initial` sets the value the first time this ID is seen.
1555pub fn ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32 {
1556    unsafe { _api_ui_slider(id, x, y, w, min, max, initial) }
1557}
1558
1559/// Render a single-line text input. Returns the current text content.
1560///
1561/// `initial` sets the text the first time this ID is seen.
1562pub fn ui_text_input(id: u32, x: f32, y: f32, w: f32, initial: &str) -> String {
1563    let mut buf = [0u8; 4096];
1564    let len = unsafe {
1565        _api_ui_text_input(
1566            id,
1567            x,
1568            y,
1569            w,
1570            initial.as_ptr() as u32,
1571            initial.len() as u32,
1572            buf.as_mut_ptr() as u32,
1573            buf.len() as u32,
1574        )
1575    };
1576    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1577}