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