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