Skip to main content

oxide_sdk/
lib.rs

1#![allow(clippy::too_many_arguments)]
2#![allow(clippy::doc_overindented_list_items)]
3
4//! # Oxide SDK
5//!
6//! Guest-side SDK for building WebAssembly applications that run inside the
7//! [Oxide browser](https://github.com/niklabh/oxide). This crate provides
8//! safe Rust wrappers around the raw host-imported functions exposed by the
9//! `"oxide"` wasm import module.
10//!
11//! The desktop shell uses [GPUI](https://www.gpui.rs/) (Zed's GPU-accelerated
12//! UI framework) to render guest draw commands. The SDK exposes a drawing API
13//! that maps directly onto GPUI primitives — filled quads, GPU-shaped text,
14//! vector paths, and image textures — so your canvas output gets full GPU
15//! acceleration without you having to link GPUI itself.
16//!
17//! ## Quick Start
18//!
19//! ```toml
20//! [lib]
21//! crate-type = ["cdylib"]
22//!
23//! [dependencies]
24//! oxide-sdk = "0.4"
25//! ```
26//!
27//! ### Static app (one-shot render)
28//!
29//! ```rust,ignore
30//! use oxide_sdk::*;
31//!
32//! #[no_mangle]
33//! pub extern "C" fn start_app() {
34//!     log("Hello from Oxide!");
35//!     canvas_clear(30, 30, 46, 255);
36//!     canvas_text(20.0, 40.0, 28.0, 255, 255, 255, 255, "Welcome to Oxide");
37//! }
38//! ```
39//!
40//! ### Interactive app (frame loop)
41//!
42//! ```rust,ignore
43//! use oxide_sdk::*;
44//!
45//! #[no_mangle]
46//! pub extern "C" fn start_app() {
47//!     log("Interactive app started");
48//! }
49//!
50//! #[no_mangle]
51//! pub extern "C" fn on_frame(_dt_ms: u32) {
52//!     canvas_clear(30, 30, 46, 255);
53//!     let (mx, my) = mouse_position();
54//!     canvas_circle(mx, my, 20.0, 255, 100, 100, 255);
55//!
56//!     if ui_button(1, 20.0, 20.0, 100.0, 30.0, "Click me!") {
57//!         log("Button was clicked!");
58//!     }
59//! }
60//! ```
61//!
62//! ### High-level drawing API
63//!
64//! The [`draw`] module provides GPUI-inspired ergonomic types for less
65//! boilerplate:
66//!
67//! ```rust,ignore
68//! use oxide_sdk::draw::*;
69//!
70//! #[no_mangle]
71//! pub extern "C" fn start_app() {
72//!     let c = Canvas::new();
73//!     c.clear(Color::hex(0x1e1e2e));
74//!     c.fill_rect(Rect::new(10.0, 10.0, 200.0, 100.0), Color::rgb(80, 120, 200));
75//!     c.fill_circle(Point2D::new(300.0, 200.0), 50.0, Color::RED);
76//!     c.text("Hello!", Point2D::new(20.0, 30.0), 24.0, Color::WHITE);
77//! }
78//! ```
79//!
80//! Build with `cargo build --target wasm32-unknown-unknown --release`.
81//!
82//! ## API Categories
83//!
84//! | Category | Key types / functions |
85//! |----------|-----------|
86//! | **Drawing (high-level)** | [`draw::Canvas`], [`draw::Color`], [`draw::Rect`], [`draw::Point2D`], [`draw::GradientStop`] |
87//! | **Canvas (low-level)** | [`canvas_clear`], [`canvas_rect`], [`canvas_circle`], [`canvas_text`], [`canvas_line`], [`canvas_image`], [`canvas_dimensions`] |
88//! | **Extended shapes** | [`canvas_rounded_rect`], [`canvas_arc`], [`canvas_bezier`], [`canvas_gradient`] |
89//! | **Canvas state** | [`canvas_save`], [`canvas_restore`], [`canvas_transform`], [`canvas_clip`], [`canvas_opacity`] |
90//! | **GPU** | [`gpu_create_buffer`], [`gpu_create_texture`], [`gpu_create_shader`], [`gpu_create_pipeline`], [`gpu_draw`], [`gpu_dispatch_compute`] |
91//! | **Console** | [`log`], [`warn`], [`error`] |
92//! | **HTTP** | [`fetch`], [`fetch_get`], [`fetch_post`], [`fetch_post_proto`], [`fetch_put`], [`fetch_delete`] |
93//! | **HTTP (streaming)** | [`fetch_begin`], [`fetch_begin_get`], [`fetch_state`], [`fetch_status`], [`fetch_recv`], [`fetch_error`], [`fetch_abort`], [`fetch_remove`] |
94//! | **Protobuf** | [`proto::ProtoEncoder`], [`proto::ProtoDecoder`] |
95//! | **Storage** | [`storage_set`], [`storage_get`], [`storage_remove`], [`kv_store_set`], [`kv_store_get`], [`kv_store_delete`] |
96//! | **Audio** | [`audio_play`], [`audio_play_url`], [`audio_detect_format`], [`audio_play_with_format`], [`audio_pause`], [`audio_channel_play`] |
97//! | **Video** | [`video_load`], [`video_load_url`], [`video_render`], [`video_play`], [`video_hls_open_variant`], [`subtitle_load_srt`] |
98//! | **Media capture** | [`camera_open`], [`camera_capture_frame`], [`microphone_open`], [`microphone_read_samples`], [`screen_capture`] |
99//! | **WebRTC** | [`rtc_create_peer`], [`rtc_create_offer`], [`rtc_create_answer`], [`rtc_create_data_channel`], [`rtc_send`], [`rtc_recv`], [`rtc_signal_connect`] |
100//! | **WebSocket** | [`ws_connect`], [`ws_send_text`], [`ws_send_binary`], [`ws_recv`], [`ws_ready_state`], [`ws_close`], [`ws_remove`] |
101//! | **MIDI** | [`midi_input_count`], [`midi_output_count`], [`midi_input_name`], [`midi_output_name`], [`midi_open_input`], [`midi_open_output`], [`midi_send`], [`midi_recv`], [`midi_close`] |
102//! | **Timers** | [`set_timeout`], [`set_interval`], [`clear_timer`], [`request_animation_frame`], [`cancel_animation_frame`], [`time_now_ms`] |
103//! | **Navigation** | [`navigate`], [`push_state`], [`replace_state`], [`get_url`], [`history_back`], [`history_forward`] |
104//! | **Input** | [`mouse_position`], [`mouse_button_down`], [`mouse_button_clicked`], [`key_down`], [`key_pressed`], [`scroll_delta`], [`modifiers`] |
105//! | **Widgets** | [`ui_button`], [`ui_checkbox`], [`ui_slider`], [`ui_text_input`] |
106//! | **Crypto** | [`hash_sha256`], [`hash_sha256_hex`], [`base64_encode`], [`base64_decode`] |
107//! | **Other** | [`clipboard_write`], [`clipboard_read`], [`random_u64`], [`random_f64`], [`notify`], [`upload_file`], [`load_module`] |
108//!
109//! ## Guest Module Contract
110//!
111//! Every `.wasm` module loaded by Oxide must:
112//!
113//! 1. **Export `start_app`** — `extern "C" fn()` entry point, called once on load.
114//! 2. **Optionally export `on_frame`** — `extern "C" fn(dt_ms: u32)` for
115//!    interactive apps with a render loop (called every frame, fuel replenished).
116//! 3. **Optionally export `on_timer`** — `extern "C" fn(callback_id: u32)`
117//!    to receive callbacks from [`set_timeout`], [`set_interval`], and [`request_animation_frame`].
118//! 4. **Compile as `cdylib`** — `crate-type = ["cdylib"]` in `Cargo.toml`.
119//! 5. **Target `wasm32-unknown-unknown`** — no WASI, pure capability-based I/O.
120//!
121//! ## Full API Documentation
122//!
123//! See <https://docs.oxide.foundation/oxide_sdk/> for the complete API
124//! reference, or browse the individual function documentation below.
125
126pub mod draw;
127pub mod proto;
128
129// ─── Raw FFI imports from the host ──────────────────────────────────────────
130
131#[link(wasm_import_module = "oxide")]
132extern "C" {
133    #[link_name = "api_log"]
134    fn _api_log(ptr: u32, len: u32);
135
136    #[link_name = "api_warn"]
137    fn _api_warn(ptr: u32, len: u32);
138
139    #[link_name = "api_error"]
140    fn _api_error(ptr: u32, len: u32);
141
142    #[link_name = "api_get_location"]
143    fn _api_get_location(out_ptr: u32, out_cap: u32) -> u32;
144
145    #[link_name = "api_upload_file"]
146    fn _api_upload_file(name_ptr: u32, name_cap: u32, data_ptr: u32, data_cap: u32) -> u64;
147
148    #[link_name = "api_canvas_clear"]
149    fn _api_canvas_clear(r: u32, g: u32, b: u32, a: u32);
150
151    #[link_name = "api_canvas_rect"]
152    fn _api_canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u32, g: u32, b: u32, a: u32);
153
154    #[link_name = "api_canvas_circle"]
155    fn _api_canvas_circle(cx: f32, cy: f32, radius: f32, r: u32, g: u32, b: u32, a: u32);
156
157    #[link_name = "api_canvas_text"]
158    fn _api_canvas_text(
159        x: f32,
160        y: f32,
161        size: f32,
162        r: u32,
163        g: u32,
164        b: u32,
165        a: u32,
166        ptr: u32,
167        len: u32,
168    );
169
170    #[link_name = "api_canvas_line"]
171    fn _api_canvas_line(
172        x1: f32,
173        y1: f32,
174        x2: f32,
175        y2: f32,
176        r: u32,
177        g: u32,
178        b: u32,
179        a: u32,
180        thickness: f32,
181    );
182
183    #[link_name = "api_canvas_dimensions"]
184    fn _api_canvas_dimensions() -> u64;
185
186    #[link_name = "api_canvas_image"]
187    fn _api_canvas_image(x: f32, y: f32, w: f32, h: f32, data_ptr: u32, data_len: u32);
188
189    // ── Extended Shape Primitives ──────────────────────────────────
190
191    #[link_name = "api_canvas_rounded_rect"]
192    fn _api_canvas_rounded_rect(
193        x: f32,
194        y: f32,
195        w: f32,
196        h: f32,
197        radius: f32,
198        r: u32,
199        g: u32,
200        b: u32,
201        a: u32,
202    );
203
204    #[link_name = "api_canvas_arc"]
205    fn _api_canvas_arc(
206        cx: f32,
207        cy: f32,
208        radius: f32,
209        start_angle: f32,
210        end_angle: f32,
211        r: u32,
212        g: u32,
213        b: u32,
214        a: u32,
215        thickness: f32,
216    );
217
218    #[link_name = "api_canvas_bezier"]
219    fn _api_canvas_bezier(
220        x1: f32,
221        y1: f32,
222        cp1x: f32,
223        cp1y: f32,
224        cp2x: f32,
225        cp2y: f32,
226        x2: f32,
227        y2: f32,
228        r: u32,
229        g: u32,
230        b: u32,
231        a: u32,
232        thickness: f32,
233    );
234
235    #[link_name = "api_canvas_gradient"]
236    fn _api_canvas_gradient(
237        x: f32,
238        y: f32,
239        w: f32,
240        h: f32,
241        kind: u32,
242        ax: f32,
243        ay: f32,
244        bx: f32,
245        by: f32,
246        stops_ptr: u32,
247        stops_len: u32,
248    );
249
250    // ── Canvas State (transform / clip / opacity) ─────────────────
251
252    #[link_name = "api_canvas_save"]
253    fn _api_canvas_save();
254
255    #[link_name = "api_canvas_restore"]
256    fn _api_canvas_restore();
257
258    #[link_name = "api_canvas_transform"]
259    fn _api_canvas_transform(a: f32, b: f32, c: f32, d: f32, tx: f32, ty: f32);
260
261    #[link_name = "api_canvas_clip"]
262    fn _api_canvas_clip(x: f32, y: f32, w: f32, h: f32);
263
264    #[link_name = "api_canvas_opacity"]
265    fn _api_canvas_opacity(alpha: f32);
266
267    #[link_name = "api_storage_set"]
268    fn _api_storage_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32);
269
270    #[link_name = "api_storage_get"]
271    fn _api_storage_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> u32;
272
273    #[link_name = "api_storage_remove"]
274    fn _api_storage_remove(key_ptr: u32, key_len: u32);
275
276    #[link_name = "api_clipboard_write"]
277    fn _api_clipboard_write(ptr: u32, len: u32);
278
279    #[link_name = "api_clipboard_read"]
280    fn _api_clipboard_read(out_ptr: u32, out_cap: u32) -> u32;
281
282    #[link_name = "api_time_now_ms"]
283    fn _api_time_now_ms() -> u64;
284
285    #[link_name = "api_set_timeout"]
286    fn _api_set_timeout(callback_id: u32, delay_ms: u32) -> u32;
287
288    #[link_name = "api_set_interval"]
289    fn _api_set_interval(callback_id: u32, interval_ms: u32) -> u32;
290
291    #[link_name = "api_clear_timer"]
292    fn _api_clear_timer(timer_id: u32);
293
294    #[link_name = "api_request_animation_frame"]
295    fn _api_request_animation_frame(callback_id: u32) -> u32;
296
297    #[link_name = "api_cancel_animation_frame"]
298    fn _api_cancel_animation_frame(request_id: u32);
299
300    #[link_name = "api_random"]
301    fn _api_random() -> u64;
302
303    #[link_name = "api_notify"]
304    fn _api_notify(title_ptr: u32, title_len: u32, body_ptr: u32, body_len: u32);
305
306    #[link_name = "api_fetch"]
307    fn _api_fetch(
308        method_ptr: u32,
309        method_len: u32,
310        url_ptr: u32,
311        url_len: u32,
312        ct_ptr: u32,
313        ct_len: u32,
314        body_ptr: u32,
315        body_len: u32,
316        out_ptr: u32,
317        out_cap: u32,
318    ) -> i64;
319
320    #[link_name = "api_fetch_begin"]
321    fn _api_fetch_begin(
322        method_ptr: u32,
323        method_len: u32,
324        url_ptr: u32,
325        url_len: u32,
326        ct_ptr: u32,
327        ct_len: u32,
328        body_ptr: u32,
329        body_len: u32,
330    ) -> u32;
331
332    #[link_name = "api_fetch_state"]
333    fn _api_fetch_state(id: u32) -> u32;
334
335    #[link_name = "api_fetch_status"]
336    fn _api_fetch_status(id: u32) -> u32;
337
338    #[link_name = "api_fetch_recv"]
339    fn _api_fetch_recv(id: u32, out_ptr: u32, out_cap: u32) -> i64;
340
341    #[link_name = "api_fetch_error"]
342    fn _api_fetch_error(id: u32, out_ptr: u32, out_cap: u32) -> i32;
343
344    #[link_name = "api_fetch_abort"]
345    fn _api_fetch_abort(id: u32) -> i32;
346
347    #[link_name = "api_fetch_remove"]
348    fn _api_fetch_remove(id: u32);
349
350    #[link_name = "api_load_module"]
351    fn _api_load_module(url_ptr: u32, url_len: u32) -> i32;
352
353    #[link_name = "api_hash_sha256"]
354    fn _api_hash_sha256(data_ptr: u32, data_len: u32, out_ptr: u32) -> u32;
355
356    #[link_name = "api_base64_encode"]
357    fn _api_base64_encode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
358
359    #[link_name = "api_base64_decode"]
360    fn _api_base64_decode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
361
362    #[link_name = "api_kv_store_set"]
363    fn _api_kv_store_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32) -> i32;
364
365    #[link_name = "api_kv_store_get"]
366    fn _api_kv_store_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> i32;
367
368    #[link_name = "api_kv_store_delete"]
369    fn _api_kv_store_delete(key_ptr: u32, key_len: u32) -> i32;
370
371    // ── Navigation ──────────────────────────────────────────────────
372
373    #[link_name = "api_navigate"]
374    fn _api_navigate(url_ptr: u32, url_len: u32) -> i32;
375
376    #[link_name = "api_push_state"]
377    fn _api_push_state(
378        state_ptr: u32,
379        state_len: u32,
380        title_ptr: u32,
381        title_len: u32,
382        url_ptr: u32,
383        url_len: u32,
384    );
385
386    #[link_name = "api_replace_state"]
387    fn _api_replace_state(
388        state_ptr: u32,
389        state_len: u32,
390        title_ptr: u32,
391        title_len: u32,
392        url_ptr: u32,
393        url_len: u32,
394    );
395
396    #[link_name = "api_get_url"]
397    fn _api_get_url(out_ptr: u32, out_cap: u32) -> u32;
398
399    #[link_name = "api_get_state"]
400    fn _api_get_state(out_ptr: u32, out_cap: u32) -> i32;
401
402    #[link_name = "api_history_length"]
403    fn _api_history_length() -> u32;
404
405    #[link_name = "api_history_back"]
406    fn _api_history_back() -> i32;
407
408    #[link_name = "api_history_forward"]
409    fn _api_history_forward() -> i32;
410
411    // ── Hyperlinks ──────────────────────────────────────────────────
412
413    #[link_name = "api_register_hyperlink"]
414    fn _api_register_hyperlink(x: f32, y: f32, w: f32, h: f32, url_ptr: u32, url_len: u32) -> i32;
415
416    #[link_name = "api_clear_hyperlinks"]
417    fn _api_clear_hyperlinks();
418
419    // ── Input Polling ────────────────────────────────────────────────
420
421    #[link_name = "api_mouse_position"]
422    fn _api_mouse_position() -> u64;
423
424    #[link_name = "api_mouse_button_down"]
425    fn _api_mouse_button_down(button: u32) -> u32;
426
427    #[link_name = "api_mouse_button_clicked"]
428    fn _api_mouse_button_clicked(button: u32) -> u32;
429
430    #[link_name = "api_key_down"]
431    fn _api_key_down(key: u32) -> u32;
432
433    #[link_name = "api_key_pressed"]
434    fn _api_key_pressed(key: u32) -> u32;
435
436    #[link_name = "api_scroll_delta"]
437    fn _api_scroll_delta() -> u64;
438
439    #[link_name = "api_modifiers"]
440    fn _api_modifiers() -> u32;
441
442    // ── Interactive Widgets ─────────────────────────────────────────
443
444    #[link_name = "api_ui_button"]
445    fn _api_ui_button(
446        id: u32,
447        x: f32,
448        y: f32,
449        w: f32,
450        h: f32,
451        label_ptr: u32,
452        label_len: u32,
453    ) -> u32;
454
455    #[link_name = "api_ui_checkbox"]
456    fn _api_ui_checkbox(
457        id: u32,
458        x: f32,
459        y: f32,
460        label_ptr: u32,
461        label_len: u32,
462        initial: u32,
463    ) -> u32;
464
465    #[link_name = "api_ui_slider"]
466    fn _api_ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32;
467
468    #[link_name = "api_ui_text_input"]
469    fn _api_ui_text_input(
470        id: u32,
471        x: f32,
472        y: f32,
473        w: f32,
474        init_ptr: u32,
475        init_len: u32,
476        out_ptr: u32,
477        out_cap: u32,
478    ) -> u32;
479
480    // ── Audio Playback ──────────────────────────────────────────────
481
482    #[link_name = "api_audio_play"]
483    fn _api_audio_play(data_ptr: u32, data_len: u32) -> i32;
484
485    #[link_name = "api_audio_play_url"]
486    fn _api_audio_play_url(url_ptr: u32, url_len: u32) -> i32;
487
488    #[link_name = "api_audio_detect_format"]
489    fn _api_audio_detect_format(data_ptr: u32, data_len: u32) -> u32;
490
491    #[link_name = "api_audio_play_with_format"]
492    fn _api_audio_play_with_format(data_ptr: u32, data_len: u32, format_hint: u32) -> i32;
493
494    #[link_name = "api_audio_last_url_content_type"]
495    fn _api_audio_last_url_content_type(out_ptr: u32, out_cap: u32) -> u32;
496
497    #[link_name = "api_audio_pause"]
498    fn _api_audio_pause();
499
500    #[link_name = "api_audio_resume"]
501    fn _api_audio_resume();
502
503    #[link_name = "api_audio_stop"]
504    fn _api_audio_stop();
505
506    #[link_name = "api_audio_set_volume"]
507    fn _api_audio_set_volume(level: f32);
508
509    #[link_name = "api_audio_get_volume"]
510    fn _api_audio_get_volume() -> f32;
511
512    #[link_name = "api_audio_is_playing"]
513    fn _api_audio_is_playing() -> u32;
514
515    #[link_name = "api_audio_position"]
516    fn _api_audio_position() -> u64;
517
518    #[link_name = "api_audio_seek"]
519    fn _api_audio_seek(position_ms: u64) -> i32;
520
521    #[link_name = "api_audio_duration"]
522    fn _api_audio_duration() -> u64;
523
524    #[link_name = "api_audio_set_loop"]
525    fn _api_audio_set_loop(enabled: u32);
526
527    #[link_name = "api_audio_channel_play"]
528    fn _api_audio_channel_play(channel: u32, data_ptr: u32, data_len: u32) -> i32;
529
530    #[link_name = "api_audio_channel_play_with_format"]
531    fn _api_audio_channel_play_with_format(
532        channel: u32,
533        data_ptr: u32,
534        data_len: u32,
535        format_hint: u32,
536    ) -> i32;
537
538    #[link_name = "api_audio_channel_stop"]
539    fn _api_audio_channel_stop(channel: u32);
540
541    #[link_name = "api_audio_channel_set_volume"]
542    fn _api_audio_channel_set_volume(channel: u32, level: f32);
543
544    // ── Video ─────────────────────────────────────────────────────────
545
546    #[link_name = "api_video_detect_format"]
547    fn _api_video_detect_format(data_ptr: u32, data_len: u32) -> u32;
548
549    #[link_name = "api_video_load"]
550    fn _api_video_load(data_ptr: u32, data_len: u32, format_hint: u32) -> i32;
551
552    #[link_name = "api_video_load_url"]
553    fn _api_video_load_url(url_ptr: u32, url_len: u32) -> i32;
554
555    #[link_name = "api_video_last_url_content_type"]
556    fn _api_video_last_url_content_type(out_ptr: u32, out_cap: u32) -> u32;
557
558    #[link_name = "api_video_hls_variant_count"]
559    fn _api_video_hls_variant_count() -> u32;
560
561    #[link_name = "api_video_hls_variant_url"]
562    fn _api_video_hls_variant_url(index: u32, out_ptr: u32, out_cap: u32) -> u32;
563
564    #[link_name = "api_video_hls_open_variant"]
565    fn _api_video_hls_open_variant(index: u32) -> i32;
566
567    #[link_name = "api_video_play"]
568    fn _api_video_play();
569
570    #[link_name = "api_video_pause"]
571    fn _api_video_pause();
572
573    #[link_name = "api_video_stop"]
574    fn _api_video_stop();
575
576    #[link_name = "api_video_seek"]
577    fn _api_video_seek(position_ms: u64) -> i32;
578
579    #[link_name = "api_video_position"]
580    fn _api_video_position() -> u64;
581
582    #[link_name = "api_video_duration"]
583    fn _api_video_duration() -> u64;
584
585    #[link_name = "api_video_render"]
586    fn _api_video_render(x: f32, y: f32, w: f32, h: f32) -> i32;
587
588    #[link_name = "api_video_set_volume"]
589    fn _api_video_set_volume(level: f32);
590
591    #[link_name = "api_video_get_volume"]
592    fn _api_video_get_volume() -> f32;
593
594    #[link_name = "api_video_set_loop"]
595    fn _api_video_set_loop(enabled: u32);
596
597    #[link_name = "api_video_set_pip"]
598    fn _api_video_set_pip(enabled: u32);
599
600    #[link_name = "api_subtitle_load_srt"]
601    fn _api_subtitle_load_srt(ptr: u32, len: u32) -> i32;
602
603    #[link_name = "api_subtitle_load_vtt"]
604    fn _api_subtitle_load_vtt(ptr: u32, len: u32) -> i32;
605
606    #[link_name = "api_subtitle_clear"]
607    fn _api_subtitle_clear();
608
609    // ── Media capture ─────────────────────────────────────────────────
610
611    #[link_name = "api_camera_open"]
612    fn _api_camera_open() -> i32;
613
614    #[link_name = "api_camera_close"]
615    fn _api_camera_close();
616
617    #[link_name = "api_camera_capture_frame"]
618    fn _api_camera_capture_frame(out_ptr: u32, out_cap: u32) -> u32;
619
620    #[link_name = "api_camera_frame_dimensions"]
621    fn _api_camera_frame_dimensions() -> u64;
622
623    #[link_name = "api_microphone_open"]
624    fn _api_microphone_open() -> i32;
625
626    #[link_name = "api_microphone_close"]
627    fn _api_microphone_close();
628
629    #[link_name = "api_microphone_sample_rate"]
630    fn _api_microphone_sample_rate() -> u32;
631
632    #[link_name = "api_microphone_read_samples"]
633    fn _api_microphone_read_samples(out_ptr: u32, max_samples: u32) -> u32;
634
635    #[link_name = "api_screen_capture"]
636    fn _api_screen_capture(out_ptr: u32, out_cap: u32) -> i32;
637
638    #[link_name = "api_screen_capture_dimensions"]
639    fn _api_screen_capture_dimensions() -> u64;
640
641    #[link_name = "api_media_pipeline_stats"]
642    fn _api_media_pipeline_stats() -> u64;
643
644    // ── GPU / WebGPU-style API ────────────────────────────────────
645
646    #[link_name = "api_gpu_create_buffer"]
647    fn _api_gpu_create_buffer(size_lo: u32, size_hi: u32, usage: u32) -> u32;
648
649    #[link_name = "api_gpu_create_texture"]
650    fn _api_gpu_create_texture(width: u32, height: u32) -> u32;
651
652    #[link_name = "api_gpu_create_shader"]
653    fn _api_gpu_create_shader(src_ptr: u32, src_len: u32) -> u32;
654
655    #[link_name = "api_gpu_create_render_pipeline"]
656    fn _api_gpu_create_render_pipeline(
657        shader: u32,
658        vs_ptr: u32,
659        vs_len: u32,
660        fs_ptr: u32,
661        fs_len: u32,
662    ) -> u32;
663
664    #[link_name = "api_gpu_create_compute_pipeline"]
665    fn _api_gpu_create_compute_pipeline(shader: u32, ep_ptr: u32, ep_len: u32) -> u32;
666
667    #[link_name = "api_gpu_write_buffer"]
668    fn _api_gpu_write_buffer(
669        handle: u32,
670        offset_lo: u32,
671        offset_hi: u32,
672        data_ptr: u32,
673        data_len: u32,
674    ) -> u32;
675
676    #[link_name = "api_gpu_draw"]
677    fn _api_gpu_draw(pipeline: u32, target: u32, vertex_count: u32, instance_count: u32) -> u32;
678
679    #[link_name = "api_gpu_dispatch_compute"]
680    fn _api_gpu_dispatch_compute(pipeline: u32, x: u32, y: u32, z: u32) -> u32;
681
682    #[link_name = "api_gpu_destroy_buffer"]
683    fn _api_gpu_destroy_buffer(handle: u32) -> u32;
684
685    #[link_name = "api_gpu_destroy_texture"]
686    fn _api_gpu_destroy_texture(handle: u32) -> u32;
687
688    // ── WebRTC / Real-Time Communication ─────────────────────────
689
690    #[link_name = "api_rtc_create_peer"]
691    fn _api_rtc_create_peer(stun_ptr: u32, stun_len: u32) -> u32;
692
693    #[link_name = "api_rtc_close_peer"]
694    fn _api_rtc_close_peer(peer_id: u32) -> u32;
695
696    #[link_name = "api_rtc_create_offer"]
697    fn _api_rtc_create_offer(peer_id: u32, out_ptr: u32, out_cap: u32) -> i32;
698
699    #[link_name = "api_rtc_create_answer"]
700    fn _api_rtc_create_answer(peer_id: u32, out_ptr: u32, out_cap: u32) -> i32;
701
702    #[link_name = "api_rtc_set_local_description"]
703    fn _api_rtc_set_local_description(
704        peer_id: u32,
705        sdp_ptr: u32,
706        sdp_len: u32,
707        is_offer: u32,
708    ) -> i32;
709
710    #[link_name = "api_rtc_set_remote_description"]
711    fn _api_rtc_set_remote_description(
712        peer_id: u32,
713        sdp_ptr: u32,
714        sdp_len: u32,
715        is_offer: u32,
716    ) -> i32;
717
718    #[link_name = "api_rtc_add_ice_candidate"]
719    fn _api_rtc_add_ice_candidate(peer_id: u32, cand_ptr: u32, cand_len: u32) -> i32;
720
721    #[link_name = "api_rtc_connection_state"]
722    fn _api_rtc_connection_state(peer_id: u32) -> u32;
723
724    #[link_name = "api_rtc_poll_ice_candidate"]
725    fn _api_rtc_poll_ice_candidate(peer_id: u32, out_ptr: u32, out_cap: u32) -> i32;
726
727    #[link_name = "api_rtc_create_data_channel"]
728    fn _api_rtc_create_data_channel(
729        peer_id: u32,
730        label_ptr: u32,
731        label_len: u32,
732        ordered: u32,
733    ) -> u32;
734
735    #[link_name = "api_rtc_send"]
736    fn _api_rtc_send(
737        peer_id: u32,
738        channel_id: u32,
739        data_ptr: u32,
740        data_len: u32,
741        is_binary: u32,
742    ) -> i32;
743
744    #[link_name = "api_rtc_recv"]
745    fn _api_rtc_recv(peer_id: u32, channel_id: u32, out_ptr: u32, out_cap: u32) -> i64;
746
747    #[link_name = "api_rtc_poll_data_channel"]
748    fn _api_rtc_poll_data_channel(peer_id: u32, out_ptr: u32, out_cap: u32) -> i32;
749
750    #[link_name = "api_rtc_add_track"]
751    fn _api_rtc_add_track(peer_id: u32, kind: u32) -> u32;
752
753    #[link_name = "api_rtc_poll_track"]
754    fn _api_rtc_poll_track(peer_id: u32, out_ptr: u32, out_cap: u32) -> i32;
755
756    #[link_name = "api_rtc_signal_connect"]
757    fn _api_rtc_signal_connect(url_ptr: u32, url_len: u32) -> u32;
758
759    #[link_name = "api_rtc_signal_join_room"]
760    fn _api_rtc_signal_join_room(room_ptr: u32, room_len: u32) -> i32;
761
762    #[link_name = "api_rtc_signal_send"]
763    fn _api_rtc_signal_send(data_ptr: u32, data_len: u32) -> i32;
764
765    #[link_name = "api_rtc_signal_recv"]
766    fn _api_rtc_signal_recv(out_ptr: u32, out_cap: u32) -> i32;
767
768    // ── WebSocket API ────────────────────────────────────────────────
769
770    #[link_name = "api_ws_connect"]
771    fn _api_ws_connect(url_ptr: u32, url_len: u32) -> u32;
772
773    #[link_name = "api_ws_send_text"]
774    fn _api_ws_send_text(id: u32, data_ptr: u32, data_len: u32) -> i32;
775
776    #[link_name = "api_ws_send_binary"]
777    fn _api_ws_send_binary(id: u32, data_ptr: u32, data_len: u32) -> i32;
778
779    #[link_name = "api_ws_recv"]
780    fn _api_ws_recv(id: u32, out_ptr: u32, out_cap: u32) -> i64;
781
782    #[link_name = "api_ws_ready_state"]
783    fn _api_ws_ready_state(id: u32) -> u32;
784
785    #[link_name = "api_ws_close"]
786    fn _api_ws_close(id: u32) -> i32;
787
788    #[link_name = "api_ws_remove"]
789    fn _api_ws_remove(id: u32);
790
791    // ── MIDI API ────────────────────────────────────────────────────
792
793    #[link_name = "api_midi_input_count"]
794    fn _api_midi_input_count() -> u32;
795
796    #[link_name = "api_midi_output_count"]
797    fn _api_midi_output_count() -> u32;
798
799    #[link_name = "api_midi_input_name"]
800    fn _api_midi_input_name(index: u32, out_ptr: u32, out_cap: u32) -> u32;
801
802    #[link_name = "api_midi_output_name"]
803    fn _api_midi_output_name(index: u32, out_ptr: u32, out_cap: u32) -> u32;
804
805    #[link_name = "api_midi_open_input"]
806    fn _api_midi_open_input(index: u32) -> u32;
807
808    #[link_name = "api_midi_open_output"]
809    fn _api_midi_open_output(index: u32) -> u32;
810
811    #[link_name = "api_midi_send"]
812    fn _api_midi_send(handle: u32, data_ptr: u32, data_len: u32) -> i32;
813
814    #[link_name = "api_midi_recv"]
815    fn _api_midi_recv(handle: u32, out_ptr: u32, out_cap: u32) -> i32;
816
817    #[link_name = "api_midi_close"]
818    fn _api_midi_close(handle: u32);
819
820    // ── URL Utilities ───────────────────────────────────────────────
821
822    #[link_name = "api_url_resolve"]
823    fn _api_url_resolve(
824        base_ptr: u32,
825        base_len: u32,
826        rel_ptr: u32,
827        rel_len: u32,
828        out_ptr: u32,
829        out_cap: u32,
830    ) -> i32;
831
832    #[link_name = "api_url_encode"]
833    fn _api_url_encode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
834
835    #[link_name = "api_url_decode"]
836    fn _api_url_decode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
837}
838
839// ─── Console API ────────────────────────────────────────────────────────────
840
841/// Print a message to the browser console (log level).
842pub fn log(msg: &str) {
843    unsafe { _api_log(msg.as_ptr() as u32, msg.len() as u32) }
844}
845
846/// Print a warning to the browser console.
847pub fn warn(msg: &str) {
848    unsafe { _api_warn(msg.as_ptr() as u32, msg.len() as u32) }
849}
850
851/// Print an error to the browser console.
852pub fn error(msg: &str) {
853    unsafe { _api_error(msg.as_ptr() as u32, msg.len() as u32) }
854}
855
856// ─── Geolocation API ────────────────────────────────────────────────────────
857
858/// Get the device's mock geolocation as a `"lat,lon"` string.
859pub fn get_location() -> String {
860    let mut buf = [0u8; 128];
861    let len = unsafe { _api_get_location(buf.as_mut_ptr() as u32, buf.len() as u32) };
862    String::from_utf8_lossy(&buf[..len as usize]).to_string()
863}
864
865// ─── File Upload API ────────────────────────────────────────────────────────
866
867/// File returned from the native file picker.
868pub struct UploadedFile {
869    pub name: String,
870    pub data: Vec<u8>,
871}
872
873/// Opens the native OS file picker and returns the selected file.
874/// Returns `None` if the user cancels.
875pub fn upload_file() -> Option<UploadedFile> {
876    let mut name_buf = [0u8; 256];
877    let mut data_buf = vec![0u8; 1024 * 1024]; // 1MB max
878
879    let result = unsafe {
880        _api_upload_file(
881            name_buf.as_mut_ptr() as u32,
882            name_buf.len() as u32,
883            data_buf.as_mut_ptr() as u32,
884            data_buf.len() as u32,
885        )
886    };
887
888    if result == 0 {
889        return None;
890    }
891
892    let name_len = (result >> 32) as usize;
893    let data_len = (result & 0xFFFF_FFFF) as usize;
894
895    Some(UploadedFile {
896        name: String::from_utf8_lossy(&name_buf[..name_len]).to_string(),
897        data: data_buf[..data_len].to_vec(),
898    })
899}
900
901// ─── Canvas API ─────────────────────────────────────────────────────────────
902
903/// Clear the canvas with a solid RGBA color.
904pub fn canvas_clear(r: u8, g: u8, b: u8, a: u8) {
905    unsafe { _api_canvas_clear(r as u32, g as u32, b as u32, a as u32) }
906}
907
908/// Draw a filled rectangle.
909pub fn canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u8, g: u8, b: u8, a: u8) {
910    unsafe { _api_canvas_rect(x, y, w, h, r as u32, g as u32, b as u32, a as u32) }
911}
912
913/// Draw a filled circle.
914pub fn canvas_circle(cx: f32, cy: f32, radius: f32, r: u8, g: u8, b: u8, a: u8) {
915    unsafe { _api_canvas_circle(cx, cy, radius, r as u32, g as u32, b as u32, a as u32) }
916}
917
918/// Draw text on the canvas with RGBA color.
919pub fn canvas_text(x: f32, y: f32, size: f32, r: u8, g: u8, b: u8, a: u8, text: &str) {
920    unsafe {
921        _api_canvas_text(
922            x,
923            y,
924            size,
925            r as u32,
926            g as u32,
927            b as u32,
928            a as u32,
929            text.as_ptr() as u32,
930            text.len() as u32,
931        )
932    }
933}
934
935/// Draw a line between two points with RGBA color.
936pub fn canvas_line(x1: f32, y1: f32, x2: f32, y2: f32, r: u8, g: u8, b: u8, a: u8, thickness: f32) {
937    unsafe {
938        _api_canvas_line(
939            x1, y1, x2, y2, r as u32, g as u32, b as u32, a as u32, thickness,
940        )
941    }
942}
943
944/// Returns `(width, height)` of the canvas in pixels.
945pub fn canvas_dimensions() -> (u32, u32) {
946    let packed = unsafe { _api_canvas_dimensions() };
947    ((packed >> 32) as u32, (packed & 0xFFFF_FFFF) as u32)
948}
949
950/// Draw an image on the canvas from encoded image bytes (PNG, JPEG, GIF, WebP).
951/// The browser decodes the image and renders it at the given rectangle.
952pub fn canvas_image(x: f32, y: f32, w: f32, h: f32, data: &[u8]) {
953    unsafe { _api_canvas_image(x, y, w, h, data.as_ptr() as u32, data.len() as u32) }
954}
955
956// ─── Extended Shape Primitives ──────────────────────────────────────────────
957
958/// Draw a filled rounded rectangle with uniform corner radius.
959pub fn canvas_rounded_rect(
960    x: f32,
961    y: f32,
962    w: f32,
963    h: f32,
964    radius: f32,
965    r: u8,
966    g: u8,
967    b: u8,
968    a: u8,
969) {
970    unsafe { _api_canvas_rounded_rect(x, y, w, h, radius, r as u32, g as u32, b as u32, a as u32) }
971}
972
973/// Draw a circular arc stroke from `start_angle` to `end_angle` (in radians, clockwise from +X).
974pub fn canvas_arc(
975    cx: f32,
976    cy: f32,
977    radius: f32,
978    start_angle: f32,
979    end_angle: f32,
980    r: u8,
981    g: u8,
982    b: u8,
983    a: u8,
984    thickness: f32,
985) {
986    unsafe {
987        _api_canvas_arc(
988            cx,
989            cy,
990            radius,
991            start_angle,
992            end_angle,
993            r as u32,
994            g as u32,
995            b as u32,
996            a as u32,
997            thickness,
998        )
999    }
1000}
1001
1002/// Draw a cubic Bézier curve stroke from `(x1,y1)` to `(x2,y2)` with two control points.
1003pub fn canvas_bezier(
1004    x1: f32,
1005    y1: f32,
1006    cp1x: f32,
1007    cp1y: f32,
1008    cp2x: f32,
1009    cp2y: f32,
1010    x2: f32,
1011    y2: f32,
1012    r: u8,
1013    g: u8,
1014    b: u8,
1015    a: u8,
1016    thickness: f32,
1017) {
1018    unsafe {
1019        _api_canvas_bezier(
1020            x1, y1, cp1x, cp1y, cp2x, cp2y, x2, y2, r as u32, g as u32, b as u32, a as u32,
1021            thickness,
1022        )
1023    }
1024}
1025
1026/// Gradient type constants.
1027pub const GRADIENT_LINEAR: u32 = 0;
1028pub const GRADIENT_RADIAL: u32 = 1;
1029
1030/// Draw a gradient-filled rectangle.
1031///
1032/// `kind`: [`GRADIENT_LINEAR`] or [`GRADIENT_RADIAL`].
1033/// For linear gradients, `(ax,ay)` and `(bx,by)` define the gradient axis.
1034/// For radial gradients, `(ax,ay)` is the center and `by` is the radius.
1035/// `stops` is a slice of `(offset, r, g, b, a)` tuples.
1036pub fn canvas_gradient(
1037    x: f32,
1038    y: f32,
1039    w: f32,
1040    h: f32,
1041    kind: u32,
1042    ax: f32,
1043    ay: f32,
1044    bx: f32,
1045    by: f32,
1046    stops: &[(f32, u8, u8, u8, u8)],
1047) {
1048    let mut buf = Vec::with_capacity(stops.len() * 8);
1049    for &(offset, r, g, b, a) in stops {
1050        buf.extend_from_slice(&offset.to_le_bytes());
1051        buf.push(r);
1052        buf.push(g);
1053        buf.push(b);
1054        buf.push(a);
1055    }
1056    unsafe {
1057        _api_canvas_gradient(
1058            x,
1059            y,
1060            w,
1061            h,
1062            kind,
1063            ax,
1064            ay,
1065            bx,
1066            by,
1067            buf.as_ptr() as u32,
1068            buf.len() as u32,
1069        )
1070    }
1071}
1072
1073// ─── Canvas State API ───────────────────────────────────────────────────────
1074
1075/// Push the current canvas state (transform, clip, opacity) onto an internal stack.
1076/// Use with [`canvas_restore`] to scope transformations and effects.
1077pub fn canvas_save() {
1078    unsafe { _api_canvas_save() }
1079}
1080
1081/// Pop and restore the most recently saved canvas state.
1082pub fn canvas_restore() {
1083    unsafe { _api_canvas_restore() }
1084}
1085
1086/// Apply a 2D affine transformation to subsequent draw commands.
1087///
1088/// The six values represent a column-major 3×2 matrix:
1089/// ```text
1090/// | a  c  tx |
1091/// | b  d  ty |
1092/// | 0  0   1 |
1093/// ```
1094///
1095/// For a simple translation, use `canvas_transform(1.0, 0.0, 0.0, 1.0, tx, ty)`.
1096pub fn canvas_transform(a: f32, b: f32, c: f32, d: f32, tx: f32, ty: f32) {
1097    unsafe { _api_canvas_transform(a, b, c, d, tx, ty) }
1098}
1099
1100/// Intersect the current clipping region with an axis-aligned rectangle.
1101/// Coordinates are in the current (possibly transformed) canvas space.
1102pub fn canvas_clip(x: f32, y: f32, w: f32, h: f32) {
1103    unsafe { _api_canvas_clip(x, y, w, h) }
1104}
1105
1106/// Set the layer opacity for subsequent draw commands (0.0 = transparent, 1.0 = opaque).
1107/// Multiplied with any parent opacity set via nested [`canvas_save`]/[`canvas_opacity`].
1108pub fn canvas_opacity(alpha: f32) {
1109    unsafe { _api_canvas_opacity(alpha) }
1110}
1111
1112// ─── GPU / WebGPU-style API ─────────────────────────────────────────────────
1113
1114/// GPU buffer usage flags (matches WebGPU `GPUBufferUsage`).
1115pub mod gpu_usage {
1116    pub const VERTEX: u32 = 0x0020;
1117    pub const INDEX: u32 = 0x0010;
1118    pub const UNIFORM: u32 = 0x0040;
1119    pub const STORAGE: u32 = 0x0080;
1120}
1121
1122/// Create a GPU buffer of `size` bytes. Returns a handle (0 = failure).
1123///
1124/// `usage` is a bitmask of [`gpu_usage`] flags.
1125pub fn gpu_create_buffer(size: u64, usage: u32) -> u32 {
1126    unsafe { _api_gpu_create_buffer(size as u32, (size >> 32) as u32, usage) }
1127}
1128
1129/// Create a 2D RGBA8 texture. Returns a handle (0 = failure).
1130pub fn gpu_create_texture(width: u32, height: u32) -> u32 {
1131    unsafe { _api_gpu_create_texture(width, height) }
1132}
1133
1134/// Compile a WGSL shader module. Returns a handle (0 = failure).
1135pub fn gpu_create_shader(source: &str) -> u32 {
1136    unsafe { _api_gpu_create_shader(source.as_ptr() as u32, source.len() as u32) }
1137}
1138
1139/// Create a render pipeline from a shader. Returns a handle (0 = failure).
1140///
1141/// `vertex_entry` and `fragment_entry` are the WGSL function names.
1142pub fn gpu_create_pipeline(shader: u32, vertex_entry: &str, fragment_entry: &str) -> u32 {
1143    unsafe {
1144        _api_gpu_create_render_pipeline(
1145            shader,
1146            vertex_entry.as_ptr() as u32,
1147            vertex_entry.len() as u32,
1148            fragment_entry.as_ptr() as u32,
1149            fragment_entry.len() as u32,
1150        )
1151    }
1152}
1153
1154/// Create a compute pipeline from a shader. Returns a handle (0 = failure).
1155pub fn gpu_create_compute_pipeline(shader: u32, entry_point: &str) -> u32 {
1156    unsafe {
1157        _api_gpu_create_compute_pipeline(
1158            shader,
1159            entry_point.as_ptr() as u32,
1160            entry_point.len() as u32,
1161        )
1162    }
1163}
1164
1165/// Write data to a GPU buffer at the given byte offset.
1166pub fn gpu_write_buffer(handle: u32, offset: u64, data: &[u8]) -> bool {
1167    unsafe {
1168        _api_gpu_write_buffer(
1169            handle,
1170            offset as u32,
1171            (offset >> 32) as u32,
1172            data.as_ptr() as u32,
1173            data.len() as u32,
1174        ) != 0
1175    }
1176}
1177
1178/// Submit a render pass: draw `vertex_count` vertices with `instance_count` instances.
1179pub fn gpu_draw(
1180    pipeline: u32,
1181    target_texture: u32,
1182    vertex_count: u32,
1183    instance_count: u32,
1184) -> bool {
1185    unsafe { _api_gpu_draw(pipeline, target_texture, vertex_count, instance_count) != 0 }
1186}
1187
1188/// Submit a compute dispatch with the given workgroup counts.
1189pub fn gpu_dispatch_compute(pipeline: u32, x: u32, y: u32, z: u32) -> bool {
1190    unsafe { _api_gpu_dispatch_compute(pipeline, x, y, z) != 0 }
1191}
1192
1193/// Destroy a GPU buffer.
1194pub fn gpu_destroy_buffer(handle: u32) -> bool {
1195    unsafe { _api_gpu_destroy_buffer(handle) != 0 }
1196}
1197
1198/// Destroy a GPU texture.
1199pub fn gpu_destroy_texture(handle: u32) -> bool {
1200    unsafe { _api_gpu_destroy_texture(handle) != 0 }
1201}
1202
1203// ─── Local Storage API ──────────────────────────────────────────────────────
1204
1205/// Store a key-value pair in sandboxed local storage.
1206pub fn storage_set(key: &str, value: &str) {
1207    unsafe {
1208        _api_storage_set(
1209            key.as_ptr() as u32,
1210            key.len() as u32,
1211            value.as_ptr() as u32,
1212            value.len() as u32,
1213        )
1214    }
1215}
1216
1217/// Retrieve a value from local storage. Returns empty string if not found.
1218pub fn storage_get(key: &str) -> String {
1219    let mut buf = [0u8; 4096];
1220    let len = unsafe {
1221        _api_storage_get(
1222            key.as_ptr() as u32,
1223            key.len() as u32,
1224            buf.as_mut_ptr() as u32,
1225            buf.len() as u32,
1226        )
1227    };
1228    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1229}
1230
1231/// Remove a key from local storage.
1232pub fn storage_remove(key: &str) {
1233    unsafe { _api_storage_remove(key.as_ptr() as u32, key.len() as u32) }
1234}
1235
1236// ─── Clipboard API ──────────────────────────────────────────────────────────
1237
1238/// Copy text to the system clipboard.
1239pub fn clipboard_write(text: &str) {
1240    unsafe { _api_clipboard_write(text.as_ptr() as u32, text.len() as u32) }
1241}
1242
1243/// Read text from the system clipboard.
1244pub fn clipboard_read() -> String {
1245    let mut buf = [0u8; 4096];
1246    let len = unsafe { _api_clipboard_read(buf.as_mut_ptr() as u32, buf.len() as u32) };
1247    String::from_utf8_lossy(&buf[..len as usize]).to_string()
1248}
1249
1250// ─── Timer / Clock API ─────────────────────────────────────────────────────
1251
1252/// Get the current time in milliseconds since the UNIX epoch.
1253pub fn time_now_ms() -> u64 {
1254    unsafe { _api_time_now_ms() }
1255}
1256
1257/// Schedule a one-shot timer that fires after `delay_ms` milliseconds.
1258/// When it fires the host calls your exported `on_timer(callback_id)`.
1259/// Returns a timer ID that can be passed to [`clear_timer`].
1260pub fn set_timeout(callback_id: u32, delay_ms: u32) -> u32 {
1261    unsafe { _api_set_timeout(callback_id, delay_ms) }
1262}
1263
1264/// Schedule a repeating timer that fires every `interval_ms` milliseconds.
1265/// When it fires the host calls your exported `on_timer(callback_id)`.
1266/// Returns a timer ID that can be passed to [`clear_timer`].
1267pub fn set_interval(callback_id: u32, interval_ms: u32) -> u32 {
1268    unsafe { _api_set_interval(callback_id, interval_ms) }
1269}
1270
1271/// Cancel a timer previously created with [`set_timeout`] or [`set_interval`].
1272pub fn clear_timer(timer_id: u32) {
1273    unsafe { _api_clear_timer(timer_id) }
1274}
1275
1276/// Schedule a callback for the next animation frame (vsync-aligned repaint).
1277///
1278/// The host calls your exported `on_timer(callback_id)` with the provided ID on the
1279/// subsequent frame. Returns a request ID usable with [`cancel_animation_frame`].
1280/// Call `request_animation_frame` again from inside the callback to keep animating.
1281pub fn request_animation_frame(callback_id: u32) -> u32 {
1282    unsafe { _api_request_animation_frame(callback_id) }
1283}
1284
1285/// Cancel a pending animation frame request.
1286pub fn cancel_animation_frame(request_id: u32) {
1287    unsafe { _api_cancel_animation_frame(request_id) }
1288}
1289
1290// ─── Random API ─────────────────────────────────────────────────────────────
1291
1292/// Get a random u64 from the host.
1293pub fn random_u64() -> u64 {
1294    unsafe { _api_random() }
1295}
1296
1297/// Get a random f64 in [0, 1).
1298pub fn random_f64() -> f64 {
1299    (random_u64() >> 11) as f64 / (1u64 << 53) as f64
1300}
1301
1302// ─── Notification API ───────────────────────────────────────────────────────
1303
1304/// Send a notification to the user (rendered in the browser console).
1305pub fn notify(title: &str, body: &str) {
1306    unsafe {
1307        _api_notify(
1308            title.as_ptr() as u32,
1309            title.len() as u32,
1310            body.as_ptr() as u32,
1311            body.len() as u32,
1312        )
1313    }
1314}
1315
1316// ─── Audio Playback API ─────────────────────────────────────────────────────
1317
1318/// Detected or hinted audio container (host codes: 0 unknown, 1 WAV, 2 MP3, 3 Ogg, 4 FLAC).
1319#[repr(u32)]
1320#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1321pub enum AudioFormat {
1322    /// Could not classify from bytes (try decode anyway).
1323    Unknown = 0,
1324    Wav = 1,
1325    Mp3 = 2,
1326    Ogg = 3,
1327    Flac = 4,
1328}
1329
1330impl From<u32> for AudioFormat {
1331    fn from(code: u32) -> Self {
1332        match code {
1333            1 => AudioFormat::Wav,
1334            2 => AudioFormat::Mp3,
1335            3 => AudioFormat::Ogg,
1336            4 => AudioFormat::Flac,
1337            _ => AudioFormat::Unknown,
1338        }
1339    }
1340}
1341
1342impl From<AudioFormat> for u32 {
1343    fn from(f: AudioFormat) -> u32 {
1344        f as u32
1345    }
1346}
1347
1348/// Play audio from encoded bytes (WAV, MP3, OGG, FLAC).
1349/// The host decodes and plays the audio. Returns 0 on success, negative on error.
1350pub fn audio_play(data: &[u8]) -> i32 {
1351    unsafe { _api_audio_play(data.as_ptr() as u32, data.len() as u32) }
1352}
1353
1354/// Sniff the container/codec from raw bytes (magic bytes / MP3 sync). Does not decode audio.
1355pub fn audio_detect_format(data: &[u8]) -> AudioFormat {
1356    let code = unsafe { _api_audio_detect_format(data.as_ptr() as u32, data.len() as u32) };
1357    AudioFormat::from(code)
1358}
1359
1360/// Play with an optional format hint (`AudioFormat::Unknown` = same as [`audio_play`]).
1361/// If the hint disagrees with what the host sniffs from the bytes, the host logs a warning but still decodes.
1362pub fn audio_play_with_format(data: &[u8], format: AudioFormat) -> i32 {
1363    unsafe {
1364        _api_audio_play_with_format(data.as_ptr() as u32, data.len() as u32, u32::from(format))
1365    }
1366}
1367
1368/// Fetch audio from a URL and play it.
1369/// The host sends an `Accept` header listing supported codecs, records the response `Content-Type`,
1370/// and rejects obvious HTML/JSON error bodies when no audio signature is found (`-4`).
1371/// Returns 0 on success, negative on error.
1372pub fn audio_play_url(url: &str) -> i32 {
1373    unsafe { _api_audio_play_url(url.as_ptr() as u32, url.len() as u32) }
1374}
1375
1376/// `Content-Type` header from the last successful [`audio_play_url`] response (may be empty).
1377pub fn audio_last_url_content_type() -> String {
1378    let mut buf = [0u8; 512];
1379    let len =
1380        unsafe { _api_audio_last_url_content_type(buf.as_mut_ptr() as u32, buf.len() as u32) };
1381    let n = (len as usize).min(buf.len());
1382    String::from_utf8_lossy(&buf[..n]).to_string()
1383}
1384
1385/// Pause audio playback.
1386pub fn audio_pause() {
1387    unsafe { _api_audio_pause() }
1388}
1389
1390/// Resume paused audio playback.
1391pub fn audio_resume() {
1392    unsafe { _api_audio_resume() }
1393}
1394
1395/// Stop audio playback and clear the queue.
1396pub fn audio_stop() {
1397    unsafe { _api_audio_stop() }
1398}
1399
1400/// Set audio volume. 1.0 is normal, 0.0 is silent, up to 2.0 for boost.
1401pub fn audio_set_volume(level: f32) {
1402    unsafe { _api_audio_set_volume(level) }
1403}
1404
1405/// Get the current audio volume.
1406pub fn audio_get_volume() -> f32 {
1407    unsafe { _api_audio_get_volume() }
1408}
1409
1410/// Returns `true` if audio is currently playing (not paused and not empty).
1411pub fn audio_is_playing() -> bool {
1412    unsafe { _api_audio_is_playing() != 0 }
1413}
1414
1415/// Get the current playback position in milliseconds.
1416pub fn audio_position() -> u64 {
1417    unsafe { _api_audio_position() }
1418}
1419
1420/// Seek to a position in milliseconds. Returns 0 on success, negative on error.
1421pub fn audio_seek(position_ms: u64) -> i32 {
1422    unsafe { _api_audio_seek(position_ms) }
1423}
1424
1425/// Get the total duration of the currently loaded track in milliseconds.
1426/// Returns 0 if unknown or nothing is loaded.
1427pub fn audio_duration() -> u64 {
1428    unsafe { _api_audio_duration() }
1429}
1430
1431/// Enable or disable looping on the default channel.
1432/// When enabled, subsequent `audio_play` calls will loop indefinitely.
1433pub fn audio_set_loop(enabled: bool) {
1434    unsafe { _api_audio_set_loop(if enabled { 1 } else { 0 }) }
1435}
1436
1437// ─── Multi-Channel Audio API ────────────────────────────────────────────────
1438
1439/// Play audio on a specific channel. Multiple channels play simultaneously.
1440/// Channel 0 is the default used by `audio_play`. Use channels 1+ for layered
1441/// sound effects, background music, etc.
1442pub fn audio_channel_play(channel: u32, data: &[u8]) -> i32 {
1443    unsafe { _api_audio_channel_play(channel, data.as_ptr() as u32, data.len() as u32) }
1444}
1445
1446/// Like [`audio_channel_play`] with an optional [`AudioFormat`] hint.
1447pub fn audio_channel_play_with_format(channel: u32, data: &[u8], format: AudioFormat) -> i32 {
1448    unsafe {
1449        _api_audio_channel_play_with_format(
1450            channel,
1451            data.as_ptr() as u32,
1452            data.len() as u32,
1453            u32::from(format),
1454        )
1455    }
1456}
1457
1458/// Stop playback on a specific channel.
1459pub fn audio_channel_stop(channel: u32) {
1460    unsafe { _api_audio_channel_stop(channel) }
1461}
1462
1463/// Set volume for a specific channel (0.0 silent, 1.0 normal, up to 2.0 boost).
1464pub fn audio_channel_set_volume(channel: u32, level: f32) {
1465    unsafe { _api_audio_channel_set_volume(channel, level) }
1466}
1467
1468// ─── Video API ─────────────────────────────────────────────────────────────
1469
1470/// Container or hint for [`video_load_with_format`] (host codes: 0 unknown, 1 MP4, 2 WebM, 3 AV1).
1471#[repr(u32)]
1472#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1473pub enum VideoFormat {
1474    Unknown = 0,
1475    Mp4 = 1,
1476    Webm = 2,
1477    Av1 = 3,
1478}
1479
1480impl From<u32> for VideoFormat {
1481    fn from(code: u32) -> Self {
1482        match code {
1483            1 => VideoFormat::Mp4,
1484            2 => VideoFormat::Webm,
1485            3 => VideoFormat::Av1,
1486            _ => VideoFormat::Unknown,
1487        }
1488    }
1489}
1490
1491impl From<VideoFormat> for u32 {
1492    fn from(f: VideoFormat) -> u32 {
1493        f as u32
1494    }
1495}
1496
1497/// Sniff container from leading bytes (magic only; does not decode).
1498pub fn video_detect_format(data: &[u8]) -> VideoFormat {
1499    let code = unsafe { _api_video_detect_format(data.as_ptr() as u32, data.len() as u32) };
1500    VideoFormat::from(code)
1501}
1502
1503/// Load video from encoded bytes (MP4, WebM, etc.). Requires FFmpeg on the host.
1504/// Returns 0 on success, negative on error.
1505pub fn video_load(data: &[u8]) -> i32 {
1506    unsafe {
1507        _api_video_load(
1508            data.as_ptr() as u32,
1509            data.len() as u32,
1510            VideoFormat::Unknown as u32,
1511        )
1512    }
1513}
1514
1515/// Load with a [`VideoFormat`] hint (unknown = same as [`video_load`]).
1516pub fn video_load_with_format(data: &[u8], format: VideoFormat) -> i32 {
1517    unsafe { _api_video_load(data.as_ptr() as u32, data.len() as u32, u32::from(format)) }
1518}
1519
1520/// Open a progressive or adaptive (HLS) URL. The host uses FFmpeg; master playlists may list variants.
1521pub fn video_load_url(url: &str) -> i32 {
1522    unsafe { _api_video_load_url(url.as_ptr() as u32, url.len() as u32) }
1523}
1524
1525/// `Content-Type` from the last successful [`video_load_url`] (may be empty).
1526pub fn video_last_url_content_type() -> String {
1527    let mut buf = [0u8; 512];
1528    let len =
1529        unsafe { _api_video_last_url_content_type(buf.as_mut_ptr() as u32, buf.len() as u32) };
1530    let n = (len as usize).min(buf.len());
1531    String::from_utf8_lossy(&buf[..n]).to_string()
1532}
1533
1534/// Number of variant stream URIs parsed from the last HLS master playlist (0 if not a master).
1535pub fn video_hls_variant_count() -> u32 {
1536    unsafe { _api_video_hls_variant_count() }
1537}
1538
1539/// Resolved variant URL for `index`, written into `buf`-style API (use fixed buffer).
1540pub fn video_hls_variant_url(index: u32) -> String {
1541    let mut buf = [0u8; 2048];
1542    let len =
1543        unsafe { _api_video_hls_variant_url(index, buf.as_mut_ptr() as u32, buf.len() as u32) };
1544    let n = (len as usize).min(buf.len());
1545    String::from_utf8_lossy(&buf[..n]).to_string()
1546}
1547
1548/// Open a variant playlist by index (after loading a master with [`video_load_url`]).
1549pub fn video_hls_open_variant(index: u32) -> i32 {
1550    unsafe { _api_video_hls_open_variant(index) }
1551}
1552
1553pub fn video_play() {
1554    unsafe { _api_video_play() }
1555}
1556
1557pub fn video_pause() {
1558    unsafe { _api_video_pause() }
1559}
1560
1561pub fn video_stop() {
1562    unsafe { _api_video_stop() }
1563}
1564
1565pub fn video_seek(position_ms: u64) -> i32 {
1566    unsafe { _api_video_seek(position_ms) }
1567}
1568
1569pub fn video_position() -> u64 {
1570    unsafe { _api_video_position() }
1571}
1572
1573pub fn video_duration() -> u64 {
1574    unsafe { _api_video_duration() }
1575}
1576
1577/// Draw the current video frame into the given rectangle (same coordinate space as canvas).
1578pub fn video_render(x: f32, y: f32, w: f32, h: f32) -> i32 {
1579    unsafe { _api_video_render(x, y, w, h) }
1580}
1581
1582/// Volume multiplier for the video track (0.0–2.0; embedded audio mixing may follow in future hosts).
1583pub fn video_set_volume(level: f32) {
1584    unsafe { _api_video_set_volume(level) }
1585}
1586
1587pub fn video_get_volume() -> f32 {
1588    unsafe { _api_video_get_volume() }
1589}
1590
1591pub fn video_set_loop(enabled: bool) {
1592    unsafe { _api_video_set_loop(if enabled { 1 } else { 0 }) }
1593}
1594
1595/// Floating picture-in-picture preview (host mirrors the last rendered frame).
1596pub fn video_set_pip(enabled: bool) {
1597    unsafe { _api_video_set_pip(if enabled { 1 } else { 0 }) }
1598}
1599
1600/// Load SubRip subtitles (cues rendered on [`video_render`]).
1601pub fn subtitle_load_srt(text: &str) -> i32 {
1602    unsafe { _api_subtitle_load_srt(text.as_ptr() as u32, text.len() as u32) }
1603}
1604
1605/// Load WebVTT subtitles.
1606pub fn subtitle_load_vtt(text: &str) -> i32 {
1607    unsafe { _api_subtitle_load_vtt(text.as_ptr() as u32, text.len() as u32) }
1608}
1609
1610pub fn subtitle_clear() {
1611    unsafe { _api_subtitle_clear() }
1612}
1613
1614// ─── Media capture API ─────────────────────────────────────────────────────
1615
1616/// Opens the default camera after a host permission dialog.
1617///
1618/// Returns `0` on success. Negative codes: `-1` user denied, `-2` no camera, `-3` open failed.
1619pub fn camera_open() -> i32 {
1620    unsafe { _api_camera_open() }
1621}
1622
1623/// Stops the camera stream opened by [`camera_open`].
1624pub fn camera_close() {
1625    unsafe { _api_camera_close() }
1626}
1627
1628/// Captures one RGBA8 frame into `out`. Returns the number of bytes written (`0` if the camera
1629/// is not open or capture failed). Query [`camera_frame_dimensions`] after a successful write.
1630pub fn camera_capture_frame(out: &mut [u8]) -> u32 {
1631    unsafe { _api_camera_capture_frame(out.as_mut_ptr() as u32, out.len() as u32) }
1632}
1633
1634/// Width and height in pixels of the last [`camera_capture_frame`] buffer.
1635pub fn camera_frame_dimensions() -> (u32, u32) {
1636    let packed = unsafe { _api_camera_frame_dimensions() };
1637    let w = (packed >> 32) as u32;
1638    let h = packed as u32;
1639    (w, h)
1640}
1641
1642/// Starts microphone capture (mono `f32` ring buffer) after a host permission dialog.
1643///
1644/// Returns `0` on success. Negative codes: `-1` denied, `-2` no input device, `-3` stream error.
1645pub fn microphone_open() -> i32 {
1646    unsafe { _api_microphone_open() }
1647}
1648
1649pub fn microphone_close() {
1650    unsafe { _api_microphone_close() }
1651}
1652
1653/// Sample rate of the opened input stream in Hz (`0` if the microphone is not open).
1654pub fn microphone_sample_rate() -> u32 {
1655    unsafe { _api_microphone_sample_rate() }
1656}
1657
1658/// Dequeues up to `out.len()` mono `f32` samples from the microphone ring buffer.
1659/// Returns how many samples were written.
1660pub fn microphone_read_samples(out: &mut [f32]) -> u32 {
1661    unsafe { _api_microphone_read_samples(out.as_mut_ptr() as u32, out.len() as u32) }
1662}
1663
1664/// Captures the primary display as RGBA8 after permission dialogs (OS may prompt separately).
1665///
1666/// Returns `Ok(bytes_written)` or an error code: `-1` denied, `-2` no display, `-3` capture failed, `-4` buffer error.
1667pub fn screen_capture(out: &mut [u8]) -> Result<usize, i32> {
1668    let n = unsafe { _api_screen_capture(out.as_mut_ptr() as u32, out.len() as u32) };
1669    if n >= 0 {
1670        Ok(n as usize)
1671    } else {
1672        Err(n)
1673    }
1674}
1675
1676/// Width and height of the last [`screen_capture`] image.
1677pub fn screen_capture_dimensions() -> (u32, u32) {
1678    let packed = unsafe { _api_screen_capture_dimensions() };
1679    let w = (packed >> 32) as u32;
1680    let h = packed as u32;
1681    (w, h)
1682}
1683
1684/// Host-side pipeline counters: total camera frames captured (high 32 bits) and current microphone
1685/// ring depth in samples (low 32 bits).
1686pub fn media_pipeline_stats() -> (u64, u32) {
1687    let packed = unsafe { _api_media_pipeline_stats() };
1688    let camera_frames = packed >> 32;
1689    let mic_ring = packed as u32;
1690    (camera_frames, mic_ring)
1691}
1692
1693// ─── WebRTC / Real-Time Communication API ───────────────────────────────────
1694
1695/// Connection state returned by [`rtc_connection_state`].
1696pub const RTC_STATE_NEW: u32 = 0;
1697/// Peer is attempting to connect.
1698pub const RTC_STATE_CONNECTING: u32 = 1;
1699/// Peer connection is established.
1700pub const RTC_STATE_CONNECTED: u32 = 2;
1701/// Transport was temporarily interrupted.
1702pub const RTC_STATE_DISCONNECTED: u32 = 3;
1703/// Connection attempt failed.
1704pub const RTC_STATE_FAILED: u32 = 4;
1705/// Peer connection has been closed.
1706pub const RTC_STATE_CLOSED: u32 = 5;
1707
1708/// Track kind: audio.
1709pub const RTC_TRACK_AUDIO: u32 = 0;
1710/// Track kind: video.
1711pub const RTC_TRACK_VIDEO: u32 = 1;
1712
1713/// Received data channel message.
1714pub struct RtcMessage {
1715    /// Channel on which the message arrived.
1716    pub channel_id: u32,
1717    /// `true` when the payload is raw bytes, `false` for UTF-8 text.
1718    pub is_binary: bool,
1719    /// Message payload.
1720    pub data: Vec<u8>,
1721}
1722
1723impl RtcMessage {
1724    /// Interpret the payload as UTF-8 text.
1725    pub fn text(&self) -> String {
1726        String::from_utf8_lossy(&self.data).to_string()
1727    }
1728}
1729
1730/// Information about a newly opened remote data channel.
1731pub struct RtcDataChannelInfo {
1732    /// Handle to use with [`rtc_send`] and [`rtc_recv`].
1733    pub channel_id: u32,
1734    /// Label chosen by the remote peer.
1735    pub label: String,
1736}
1737
1738/// Create a new WebRTC peer connection.
1739///
1740/// `stun_servers` is a comma-separated list of STUN/TURN URLs (e.g.
1741/// `"stun:stun.l.google.com:19302"`). Pass `""` for the built-in default.
1742///
1743/// Returns a peer handle (`> 0`) or `0` on failure.
1744pub fn rtc_create_peer(stun_servers: &str) -> u32 {
1745    unsafe { _api_rtc_create_peer(stun_servers.as_ptr() as u32, stun_servers.len() as u32) }
1746}
1747
1748/// Close and release a peer connection.
1749pub fn rtc_close_peer(peer_id: u32) -> bool {
1750    unsafe { _api_rtc_close_peer(peer_id) != 0 }
1751}
1752
1753/// Generate an SDP offer for the peer and set it as the local description.
1754///
1755/// Returns the SDP string or an error code.
1756pub fn rtc_create_offer(peer_id: u32) -> Result<String, i32> {
1757    let mut buf = vec![0u8; 16 * 1024];
1758    let n = unsafe { _api_rtc_create_offer(peer_id, buf.as_mut_ptr() as u32, buf.len() as u32) };
1759    if n < 0 {
1760        Err(n)
1761    } else {
1762        Ok(String::from_utf8_lossy(&buf[..n as usize]).to_string())
1763    }
1764}
1765
1766/// Generate an SDP answer (after setting the remote offer) and set it as the local description.
1767pub fn rtc_create_answer(peer_id: u32) -> Result<String, i32> {
1768    let mut buf = vec![0u8; 16 * 1024];
1769    let n = unsafe { _api_rtc_create_answer(peer_id, buf.as_mut_ptr() as u32, buf.len() as u32) };
1770    if n < 0 {
1771        Err(n)
1772    } else {
1773        Ok(String::from_utf8_lossy(&buf[..n as usize]).to_string())
1774    }
1775}
1776
1777/// Set the local SDP description explicitly.
1778///
1779/// `is_offer` — `true` for an offer, `false` for an answer.
1780pub fn rtc_set_local_description(peer_id: u32, sdp: &str, is_offer: bool) -> i32 {
1781    unsafe {
1782        _api_rtc_set_local_description(
1783            peer_id,
1784            sdp.as_ptr() as u32,
1785            sdp.len() as u32,
1786            if is_offer { 1 } else { 0 },
1787        )
1788    }
1789}
1790
1791/// Set the remote SDP description received from the other peer.
1792pub fn rtc_set_remote_description(peer_id: u32, sdp: &str, is_offer: bool) -> i32 {
1793    unsafe {
1794        _api_rtc_set_remote_description(
1795            peer_id,
1796            sdp.as_ptr() as u32,
1797            sdp.len() as u32,
1798            if is_offer { 1 } else { 0 },
1799        )
1800    }
1801}
1802
1803/// Add a trickled ICE candidate (JSON string from the remote peer).
1804pub fn rtc_add_ice_candidate(peer_id: u32, candidate_json: &str) -> i32 {
1805    unsafe {
1806        _api_rtc_add_ice_candidate(
1807            peer_id,
1808            candidate_json.as_ptr() as u32,
1809            candidate_json.len() as u32,
1810        )
1811    }
1812}
1813
1814/// Poll the current connection state of a peer.
1815pub fn rtc_connection_state(peer_id: u32) -> u32 {
1816    unsafe { _api_rtc_connection_state(peer_id) }
1817}
1818
1819/// Poll for a locally gathered ICE candidate (JSON). Returns `None` when the
1820/// queue is empty.
1821pub fn rtc_poll_ice_candidate(peer_id: u32) -> Option<String> {
1822    let mut buf = vec![0u8; 4096];
1823    let n =
1824        unsafe { _api_rtc_poll_ice_candidate(peer_id, buf.as_mut_ptr() as u32, buf.len() as u32) };
1825    if n <= 0 {
1826        None
1827    } else {
1828        Some(String::from_utf8_lossy(&buf[..n as usize]).to_string())
1829    }
1830}
1831
1832/// Create a data channel on a peer connection.
1833///
1834/// `ordered` — `true` for reliable ordered delivery (TCP-like), `false` for
1835/// unordered (UDP-like). Returns a channel handle (`> 0`) or `0` on failure.
1836pub fn rtc_create_data_channel(peer_id: u32, label: &str, ordered: bool) -> u32 {
1837    unsafe {
1838        _api_rtc_create_data_channel(
1839            peer_id,
1840            label.as_ptr() as u32,
1841            label.len() as u32,
1842            if ordered { 1 } else { 0 },
1843        )
1844    }
1845}
1846
1847/// Send a UTF-8 text message on a data channel.
1848pub fn rtc_send_text(peer_id: u32, channel_id: u32, text: &str) -> i32 {
1849    unsafe {
1850        _api_rtc_send(
1851            peer_id,
1852            channel_id,
1853            text.as_ptr() as u32,
1854            text.len() as u32,
1855            0,
1856        )
1857    }
1858}
1859
1860/// Send binary data on a data channel.
1861pub fn rtc_send_binary(peer_id: u32, channel_id: u32, data: &[u8]) -> i32 {
1862    unsafe {
1863        _api_rtc_send(
1864            peer_id,
1865            channel_id,
1866            data.as_ptr() as u32,
1867            data.len() as u32,
1868            1,
1869        )
1870    }
1871}
1872
1873/// Send data on a channel, choosing text or binary mode.
1874pub fn rtc_send(peer_id: u32, channel_id: u32, data: &[u8], is_binary: bool) -> i32 {
1875    unsafe {
1876        _api_rtc_send(
1877            peer_id,
1878            channel_id,
1879            data.as_ptr() as u32,
1880            data.len() as u32,
1881            if is_binary { 1 } else { 0 },
1882        )
1883    }
1884}
1885
1886/// Poll for an incoming message on any channel of the peer (pass `channel_id = 0`)
1887/// or on a specific channel.
1888///
1889/// Returns `None` when no message is queued.
1890pub fn rtc_recv(peer_id: u32, channel_id: u32) -> Option<RtcMessage> {
1891    let mut buf = vec![0u8; 64 * 1024];
1892    let packed = unsafe {
1893        _api_rtc_recv(
1894            peer_id,
1895            channel_id,
1896            buf.as_mut_ptr() as u32,
1897            buf.len() as u32,
1898        )
1899    };
1900    if packed <= 0 {
1901        return None;
1902    }
1903    let packed = packed as u64;
1904    let data_len = (packed & 0xFFFF_FFFF) as usize;
1905    let is_binary = (packed >> 32) & 1 != 0;
1906    let ch = (packed >> 48) as u32;
1907    Some(RtcMessage {
1908        channel_id: ch,
1909        is_binary,
1910        data: buf[..data_len].to_vec(),
1911    })
1912}
1913
1914/// Poll for a remotely-created data channel that the peer opened.
1915///
1916/// Returns `None` when no new channels are pending.
1917pub fn rtc_poll_data_channel(peer_id: u32) -> Option<RtcDataChannelInfo> {
1918    let mut buf = vec![0u8; 1024];
1919    let n =
1920        unsafe { _api_rtc_poll_data_channel(peer_id, buf.as_mut_ptr() as u32, buf.len() as u32) };
1921    if n <= 0 {
1922        return None;
1923    }
1924    let info = String::from_utf8_lossy(&buf[..n as usize]).to_string();
1925    let (id_str, label) = info.split_once(':').unwrap_or(("0", ""));
1926    Some(RtcDataChannelInfo {
1927        channel_id: id_str.parse().unwrap_or(0),
1928        label: label.to_string(),
1929    })
1930}
1931
1932/// Attach a media track (audio or video) to a peer connection.
1933///
1934/// `kind` — [`RTC_TRACK_AUDIO`] or [`RTC_TRACK_VIDEO`].
1935/// Returns a track handle (`> 0`) or `0` on failure.
1936pub fn rtc_add_track(peer_id: u32, kind: u32) -> u32 {
1937    unsafe { _api_rtc_add_track(peer_id, kind) }
1938}
1939
1940/// Information about a remote media track received from a peer.
1941pub struct RtcTrackInfo {
1942    /// `RTC_TRACK_AUDIO` (0) or `RTC_TRACK_VIDEO` (1).
1943    pub kind: u32,
1944    /// Track identifier chosen by the remote peer.
1945    pub id: String,
1946    /// Media stream identifier the track belongs to.
1947    pub stream_id: String,
1948}
1949
1950/// Poll for a remote media track added by the peer.
1951///
1952/// Returns `None` when no new tracks are pending.
1953pub fn rtc_poll_track(peer_id: u32) -> Option<RtcTrackInfo> {
1954    let mut buf = vec![0u8; 1024];
1955    let n = unsafe { _api_rtc_poll_track(peer_id, buf.as_mut_ptr() as u32, buf.len() as u32) };
1956    if n <= 0 {
1957        return None;
1958    }
1959    let info = String::from_utf8_lossy(&buf[..n as usize]).to_string();
1960    let mut parts = info.splitn(3, ':');
1961    let kind = parts.next().unwrap_or("2").parse().unwrap_or(2);
1962    let id = parts.next().unwrap_or("").to_string();
1963    let stream_id = parts.next().unwrap_or("").to_string();
1964    Some(RtcTrackInfo {
1965        kind,
1966        id,
1967        stream_id,
1968    })
1969}
1970
1971/// Connect to a signaling server at `url` for bootstrapping peer connections.
1972///
1973/// Returns `1` on success, `0` on failure.
1974pub fn rtc_signal_connect(url: &str) -> bool {
1975    unsafe { _api_rtc_signal_connect(url.as_ptr() as u32, url.len() as u32) != 0 }
1976}
1977
1978/// Join (or create) a signaling room for peer discovery.
1979pub fn rtc_signal_join_room(room: &str) -> i32 {
1980    unsafe { _api_rtc_signal_join_room(room.as_ptr() as u32, room.len() as u32) }
1981}
1982
1983/// Send a signaling message (JSON bytes) to the connected signaling server.
1984pub fn rtc_signal_send(data: &[u8]) -> i32 {
1985    unsafe { _api_rtc_signal_send(data.as_ptr() as u32, data.len() as u32) }
1986}
1987
1988/// Poll for an incoming signaling message.
1989pub fn rtc_signal_recv() -> Option<Vec<u8>> {
1990    let mut buf = vec![0u8; 16 * 1024];
1991    let n = unsafe { _api_rtc_signal_recv(buf.as_mut_ptr() as u32, buf.len() as u32) };
1992    if n <= 0 {
1993        None
1994    } else {
1995        Some(buf[..n as usize].to_vec())
1996    }
1997}
1998
1999// ─── WebSocket API ───────────────────────────────────────────────────────────
2000
2001/// WebSocket ready-state: connection is being established.
2002pub const WS_CONNECTING: u32 = 0;
2003/// WebSocket ready-state: connection is open and ready.
2004pub const WS_OPEN: u32 = 1;
2005/// WebSocket ready-state: close handshake in progress.
2006pub const WS_CLOSING: u32 = 2;
2007/// WebSocket ready-state: connection is closed.
2008pub const WS_CLOSED: u32 = 3;
2009
2010/// A received WebSocket message.
2011pub struct WsMessage {
2012    /// `true` when the payload is raw binary; `false` for UTF-8 text.
2013    pub is_binary: bool,
2014    /// Frame payload.
2015    pub data: Vec<u8>,
2016}
2017
2018impl WsMessage {
2019    /// Interpret the payload as a UTF-8 string.
2020    pub fn text(&self) -> String {
2021        String::from_utf8_lossy(&self.data).to_string()
2022    }
2023}
2024
2025/// Open a WebSocket connection to `url` (e.g. `"ws://example.com/chat"`).
2026///
2027/// Returns a connection handle (`> 0`) on success, or `0` on error.
2028/// The connection is established asynchronously; poll [`ws_ready_state`] until
2029/// it returns [`WS_OPEN`] before sending frames.
2030pub fn ws_connect(url: &str) -> u32 {
2031    unsafe { _api_ws_connect(url.as_ptr() as u32, url.len() as u32) }
2032}
2033
2034/// Send a UTF-8 text frame on the given connection.
2035///
2036/// Returns `0` on success, `-1` if the connection is unknown or closed.
2037pub fn ws_send_text(id: u32, text: &str) -> i32 {
2038    unsafe { _api_ws_send_text(id, text.as_ptr() as u32, text.len() as u32) }
2039}
2040
2041/// Send a binary frame on the given connection.
2042///
2043/// Returns `0` on success, `-1` if the connection is unknown or closed.
2044pub fn ws_send_binary(id: u32, data: &[u8]) -> i32 {
2045    unsafe { _api_ws_send_binary(id, data.as_ptr() as u32, data.len() as u32) }
2046}
2047
2048/// Poll for the next queued incoming frame on `id`.
2049///
2050/// Returns `Some(WsMessage)` if a frame is available, or `None` if the queue
2051/// is empty.  The internal receive buffer is 64 KB; larger frames are
2052/// truncated to that size.
2053pub fn ws_recv(id: u32) -> Option<WsMessage> {
2054    let mut buf = vec![0u8; 64 * 1024];
2055    let result = unsafe { _api_ws_recv(id, buf.as_mut_ptr() as u32, buf.len() as u32) };
2056    if result < 0 {
2057        return None;
2058    }
2059    let len = (result & 0xFFFF_FFFF) as usize;
2060    let is_binary = (result >> 32) & 1 == 1;
2061    Some(WsMessage {
2062        is_binary,
2063        data: buf[..len].to_vec(),
2064    })
2065}
2066
2067/// Query the current ready-state of a connection.
2068///
2069/// Returns one of [`WS_CONNECTING`], [`WS_OPEN`], [`WS_CLOSING`], or [`WS_CLOSED`].
2070pub fn ws_ready_state(id: u32) -> u32 {
2071    unsafe { _api_ws_ready_state(id) }
2072}
2073
2074/// Initiate a graceful close handshake on `id`.
2075///
2076/// Returns `1` if the close was initiated, `0` if the handle is unknown.
2077/// After calling this function the connection will transition to [`WS_CLOSED`]
2078/// asynchronously.  Call [`ws_remove`] once the state is [`WS_CLOSED`] to free
2079/// host resources.
2080pub fn ws_close(id: u32) -> i32 {
2081    unsafe { _api_ws_close(id) }
2082}
2083
2084/// Release host-side resources for a closed connection.
2085///
2086/// Call this after [`ws_ready_state`] returns [`WS_CLOSED`] to avoid resource
2087/// leaks.
2088pub fn ws_remove(id: u32) {
2089    unsafe { _api_ws_remove(id) }
2090}
2091
2092// ─── MIDI API ────────────────────────────────────────────────────────────────
2093
2094/// Number of available MIDI input ports (physical and virtual).
2095pub fn midi_input_count() -> u32 {
2096    unsafe { _api_midi_input_count() }
2097}
2098
2099/// Number of available MIDI output ports.
2100pub fn midi_output_count() -> u32 {
2101    unsafe { _api_midi_output_count() }
2102}
2103
2104/// Name of the MIDI input port at `index`.
2105///
2106/// Returns an empty string if the index is out of range.
2107pub fn midi_input_name(index: u32) -> String {
2108    let mut buf = [0u8; 128];
2109    let len = unsafe { _api_midi_input_name(index, buf.as_mut_ptr() as u32, buf.len() as u32) };
2110    String::from_utf8_lossy(&buf[..len as usize]).to_string()
2111}
2112
2113/// Name of the MIDI output port at `index`.
2114///
2115/// Returns an empty string if the index is out of range.
2116pub fn midi_output_name(index: u32) -> String {
2117    let mut buf = [0u8; 128];
2118    let len = unsafe { _api_midi_output_name(index, buf.as_mut_ptr() as u32, buf.len() as u32) };
2119    String::from_utf8_lossy(&buf[..len as usize]).to_string()
2120}
2121
2122/// Open a MIDI input port by index and start receiving messages.
2123///
2124/// Returns a handle (`> 0`) on success, or `0` if the port could not be opened.
2125/// Incoming messages are queued internally; drain them with [`midi_recv`].
2126pub fn midi_open_input(index: u32) -> u32 {
2127    unsafe { _api_midi_open_input(index) }
2128}
2129
2130/// Open a MIDI output port by index for sending messages.
2131///
2132/// Returns a handle (`> 0`) on success, or `0` on failure.
2133pub fn midi_open_output(index: u32) -> u32 {
2134    unsafe { _api_midi_open_output(index) }
2135}
2136
2137/// Send raw MIDI bytes on an output `handle`.
2138///
2139/// Returns `0` on success, `-1` if the handle is unknown or the send failed.
2140pub fn midi_send(handle: u32, data: &[u8]) -> i32 {
2141    unsafe { _api_midi_send(handle, data.as_ptr() as u32, data.len() as u32) }
2142}
2143
2144/// Poll for the next queued MIDI message on an input `handle`.
2145///
2146/// Returns `Some(bytes)` with exactly one MIDI message if one is available,
2147/// or `None` if the queue is empty. Channel-voice messages are 2–3 bytes;
2148/// SysEx can be longer. The wrapper first tries a 256-byte stack buffer and
2149/// transparently retries with a 64 KB heap buffer for large SysEx dumps.
2150pub fn midi_recv(handle: u32) -> Option<Vec<u8>> {
2151    let mut buf = [0u8; 256];
2152    let n = unsafe { _api_midi_recv(handle, buf.as_mut_ptr() as u32, buf.len() as u32) };
2153    if n >= 0 {
2154        return Some(buf[..n as usize].to_vec());
2155    }
2156    // -2 = buffer too small; message is still queued. Retry with 64 KB heap buffer.
2157    if n == -2 {
2158        let mut big = vec![0u8; 64 * 1024];
2159        let n2 = unsafe { _api_midi_recv(handle, big.as_mut_ptr() as u32, big.len() as u32) };
2160        if n2 >= 0 {
2161            big.truncate(n2 as usize);
2162            return Some(big);
2163        }
2164    }
2165    None
2166}
2167
2168/// Close a MIDI input or output handle and free host-side resources.
2169pub fn midi_close(handle: u32) {
2170    unsafe { _api_midi_close(handle) }
2171}
2172
2173// ─── HTTP Fetch API ─────────────────────────────────────────────────────────
2174
2175/// Response from an HTTP fetch call.
2176pub struct FetchResponse {
2177    pub status: u32,
2178    pub body: Vec<u8>,
2179}
2180
2181impl FetchResponse {
2182    /// Interpret the response body as UTF-8 text.
2183    pub fn text(&self) -> String {
2184        String::from_utf8_lossy(&self.body).to_string()
2185    }
2186}
2187
2188/// Perform an HTTP request.  Returns the status code and response body.
2189///
2190/// `content_type` sets the `Content-Type` header (pass `""` to omit).
2191/// Protobuf is the native format — use `"application/protobuf"` for binary
2192/// payloads.
2193pub fn fetch(
2194    method: &str,
2195    url: &str,
2196    content_type: &str,
2197    body: &[u8],
2198) -> Result<FetchResponse, i64> {
2199    let mut out_buf = vec![0u8; 4 * 1024 * 1024]; // 4 MB response buffer
2200    let result = unsafe {
2201        _api_fetch(
2202            method.as_ptr() as u32,
2203            method.len() as u32,
2204            url.as_ptr() as u32,
2205            url.len() as u32,
2206            content_type.as_ptr() as u32,
2207            content_type.len() as u32,
2208            body.as_ptr() as u32,
2209            body.len() as u32,
2210            out_buf.as_mut_ptr() as u32,
2211            out_buf.len() as u32,
2212        )
2213    };
2214    if result < 0 {
2215        return Err(result);
2216    }
2217    let status = (result >> 32) as u32;
2218    let body_len = (result & 0xFFFF_FFFF) as usize;
2219    Ok(FetchResponse {
2220        status,
2221        body: out_buf[..body_len].to_vec(),
2222    })
2223}
2224
2225/// HTTP GET request.
2226pub fn fetch_get(url: &str) -> Result<FetchResponse, i64> {
2227    fetch("GET", url, "", &[])
2228}
2229
2230/// HTTP POST with raw bytes.
2231pub fn fetch_post(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
2232    fetch("POST", url, content_type, body)
2233}
2234
2235/// HTTP POST with protobuf body (sets `Content-Type: application/protobuf`).
2236pub fn fetch_post_proto(url: &str, msg: &proto::ProtoEncoder) -> Result<FetchResponse, i64> {
2237    fetch("POST", url, "application/protobuf", msg.as_bytes())
2238}
2239
2240/// HTTP PUT with raw bytes.
2241pub fn fetch_put(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
2242    fetch("PUT", url, content_type, body)
2243}
2244
2245/// HTTP DELETE.
2246pub fn fetch_delete(url: &str) -> Result<FetchResponse, i64> {
2247    fetch("DELETE", url, "", &[])
2248}
2249
2250// ─── Streaming / non-blocking fetch ─────────────────────────────────────────
2251//
2252// The [`fetch`] family above blocks the guest until the response is fully
2253// downloaded. For LLM token streams, large downloads, chunked feeds, or any
2254// app that wants to keep rendering while a request is in flight, use the
2255// handle-based API below. It mirrors the WebSocket API: dispatch with
2256// `fetch_begin`, then poll `fetch_state`, `fetch_status`, and `fetch_recv`.
2257
2258/// Request dispatched; waiting for response headers.
2259pub const FETCH_PENDING: u32 = 0;
2260/// Headers received; body chunks may still be arriving.
2261pub const FETCH_STREAMING: u32 = 1;
2262/// Body fully delivered (the queue may still have trailing chunks to drain).
2263pub const FETCH_DONE: u32 = 2;
2264/// Request failed. Call [`fetch_error`] for the message.
2265pub const FETCH_ERROR: u32 = 3;
2266/// Request was aborted by the guest.
2267pub const FETCH_ABORTED: u32 = 4;
2268
2269/// Result of a non-blocking [`fetch_recv`] poll.
2270pub enum FetchChunk {
2271    /// One body chunk (may be part of a larger network chunk if it didn't fit
2272    /// in the caller's buffer).
2273    Data(Vec<u8>),
2274    /// No chunk is available right now, but more may still arrive. Call
2275    /// [`fetch_recv`] again next frame.
2276    Pending,
2277    /// The body has been fully delivered and all chunks have been drained.
2278    End,
2279    /// The request failed or was aborted. Inspect [`fetch_state`] and
2280    /// [`fetch_error`] for details.
2281    Error,
2282}
2283
2284/// Dispatch an HTTP request that streams its response back to the guest.
2285///
2286/// Returns a handle (`> 0`) that identifies the request for subsequent polls,
2287/// or `0` if the host could not initialise the fetch subsystem. The call
2288/// returns immediately — the request is driven by a background task.
2289///
2290/// Pass `""` for `content_type` to omit the header, and `&[]` for `body` on
2291/// requests without a payload.
2292pub fn fetch_begin(method: &str, url: &str, content_type: &str, body: &[u8]) -> u32 {
2293    unsafe {
2294        _api_fetch_begin(
2295            method.as_ptr() as u32,
2296            method.len() as u32,
2297            url.as_ptr() as u32,
2298            url.len() as u32,
2299            content_type.as_ptr() as u32,
2300            content_type.len() as u32,
2301            body.as_ptr() as u32,
2302            body.len() as u32,
2303        )
2304    }
2305}
2306
2307/// Convenience wrapper for GET.
2308pub fn fetch_begin_get(url: &str) -> u32 {
2309    fetch_begin("GET", url, "", &[])
2310}
2311
2312/// Current lifecycle state of a streaming request. See the `FETCH_*` constants.
2313pub fn fetch_state(handle: u32) -> u32 {
2314    unsafe { _api_fetch_state(handle) }
2315}
2316
2317/// HTTP status code for `handle`, or `0` until the response headers arrive.
2318pub fn fetch_status(handle: u32) -> u32 {
2319    unsafe { _api_fetch_status(handle) }
2320}
2321
2322/// Poll the next body chunk into a caller-provided scratch buffer.
2323///
2324/// Use this form when you want to avoid per-chunk heap allocations. Prefer
2325/// [`fetch_recv`] for ergonomics in higher-level code.
2326///
2327/// Returns the number of bytes written into `buf` (which may be smaller than
2328/// the chunk the host has queued — in which case the remainder will be
2329/// returned on the next call), or one of the negative sentinels documented by
2330/// the host (`-1` pending, `-2` EOF, `-3` error, `-4` unknown handle).
2331pub fn fetch_recv_into(handle: u32, buf: &mut [u8]) -> i64 {
2332    unsafe { _api_fetch_recv(handle, buf.as_mut_ptr() as u32, buf.len() as u32) }
2333}
2334
2335/// Poll the next body chunk as an owned `Vec<u8>`.
2336///
2337/// Chunks larger than 64 KiB are read in 64 KiB slices; call `fetch_recv`
2338/// repeatedly to drain the full network chunk.
2339pub fn fetch_recv(handle: u32) -> FetchChunk {
2340    let mut buf = vec![0u8; 64 * 1024];
2341    let n = fetch_recv_into(handle, &mut buf);
2342    match n {
2343        -1 => FetchChunk::Pending,
2344        -2 => FetchChunk::End,
2345        -3 | -4 => FetchChunk::Error,
2346        n if n >= 0 => {
2347            buf.truncate(n as usize);
2348            FetchChunk::Data(buf)
2349        }
2350        _ => FetchChunk::Error,
2351    }
2352}
2353
2354/// Retrieve the error message for a failed request, if any.
2355pub fn fetch_error(handle: u32) -> Option<String> {
2356    let mut buf = [0u8; 512];
2357    let n = unsafe { _api_fetch_error(handle, buf.as_mut_ptr() as u32, buf.len() as u32) };
2358    if n < 0 {
2359        None
2360    } else {
2361        Some(String::from_utf8_lossy(&buf[..n as usize]).into_owned())
2362    }
2363}
2364
2365/// Abort an in-flight request. Returns `true` if the handle was known.
2366///
2367/// The request transitions to [`FETCH_ABORTED`]; any body chunks already
2368/// queued remain readable via [`fetch_recv`] until drained.
2369pub fn fetch_abort(handle: u32) -> bool {
2370    unsafe { _api_fetch_abort(handle) != 0 }
2371}
2372
2373/// Free host-side resources for a completed or aborted request.
2374///
2375/// Call this once you've finished draining [`fetch_recv`]. After removal the
2376/// handle is invalid.
2377pub fn fetch_remove(handle: u32) {
2378    unsafe { _api_fetch_remove(handle) }
2379}
2380
2381// ─── Dynamic Module Loading ─────────────────────────────────────────────────
2382
2383/// Fetch and execute another `.wasm` module from a URL.
2384/// The loaded module shares the same canvas, console, and storage context.
2385/// Returns 0 on success, negative error code on failure.
2386pub fn load_module(url: &str) -> i32 {
2387    unsafe { _api_load_module(url.as_ptr() as u32, url.len() as u32) }
2388}
2389
2390// ─── Crypto / Hash API ─────────────────────────────────────────────────────
2391
2392/// Compute the SHA-256 hash of the given data. Returns 32 bytes.
2393pub fn hash_sha256(data: &[u8]) -> [u8; 32] {
2394    let mut out = [0u8; 32];
2395    unsafe {
2396        _api_hash_sha256(
2397            data.as_ptr() as u32,
2398            data.len() as u32,
2399            out.as_mut_ptr() as u32,
2400        );
2401    }
2402    out
2403}
2404
2405/// Return SHA-256 hash as a lowercase hex string.
2406pub fn hash_sha256_hex(data: &[u8]) -> String {
2407    let hash = hash_sha256(data);
2408    let mut hex = String::with_capacity(64);
2409    for byte in &hash {
2410        hex.push(HEX_CHARS[(*byte >> 4) as usize]);
2411        hex.push(HEX_CHARS[(*byte & 0x0F) as usize]);
2412    }
2413    hex
2414}
2415
2416const HEX_CHARS: [char; 16] = [
2417    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
2418];
2419
2420// ─── Base64 API ─────────────────────────────────────────────────────────────
2421
2422/// Base64-encode arbitrary bytes.
2423pub fn base64_encode(data: &[u8]) -> String {
2424    let mut buf = vec![0u8; data.len() * 4 / 3 + 8];
2425    let len = unsafe {
2426        _api_base64_encode(
2427            data.as_ptr() as u32,
2428            data.len() as u32,
2429            buf.as_mut_ptr() as u32,
2430            buf.len() as u32,
2431        )
2432    };
2433    String::from_utf8_lossy(&buf[..len as usize]).to_string()
2434}
2435
2436/// Decode a base64-encoded string back to bytes.
2437pub fn base64_decode(encoded: &str) -> Vec<u8> {
2438    let mut buf = vec![0u8; encoded.len()];
2439    let len = unsafe {
2440        _api_base64_decode(
2441            encoded.as_ptr() as u32,
2442            encoded.len() as u32,
2443            buf.as_mut_ptr() as u32,
2444            buf.len() as u32,
2445        )
2446    };
2447    buf[..len as usize].to_vec()
2448}
2449
2450// ─── Persistent Key-Value Store API ─────────────────────────────────────────
2451
2452/// Store a key-value pair in the persistent on-disk KV store.
2453/// Returns `true` on success.
2454pub fn kv_store_set(key: &str, value: &[u8]) -> bool {
2455    let rc = unsafe {
2456        _api_kv_store_set(
2457            key.as_ptr() as u32,
2458            key.len() as u32,
2459            value.as_ptr() as u32,
2460            value.len() as u32,
2461        )
2462    };
2463    rc == 0
2464}
2465
2466/// Convenience wrapper: store a UTF-8 string value.
2467pub fn kv_store_set_str(key: &str, value: &str) -> bool {
2468    kv_store_set(key, value.as_bytes())
2469}
2470
2471/// Retrieve a value from the persistent KV store.
2472/// Returns `None` if the key does not exist.
2473pub fn kv_store_get(key: &str) -> Option<Vec<u8>> {
2474    let mut buf = vec![0u8; 64 * 1024]; // 64 KB read buffer
2475    let rc = unsafe {
2476        _api_kv_store_get(
2477            key.as_ptr() as u32,
2478            key.len() as u32,
2479            buf.as_mut_ptr() as u32,
2480            buf.len() as u32,
2481        )
2482    };
2483    if rc < 0 {
2484        return None;
2485    }
2486    Some(buf[..rc as usize].to_vec())
2487}
2488
2489/// Convenience wrapper: retrieve a UTF-8 string value.
2490pub fn kv_store_get_str(key: &str) -> Option<String> {
2491    kv_store_get(key).map(|v| String::from_utf8_lossy(&v).into_owned())
2492}
2493
2494/// Delete a key from the persistent KV store. Returns `true` on success.
2495pub fn kv_store_delete(key: &str) -> bool {
2496    let rc = unsafe { _api_kv_store_delete(key.as_ptr() as u32, key.len() as u32) };
2497    rc == 0
2498}
2499
2500// ─── Navigation API ─────────────────────────────────────────────────────────
2501
2502/// Navigate to a new URL.  The URL can be absolute or relative to the current
2503/// page.  Navigation happens asynchronously after the current `start_app`
2504/// returns.  Returns 0 on success, negative on invalid URL.
2505pub fn navigate(url: &str) -> i32 {
2506    unsafe { _api_navigate(url.as_ptr() as u32, url.len() as u32) }
2507}
2508
2509/// Push a new entry onto the browser's history stack without triggering a
2510/// module reload.  This is analogous to `history.pushState()` in web browsers.
2511///
2512/// - `state`:  Opaque binary data retrievable later via [`get_state`].
2513/// - `title`:  Human-readable title for the history entry.
2514/// - `url`:    The URL to display in the address bar (relative or absolute).
2515///             Pass `""` to keep the current URL.
2516pub fn push_state(state: &[u8], title: &str, url: &str) {
2517    unsafe {
2518        _api_push_state(
2519            state.as_ptr() as u32,
2520            state.len() as u32,
2521            title.as_ptr() as u32,
2522            title.len() as u32,
2523            url.as_ptr() as u32,
2524            url.len() as u32,
2525        )
2526    }
2527}
2528
2529/// Replace the current history entry (no new entry is pushed).
2530/// Analogous to `history.replaceState()`.
2531pub fn replace_state(state: &[u8], title: &str, url: &str) {
2532    unsafe {
2533        _api_replace_state(
2534            state.as_ptr() as u32,
2535            state.len() as u32,
2536            title.as_ptr() as u32,
2537            title.len() as u32,
2538            url.as_ptr() as u32,
2539            url.len() as u32,
2540        )
2541    }
2542}
2543
2544/// Get the URL of the currently loaded page.
2545pub fn get_url() -> String {
2546    let mut buf = [0u8; 4096];
2547    let len = unsafe { _api_get_url(buf.as_mut_ptr() as u32, buf.len() as u32) };
2548    String::from_utf8_lossy(&buf[..len as usize]).to_string()
2549}
2550
2551/// Retrieve the opaque state bytes attached to the current history entry.
2552/// Returns `None` if no state has been set.
2553pub fn get_state() -> Option<Vec<u8>> {
2554    let mut buf = vec![0u8; 64 * 1024]; // 64 KB
2555    let rc = unsafe { _api_get_state(buf.as_mut_ptr() as u32, buf.len() as u32) };
2556    if rc < 0 {
2557        return None;
2558    }
2559    Some(buf[..rc as usize].to_vec())
2560}
2561
2562/// Return the total number of entries in the history stack.
2563pub fn history_length() -> u32 {
2564    unsafe { _api_history_length() }
2565}
2566
2567/// Navigate backward in history.  Returns `true` if a navigation was queued.
2568pub fn history_back() -> bool {
2569    unsafe { _api_history_back() == 1 }
2570}
2571
2572/// Navigate forward in history.  Returns `true` if a navigation was queued.
2573pub fn history_forward() -> bool {
2574    unsafe { _api_history_forward() == 1 }
2575}
2576
2577// ─── Hyperlink API ──────────────────────────────────────────────────────────
2578
2579/// Register a rectangular region on the canvas as a clickable hyperlink.
2580///
2581/// When the user clicks inside the rectangle the browser navigates to `url`.
2582/// Coordinates are in the same canvas-local space used by the drawing APIs.
2583/// Returns 0 on success.
2584pub fn register_hyperlink(x: f32, y: f32, w: f32, h: f32, url: &str) -> i32 {
2585    unsafe { _api_register_hyperlink(x, y, w, h, url.as_ptr() as u32, url.len() as u32) }
2586}
2587
2588/// Remove all previously registered hyperlinks.
2589pub fn clear_hyperlinks() {
2590    unsafe { _api_clear_hyperlinks() }
2591}
2592
2593// ─── URL Utility API ────────────────────────────────────────────────────────
2594
2595/// Resolve a relative URL against a base URL (WHATWG algorithm).
2596/// Returns `None` if either URL is invalid.
2597pub fn url_resolve(base: &str, relative: &str) -> Option<String> {
2598    let mut buf = [0u8; 4096];
2599    let rc = unsafe {
2600        _api_url_resolve(
2601            base.as_ptr() as u32,
2602            base.len() as u32,
2603            relative.as_ptr() as u32,
2604            relative.len() as u32,
2605            buf.as_mut_ptr() as u32,
2606            buf.len() as u32,
2607        )
2608    };
2609    if rc < 0 {
2610        return None;
2611    }
2612    Some(String::from_utf8_lossy(&buf[..rc as usize]).to_string())
2613}
2614
2615/// Percent-encode a string for safe inclusion in URL components.
2616pub fn url_encode(input: &str) -> String {
2617    let mut buf = vec![0u8; input.len() * 3 + 4];
2618    let len = unsafe {
2619        _api_url_encode(
2620            input.as_ptr() as u32,
2621            input.len() as u32,
2622            buf.as_mut_ptr() as u32,
2623            buf.len() as u32,
2624        )
2625    };
2626    String::from_utf8_lossy(&buf[..len as usize]).to_string()
2627}
2628
2629/// Decode a percent-encoded string.
2630pub fn url_decode(input: &str) -> String {
2631    let mut buf = vec![0u8; input.len() + 4];
2632    let len = unsafe {
2633        _api_url_decode(
2634            input.as_ptr() as u32,
2635            input.len() as u32,
2636            buf.as_mut_ptr() as u32,
2637            buf.len() as u32,
2638        )
2639    };
2640    String::from_utf8_lossy(&buf[..len as usize]).to_string()
2641}
2642
2643// ─── Input Polling API ──────────────────────────────────────────────────────
2644
2645/// Get the mouse position in canvas-local coordinates.
2646pub fn mouse_position() -> (f32, f32) {
2647    let packed = unsafe { _api_mouse_position() };
2648    let x = f32::from_bits((packed >> 32) as u32);
2649    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
2650    (x, y)
2651}
2652
2653/// Returns `true` if the given mouse button is currently held down.
2654/// Button 0 = primary (left), 1 = secondary (right), 2 = middle.
2655pub fn mouse_button_down(button: u32) -> bool {
2656    unsafe { _api_mouse_button_down(button) != 0 }
2657}
2658
2659/// Returns `true` if the given mouse button was clicked this frame.
2660pub fn mouse_button_clicked(button: u32) -> bool {
2661    unsafe { _api_mouse_button_clicked(button) != 0 }
2662}
2663
2664/// Returns `true` if the given key is currently held down.
2665/// See `KEY_*` constants for key codes.
2666pub fn key_down(key: u32) -> bool {
2667    unsafe { _api_key_down(key) != 0 }
2668}
2669
2670/// Returns `true` if the given key was pressed this frame.
2671pub fn key_pressed(key: u32) -> bool {
2672    unsafe { _api_key_pressed(key) != 0 }
2673}
2674
2675/// Get the scroll wheel delta for this frame.
2676pub fn scroll_delta() -> (f32, f32) {
2677    let packed = unsafe { _api_scroll_delta() };
2678    let x = f32::from_bits((packed >> 32) as u32);
2679    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
2680    (x, y)
2681}
2682
2683/// Returns modifier key state as a bitmask: bit 0 = Shift, bit 1 = Ctrl, bit 2 = Alt.
2684pub fn modifiers() -> u32 {
2685    unsafe { _api_modifiers() }
2686}
2687
2688/// Returns `true` if Shift is held.
2689pub fn shift_held() -> bool {
2690    modifiers() & 1 != 0
2691}
2692
2693/// Returns `true` if Ctrl (or Cmd on macOS) is held.
2694pub fn ctrl_held() -> bool {
2695    modifiers() & 2 != 0
2696}
2697
2698/// Returns `true` if Alt is held.
2699pub fn alt_held() -> bool {
2700    modifiers() & 4 != 0
2701}
2702
2703// ─── Key Constants ──────────────────────────────────────────────────────────
2704
2705pub const KEY_A: u32 = 0;
2706pub const KEY_B: u32 = 1;
2707pub const KEY_C: u32 = 2;
2708pub const KEY_D: u32 = 3;
2709pub const KEY_E: u32 = 4;
2710pub const KEY_F: u32 = 5;
2711pub const KEY_G: u32 = 6;
2712pub const KEY_H: u32 = 7;
2713pub const KEY_I: u32 = 8;
2714pub const KEY_J: u32 = 9;
2715pub const KEY_K: u32 = 10;
2716pub const KEY_L: u32 = 11;
2717pub const KEY_M: u32 = 12;
2718pub const KEY_N: u32 = 13;
2719pub const KEY_O: u32 = 14;
2720pub const KEY_P: u32 = 15;
2721pub const KEY_Q: u32 = 16;
2722pub const KEY_R: u32 = 17;
2723pub const KEY_S: u32 = 18;
2724pub const KEY_T: u32 = 19;
2725pub const KEY_U: u32 = 20;
2726pub const KEY_V: u32 = 21;
2727pub const KEY_W: u32 = 22;
2728pub const KEY_X: u32 = 23;
2729pub const KEY_Y: u32 = 24;
2730pub const KEY_Z: u32 = 25;
2731pub const KEY_0: u32 = 26;
2732pub const KEY_1: u32 = 27;
2733pub const KEY_2: u32 = 28;
2734pub const KEY_3: u32 = 29;
2735pub const KEY_4: u32 = 30;
2736pub const KEY_5: u32 = 31;
2737pub const KEY_6: u32 = 32;
2738pub const KEY_7: u32 = 33;
2739pub const KEY_8: u32 = 34;
2740pub const KEY_9: u32 = 35;
2741pub const KEY_ENTER: u32 = 36;
2742pub const KEY_ESCAPE: u32 = 37;
2743pub const KEY_TAB: u32 = 38;
2744pub const KEY_BACKSPACE: u32 = 39;
2745pub const KEY_DELETE: u32 = 40;
2746pub const KEY_SPACE: u32 = 41;
2747pub const KEY_UP: u32 = 42;
2748pub const KEY_DOWN: u32 = 43;
2749pub const KEY_LEFT: u32 = 44;
2750pub const KEY_RIGHT: u32 = 45;
2751pub const KEY_HOME: u32 = 46;
2752pub const KEY_END: u32 = 47;
2753pub const KEY_PAGE_UP: u32 = 48;
2754pub const KEY_PAGE_DOWN: u32 = 49;
2755
2756// ─── Interactive Widget API ─────────────────────────────────────────────────
2757
2758/// Render a button at the given position. Returns `true` if it was clicked
2759/// on the previous frame.
2760///
2761/// Must be called from `on_frame()` — widgets are only rendered for
2762/// interactive applications that export a frame loop.
2763pub fn ui_button(id: u32, x: f32, y: f32, w: f32, h: f32, label: &str) -> bool {
2764    unsafe { _api_ui_button(id, x, y, w, h, label.as_ptr() as u32, label.len() as u32) != 0 }
2765}
2766
2767/// Render a checkbox. Returns the current checked state.
2768///
2769/// `initial` sets the value the first time this ID is seen.
2770pub fn ui_checkbox(id: u32, x: f32, y: f32, label: &str, initial: bool) -> bool {
2771    unsafe {
2772        _api_ui_checkbox(
2773            id,
2774            x,
2775            y,
2776            label.as_ptr() as u32,
2777            label.len() as u32,
2778            if initial { 1 } else { 0 },
2779        ) != 0
2780    }
2781}
2782
2783/// Render a slider. Returns the current value.
2784///
2785/// `initial` sets the value the first time this ID is seen.
2786pub fn ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32 {
2787    unsafe { _api_ui_slider(id, x, y, w, min, max, initial) }
2788}
2789
2790/// Render a single-line text input. Returns the current text content.
2791///
2792/// `initial` sets the text the first time this ID is seen.
2793pub fn ui_text_input(id: u32, x: f32, y: f32, w: f32, initial: &str) -> String {
2794    let mut buf = [0u8; 4096];
2795    let len = unsafe {
2796        _api_ui_text_input(
2797            id,
2798            x,
2799            y,
2800            w,
2801            initial.as_ptr() as u32,
2802            initial.len() as u32,
2803            buf.as_mut_ptr() as u32,
2804            buf.len() as u32,
2805        )
2806    };
2807    String::from_utf8_lossy(&buf[..len as usize]).to_string()
2808}