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