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