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