Skip to main content

oxide_sdk/
lib.rs

1//! # Oxide SDK
2//!
3//! Guest-side SDK for building WebAssembly applications that run inside the
4//! Oxide browser. This crate provides safe Rust wrappers around the raw
5//! host-imported functions exposed by the `"oxide"` module.
6//!
7//! ## Quick Start
8//!
9//! ```rust,ignore
10//! use oxide_sdk::*;
11//!
12//! #[no_mangle]
13//! pub extern "C" fn start_app() {
14//!     log("Hello from Oxide!");
15//!     canvas_clear(30, 30, 46, 255);
16//!     canvas_text(20.0, 40.0, 28.0, 255, 255, 255, "Welcome to Oxide");
17//! }
18//! ```
19
20pub mod proto;
21
22// ─── Raw FFI imports from the host ──────────────────────────────────────────
23
24#[link(wasm_import_module = "oxide")]
25extern "C" {
26    #[link_name = "api_log"]
27    fn _api_log(ptr: u32, len: u32);
28
29    #[link_name = "api_warn"]
30    fn _api_warn(ptr: u32, len: u32);
31
32    #[link_name = "api_error"]
33    fn _api_error(ptr: u32, len: u32);
34
35    #[link_name = "api_get_location"]
36    fn _api_get_location(out_ptr: u32, out_cap: u32) -> u32;
37
38    #[link_name = "api_upload_file"]
39    fn _api_upload_file(name_ptr: u32, name_cap: u32, data_ptr: u32, data_cap: u32) -> u64;
40
41    #[link_name = "api_canvas_clear"]
42    fn _api_canvas_clear(r: u32, g: u32, b: u32, a: u32);
43
44    #[link_name = "api_canvas_rect"]
45    fn _api_canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u32, g: u32, b: u32, a: u32);
46
47    #[link_name = "api_canvas_circle"]
48    fn _api_canvas_circle(cx: f32, cy: f32, radius: f32, r: u32, g: u32, b: u32, a: u32);
49
50    #[link_name = "api_canvas_text"]
51    fn _api_canvas_text(x: f32, y: f32, size: f32, r: u32, g: u32, b: u32, ptr: u32, len: u32);
52
53    #[link_name = "api_canvas_line"]
54    fn _api_canvas_line(x1: f32, y1: f32, x2: f32, y2: f32, r: u32, g: u32, b: u32, thickness: f32);
55
56    #[link_name = "api_canvas_dimensions"]
57    fn _api_canvas_dimensions() -> u64;
58
59    #[link_name = "api_canvas_image"]
60    fn _api_canvas_image(x: f32, y: f32, w: f32, h: f32, data_ptr: u32, data_len: u32);
61
62    #[link_name = "api_storage_set"]
63    fn _api_storage_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32);
64
65    #[link_name = "api_storage_get"]
66    fn _api_storage_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> u32;
67
68    #[link_name = "api_storage_remove"]
69    fn _api_storage_remove(key_ptr: u32, key_len: u32);
70
71    #[link_name = "api_clipboard_write"]
72    fn _api_clipboard_write(ptr: u32, len: u32);
73
74    #[link_name = "api_clipboard_read"]
75    fn _api_clipboard_read(out_ptr: u32, out_cap: u32) -> u32;
76
77    #[link_name = "api_time_now_ms"]
78    fn _api_time_now_ms() -> u64;
79
80    #[link_name = "api_random"]
81    fn _api_random() -> u64;
82
83    #[link_name = "api_notify"]
84    fn _api_notify(title_ptr: u32, title_len: u32, body_ptr: u32, body_len: u32);
85
86    #[link_name = "api_fetch"]
87    fn _api_fetch(
88        method_ptr: u32,
89        method_len: u32,
90        url_ptr: u32,
91        url_len: u32,
92        ct_ptr: u32,
93        ct_len: u32,
94        body_ptr: u32,
95        body_len: u32,
96        out_ptr: u32,
97        out_cap: u32,
98    ) -> i64;
99
100    #[link_name = "api_load_module"]
101    fn _api_load_module(url_ptr: u32, url_len: u32) -> i32;
102
103    #[link_name = "api_hash_sha256"]
104    fn _api_hash_sha256(data_ptr: u32, data_len: u32, out_ptr: u32) -> u32;
105
106    #[link_name = "api_base64_encode"]
107    fn _api_base64_encode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
108
109    #[link_name = "api_base64_decode"]
110    fn _api_base64_decode(data_ptr: u32, data_len: u32, out_ptr: u32, out_cap: u32) -> u32;
111
112    #[link_name = "api_kv_store_set"]
113    fn _api_kv_store_set(key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32) -> i32;
114
115    #[link_name = "api_kv_store_get"]
116    fn _api_kv_store_get(key_ptr: u32, key_len: u32, out_ptr: u32, out_cap: u32) -> i32;
117
118    #[link_name = "api_kv_store_delete"]
119    fn _api_kv_store_delete(key_ptr: u32, key_len: u32) -> i32;
120
121    // ── Navigation ──────────────────────────────────────────────────
122
123    #[link_name = "api_navigate"]
124    fn _api_navigate(url_ptr: u32, url_len: u32) -> i32;
125
126    #[link_name = "api_push_state"]
127    fn _api_push_state(
128        state_ptr: u32,
129        state_len: u32,
130        title_ptr: u32,
131        title_len: u32,
132        url_ptr: u32,
133        url_len: u32,
134    );
135
136    #[link_name = "api_replace_state"]
137    fn _api_replace_state(
138        state_ptr: u32,
139        state_len: u32,
140        title_ptr: u32,
141        title_len: u32,
142        url_ptr: u32,
143        url_len: u32,
144    );
145
146    #[link_name = "api_get_url"]
147    fn _api_get_url(out_ptr: u32, out_cap: u32) -> u32;
148
149    #[link_name = "api_get_state"]
150    fn _api_get_state(out_ptr: u32, out_cap: u32) -> i32;
151
152    #[link_name = "api_history_length"]
153    fn _api_history_length() -> u32;
154
155    #[link_name = "api_history_back"]
156    fn _api_history_back() -> i32;
157
158    #[link_name = "api_history_forward"]
159    fn _api_history_forward() -> i32;
160
161    // ── Hyperlinks ──────────────────────────────────────────────────
162
163    #[link_name = "api_register_hyperlink"]
164    fn _api_register_hyperlink(x: f32, y: f32, w: f32, h: f32, url_ptr: u32, url_len: u32) -> i32;
165
166    #[link_name = "api_clear_hyperlinks"]
167    fn _api_clear_hyperlinks();
168
169    // ── Input Polling ────────────────────────────────────────────────
170
171    #[link_name = "api_mouse_position"]
172    fn _api_mouse_position() -> u64;
173
174    #[link_name = "api_mouse_button_down"]
175    fn _api_mouse_button_down(button: u32) -> u32;
176
177    #[link_name = "api_mouse_button_clicked"]
178    fn _api_mouse_button_clicked(button: u32) -> u32;
179
180    #[link_name = "api_key_down"]
181    fn _api_key_down(key: u32) -> u32;
182
183    #[link_name = "api_key_pressed"]
184    fn _api_key_pressed(key: u32) -> u32;
185
186    #[link_name = "api_scroll_delta"]
187    fn _api_scroll_delta() -> u64;
188
189    #[link_name = "api_modifiers"]
190    fn _api_modifiers() -> u32;
191
192    // ── Interactive Widgets ─────────────────────────────────────────
193
194    #[link_name = "api_ui_button"]
195    fn _api_ui_button(
196        id: u32,
197        x: f32,
198        y: f32,
199        w: f32,
200        h: f32,
201        label_ptr: u32,
202        label_len: u32,
203    ) -> u32;
204
205    #[link_name = "api_ui_checkbox"]
206    fn _api_ui_checkbox(
207        id: u32,
208        x: f32,
209        y: f32,
210        label_ptr: u32,
211        label_len: u32,
212        initial: u32,
213    ) -> u32;
214
215    #[link_name = "api_ui_slider"]
216    fn _api_ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32;
217
218    #[link_name = "api_ui_text_input"]
219    fn _api_ui_text_input(
220        id: u32,
221        x: f32,
222        y: f32,
223        w: f32,
224        init_ptr: u32,
225        init_len: u32,
226        out_ptr: u32,
227        out_cap: u32,
228    ) -> u32;
229
230    // ── URL Utilities ───────────────────────────────────────────────
231
232    #[link_name = "api_url_resolve"]
233    fn _api_url_resolve(
234        base_ptr: u32,
235        base_len: u32,
236        rel_ptr: u32,
237        rel_len: u32,
238        out_ptr: u32,
239        out_cap: u32,
240    ) -> i32;
241
242    #[link_name = "api_url_encode"]
243    fn _api_url_encode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
244
245    #[link_name = "api_url_decode"]
246    fn _api_url_decode(input_ptr: u32, input_len: u32, out_ptr: u32, out_cap: u32) -> u32;
247}
248
249// ─── Console API ────────────────────────────────────────────────────────────
250
251/// Print a message to the browser console (log level).
252pub fn log(msg: &str) {
253    unsafe { _api_log(msg.as_ptr() as u32, msg.len() as u32) }
254}
255
256/// Print a warning to the browser console.
257pub fn warn(msg: &str) {
258    unsafe { _api_warn(msg.as_ptr() as u32, msg.len() as u32) }
259}
260
261/// Print an error to the browser console.
262pub fn error(msg: &str) {
263    unsafe { _api_error(msg.as_ptr() as u32, msg.len() as u32) }
264}
265
266// ─── Geolocation API ────────────────────────────────────────────────────────
267
268/// Get the device's mock geolocation as a `"lat,lon"` string.
269pub fn get_location() -> String {
270    let mut buf = [0u8; 128];
271    let len = unsafe { _api_get_location(buf.as_mut_ptr() as u32, buf.len() as u32) };
272    String::from_utf8_lossy(&buf[..len as usize]).to_string()
273}
274
275// ─── File Upload API ────────────────────────────────────────────────────────
276
277/// File returned from the native file picker.
278pub struct UploadedFile {
279    pub name: String,
280    pub data: Vec<u8>,
281}
282
283/// Opens the native OS file picker and returns the selected file.
284/// Returns `None` if the user cancels.
285pub fn upload_file() -> Option<UploadedFile> {
286    let mut name_buf = [0u8; 256];
287    let mut data_buf = vec![0u8; 1024 * 1024]; // 1MB max
288
289    let result = unsafe {
290        _api_upload_file(
291            name_buf.as_mut_ptr() as u32,
292            name_buf.len() as u32,
293            data_buf.as_mut_ptr() as u32,
294            data_buf.len() as u32,
295        )
296    };
297
298    if result == 0 {
299        return None;
300    }
301
302    let name_len = (result >> 32) as usize;
303    let data_len = (result & 0xFFFF_FFFF) as usize;
304
305    Some(UploadedFile {
306        name: String::from_utf8_lossy(&name_buf[..name_len]).to_string(),
307        data: data_buf[..data_len].to_vec(),
308    })
309}
310
311// ─── Canvas API ─────────────────────────────────────────────────────────────
312
313/// Clear the canvas with a solid RGBA color.
314pub fn canvas_clear(r: u8, g: u8, b: u8, a: u8) {
315    unsafe { _api_canvas_clear(r as u32, g as u32, b as u32, a as u32) }
316}
317
318/// Draw a filled rectangle.
319pub fn canvas_rect(x: f32, y: f32, w: f32, h: f32, r: u8, g: u8, b: u8, a: u8) {
320    unsafe { _api_canvas_rect(x, y, w, h, r as u32, g as u32, b as u32, a as u32) }
321}
322
323/// Draw a filled circle.
324pub fn canvas_circle(cx: f32, cy: f32, radius: f32, r: u8, g: u8, b: u8, a: u8) {
325    unsafe { _api_canvas_circle(cx, cy, radius, r as u32, g as u32, b as u32, a as u32) }
326}
327
328/// Draw text on the canvas.
329pub fn canvas_text(x: f32, y: f32, size: f32, r: u8, g: u8, b: u8, text: &str) {
330    unsafe {
331        _api_canvas_text(
332            x,
333            y,
334            size,
335            r as u32,
336            g as u32,
337            b as u32,
338            text.as_ptr() as u32,
339            text.len() as u32,
340        )
341    }
342}
343
344/// Draw a line between two points.
345pub fn canvas_line(x1: f32, y1: f32, x2: f32, y2: f32, r: u8, g: u8, b: u8, thickness: f32) {
346    unsafe { _api_canvas_line(x1, y1, x2, y2, r as u32, g as u32, b as u32, thickness) }
347}
348
349/// Returns `(width, height)` of the canvas in pixels.
350pub fn canvas_dimensions() -> (u32, u32) {
351    let packed = unsafe { _api_canvas_dimensions() };
352    ((packed >> 32) as u32, (packed & 0xFFFF_FFFF) as u32)
353}
354
355/// Draw an image on the canvas from encoded image bytes (PNG, JPEG, GIF, WebP).
356/// The browser decodes the image and renders it at the given rectangle.
357pub fn canvas_image(x: f32, y: f32, w: f32, h: f32, data: &[u8]) {
358    unsafe { _api_canvas_image(x, y, w, h, data.as_ptr() as u32, data.len() as u32) }
359}
360
361// ─── Local Storage API ──────────────────────────────────────────────────────
362
363/// Store a key-value pair in sandboxed local storage.
364pub fn storage_set(key: &str, value: &str) {
365    unsafe {
366        _api_storage_set(
367            key.as_ptr() as u32,
368            key.len() as u32,
369            value.as_ptr() as u32,
370            value.len() as u32,
371        )
372    }
373}
374
375/// Retrieve a value from local storage. Returns empty string if not found.
376pub fn storage_get(key: &str) -> String {
377    let mut buf = [0u8; 4096];
378    let len = unsafe {
379        _api_storage_get(
380            key.as_ptr() as u32,
381            key.len() as u32,
382            buf.as_mut_ptr() as u32,
383            buf.len() as u32,
384        )
385    };
386    String::from_utf8_lossy(&buf[..len as usize]).to_string()
387}
388
389/// Remove a key from local storage.
390pub fn storage_remove(key: &str) {
391    unsafe { _api_storage_remove(key.as_ptr() as u32, key.len() as u32) }
392}
393
394// ─── Clipboard API ──────────────────────────────────────────────────────────
395
396/// Copy text to the system clipboard.
397pub fn clipboard_write(text: &str) {
398    unsafe { _api_clipboard_write(text.as_ptr() as u32, text.len() as u32) }
399}
400
401/// Read text from the system clipboard.
402pub fn clipboard_read() -> String {
403    let mut buf = [0u8; 4096];
404    let len = unsafe { _api_clipboard_read(buf.as_mut_ptr() as u32, buf.len() as u32) };
405    String::from_utf8_lossy(&buf[..len as usize]).to_string()
406}
407
408// ─── Timer / Clock API ─────────────────────────────────────────────────────
409
410/// Get the current time in milliseconds since the UNIX epoch.
411pub fn time_now_ms() -> u64 {
412    unsafe { _api_time_now_ms() }
413}
414
415// ─── Random API ─────────────────────────────────────────────────────────────
416
417/// Get a random u64 from the host.
418pub fn random_u64() -> u64 {
419    unsafe { _api_random() }
420}
421
422/// Get a random f64 in [0, 1).
423pub fn random_f64() -> f64 {
424    (random_u64() >> 11) as f64 / (1u64 << 53) as f64
425}
426
427// ─── Notification API ───────────────────────────────────────────────────────
428
429/// Send a notification to the user (rendered in the browser console).
430pub fn notify(title: &str, body: &str) {
431    unsafe {
432        _api_notify(
433            title.as_ptr() as u32,
434            title.len() as u32,
435            body.as_ptr() as u32,
436            body.len() as u32,
437        )
438    }
439}
440
441// ─── HTTP Fetch API ─────────────────────────────────────────────────────────
442
443/// Response from an HTTP fetch call.
444pub struct FetchResponse {
445    pub status: u32,
446    pub body: Vec<u8>,
447}
448
449impl FetchResponse {
450    /// Interpret the response body as UTF-8 text.
451    pub fn text(&self) -> String {
452        String::from_utf8_lossy(&self.body).to_string()
453    }
454}
455
456/// Perform an HTTP request.  Returns the status code and response body.
457///
458/// `content_type` sets the `Content-Type` header (pass `""` to omit).
459/// Protobuf is the native format — use `"application/protobuf"` for binary
460/// payloads.
461pub fn fetch(
462    method: &str,
463    url: &str,
464    content_type: &str,
465    body: &[u8],
466) -> Result<FetchResponse, i64> {
467    let mut out_buf = vec![0u8; 4 * 1024 * 1024]; // 4 MB response buffer
468    let result = unsafe {
469        _api_fetch(
470            method.as_ptr() as u32,
471            method.len() as u32,
472            url.as_ptr() as u32,
473            url.len() as u32,
474            content_type.as_ptr() as u32,
475            content_type.len() as u32,
476            body.as_ptr() as u32,
477            body.len() as u32,
478            out_buf.as_mut_ptr() as u32,
479            out_buf.len() as u32,
480        )
481    };
482    if result < 0 {
483        return Err(result);
484    }
485    let status = (result >> 32) as u32;
486    let body_len = (result & 0xFFFF_FFFF) as usize;
487    Ok(FetchResponse {
488        status,
489        body: out_buf[..body_len].to_vec(),
490    })
491}
492
493/// HTTP GET request.
494pub fn fetch_get(url: &str) -> Result<FetchResponse, i64> {
495    fetch("GET", url, "", &[])
496}
497
498/// HTTP POST with raw bytes.
499pub fn fetch_post(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
500    fetch("POST", url, content_type, body)
501}
502
503/// HTTP POST with protobuf body (sets `Content-Type: application/protobuf`).
504pub fn fetch_post_proto(url: &str, msg: &proto::ProtoEncoder) -> Result<FetchResponse, i64> {
505    fetch("POST", url, "application/protobuf", msg.as_bytes())
506}
507
508/// HTTP PUT with raw bytes.
509pub fn fetch_put(url: &str, content_type: &str, body: &[u8]) -> Result<FetchResponse, i64> {
510    fetch("PUT", url, content_type, body)
511}
512
513/// HTTP DELETE.
514pub fn fetch_delete(url: &str) -> Result<FetchResponse, i64> {
515    fetch("DELETE", url, "", &[])
516}
517
518// ─── Dynamic Module Loading ─────────────────────────────────────────────────
519
520/// Fetch and execute another `.wasm` module from a URL.
521/// The loaded module shares the same canvas, console, and storage context.
522/// Returns 0 on success, negative error code on failure.
523pub fn load_module(url: &str) -> i32 {
524    unsafe { _api_load_module(url.as_ptr() as u32, url.len() as u32) }
525}
526
527// ─── Crypto / Hash API ─────────────────────────────────────────────────────
528
529/// Compute the SHA-256 hash of the given data. Returns 32 bytes.
530pub fn hash_sha256(data: &[u8]) -> [u8; 32] {
531    let mut out = [0u8; 32];
532    unsafe {
533        _api_hash_sha256(
534            data.as_ptr() as u32,
535            data.len() as u32,
536            out.as_mut_ptr() as u32,
537        );
538    }
539    out
540}
541
542/// Return SHA-256 hash as a lowercase hex string.
543pub fn hash_sha256_hex(data: &[u8]) -> String {
544    let hash = hash_sha256(data);
545    let mut hex = String::with_capacity(64);
546    for byte in &hash {
547        hex.push(HEX_CHARS[(*byte >> 4) as usize]);
548        hex.push(HEX_CHARS[(*byte & 0x0F) as usize]);
549    }
550    hex
551}
552
553const HEX_CHARS: [char; 16] = [
554    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
555];
556
557// ─── Base64 API ─────────────────────────────────────────────────────────────
558
559/// Base64-encode arbitrary bytes.
560pub fn base64_encode(data: &[u8]) -> String {
561    let mut buf = vec![0u8; data.len() * 4 / 3 + 8];
562    let len = unsafe {
563        _api_base64_encode(
564            data.as_ptr() as u32,
565            data.len() as u32,
566            buf.as_mut_ptr() as u32,
567            buf.len() as u32,
568        )
569    };
570    String::from_utf8_lossy(&buf[..len as usize]).to_string()
571}
572
573/// Decode a base64-encoded string back to bytes.
574pub fn base64_decode(encoded: &str) -> Vec<u8> {
575    let mut buf = vec![0u8; encoded.len()];
576    let len = unsafe {
577        _api_base64_decode(
578            encoded.as_ptr() as u32,
579            encoded.len() as u32,
580            buf.as_mut_ptr() as u32,
581            buf.len() as u32,
582        )
583    };
584    buf[..len as usize].to_vec()
585}
586
587// ─── Persistent Key-Value Store API ─────────────────────────────────────────
588
589/// Store a key-value pair in the persistent on-disk KV store.
590/// Returns `true` on success.
591pub fn kv_store_set(key: &str, value: &[u8]) -> bool {
592    let rc = unsafe {
593        _api_kv_store_set(
594            key.as_ptr() as u32,
595            key.len() as u32,
596            value.as_ptr() as u32,
597            value.len() as u32,
598        )
599    };
600    rc == 0
601}
602
603/// Convenience wrapper: store a UTF-8 string value.
604pub fn kv_store_set_str(key: &str, value: &str) -> bool {
605    kv_store_set(key, value.as_bytes())
606}
607
608/// Retrieve a value from the persistent KV store.
609/// Returns `None` if the key does not exist.
610pub fn kv_store_get(key: &str) -> Option<Vec<u8>> {
611    let mut buf = vec![0u8; 64 * 1024]; // 64 KB read buffer
612    let rc = unsafe {
613        _api_kv_store_get(
614            key.as_ptr() as u32,
615            key.len() as u32,
616            buf.as_mut_ptr() as u32,
617            buf.len() as u32,
618        )
619    };
620    if rc < 0 {
621        return None;
622    }
623    Some(buf[..rc as usize].to_vec())
624}
625
626/// Convenience wrapper: retrieve a UTF-8 string value.
627pub fn kv_store_get_str(key: &str) -> Option<String> {
628    kv_store_get(key).map(|v| String::from_utf8_lossy(&v).into_owned())
629}
630
631/// Delete a key from the persistent KV store. Returns `true` on success.
632pub fn kv_store_delete(key: &str) -> bool {
633    let rc = unsafe { _api_kv_store_delete(key.as_ptr() as u32, key.len() as u32) };
634    rc == 0
635}
636
637// ─── Navigation API ─────────────────────────────────────────────────────────
638
639/// Navigate to a new URL.  The URL can be absolute or relative to the current
640/// page.  Navigation happens asynchronously after the current `start_app`
641/// returns.  Returns 0 on success, negative on invalid URL.
642pub fn navigate(url: &str) -> i32 {
643    unsafe { _api_navigate(url.as_ptr() as u32, url.len() as u32) }
644}
645
646/// Push a new entry onto the browser's history stack without triggering a
647/// module reload.  This is analogous to `history.pushState()` in web browsers.
648///
649/// - `state`:  Opaque binary data retrievable later via [`get_state`].
650/// - `title`:  Human-readable title for the history entry.
651/// - `url`:    The URL to display in the address bar (relative or absolute).
652///             Pass `""` to keep the current URL.
653pub fn push_state(state: &[u8], title: &str, url: &str) {
654    unsafe {
655        _api_push_state(
656            state.as_ptr() as u32,
657            state.len() as u32,
658            title.as_ptr() as u32,
659            title.len() as u32,
660            url.as_ptr() as u32,
661            url.len() as u32,
662        )
663    }
664}
665
666/// Replace the current history entry (no new entry is pushed).
667/// Analogous to `history.replaceState()`.
668pub fn replace_state(state: &[u8], title: &str, url: &str) {
669    unsafe {
670        _api_replace_state(
671            state.as_ptr() as u32,
672            state.len() as u32,
673            title.as_ptr() as u32,
674            title.len() as u32,
675            url.as_ptr() as u32,
676            url.len() as u32,
677        )
678    }
679}
680
681/// Get the URL of the currently loaded page.
682pub fn get_url() -> String {
683    let mut buf = [0u8; 4096];
684    let len = unsafe { _api_get_url(buf.as_mut_ptr() as u32, buf.len() as u32) };
685    String::from_utf8_lossy(&buf[..len as usize]).to_string()
686}
687
688/// Retrieve the opaque state bytes attached to the current history entry.
689/// Returns `None` if no state has been set.
690pub fn get_state() -> Option<Vec<u8>> {
691    let mut buf = vec![0u8; 64 * 1024]; // 64 KB
692    let rc = unsafe { _api_get_state(buf.as_mut_ptr() as u32, buf.len() as u32) };
693    if rc < 0 {
694        return None;
695    }
696    Some(buf[..rc as usize].to_vec())
697}
698
699/// Return the total number of entries in the history stack.
700pub fn history_length() -> u32 {
701    unsafe { _api_history_length() }
702}
703
704/// Navigate backward in history.  Returns `true` if a navigation was queued.
705pub fn history_back() -> bool {
706    unsafe { _api_history_back() == 1 }
707}
708
709/// Navigate forward in history.  Returns `true` if a navigation was queued.
710pub fn history_forward() -> bool {
711    unsafe { _api_history_forward() == 1 }
712}
713
714// ─── Hyperlink API ──────────────────────────────────────────────────────────
715
716/// Register a rectangular region on the canvas as a clickable hyperlink.
717///
718/// When the user clicks inside the rectangle the browser navigates to `url`.
719/// Coordinates are in the same canvas-local space used by the drawing APIs.
720/// Returns 0 on success.
721pub fn register_hyperlink(x: f32, y: f32, w: f32, h: f32, url: &str) -> i32 {
722    unsafe { _api_register_hyperlink(x, y, w, h, url.as_ptr() as u32, url.len() as u32) }
723}
724
725/// Remove all previously registered hyperlinks.
726pub fn clear_hyperlinks() {
727    unsafe { _api_clear_hyperlinks() }
728}
729
730// ─── URL Utility API ────────────────────────────────────────────────────────
731
732/// Resolve a relative URL against a base URL (WHATWG algorithm).
733/// Returns `None` if either URL is invalid.
734pub fn url_resolve(base: &str, relative: &str) -> Option<String> {
735    let mut buf = [0u8; 4096];
736    let rc = unsafe {
737        _api_url_resolve(
738            base.as_ptr() as u32,
739            base.len() as u32,
740            relative.as_ptr() as u32,
741            relative.len() as u32,
742            buf.as_mut_ptr() as u32,
743            buf.len() as u32,
744        )
745    };
746    if rc < 0 {
747        return None;
748    }
749    Some(String::from_utf8_lossy(&buf[..rc as usize]).to_string())
750}
751
752/// Percent-encode a string for safe inclusion in URL components.
753pub fn url_encode(input: &str) -> String {
754    let mut buf = vec![0u8; input.len() * 3 + 4];
755    let len = unsafe {
756        _api_url_encode(
757            input.as_ptr() as u32,
758            input.len() as u32,
759            buf.as_mut_ptr() as u32,
760            buf.len() as u32,
761        )
762    };
763    String::from_utf8_lossy(&buf[..len as usize]).to_string()
764}
765
766/// Decode a percent-encoded string.
767pub fn url_decode(input: &str) -> String {
768    let mut buf = vec![0u8; input.len() + 4];
769    let len = unsafe {
770        _api_url_decode(
771            input.as_ptr() as u32,
772            input.len() as u32,
773            buf.as_mut_ptr() as u32,
774            buf.len() as u32,
775        )
776    };
777    String::from_utf8_lossy(&buf[..len as usize]).to_string()
778}
779
780// ─── Input Polling API ──────────────────────────────────────────────────────
781
782/// Get the mouse position in canvas-local coordinates.
783pub fn mouse_position() -> (f32, f32) {
784    let packed = unsafe { _api_mouse_position() };
785    let x = f32::from_bits((packed >> 32) as u32);
786    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
787    (x, y)
788}
789
790/// Returns `true` if the given mouse button is currently held down.
791/// Button 0 = primary (left), 1 = secondary (right), 2 = middle.
792pub fn mouse_button_down(button: u32) -> bool {
793    unsafe { _api_mouse_button_down(button) != 0 }
794}
795
796/// Returns `true` if the given mouse button was clicked this frame.
797pub fn mouse_button_clicked(button: u32) -> bool {
798    unsafe { _api_mouse_button_clicked(button) != 0 }
799}
800
801/// Returns `true` if the given key is currently held down.
802/// See `KEY_*` constants for key codes.
803pub fn key_down(key: u32) -> bool {
804    unsafe { _api_key_down(key) != 0 }
805}
806
807/// Returns `true` if the given key was pressed this frame.
808pub fn key_pressed(key: u32) -> bool {
809    unsafe { _api_key_pressed(key) != 0 }
810}
811
812/// Get the scroll wheel delta for this frame.
813pub fn scroll_delta() -> (f32, f32) {
814    let packed = unsafe { _api_scroll_delta() };
815    let x = f32::from_bits((packed >> 32) as u32);
816    let y = f32::from_bits((packed & 0xFFFF_FFFF) as u32);
817    (x, y)
818}
819
820/// Returns modifier key state as a bitmask: bit 0 = Shift, bit 1 = Ctrl, bit 2 = Alt.
821pub fn modifiers() -> u32 {
822    unsafe { _api_modifiers() }
823}
824
825/// Returns `true` if Shift is held.
826pub fn shift_held() -> bool {
827    modifiers() & 1 != 0
828}
829
830/// Returns `true` if Ctrl (or Cmd on macOS) is held.
831pub fn ctrl_held() -> bool {
832    modifiers() & 2 != 0
833}
834
835/// Returns `true` if Alt is held.
836pub fn alt_held() -> bool {
837    modifiers() & 4 != 0
838}
839
840// ─── Key Constants ──────────────────────────────────────────────────────────
841
842pub const KEY_A: u32 = 0;
843pub const KEY_B: u32 = 1;
844pub const KEY_C: u32 = 2;
845pub const KEY_D: u32 = 3;
846pub const KEY_E: u32 = 4;
847pub const KEY_F: u32 = 5;
848pub const KEY_G: u32 = 6;
849pub const KEY_H: u32 = 7;
850pub const KEY_I: u32 = 8;
851pub const KEY_J: u32 = 9;
852pub const KEY_K: u32 = 10;
853pub const KEY_L: u32 = 11;
854pub const KEY_M: u32 = 12;
855pub const KEY_N: u32 = 13;
856pub const KEY_O: u32 = 14;
857pub const KEY_P: u32 = 15;
858pub const KEY_Q: u32 = 16;
859pub const KEY_R: u32 = 17;
860pub const KEY_S: u32 = 18;
861pub const KEY_T: u32 = 19;
862pub const KEY_U: u32 = 20;
863pub const KEY_V: u32 = 21;
864pub const KEY_W: u32 = 22;
865pub const KEY_X: u32 = 23;
866pub const KEY_Y: u32 = 24;
867pub const KEY_Z: u32 = 25;
868pub const KEY_0: u32 = 26;
869pub const KEY_1: u32 = 27;
870pub const KEY_2: u32 = 28;
871pub const KEY_3: u32 = 29;
872pub const KEY_4: u32 = 30;
873pub const KEY_5: u32 = 31;
874pub const KEY_6: u32 = 32;
875pub const KEY_7: u32 = 33;
876pub const KEY_8: u32 = 34;
877pub const KEY_9: u32 = 35;
878pub const KEY_ENTER: u32 = 36;
879pub const KEY_ESCAPE: u32 = 37;
880pub const KEY_TAB: u32 = 38;
881pub const KEY_BACKSPACE: u32 = 39;
882pub const KEY_DELETE: u32 = 40;
883pub const KEY_SPACE: u32 = 41;
884pub const KEY_UP: u32 = 42;
885pub const KEY_DOWN: u32 = 43;
886pub const KEY_LEFT: u32 = 44;
887pub const KEY_RIGHT: u32 = 45;
888pub const KEY_HOME: u32 = 46;
889pub const KEY_END: u32 = 47;
890pub const KEY_PAGE_UP: u32 = 48;
891pub const KEY_PAGE_DOWN: u32 = 49;
892
893// ─── Interactive Widget API ─────────────────────────────────────────────────
894
895/// Render a button at the given position. Returns `true` if it was clicked
896/// on the previous frame.
897///
898/// Must be called from `on_frame()` — widgets are only rendered for
899/// interactive applications that export a frame loop.
900pub fn ui_button(id: u32, x: f32, y: f32, w: f32, h: f32, label: &str) -> bool {
901    unsafe { _api_ui_button(id, x, y, w, h, label.as_ptr() as u32, label.len() as u32) != 0 }
902}
903
904/// Render a checkbox. Returns the current checked state.
905///
906/// `initial` sets the value the first time this ID is seen.
907pub fn ui_checkbox(id: u32, x: f32, y: f32, label: &str, initial: bool) -> bool {
908    unsafe {
909        _api_ui_checkbox(
910            id,
911            x,
912            y,
913            label.as_ptr() as u32,
914            label.len() as u32,
915            if initial { 1 } else { 0 },
916        ) != 0
917    }
918}
919
920/// Render a slider. Returns the current value.
921///
922/// `initial` sets the value the first time this ID is seen.
923pub fn ui_slider(id: u32, x: f32, y: f32, w: f32, min: f32, max: f32, initial: f32) -> f32 {
924    unsafe { _api_ui_slider(id, x, y, w, min, max, initial) }
925}
926
927/// Render a single-line text input. Returns the current text content.
928///
929/// `initial` sets the text the first time this ID is seen.
930pub fn ui_text_input(id: u32, x: f32, y: f32, w: f32, initial: &str) -> String {
931    let mut buf = [0u8; 4096];
932    let len = unsafe {
933        _api_ui_text_input(
934            id,
935            x,
936            y,
937            w,
938            initial.as_ptr() as u32,
939            initial.len() as u32,
940            buf.as_mut_ptr() as u32,
941            buf.len() as u32,
942        )
943    };
944    String::from_utf8_lossy(&buf[..len as usize]).to_string()
945}