1use std::collections::{HashMap, HashSet};
14use std::sync::atomic::AtomicBool;
15use std::sync::{Arc, Mutex};
16use std::time::{Duration, Instant};
17
18use anyhow::{Context, Result};
19use image::GenericImageView;
20use reqwest::header::{ACCEPT, CONTENT_TYPE};
21use wasmtime::*;
22
23use crate::audio_format;
24use crate::bookmarks::SharedBookmarkStore;
25use crate::download::DownloadManager;
26use crate::engine::ModuleLoader;
27use crate::history::SharedHistoryStore;
28use crate::navigation::NavigationStack;
29use crate::subtitle;
30use crate::url as oxide_url;
31use crate::video::{self, VideoPlaybackState};
32use crate::video_format;
33
34struct AudioChannel {
36 player: rodio::Player,
37 duration_ms: u64,
38 looping: bool,
39}
40
41pub struct AudioEngine {
47 _device_sink: rodio::stream::MixerDeviceSink,
48 channels: HashMap<u32, AudioChannel>,
49}
50
51impl AudioEngine {
52 fn try_new() -> Option<Self> {
53 let mut device_sink = rodio::DeviceSinkBuilder::open_default_sink().ok()?;
54 device_sink.log_on_drop(false);
55 Some(Self {
56 _device_sink: device_sink,
57 channels: HashMap::new(),
58 })
59 }
60
61 fn ensure_channel(&mut self, id: u32) -> &mut AudioChannel {
62 if !self.channels.contains_key(&id) {
63 let player = rodio::Player::connect_new(self._device_sink.mixer());
64 self.channels.insert(
65 id,
66 AudioChannel {
67 player,
68 duration_ms: 0,
69 looping: false,
70 },
71 );
72 }
73 self.channels.get_mut(&id).unwrap()
74 }
75
76 fn play_bytes_on(&mut self, channel_id: u32, data: Vec<u8>) -> bool {
77 use rodio::Source;
78
79 let cursor = std::io::Cursor::new(data);
80 let reader = std::io::BufReader::new(cursor);
81 let source = match rodio::Decoder::try_from(reader) {
82 Ok(s) => s,
83 Err(_) => return false,
84 };
85
86 let duration_ms = source
87 .total_duration()
88 .map(|d| d.as_millis() as u64)
89 .unwrap_or(0);
90
91 let ch = self.ensure_channel(channel_id);
92 ch.player.clear();
93 ch.duration_ms = duration_ms;
94
95 if ch.looping {
96 ch.player.append(source.repeat_infinite());
97 } else {
98 ch.player.append(source);
99 }
100 ch.player.play();
101 true
102 }
103}
104
105#[derive(Clone, Debug)]
109pub struct AnimationRequest {
110 pub id: u32,
112 pub callback_id: u32,
114}
115
116pub fn drain_animation_frame_requests(requests: &Arc<Mutex<Vec<AnimationRequest>>>) -> Vec<u32> {
119 let mut guard = requests.lock().unwrap();
120 let callback_ids: Vec<u32> = guard.iter().map(|r| r.callback_id).collect();
121 guard.clear();
122 callback_ids
123}
124
125#[derive(Clone)]
132pub struct HostState {
133 pub console: Arc<Mutex<Vec<ConsoleEntry>>>,
135 pub canvas: Arc<Mutex<CanvasState>>,
137 pub storage: Arc<Mutex<HashMap<String, String>>>,
139 pub timers: Arc<Mutex<Vec<TimerEntry>>>,
141 pub animation_requests: Arc<Mutex<Vec<AnimationRequest>>>,
144 pub timer_next_id: Arc<Mutex<u32>>,
146 pub clipboard: Arc<Mutex<String>>,
148 pub clipboard_allowed: Arc<Mutex<bool>>,
150 pub kv_db: Option<Arc<sled::Db>>,
152 pub memory: Option<Memory>,
154 pub module_loader: Option<Arc<ModuleLoader>>,
156 pub navigation: Arc<Mutex<NavigationStack>>,
158 pub hyperlinks: Arc<Mutex<Vec<Hyperlink>>>,
160 pub pending_navigation: Arc<Mutex<Option<String>>>,
162 pub current_url: Arc<Mutex<String>>,
164 pub input_state: Arc<Mutex<InputState>>,
166 pub widget_commands: Arc<Mutex<Vec<WidgetCommand>>>,
168 pub widget_states: Arc<Mutex<HashMap<u32, WidgetValue>>>,
170 pub widget_clicked: Arc<Mutex<HashSet<u32>>>,
172 pub canvas_offset: Arc<Mutex<(f32, f32)>>,
174 pub bookmark_store: SharedBookmarkStore,
176 pub history_store: SharedHistoryStore,
178 pub audio: Arc<Mutex<Option<AudioEngine>>>,
180 pub last_audio_url_content_type: Arc<Mutex<String>>,
182 pub video: Arc<Mutex<VideoPlaybackState>>,
184 pub video_pip_frame: Arc<Mutex<Option<DecodedImage>>>,
186 pub video_pip_serial: Arc<Mutex<u64>>,
188 pub media_capture: Arc<Mutex<crate::media_capture::MediaCaptureState>>,
190 pub gpu: Arc<Mutex<Option<crate::gpu::GpuState>>>,
192 pub rtc: Arc<Mutex<Option<crate::rtc::RtcState>>>,
194 pub ws: Arc<Mutex<Option<crate::websocket::WsState>>>,
196 pub midi: Arc<Mutex<Option<crate::midi::MidiState>>>,
198 pub fetch: Arc<Mutex<Option<crate::fetch::FetchState>>>,
200 pub file_picker: Arc<Mutex<crate::file_picker::FilePickerState>>,
203 pub events: Arc<Mutex<crate::events::EventState>>,
206 pub download_manager: DownloadManager,
208 pub focused: Arc<AtomicBool>,
212 pub text_system: Arc<Mutex<Option<Arc<gpui::WindowTextSystem>>>>,
217 pub content_width: Arc<Mutex<u32>>,
219 pub content_height: Arc<Mutex<u32>>,
221 pub scroll_x: Arc<Mutex<f32>>,
223 pub scroll_y: Arc<Mutex<f32>>,
225}
226
227#[derive(Clone, Debug)]
229pub struct ConsoleEntry {
230 pub timestamp: String,
232 pub level: ConsoleLevel,
234 pub message: String,
236}
237
238#[derive(Clone, Debug)]
240pub enum ConsoleLevel {
241 Log,
243 Warn,
245 Error,
247}
248
249#[derive(Clone, Debug)]
251pub struct CanvasState {
252 pub commands: Vec<DrawCommand>,
254 pub width: u32,
256 pub height: u32,
258 pub images: Vec<DecodedImage>,
260 pub generation: u64,
262}
263
264#[derive(Clone, Debug)]
266pub struct DecodedImage {
267 pub width: u32,
269 pub height: u32,
271 pub pixels: Vec<u8>,
273}
274
275#[derive(Clone, Debug)]
277pub struct GradientStop {
278 pub offset: f32,
280 pub r: u8,
281 pub g: u8,
282 pub b: u8,
283 pub a: u8,
284}
285
286#[derive(Clone, Debug)]
288pub enum DrawCommand {
289 Clear { r: u8, g: u8, b: u8, a: u8 },
291 Rect {
293 x: f32,
294 y: f32,
295 w: f32,
296 h: f32,
297 r: u8,
298 g: u8,
299 b: u8,
300 a: u8,
301 },
302 Circle {
304 cx: f32,
305 cy: f32,
306 radius: f32,
307 r: u8,
308 g: u8,
309 b: u8,
310 a: u8,
311 },
312 Text {
314 x: f32,
315 y: f32,
316 size: f32,
317 r: u8,
318 g: u8,
319 b: u8,
320 a: u8,
321 text: String,
322 },
323 Line {
325 x1: f32,
326 y1: f32,
327 x2: f32,
328 y2: f32,
329 r: u8,
330 g: u8,
331 b: u8,
332 a: u8,
333 thickness: f32,
334 },
335 Image {
337 x: f32,
338 y: f32,
339 w: f32,
340 h: f32,
341 image_id: usize,
342 },
343 RoundedRect {
345 x: f32,
346 y: f32,
347 w: f32,
348 h: f32,
349 radius: f32,
350 r: u8,
351 g: u8,
352 b: u8,
353 a: u8,
354 },
355 Arc {
357 cx: f32,
358 cy: f32,
359 radius: f32,
360 start_angle: f32,
361 end_angle: f32,
362 r: u8,
363 g: u8,
364 b: u8,
365 a: u8,
366 thickness: f32,
367 },
368 Bezier {
370 x1: f32,
371 y1: f32,
372 cp1x: f32,
373 cp1y: f32,
374 cp2x: f32,
375 cp2y: f32,
376 x2: f32,
377 y2: f32,
378 r: u8,
379 g: u8,
380 b: u8,
381 a: u8,
382 thickness: f32,
383 },
384 Gradient {
386 x: f32,
387 y: f32,
388 w: f32,
389 h: f32,
390 kind: u8,
392 ax: f32,
394 ay: f32,
396 bx: f32,
398 by: f32,
400 stops: Vec<GradientStop>,
402 },
403 Save,
405 Restore,
407 Transform {
409 a: f32,
410 b: f32,
411 c: f32,
412 d: f32,
413 tx: f32,
414 ty: f32,
415 },
416 Clip { x: f32, y: f32, w: f32, h: f32 },
418 Opacity { alpha: f32 },
420 TextEx {
424 x: f32,
425 y: f32,
426 size: f32,
427 r: u8,
428 g: u8,
429 b: u8,
430 a: u8,
431 family: String,
434 weight: u16,
436 style: u8,
438 align: u8,
440 text: String,
441 },
442}
443
444#[derive(Clone, Debug)]
446pub struct TimerEntry {
447 pub id: u32,
449 pub fire_at: Instant,
451 pub interval: Option<Duration>,
453 pub callback_id: u32,
455}
456
457pub fn drain_expired_timers(timers: &Arc<Mutex<Vec<TimerEntry>>>) -> Vec<u32> {
464 let now = Instant::now();
465 let mut guard = timers.lock().unwrap();
466 let mut fired = Vec::new();
467 let mut i = 0;
468 while i < guard.len() {
469 if guard[i].fire_at <= now {
470 fired.push(guard[i].callback_id);
471 if let Some(interval) = guard[i].interval {
472 guard[i].fire_at = now + interval;
473 i += 1;
474 } else {
475 guard.swap_remove(i);
476 }
477 } else {
478 i += 1;
479 }
480 }
481 fired
482}
483
484#[derive(Clone, Debug)]
489pub struct Hyperlink {
490 pub x: f32,
492 pub y: f32,
494 pub w: f32,
496 pub h: f32,
498 pub url: String,
500}
501
502#[derive(Clone, Debug, Default)]
504pub struct InputState {
505 pub mouse_x: f32,
507 pub mouse_y: f32,
509 pub mouse_buttons_down: [bool; 3],
511 pub mouse_buttons_clicked: [bool; 3],
513 pub keys_down: Vec<u32>,
515 pub keys_pressed: Vec<u32>,
517 pub modifiers_shift: bool,
519 pub modifiers_ctrl: bool,
521 pub modifiers_alt: bool,
523 pub scroll_x: f32,
525 pub scroll_y: f32,
527}
528
529#[derive(Clone, Debug)]
533pub enum WidgetCommand {
534 Button {
536 id: u32,
537 x: f32,
538 y: f32,
539 w: f32,
540 h: f32,
541 label: String,
542 },
543 Checkbox {
545 id: u32,
546 x: f32,
547 y: f32,
548 label: String,
549 },
550 Slider {
552 id: u32,
553 x: f32,
554 y: f32,
555 w: f32,
556 min: f32,
557 max: f32,
558 },
559 TextInput { id: u32, x: f32, y: f32, w: f32 },
561}
562
563#[derive(Clone, Debug)]
565pub enum WidgetValue {
566 Bool(bool),
568 Float(f32),
570 Text(String),
572}
573
574impl Default for HostState {
575 fn default() -> Self {
576 Self {
577 console: Arc::new(Mutex::new(Vec::new())),
578 canvas: Arc::new(Mutex::new(CanvasState {
579 commands: Vec::new(),
580 width: 800,
581 height: 600,
582 images: Vec::new(),
583 generation: 0,
584 })),
585 storage: Arc::new(Mutex::new(HashMap::new())),
586 timers: Arc::new(Mutex::new(Vec::new())),
587 animation_requests: Arc::new(Mutex::new(Vec::new())),
588 timer_next_id: Arc::new(Mutex::new(1)),
589 clipboard: Arc::new(Mutex::new(String::new())),
590 clipboard_allowed: Arc::new(Mutex::new(false)),
591 kv_db: None,
592 memory: None,
593 module_loader: None,
594 navigation: Arc::new(Mutex::new(NavigationStack::new())),
595 hyperlinks: Arc::new(Mutex::new(Vec::new())),
596 pending_navigation: Arc::new(Mutex::new(None)),
597 current_url: Arc::new(Mutex::new(String::new())),
598 input_state: Arc::new(Mutex::new(InputState::default())),
599 widget_commands: Arc::new(Mutex::new(Vec::new())),
600 widget_states: Arc::new(Mutex::new(HashMap::new())),
601 widget_clicked: Arc::new(Mutex::new(HashSet::new())),
602 canvas_offset: Arc::new(Mutex::new((0.0, 0.0))),
603 bookmark_store: crate::bookmarks::new_shared(),
604 history_store: Arc::new(Mutex::new(None)),
605 audio: Arc::new(Mutex::new(None)),
606 last_audio_url_content_type: Arc::new(Mutex::new(String::new())),
607 video: Arc::new(Mutex::new(VideoPlaybackState::default())),
608 video_pip_frame: Arc::new(Mutex::new(None)),
609 video_pip_serial: Arc::new(Mutex::new(0)),
610 media_capture: Arc::new(Mutex::new(
611 crate::media_capture::MediaCaptureState::default(),
612 )),
613 gpu: Arc::new(Mutex::new(None)),
614 rtc: Arc::new(Mutex::new(None)),
615 ws: Arc::new(Mutex::new(None)),
616 midi: Arc::new(Mutex::new(None)),
617 fetch: Arc::new(Mutex::new(None)),
618 file_picker: Arc::new(Mutex::new(crate::file_picker::FilePickerState::default())),
619 events: Arc::new(Mutex::new(crate::events::EventState::default())),
620 download_manager: DownloadManager::new(),
621 focused: Arc::new(AtomicBool::new(true)),
622 text_system: Arc::new(Mutex::new(None)),
623 content_width: Arc::new(Mutex::new(0)),
624 content_height: Arc::new(Mutex::new(0)),
625 scroll_x: Arc::new(Mutex::new(0.0)),
626 scroll_y: Arc::new(Mutex::new(0.0)),
627 }
628 }
629}
630
631#[allow(clippy::too_many_arguments)]
632fn video_render_at(
633 video: &Arc<Mutex<VideoPlaybackState>>,
634 pip_frame: &Arc<Mutex<Option<DecodedImage>>>,
635 pip_serial: &Arc<Mutex<u64>>,
636 canvas: &Arc<Mutex<CanvasState>>,
637 x: f32,
638 y: f32,
639 w: f32,
640 h: f32,
641) -> Result<(), String> {
642 let t = {
643 let g = video.lock().unwrap();
644 g.current_position_ms()
645 };
646 let mut g = video.lock().unwrap();
647 let player = g
648 .player
649 .as_mut()
650 .ok_or_else(|| "no video loaded".to_string())?;
651 let (pixels, pw, ph) = player.decode_frame_at(t)?;
652 let pip_on = g.pip;
653 let subtitle_text = subtitle::cue_text_at(&g.subtitles, t).map(|s| s.to_string());
654 drop(g);
655
656 let decoded = DecodedImage {
657 width: pw,
658 height: ph,
659 pixels,
660 };
661 if pip_on {
662 *pip_frame.lock().unwrap() = Some(decoded.clone());
663 if let Ok(mut s) = pip_serial.lock() {
664 *s = s.saturating_add(1);
665 }
666 }
667 let mut canvas = canvas.lock().unwrap();
668 let image_id = canvas.images.len();
669 canvas.images.push(decoded);
670 canvas.commands.push(DrawCommand::Image {
671 x,
672 y,
673 w,
674 h,
675 image_id,
676 });
677 if let Some(text) = subtitle_text {
678 let ty = (y + h - 24.0).max(y + 12.0);
679 canvas.commands.push(DrawCommand::Text {
680 x: x + 8.0,
681 y: ty,
682 size: 16.0,
683 r: 255,
684 g: 255,
685 b: 255,
686 a: 255,
687 text,
688 });
689 }
690 Ok(())
691}
692
693pub(crate) fn read_guest_string(
694 memory: &Memory,
695 store: &impl AsContext,
696 ptr: u32,
697 len: u32,
698) -> Result<String> {
699 let start = ptr as usize;
700 let end = start
701 .checked_add(len as usize)
702 .context("guest string pointer arithmetic overflow")?;
703 let data = memory
704 .data(store)
705 .get(start..end)
706 .context("guest string out of bounds")?;
707 String::from_utf8(data.to_vec()).context("guest string is not valid utf-8")
708}
709
710pub(crate) fn read_guest_bytes(
711 memory: &Memory,
712 store: &impl AsContext,
713 ptr: u32,
714 len: u32,
715) -> Result<Vec<u8>> {
716 let start = ptr as usize;
717 let end = start
718 .checked_add(len as usize)
719 .context("guest buffer pointer arithmetic overflow")?;
720 let data = memory
721 .data(store)
722 .get(start..end)
723 .context("guest buffer out of bounds")?;
724 Ok(data.to_vec())
725}
726
727pub(crate) fn write_guest_bytes(
728 memory: &Memory,
729 store: &mut impl AsContextMut,
730 ptr: u32,
731 bytes: &[u8],
732) -> Result<()> {
733 let start = ptr as usize;
734 let end = start
735 .checked_add(bytes.len())
736 .context("guest write pointer arithmetic overflow")?;
737 memory
738 .data_mut(store)
739 .get_mut(start..end)
740 .context("guest buffer out of bounds")?
741 .copy_from_slice(bytes);
742 Ok(())
743}
744
745pub(crate) fn clamp_weight(weight: u32) -> u16 {
748 if weight == 0 {
749 400
750 } else {
751 weight.clamp(100, 900) as u16
752 }
753}
754
755pub(crate) fn make_gpui_font(family: &str, weight: u16, style: u8) -> gpui::Font {
758 let family_name = if family.is_empty() {
759 ".SystemUIFont"
760 } else {
761 family
762 };
763 let mut f = gpui::font(family_name.to_string());
764 f.weight = gpui::FontWeight(weight as f32);
765 f.style = match style {
766 1 => gpui::FontStyle::Italic,
767 2 => gpui::FontStyle::Oblique,
768 _ => gpui::FontStyle::Normal,
769 };
770 f
771}
772
773pub fn console_log(console: &Arc<Mutex<Vec<ConsoleEntry>>>, level: ConsoleLevel, message: String) {
777 console.lock().unwrap().push(ConsoleEntry {
778 timestamp: chrono::Local::now().format("%H:%M:%S%.3f").to_string(),
779 level,
780 message,
781 });
782}
783
784fn audio_try_play(
785 engine: &mut AudioEngine,
786 channel: u32,
787 data: Vec<u8>,
788 format_hint: u32,
789 console: &Arc<Mutex<Vec<ConsoleEntry>>>,
790) -> bool {
791 let sniffed = audio_format::sniff_audio_format(&data);
792 if format_hint != 0
793 && format_hint != audio_format::AUDIO_FORMAT_UNKNOWN
794 && sniffed != audio_format::AUDIO_FORMAT_UNKNOWN
795 && sniffed != format_hint
796 {
797 console_log(
798 console,
799 ConsoleLevel::Warn,
800 format!("[AUDIO] Format hint {format_hint} does not match sniffed container {sniffed}"),
801 );
802 }
803 engine.play_bytes_on(channel, data)
804}
805
806fn render_canvas_to_pdf(canvas: &CanvasState, user_filename: &str) -> anyhow::Result<()> {
810 use std::io::{Cursor, Seek, Write};
811
812 const PT_PER_PX: f32 = 0.75;
813 let w_pt = canvas.width as f32 * PT_PER_PX;
814 let h_pt = canvas.height as f32 * PT_PER_PX;
815
816 let mut buf = Cursor::new(Vec::new());
817 let mut offsets: Vec<u64> = Vec::new();
818
819 offsets.push(buf.stream_position()?);
821 writeln!(buf, "1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj")?;
822
823 offsets.push(buf.stream_position()?);
825 writeln!(buf, "2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj")?;
826
827 offsets.push(buf.stream_position()?);
829 writeln!(
830 buf,
831 "5 0 obj<</Type/Font/Subtype/Type1/BaseFont/Helvetica>>endobj"
832 )?;
833
834 let mut content = Vec::new();
835 let flip_y = |y: f32| -> f32 { h_pt - y };
836
837 for cmd in &canvas.commands {
838 match cmd {
839 DrawCommand::Clear { r, g, b, a: _ } => {
840 let rf = *r as f32 / 255.0;
841 let gf = *g as f32 / 255.0;
842 let bf = *b as f32 / 255.0;
843 write!(content, "{rf:.3} {gf:.3} {bf:.3} rg ")?;
844 writeln!(content, "0 0 {w_pt:.1} {h_pt:.1} re f")?;
845 }
846 DrawCommand::Rect {
847 x,
848 y,
849 w: rw,
850 h: rh,
851 r,
852 g,
853 b,
854 a: _,
855 } => {
856 let rf = *r as f32 / 255.0;
857 let gf = *g as f32 / 255.0;
858 let bf = *b as f32 / 255.0;
859 let xp = *x * PT_PER_PX;
860 let yp = flip_y((*y + *rh) * PT_PER_PX);
861 let wp = *rw * PT_PER_PX;
862 let hp = *rh * PT_PER_PX;
863 write!(content, "{rf:.3} {gf:.3} {bf:.3} rg ")?;
864 writeln!(content, "{xp:.1} {yp:.1} {wp:.1} {hp:.1} re f")?;
865 }
866 DrawCommand::Text {
867 x,
868 y,
869 size,
870 r,
871 g,
872 b,
873 a: _,
874 text,
875 } => {
876 let rf = *r as f32 / 255.0;
877 let gf = *g as f32 / 255.0;
878 let bf = *b as f32 / 255.0;
879 let font_size = *size * PT_PER_PX;
880 let xp = *x * PT_PER_PX;
881 let yp = flip_y(*y * PT_PER_PX);
882 let escaped = escape_pdf_string(text);
883 write!(content, "BT {rf:.3} {gf:.3} {bf:.3} rg ")?;
884 writeln!(
885 content,
886 "/F1 {font_size:.1} Tf {xp:.1} {yp:.1} Td ({escaped}) Tj ET"
887 )?;
888 }
889 DrawCommand::Line {
890 x1,
891 y1,
892 x2,
893 y2,
894 r,
895 g,
896 b,
897 a: _,
898 thickness,
899 } => {
900 let rf = *r as f32 / 255.0;
901 let gf = *g as f32 / 255.0;
902 let bf = *b as f32 / 255.0;
903 let tp = *thickness * PT_PER_PX;
904 let x1p = *x1 * PT_PER_PX;
905 let y1p = flip_y(*y1 * PT_PER_PX);
906 let x2p = *x2 * PT_PER_PX;
907 let y2p = flip_y(*y2 * PT_PER_PX);
908 write!(content, "{rf:.3} {gf:.3} {bf:.3} RG {tp:.1} w ")?;
909 writeln!(content, "{x1p:.1} {y1p:.1} m {x2p:.1} {y2p:.1} l S")?;
910 }
911 DrawCommand::Circle {
912 cx,
913 cy,
914 radius,
915 r,
916 g,
917 b,
918 a: _,
919 } => {
920 let rf = *r as f32 / 255.0;
921 let gf = *g as f32 / 255.0;
922 let bf = *b as f32 / 255.0;
923 let cxp = *cx * PT_PER_PX;
924 let cyp = flip_y(*cy * PT_PER_PX);
925 let rp = *radius * PT_PER_PX;
926 let k = 0.5522848f32;
927 let kr = k * rp;
928 write!(content, "{rf:.3} {gf:.3} {bf:.3} rg ")?;
929 write!(content, "{:.1} {:.1} m ", cxp + rp, cyp)?;
930 write!(
931 content,
932 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c ",
933 cxp + rp,
934 cyp + kr,
935 cxp + kr,
936 cyp + rp,
937 cxp,
938 cyp + rp
939 )?;
940 write!(
941 content,
942 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c ",
943 cxp - kr,
944 cyp + rp,
945 cxp - rp,
946 cyp + kr,
947 cxp - rp,
948 cyp
949 )?;
950 write!(
951 content,
952 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c ",
953 cxp - rp,
954 cyp - kr,
955 cxp - kr,
956 cyp - rp,
957 cxp,
958 cyp - rp
959 )?;
960 writeln!(
961 content,
962 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c f",
963 cxp + kr,
964 cyp - rp,
965 cxp + rp,
966 cyp - kr,
967 cxp + rp,
968 cyp
969 )?;
970 }
971 DrawCommand::RoundedRect {
972 x,
973 y,
974 w: rw,
975 h: rh,
976 radius,
977 r,
978 g,
979 b,
980 a: _,
981 } => {
982 let rf = *r as f32 / 255.0;
983 let gf = *g as f32 / 255.0;
984 let bf = *b as f32 / 255.0;
985 let xp = *x * PT_PER_PX;
986 let yp = flip_y(*y * PT_PER_PX);
987 let wp = *rw * PT_PER_PX;
988 let hp = *rh * PT_PER_PX;
989 let rad = (*radius * PT_PER_PX).min(wp / 2.0).min(hp / 2.0);
990 let k = 0.5522848f32;
991 let kr = k * rad;
992 let x0 = xp;
993 let y0 = yp - hp;
994 let x1 = xp + wp;
995 let y1 = yp;
996 write!(content, "{rf:.3} {gf:.3} {bf:.3} rg ")?;
997 write!(content, "{:.1} {:.1} m ", x0 + rad, y0)?;
998 write!(content, "{:.1} {:.1} l ", x1 - rad, y0)?;
999 write!(
1000 content,
1001 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c ",
1002 x1 - rad + kr,
1003 y0,
1004 x1,
1005 y0 + rad - kr,
1006 x1,
1007 y0 + rad
1008 )?;
1009 write!(content, "{:.1} {:.1} l ", x1, y1 - rad)?;
1010 write!(
1011 content,
1012 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c ",
1013 x1,
1014 y1 - rad + kr,
1015 x1 - rad + kr,
1016 y1,
1017 x1 - rad,
1018 y1
1019 )?;
1020 write!(content, "{:.1} {:.1} l ", x0 + rad, y1)?;
1021 write!(
1022 content,
1023 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c ",
1024 x0 + rad - kr,
1025 y1,
1026 x0,
1027 y1 - rad + kr,
1028 x0,
1029 y1 - rad
1030 )?;
1031 write!(content, "{:.1} {:.1} l ", x0, y0 + rad)?;
1032 writeln!(
1033 content,
1034 "{:.1} {:.1} {:.1} {:.1} {:.1} {:.1} c f",
1035 x0,
1036 y0 + rad - kr,
1037 x0 + rad - kr,
1038 y0,
1039 x0 + rad,
1040 y0
1041 )?;
1042 }
1043 DrawCommand::Arc {
1044 cx,
1045 cy,
1046 radius,
1047 start_angle,
1048 end_angle,
1049 r,
1050 g,
1051 b,
1052 a: _,
1053 thickness,
1054 } => {
1055 let rf = *r as f32 / 255.0;
1056 let gf = *g as f32 / 255.0;
1057 let bf = *b as f32 / 255.0;
1058 let tp = *thickness * PT_PER_PX;
1059 let cxp = *cx * PT_PER_PX;
1060 let cyp = flip_y(*cy * PT_PER_PX);
1061 let rp = *radius * PT_PER_PX;
1062 let segs = 32u32;
1063 let angle_range = if *end_angle > *start_angle {
1064 *end_angle - *start_angle
1065 } else {
1066 *end_angle + 2.0 * std::f32::consts::PI - *start_angle
1067 };
1068 write!(content, "{rf:.3} {gf:.3} {bf:.3} RG {tp:.1} w ")?;
1069 let a0 = *start_angle;
1070 let mut first = true;
1071 for i in 0..=segs {
1072 let a = a0 + angle_range * i as f32 / segs as f32;
1073 let px = cxp + rp * a.cos();
1074 let py = cyp - rp * a.sin();
1075 if first {
1076 write!(content, "{px:.1} {py:.1} m ")?;
1077 first = false;
1078 } else {
1079 write!(content, "{px:.1} {py:.1} l ")?;
1080 }
1081 }
1082 writeln!(content, "S")?;
1083 }
1084 DrawCommand::Bezier {
1085 x1,
1086 y1,
1087 cp1x,
1088 cp1y,
1089 cp2x,
1090 cp2y,
1091 x2,
1092 y2,
1093 r,
1094 g,
1095 b,
1096 a: _,
1097 thickness,
1098 } => {
1099 let rf = *r as f32 / 255.0;
1100 let gf = *g as f32 / 255.0;
1101 let bf = *b as f32 / 255.0;
1102 let tp = *thickness * PT_PER_PX;
1103 let x1p = *x1 * PT_PER_PX;
1104 let y1p = flip_y(*y1 * PT_PER_PX);
1105 let cp1xp = *cp1x * PT_PER_PX;
1106 let cp1yp = flip_y(*cp1y * PT_PER_PX);
1107 let cp2xp = *cp2x * PT_PER_PX;
1108 let cp2yp = flip_y(*cp2y * PT_PER_PX);
1109 let x2p = *x2 * PT_PER_PX;
1110 let y2p = flip_y(*y2 * PT_PER_PX);
1111 write!(content, "{rf:.3} {gf:.3} {bf:.3} RG {tp:.1} w ")?;
1112 writeln!(content, "{x1p:.1} {y1p:.1} m {cp1xp:.1} {cp1yp:.1} {cp2xp:.1} {cp2yp:.1} {x2p:.1} {y2p:.1} c S")?;
1113 }
1114 DrawCommand::Image { .. }
1115 | DrawCommand::Gradient { .. }
1116 | DrawCommand::TextEx { .. }
1117 | DrawCommand::Save
1118 | DrawCommand::Restore
1119 | DrawCommand::Transform { .. }
1120 | DrawCommand::Clip { .. }
1121 | DrawCommand::Opacity { .. } => {}
1122 }
1123 }
1124
1125 let content_len = content.len();
1127 offsets.push(buf.stream_position()?);
1128 writeln!(buf, "4 0 obj<</Length {content_len}>>stream")?;
1129 buf.write_all(&content)?;
1130 writeln!(buf)?;
1131 writeln!(buf, "endstream")?;
1132 writeln!(buf, "endobj")?;
1133
1134 offsets.push(buf.stream_position()?);
1136 writeln!(
1137 buf,
1138 "3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 {w_pt:.1} {h_pt:.1}]/Contents 4 0 R/Resources<</Font<</F1 5 0 R>>>>>>endobj"
1139 )?;
1140
1141 let xref_offset = buf.stream_position()?;
1143 let obj_count = offsets.len() as u64 + 1;
1144 writeln!(buf, "xref\n0 {obj_count}\n0000000000 65535 f ")?;
1145 for off in &offsets {
1146 writeln!(buf, "{off:010} 00000 n ")?;
1147 }
1148
1149 write!(
1150 buf,
1151 "trailer<</Size {obj_count}/Root 1 0 R>>\nstartxref\n{xref_offset}\n%%EOF\n"
1152 )?;
1153
1154 let pdf_bytes = buf.into_inner();
1155 let dest_dir = dirs::download_dir()
1156 .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from(".")));
1157 let name_to_save = if user_filename.trim().is_empty() {
1158 format!(
1159 "{}",
1160 chrono::Local::now().format("oxide-canvas-%Y%m%d-%H%M%S.pdf")
1161 )
1162 } else if user_filename.to_lowercase().ends_with(".pdf") {
1163 user_filename.to_string()
1164 } else {
1165 format!("{}.pdf", user_filename)
1166 };
1167 let dest = crate::download::unique_path(&dest_dir, &name_to_save);
1168 std::fs::write(&dest, &pdf_bytes)?;
1169 Ok(())
1170}
1171
1172fn escape_pdf_string(s: &str) -> String {
1173 let mut out = String::with_capacity(s.len());
1174 for ch in s.chars() {
1175 match ch {
1176 '(' => out.push_str("\\("),
1177 ')' => out.push_str("\\)"),
1178 '\\' => out.push_str("\\\\"),
1179 '\n' => out.push_str("\\n"),
1180 '\r' => out.push_str("\\r"),
1181 '\t' => out.push_str("\\t"),
1182 c if (c as u32) < 0x80 => out.push(c),
1183 _ => {} }
1185 }
1186 out
1187}
1188
1189pub fn register_host_functions(linker: &mut Linker<HostState>) -> Result<()> {
1200 linker.func_wrap(
1203 "oxide",
1204 "api_log",
1205 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
1206 let mem = caller.data().memory.expect("memory not set");
1207 let msg = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
1208 console_log(&caller.data().console, ConsoleLevel::Log, msg);
1209 },
1210 )?;
1211
1212 linker.func_wrap(
1213 "oxide",
1214 "api_warn",
1215 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
1216 let mem = caller.data().memory.expect("memory not set");
1217 let msg = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
1218 console_log(&caller.data().console, ConsoleLevel::Warn, msg);
1219 },
1220 )?;
1221
1222 linker.func_wrap(
1223 "oxide",
1224 "api_error",
1225 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
1226 let mem = caller.data().memory.expect("memory not set");
1227 let msg = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
1228 console_log(&caller.data().console, ConsoleLevel::Error, msg);
1229 },
1230 )?;
1231
1232 linker.func_wrap(
1235 "oxide",
1236 "api_get_location",
1237 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
1238 let location = "37.7749,-122.4194"; let bytes = location.as_bytes();
1240 let write_len = bytes.len().min(out_cap as usize);
1241 let mem = caller.data().memory.expect("memory not set");
1242 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1243 write_len as u32
1244 },
1245 )?;
1246
1247 linker.func_wrap(
1250 "oxide",
1251 "api_upload_file",
1252 |mut caller: Caller<'_, HostState>,
1253 name_ptr: u32,
1254 name_cap: u32,
1255 data_ptr: u32,
1256 data_cap: u32|
1257 -> u64 {
1258 let dialog = rfd::FileDialog::new()
1259 .set_title("Oxide: Select a file to upload")
1260 .pick_file();
1261
1262 match dialog {
1263 Some(path) => {
1264 let file_name = path
1265 .file_name()
1266 .map(|n| n.to_string_lossy().to_string())
1267 .unwrap_or_default();
1268 let file_data = std::fs::read(&path).unwrap_or_default();
1269
1270 let mem = caller.data().memory.expect("memory not set");
1271
1272 let name_bytes = file_name.as_bytes();
1273 let name_written = name_bytes.len().min(name_cap as usize);
1274 write_guest_bytes(&mem, &mut caller, name_ptr, &name_bytes[..name_written])
1275 .ok();
1276
1277 let data_written = file_data.len().min(data_cap as usize);
1278 write_guest_bytes(&mem, &mut caller, data_ptr, &file_data[..data_written]).ok();
1279
1280 ((name_written as u64) << 32) | (data_written as u64)
1281 }
1282 None => 0,
1283 }
1284 },
1285 )?;
1286
1287 linker.func_wrap(
1290 "oxide",
1291 "api_canvas_clear",
1292 |caller: Caller<'_, HostState>, r: u32, g: u32, b: u32, a: u32| {
1293 let mut canvas = caller.data().canvas.lock().unwrap();
1294 canvas.commands.clear();
1295 canvas.images.clear();
1296 canvas.generation += 1;
1297 canvas.commands.push(DrawCommand::Clear {
1298 r: r as u8,
1299 g: g as u8,
1300 b: b as u8,
1301 a: a as u8,
1302 });
1303 },
1304 )?;
1305
1306 linker.func_wrap(
1307 "oxide",
1308 "api_canvas_rect",
1309 |caller: Caller<'_, HostState>,
1310 x: f32,
1311 y: f32,
1312 w: f32,
1313 h: f32,
1314 r: u32,
1315 g: u32,
1316 b: u32,
1317 a: u32| {
1318 caller
1319 .data()
1320 .canvas
1321 .lock()
1322 .unwrap()
1323 .commands
1324 .push(DrawCommand::Rect {
1325 x,
1326 y,
1327 w,
1328 h,
1329 r: r as u8,
1330 g: g as u8,
1331 b: b as u8,
1332 a: a as u8,
1333 });
1334 },
1335 )?;
1336
1337 linker.func_wrap(
1338 "oxide",
1339 "api_canvas_circle",
1340 |caller: Caller<'_, HostState>,
1341 cx: f32,
1342 cy: f32,
1343 radius: f32,
1344 r: u32,
1345 g: u32,
1346 b: u32,
1347 a: u32| {
1348 caller
1349 .data()
1350 .canvas
1351 .lock()
1352 .unwrap()
1353 .commands
1354 .push(DrawCommand::Circle {
1355 cx,
1356 cy,
1357 radius,
1358 r: r as u8,
1359 g: g as u8,
1360 b: b as u8,
1361 a: a as u8,
1362 });
1363 },
1364 )?;
1365
1366 linker.func_wrap(
1367 "oxide",
1368 "api_canvas_text",
1369 |caller: Caller<'_, HostState>,
1370 x: f32,
1371 y: f32,
1372 size: f32,
1373 r: u32,
1374 g: u32,
1375 b: u32,
1376 a: u32,
1377 txt_ptr: u32,
1378 txt_len: u32| {
1379 let mem = caller.data().memory.expect("memory not set");
1380 let text = read_guest_string(&mem, &caller, txt_ptr, txt_len).unwrap_or_default();
1381 caller
1382 .data()
1383 .canvas
1384 .lock()
1385 .unwrap()
1386 .commands
1387 .push(DrawCommand::Text {
1388 x,
1389 y,
1390 size,
1391 r: r as u8,
1392 g: g as u8,
1393 b: b as u8,
1394 a: a as u8,
1395 text,
1396 });
1397 },
1398 )?;
1399
1400 linker.func_wrap(
1401 "oxide",
1402 "api_canvas_text_ex",
1403 |caller: Caller<'_, HostState>,
1404 x: f32,
1405 y: f32,
1406 size: f32,
1407 r: u32,
1408 g: u32,
1409 b: u32,
1410 a: u32,
1411 fam_ptr: u32,
1412 fam_len: u32,
1413 weight: u32,
1414 style: u32,
1415 align: u32,
1416 txt_ptr: u32,
1417 txt_len: u32| {
1418 let mem = caller.data().memory.expect("memory not set");
1419 let family = read_guest_string(&mem, &caller, fam_ptr, fam_len).unwrap_or_default();
1420 let text = read_guest_string(&mem, &caller, txt_ptr, txt_len).unwrap_or_default();
1421 let weight = clamp_weight(weight);
1422 let style = (style.min(2)) as u8;
1423 let align = (align.min(2)) as u8;
1424 caller
1425 .data()
1426 .canvas
1427 .lock()
1428 .unwrap()
1429 .commands
1430 .push(DrawCommand::TextEx {
1431 x,
1432 y,
1433 size,
1434 r: r as u8,
1435 g: g as u8,
1436 b: b as u8,
1437 a: a as u8,
1438 family,
1439 weight,
1440 style,
1441 align,
1442 text,
1443 });
1444 },
1445 )?;
1446
1447 linker.func_wrap(
1451 "oxide",
1452 "api_canvas_measure_text",
1453 |mut caller: Caller<'_, HostState>,
1454 size: f32,
1455 fam_ptr: u32,
1456 fam_len: u32,
1457 weight: u32,
1458 style: u32,
1459 txt_ptr: u32,
1460 txt_len: u32,
1461 out_ptr: u32|
1462 -> u32 {
1463 let mem = caller.data().memory.expect("memory not set");
1464 let family = read_guest_string(&mem, &caller, fam_ptr, fam_len).unwrap_or_default();
1465 let text = read_guest_string(&mem, &caller, txt_ptr, txt_len).unwrap_or_default();
1466 let weight = clamp_weight(weight);
1467 let style = (style.min(2)) as u8;
1468 let ts = caller.data().text_system.lock().unwrap().clone();
1469 let Some(ts) = ts else { return 0 };
1470 let font = make_gpui_font(&family, weight, style);
1471 let run = gpui::TextRun {
1472 len: text.len(),
1473 font,
1474 color: gpui::rgba(0xffffffff).into(),
1475 background_color: None,
1476 underline: None,
1477 strikethrough: None,
1478 };
1479 let layout = ts.layout_line(&text, gpui::px(size), &[run], None);
1480 let mut buf = [0u8; 12];
1481 buf[0..4].copy_from_slice(&f32::from(layout.width).to_le_bytes());
1482 buf[4..8].copy_from_slice(&f32::from(layout.ascent).to_le_bytes());
1483 buf[8..12].copy_from_slice(&f32::from(layout.descent).to_le_bytes());
1484 if write_guest_bytes(&mem, &mut caller, out_ptr, &buf).is_ok() {
1485 1
1486 } else {
1487 0
1488 }
1489 },
1490 )?;
1491
1492 linker.func_wrap(
1493 "oxide",
1494 "api_canvas_line",
1495 |caller: Caller<'_, HostState>,
1496 x1: f32,
1497 y1: f32,
1498 x2: f32,
1499 y2: f32,
1500 r: u32,
1501 g: u32,
1502 b: u32,
1503 a: u32,
1504 thickness: f32| {
1505 caller
1506 .data()
1507 .canvas
1508 .lock()
1509 .unwrap()
1510 .commands
1511 .push(DrawCommand::Line {
1512 x1,
1513 y1,
1514 x2,
1515 y2,
1516 r: r as u8,
1517 g: g as u8,
1518 b: b as u8,
1519 a: a as u8,
1520 thickness,
1521 });
1522 },
1523 )?;
1524
1525 linker.func_wrap(
1526 "oxide",
1527 "api_canvas_dimensions",
1528 |caller: Caller<'_, HostState>| -> u64 {
1529 let canvas = caller.data().canvas.lock().unwrap();
1530 ((canvas.width as u64) << 32) | (canvas.height as u64)
1531 },
1532 )?;
1533
1534 linker.func_wrap(
1535 "oxide",
1536 "api_set_content_size",
1537 |caller: Caller<'_, HostState>, w: u32, h: u32| {
1538 *caller.data().content_width.lock().unwrap() = w;
1539 *caller.data().content_height.lock().unwrap() = h;
1540 },
1541 )?;
1542
1543 linker.func_wrap(
1544 "oxide",
1545 "api_get_scroll_position",
1546 |caller: Caller<'_, HostState>| -> u64 {
1547 let x = *caller.data().scroll_x.lock().unwrap();
1548 let y = *caller.data().scroll_y.lock().unwrap();
1549 ((x.to_bits() as u64) << 32) | (y.to_bits() as u64)
1550 },
1551 )?;
1552
1553 linker.func_wrap(
1554 "oxide",
1555 "api_set_scroll_position",
1556 |caller: Caller<'_, HostState>, x: f32, y: f32| {
1557 let content_w = *caller.data().content_width.lock().unwrap();
1558 let content_h = *caller.data().content_height.lock().unwrap();
1559
1560 let viewport_w = caller.data().canvas.lock().unwrap().width;
1561 let viewport_h = caller.data().canvas.lock().unwrap().height;
1562
1563 let max_x = (content_w as f32 - viewport_w as f32).max(0.0);
1564 let max_y = (content_h as f32 - viewport_h as f32).max(0.0);
1565
1566 *caller.data().scroll_x.lock().unwrap() = x.clamp(0.0, max_x);
1567 *caller.data().scroll_y.lock().unwrap() = y.clamp(0.0, max_y);
1568 },
1569 )?;
1570
1571 linker.func_wrap(
1574 "oxide",
1575 "api_canvas_rounded_rect",
1576 |caller: Caller<'_, HostState>,
1577 x: f32,
1578 y: f32,
1579 w: f32,
1580 h: f32,
1581 radius: f32,
1582 r: u32,
1583 g: u32,
1584 b: u32,
1585 a: u32| {
1586 caller
1587 .data()
1588 .canvas
1589 .lock()
1590 .unwrap()
1591 .commands
1592 .push(DrawCommand::RoundedRect {
1593 x,
1594 y,
1595 w,
1596 h,
1597 radius,
1598 r: r as u8,
1599 g: g as u8,
1600 b: b as u8,
1601 a: a as u8,
1602 });
1603 },
1604 )?;
1605
1606 linker.func_wrap(
1607 "oxide",
1608 "api_canvas_arc",
1609 |caller: Caller<'_, HostState>,
1610 cx: f32,
1611 cy: f32,
1612 radius: f32,
1613 start_angle: f32,
1614 end_angle: f32,
1615 r: u32,
1616 g: u32,
1617 b: u32,
1618 a: u32,
1619 thickness: f32| {
1620 caller
1621 .data()
1622 .canvas
1623 .lock()
1624 .unwrap()
1625 .commands
1626 .push(DrawCommand::Arc {
1627 cx,
1628 cy,
1629 radius,
1630 start_angle,
1631 end_angle,
1632 r: r as u8,
1633 g: g as u8,
1634 b: b as u8,
1635 a: a as u8,
1636 thickness,
1637 });
1638 },
1639 )?;
1640
1641 linker.func_wrap(
1642 "oxide",
1643 "api_canvas_bezier",
1644 |caller: Caller<'_, HostState>,
1645 x1: f32,
1646 y1: f32,
1647 cp1x: f32,
1648 cp1y: f32,
1649 cp2x: f32,
1650 cp2y: f32,
1651 x2: f32,
1652 y2: f32,
1653 r: u32,
1654 g: u32,
1655 b: u32,
1656 a: u32,
1657 thickness: f32| {
1658 caller
1659 .data()
1660 .canvas
1661 .lock()
1662 .unwrap()
1663 .commands
1664 .push(DrawCommand::Bezier {
1665 x1,
1666 y1,
1667 cp1x,
1668 cp1y,
1669 cp2x,
1670 cp2y,
1671 x2,
1672 y2,
1673 r: r as u8,
1674 g: g as u8,
1675 b: b as u8,
1676 a: a as u8,
1677 thickness,
1678 });
1679 },
1680 )?;
1681
1682 linker.func_wrap(
1683 "oxide",
1684 "api_canvas_gradient",
1685 |caller: Caller<'_, HostState>,
1686 x: f32,
1687 y: f32,
1688 w: f32,
1689 h: f32,
1690 kind: u32,
1691 ax: f32,
1692 ay: f32,
1693 bx: f32,
1694 by: f32,
1695 stops_ptr: u32,
1696 stops_len: u32| {
1697 let mem = caller.data().memory.expect("memory not set");
1698 let bytes = read_guest_bytes(&mem, &caller, stops_ptr, stops_len).unwrap_or_default();
1699 let mut stops = Vec::new();
1700 let mut i = 0;
1702 while i + 8 <= bytes.len() {
1703 let offset =
1704 f32::from_le_bytes([bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3]]);
1705 let sr = bytes[i + 4];
1706 let sg = bytes[i + 5];
1707 let sb = bytes[i + 6];
1708 let sa = bytes[i + 7];
1709 stops.push(GradientStop {
1710 offset,
1711 r: sr,
1712 g: sg,
1713 b: sb,
1714 a: sa,
1715 });
1716 i += 8;
1717 }
1718 caller
1719 .data()
1720 .canvas
1721 .lock()
1722 .unwrap()
1723 .commands
1724 .push(DrawCommand::Gradient {
1725 x,
1726 y,
1727 w,
1728 h,
1729 kind: kind as u8,
1730 ax,
1731 ay,
1732 bx,
1733 by,
1734 stops,
1735 });
1736 },
1737 )?;
1738
1739 linker.func_wrap(
1742 "oxide",
1743 "api_canvas_save",
1744 |caller: Caller<'_, HostState>| {
1745 caller
1746 .data()
1747 .canvas
1748 .lock()
1749 .unwrap()
1750 .commands
1751 .push(DrawCommand::Save);
1752 },
1753 )?;
1754
1755 linker.func_wrap(
1756 "oxide",
1757 "api_canvas_restore",
1758 |caller: Caller<'_, HostState>| {
1759 caller
1760 .data()
1761 .canvas
1762 .lock()
1763 .unwrap()
1764 .commands
1765 .push(DrawCommand::Restore);
1766 },
1767 )?;
1768
1769 linker.func_wrap(
1770 "oxide",
1771 "api_canvas_transform",
1772 |caller: Caller<'_, HostState>, a: f32, b: f32, c: f32, d: f32, tx: f32, ty: f32| {
1773 caller
1774 .data()
1775 .canvas
1776 .lock()
1777 .unwrap()
1778 .commands
1779 .push(DrawCommand::Transform { a, b, c, d, tx, ty });
1780 },
1781 )?;
1782
1783 linker.func_wrap(
1784 "oxide",
1785 "api_canvas_clip",
1786 |caller: Caller<'_, HostState>, x: f32, y: f32, w: f32, h: f32| {
1787 caller
1788 .data()
1789 .canvas
1790 .lock()
1791 .unwrap()
1792 .commands
1793 .push(DrawCommand::Clip { x, y, w, h });
1794 },
1795 )?;
1796
1797 linker.func_wrap(
1798 "oxide",
1799 "api_canvas_opacity",
1800 |caller: Caller<'_, HostState>, alpha: f32| {
1801 caller
1802 .data()
1803 .canvas
1804 .lock()
1805 .unwrap()
1806 .commands
1807 .push(DrawCommand::Opacity { alpha });
1808 },
1809 )?;
1810
1811 linker.func_wrap(
1814 "oxide",
1815 "api_canvas_image",
1816 |caller: Caller<'_, HostState>,
1817 x: f32,
1818 y: f32,
1819 w: f32,
1820 h: f32,
1821 data_ptr: u32,
1822 data_len: u32| {
1823 let mem = caller.data().memory.expect("memory not set");
1824 let raw = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
1825 match image::load_from_memory(&raw) {
1826 Ok(img) => {
1827 let (iw, ih) = img.dimensions();
1828 const MAX_IMAGE_PIXELS: u32 = 4096 * 4096; if iw.saturating_mul(ih) > MAX_IMAGE_PIXELS {
1830 console_log(
1831 &caller.data().console,
1832 ConsoleLevel::Error,
1833 format!(
1834 "[IMAGE] Rejected: {iw}x{ih} exceeds maximum of {MAX_IMAGE_PIXELS} pixels"
1835 ),
1836 );
1837 return;
1838 }
1839 let rgba = img.to_rgba8();
1840 let (iw, ih) = (rgba.width(), rgba.height());
1841 let decoded = DecodedImage {
1842 width: iw,
1843 height: ih,
1844 pixels: rgba.into_raw(),
1845 };
1846 let mut canvas = caller.data().canvas.lock().unwrap();
1847 let image_id = canvas.images.len();
1848 canvas.images.push(decoded);
1849 canvas.commands.push(DrawCommand::Image {
1850 x,
1851 y,
1852 w,
1853 h,
1854 image_id,
1855 });
1856 }
1857 Err(e) => {
1858 console_log(
1859 &caller.data().console,
1860 ConsoleLevel::Error,
1861 format!("[IMAGE] Failed to decode: {e}"),
1862 );
1863 }
1864 }
1865 },
1866 )?;
1867
1868 linker.func_wrap(
1871 "oxide",
1872 "api_storage_set",
1873 |caller: Caller<'_, HostState>, key_ptr: u32, key_len: u32, val_ptr: u32, val_len: u32| {
1874 let mem = caller.data().memory.expect("memory not set");
1875 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
1876 let val = read_guest_string(&mem, &caller, val_ptr, val_len).unwrap_or_default();
1877 caller.data().storage.lock().unwrap().insert(key, val);
1878 },
1879 )?;
1880
1881 linker.func_wrap(
1882 "oxide",
1883 "api_storage_get",
1884 |mut caller: Caller<'_, HostState>,
1885 key_ptr: u32,
1886 key_len: u32,
1887 out_ptr: u32,
1888 out_cap: u32|
1889 -> u32 {
1890 let mem = caller.data().memory.expect("memory not set");
1891 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
1892 let val = caller
1893 .data()
1894 .storage
1895 .lock()
1896 .unwrap()
1897 .get(&key)
1898 .cloned()
1899 .unwrap_or_default();
1900 let bytes = val.as_bytes();
1901 let write_len = bytes.len().min(out_cap as usize);
1902 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1903 write_len as u32
1904 },
1905 )?;
1906
1907 linker.func_wrap(
1908 "oxide",
1909 "api_storage_remove",
1910 |caller: Caller<'_, HostState>, key_ptr: u32, key_len: u32| {
1911 let mem = caller.data().memory.expect("memory not set");
1912 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
1913 caller.data().storage.lock().unwrap().remove(&key);
1914 },
1915 )?;
1916
1917 linker.func_wrap(
1920 "oxide",
1921 "api_clipboard_write",
1922 |caller: Caller<'_, HostState>, ptr: u32, len: u32| {
1923 let allowed = *caller.data().clipboard_allowed.lock().unwrap();
1924 if !allowed {
1925 console_log(
1926 &caller.data().console,
1927 ConsoleLevel::Warn,
1928 "[CLIPBOARD] Write blocked — clipboard access not permitted".into(),
1929 );
1930 return;
1931 }
1932 let mem = caller.data().memory.expect("memory not set");
1933 let text = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
1934 *caller.data().clipboard.lock().unwrap() = text.clone();
1935 if let Ok(mut ctx) = arboard::Clipboard::new() {
1936 let _ = ctx.set_text(text);
1937 }
1938 },
1939 )?;
1940
1941 linker.func_wrap(
1942 "oxide",
1943 "api_clipboard_read",
1944 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
1945 let allowed = *caller.data().clipboard_allowed.lock().unwrap();
1946 if !allowed {
1947 console_log(
1948 &caller.data().console,
1949 ConsoleLevel::Warn,
1950 "[CLIPBOARD] Read blocked — clipboard access not permitted".into(),
1951 );
1952 return 0;
1953 }
1954 let text = arboard::Clipboard::new()
1955 .and_then(|mut ctx| ctx.get_text())
1956 .unwrap_or_default();
1957 let bytes = text.as_bytes();
1958 let write_len = bytes.len().min(out_cap as usize);
1959 let mem = caller.data().memory.expect("memory not set");
1960 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
1961 write_len as u32
1962 },
1963 )?;
1964
1965 linker.func_wrap(
1968 "oxide",
1969 "api_time_now_ms",
1970 |_caller: Caller<'_, HostState>| -> u64 {
1971 std::time::SystemTime::now()
1972 .duration_since(std::time::UNIX_EPOCH)
1973 .unwrap_or_default()
1974 .as_millis() as u64
1975 },
1976 )?;
1977
1978 linker.func_wrap(
1983 "oxide",
1984 "api_set_timeout",
1985 |caller: Caller<'_, HostState>, callback_id: u32, delay_ms: u32| -> u32 {
1986 let mut next = caller.data().timer_next_id.lock().unwrap();
1987 let id = *next;
1988 *next = next.wrapping_add(1).max(1);
1989 drop(next);
1990
1991 let entry = TimerEntry {
1992 id,
1993 fire_at: Instant::now() + Duration::from_millis(delay_ms as u64),
1994 interval: None,
1995 callback_id,
1996 };
1997 caller.data().timers.lock().unwrap().push(entry);
1998 id
1999 },
2000 )?;
2001
2002 linker.func_wrap(
2003 "oxide",
2004 "api_set_interval",
2005 |caller: Caller<'_, HostState>, callback_id: u32, interval_ms: u32| -> u32 {
2006 let mut next = caller.data().timer_next_id.lock().unwrap();
2007 let id = *next;
2008 *next = next.wrapping_add(1).max(1);
2009 drop(next);
2010
2011 let interval = Duration::from_millis(interval_ms as u64);
2012 let entry = TimerEntry {
2013 id,
2014 fire_at: Instant::now() + interval,
2015 interval: Some(interval),
2016 callback_id,
2017 };
2018 caller.data().timers.lock().unwrap().push(entry);
2019 id
2020 },
2021 )?;
2022
2023 linker.func_wrap(
2024 "oxide",
2025 "api_clear_timer",
2026 |caller: Caller<'_, HostState>, timer_id: u32| {
2027 caller
2028 .data()
2029 .timers
2030 .lock()
2031 .unwrap()
2032 .retain(|t| t.id != timer_id);
2033 },
2034 )?;
2035
2036 linker.func_wrap(
2042 "oxide",
2043 "api_request_animation_frame",
2044 |caller: Caller<'_, HostState>, callback_id: u32| -> u32 {
2045 let mut next = caller.data().timer_next_id.lock().unwrap();
2046 let id = *next;
2047 *next = next.wrapping_add(1).max(1);
2048 drop(next);
2049
2050 let req = AnimationRequest { id, callback_id };
2051 caller.data().animation_requests.lock().unwrap().push(req);
2052 id
2053 },
2054 )?;
2055
2056 linker.func_wrap(
2057 "oxide",
2058 "api_cancel_animation_frame",
2059 |caller: Caller<'_, HostState>, request_id: u32| {
2060 caller
2061 .data()
2062 .animation_requests
2063 .lock()
2064 .unwrap()
2065 .retain(|r| r.id != request_id);
2066 },
2067 )?;
2068
2069 linker.func_wrap(
2072 "oxide",
2073 "api_random",
2074 |_caller: Caller<'_, HostState>| -> u64 {
2075 let mut buf = [0u8; 8];
2076 getrandom(&mut buf);
2077 u64::from_le_bytes(buf)
2078 },
2079 )?;
2080
2081 linker.func_wrap(
2084 "oxide",
2085 "api_notify",
2086 |caller: Caller<'_, HostState>,
2087 title_ptr: u32,
2088 title_len: u32,
2089 body_ptr: u32,
2090 body_len: u32| {
2091 let mem = caller.data().memory.expect("memory not set");
2092 let title = read_guest_string(&mem, &caller, title_ptr, title_len).unwrap_or_default();
2093 let body = read_guest_string(&mem, &caller, body_ptr, body_len).unwrap_or_default();
2094 console_log(
2095 &caller.data().console,
2096 ConsoleLevel::Log,
2097 format!("[NOTIFICATION] {title}: {body}"),
2098 );
2099 },
2100 )?;
2101
2102 linker.func_wrap(
2108 "oxide",
2109 "api_fetch",
2110 |mut caller: Caller<'_, HostState>,
2111 method_ptr: u32,
2112 method_len: u32,
2113 url_ptr: u32,
2114 url_len: u32,
2115 ct_ptr: u32,
2116 ct_len: u32,
2117 body_ptr: u32,
2118 body_len: u32,
2119 out_ptr: u32,
2120 out_cap: u32|
2121 -> i64 {
2122 let mem = caller.data().memory.expect("memory not set");
2123 let method =
2124 read_guest_string(&mem, &caller, method_ptr, method_len).unwrap_or_default();
2125 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
2126 let content_type = read_guest_string(&mem, &caller, ct_ptr, ct_len).unwrap_or_default();
2127 let body = if body_len > 0 {
2128 read_guest_bytes(&mem, &caller, body_ptr, body_len).unwrap_or_default()
2129 } else {
2130 Vec::new()
2131 };
2132
2133 console_log(
2134 &caller.data().console,
2135 ConsoleLevel::Log,
2136 format!("[FETCH] {method} {url}"),
2137 );
2138
2139 let (resp_tx, resp_rx) =
2140 std::sync::mpsc::sync_channel::<Result<(u16, Vec<u8>), String>>(1);
2141
2142 std::thread::spawn(move || {
2143 let result = (|| -> Result<(u16, Vec<u8>), String> {
2144 let client = reqwest::blocking::Client::builder()
2145 .timeout(Duration::from_secs(30))
2146 .build()
2147 .map_err(|e| e.to_string())?;
2148 let parsed: reqwest::Method = method.parse().unwrap_or(reqwest::Method::GET);
2149 let mut req = client.request(parsed, &url);
2150 if !content_type.is_empty() {
2151 req = req.header("Content-Type", &content_type);
2152 }
2153 if !body.is_empty() {
2154 req = req.body(body);
2155 }
2156 let resp = req.send().map_err(|e| e.to_string())?;
2157 let status = resp.status().as_u16();
2158 let bytes = resp.bytes().map_err(|e| e.to_string())?.to_vec();
2159 Ok((status, bytes))
2160 })();
2161 let _ = resp_tx.send(result);
2162 });
2163
2164 match resp_rx.recv() {
2165 Ok(Ok((status, response_body))) => {
2166 let write_len = response_body.len().min(out_cap as usize);
2167 write_guest_bytes(&mem, &mut caller, out_ptr, &response_body[..write_len]).ok();
2168 ((status as i64) << 32) | (write_len as i64)
2169 }
2170 Ok(Err(e)) => {
2171 console_log(
2172 &caller.data().console,
2173 ConsoleLevel::Error,
2174 format!("[FETCH ERROR] {e}"),
2175 );
2176 -1
2177 }
2178 Err(_) => -1,
2179 }
2180 },
2181 )?;
2182
2183 linker.func_wrap(
2190 "oxide",
2191 "api_load_module",
2192 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
2193 let mem = caller.data().memory.expect("memory not set");
2194 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
2195 let loader = match &caller.data().module_loader {
2196 Some(l) => l.clone(),
2197 None => return -1,
2198 };
2199 let mut child_state = caller.data().clone();
2200 child_state.memory = None;
2201 let console = caller.data().console.clone();
2202
2203 console_log(
2204 &console,
2205 ConsoleLevel::Log,
2206 format!("[LOAD] Fetching module: {url}"),
2207 );
2208
2209 let (tx, rx) = std::sync::mpsc::sync_channel::<Result<Vec<u8>, String>>(1);
2210 let fetch_url = url.clone();
2211 std::thread::spawn(move || {
2212 let result = (|| -> Result<Vec<u8>, String> {
2213 let client = reqwest::blocking::Client::builder()
2214 .timeout(Duration::from_secs(30))
2215 .build()
2216 .map_err(|e| e.to_string())?;
2217 let resp = client
2218 .get(&fetch_url)
2219 .header("Accept", "application/wasm")
2220 .send()
2221 .map_err(|e| e.to_string())?;
2222 if !resp.status().is_success() {
2223 return Err(format!("HTTP {}", resp.status()));
2224 }
2225 resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())
2226 })();
2227 let _ = tx.send(result);
2228 });
2229
2230 let wasm_bytes = match rx.recv() {
2231 Ok(Ok(bytes)) => bytes,
2232 Ok(Err(e)) => {
2233 console_log(&console, ConsoleLevel::Error, format!("[LOAD ERROR] {e}"));
2234 return -1;
2235 }
2236 Err(_) => return -1,
2237 };
2238
2239 let module = match Module::new(&loader.engine, &wasm_bytes) {
2240 Ok(m) => m,
2241 Err(e) => {
2242 console_log(
2243 &console,
2244 ConsoleLevel::Error,
2245 format!("[LOAD ERROR] Compile: {e}"),
2246 );
2247 return -2;
2248 }
2249 };
2250
2251 let mut store = Store::new(&loader.engine, child_state);
2252 if store.set_fuel(loader.fuel_limit).is_err() {
2253 return -3;
2254 }
2255
2256 let mut child_linker = Linker::new(&loader.engine);
2257 if register_host_functions(&mut child_linker).is_err() {
2258 return -3;
2259 }
2260
2261 let mem_type = MemoryType::new(1, Some(loader.max_memory_pages));
2262 let memory = match Memory::new(&mut store, mem_type) {
2263 Ok(m) => m,
2264 Err(_) => return -4,
2265 };
2266
2267 if child_linker
2268 .define(&store, "oxide", "memory", memory)
2269 .is_err()
2270 {
2271 return -5;
2272 }
2273 store.data_mut().memory = Some(memory);
2274
2275 let instance = match child_linker.instantiate(&mut store, &module) {
2276 Ok(i) => i,
2277 Err(e) => {
2278 console_log(
2279 &console,
2280 ConsoleLevel::Error,
2281 format!("[LOAD ERROR] Instantiate: {e}"),
2282 );
2283 return -6;
2284 }
2285 };
2286
2287 if let Some(guest_mem) = instance.get_memory(&mut store, "memory") {
2289 store.data_mut().memory = Some(guest_mem);
2290 }
2291
2292 let start_fn = match instance.get_typed_func::<(), ()>(&mut store, "start_app") {
2293 Ok(f) => f,
2294 Err(_) => {
2295 console_log(
2296 &console,
2297 ConsoleLevel::Error,
2298 "[LOAD ERROR] Module missing start_app".into(),
2299 );
2300 return -7;
2301 }
2302 };
2303
2304 match start_fn.call(&mut store, ()) {
2305 Ok(()) => {
2306 console_log(
2307 &console,
2308 ConsoleLevel::Log,
2309 format!("[LOAD] Module {url} executed successfully"),
2310 );
2311 0
2312 }
2313 Err(e) => {
2314 let msg = if e.to_string().contains("fuel") {
2315 "[LOAD ERROR] Child module fuel limit exceeded".to_string()
2316 } else {
2317 format!("[LOAD ERROR] Runtime: {e}")
2318 };
2319 console_log(&console, ConsoleLevel::Error, msg);
2320 -8
2321 }
2322 }
2323 },
2324 )?;
2325
2326 linker.func_wrap(
2329 "oxide",
2330 "api_hash_sha256",
2331 |mut caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32, out_ptr: u32| -> u32 {
2332 use sha2::{Digest, Sha256};
2333 let mem = caller.data().memory.expect("memory not set");
2334 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
2335 let hash = Sha256::digest(&data);
2336 write_guest_bytes(&mem, &mut caller, out_ptr, &hash).ok();
2337 hash.len() as u32
2338 },
2339 )?;
2340
2341 linker.func_wrap(
2344 "oxide",
2345 "api_base64_encode",
2346 |mut caller: Caller<'_, HostState>,
2347 data_ptr: u32,
2348 data_len: u32,
2349 out_ptr: u32,
2350 out_cap: u32|
2351 -> u32 {
2352 use base64::Engine;
2353 let mem = caller.data().memory.expect("memory not set");
2354 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
2355 let encoded = base64::engine::general_purpose::STANDARD.encode(&data);
2356 let bytes = encoded.as_bytes();
2357 let write_len = bytes.len().min(out_cap as usize);
2358 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2359 write_len as u32
2360 },
2361 )?;
2362
2363 linker.func_wrap(
2364 "oxide",
2365 "api_base64_decode",
2366 |mut caller: Caller<'_, HostState>,
2367 data_ptr: u32,
2368 data_len: u32,
2369 out_ptr: u32,
2370 out_cap: u32|
2371 -> u32 {
2372 use base64::Engine;
2373 let mem = caller.data().memory.expect("memory not set");
2374 let encoded = read_guest_string(&mem, &caller, data_ptr, data_len).unwrap_or_default();
2375 match base64::engine::general_purpose::STANDARD.decode(&encoded) {
2376 Ok(decoded) => {
2377 let write_len = decoded.len().min(out_cap as usize);
2378 write_guest_bytes(&mem, &mut caller, out_ptr, &decoded[..write_len]).ok();
2379 write_len as u32
2380 }
2381 Err(_) => 0,
2382 }
2383 },
2384 )?;
2385
2386 linker.func_wrap(
2391 "oxide",
2392 "api_kv_store_set",
2393 |caller: Caller<'_, HostState>,
2394 key_ptr: u32,
2395 key_len: u32,
2396 val_ptr: u32,
2397 val_len: u32|
2398 -> i32 {
2399 let mem = caller.data().memory.expect("memory not set");
2400 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
2401 let val = read_guest_bytes(&mem, &caller, val_ptr, val_len).unwrap_or_default();
2402 let origin = caller.data().current_url.lock().unwrap().clone();
2403 let prefixed_key = format!("{origin}::{key}");
2404 match &caller.data().kv_db {
2405 Some(db) => match db.insert(prefixed_key.as_bytes(), val) {
2406 Ok(_) => {
2407 let _ = db.flush();
2408 0
2409 }
2410 Err(e) => {
2411 console_log(
2412 &caller.data().console,
2413 ConsoleLevel::Error,
2414 format!("[KV] set failed: {e}"),
2415 );
2416 -1
2417 }
2418 },
2419 None => {
2420 console_log(
2421 &caller.data().console,
2422 ConsoleLevel::Error,
2423 "[KV] store not initialised".into(),
2424 );
2425 -1
2426 }
2427 }
2428 },
2429 )?;
2430
2431 linker.func_wrap(
2432 "oxide",
2433 "api_kv_store_get",
2434 |mut caller: Caller<'_, HostState>,
2435 key_ptr: u32,
2436 key_len: u32,
2437 out_ptr: u32,
2438 out_cap: u32|
2439 -> i32 {
2440 let mem = caller.data().memory.expect("memory not set");
2441 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
2442 let origin = caller.data().current_url.lock().unwrap().clone();
2443 let prefixed_key = format!("{origin}::{key}");
2444 match &caller.data().kv_db {
2445 Some(db) => match db.get(prefixed_key.as_bytes()) {
2446 Ok(Some(val)) => {
2447 let bytes = val.as_ref();
2448 let write_len = bytes.len().min(out_cap as usize);
2449 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2450 write_len as i32
2451 }
2452 Ok(None) => -1,
2453 Err(e) => {
2454 console_log(
2455 &caller.data().console,
2456 ConsoleLevel::Error,
2457 format!("[KV] get failed: {e}"),
2458 );
2459 -2
2460 }
2461 },
2462 None => -2,
2463 }
2464 },
2465 )?;
2466
2467 linker.func_wrap(
2468 "oxide",
2469 "api_kv_store_delete",
2470 |caller: Caller<'_, HostState>, key_ptr: u32, key_len: u32| -> i32 {
2471 let mem = caller.data().memory.expect("memory not set");
2472 let key = read_guest_string(&mem, &caller, key_ptr, key_len).unwrap_or_default();
2473 let origin = caller.data().current_url.lock().unwrap().clone();
2474 let prefixed_key = format!("{origin}::{key}");
2475 match &caller.data().kv_db {
2476 Some(db) => match db.remove(prefixed_key.as_bytes()) {
2477 Ok(_) => {
2478 let _ = db.flush();
2479 0
2480 }
2481 Err(e) => {
2482 console_log(
2483 &caller.data().console,
2484 ConsoleLevel::Error,
2485 format!("[KV] delete failed: {e}"),
2486 );
2487 -1
2488 }
2489 },
2490 None => -1,
2491 }
2492 },
2493 )?;
2494
2495 linker.func_wrap(
2498 "oxide",
2499 "api_navigate",
2500 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
2501 let mem = caller.data().memory.expect("memory not set");
2502 let raw_url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
2503
2504 let resolved = {
2505 let cur = caller.data().current_url.lock().unwrap();
2506 if cur.is_empty() {
2507 raw_url.clone()
2508 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
2509 base.join(&raw_url)
2510 .map(|u| u.as_str().to_string())
2511 .unwrap_or(raw_url.clone())
2512 } else {
2513 raw_url.clone()
2514 }
2515 };
2516
2517 if oxide_url::OxideUrl::parse(&resolved).is_err() {
2518 console_log(
2519 &caller.data().console,
2520 ConsoleLevel::Error,
2521 format!("[NAV] invalid URL: {resolved}"),
2522 );
2523 return -1;
2524 }
2525
2526 console_log(
2527 &caller.data().console,
2528 ConsoleLevel::Log,
2529 format!("[NAV] navigate → {resolved}"),
2530 );
2531 *caller.data().pending_navigation.lock().unwrap() = Some(resolved);
2532 0
2533 },
2534 )?;
2535
2536 linker.func_wrap(
2537 "oxide",
2538 "api_push_state",
2539 |caller: Caller<'_, HostState>,
2540 state_ptr: u32,
2541 state_len: u32,
2542 title_ptr: u32,
2543 title_len: u32,
2544 url_ptr: u32,
2545 url_len: u32| {
2546 let mem = caller.data().memory.expect("memory not set");
2547 let state = read_guest_bytes(&mem, &caller, state_ptr, state_len).unwrap_or_default();
2548 let title = read_guest_string(&mem, &caller, title_ptr, title_len).unwrap_or_default();
2549 let url_arg = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
2550
2551 let resolved_url = if url_arg.is_empty() {
2552 caller.data().current_url.lock().unwrap().clone()
2553 } else {
2554 let cur = caller.data().current_url.lock().unwrap();
2555 if cur.is_empty() {
2556 url_arg
2557 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
2558 base.join(&url_arg)
2559 .map(|u| u.as_str().to_string())
2560 .unwrap_or(url_arg)
2561 } else {
2562 url_arg
2563 }
2564 };
2565
2566 let entry = crate::navigation::HistoryEntry::new(&resolved_url)
2567 .with_title(title)
2568 .with_state(state);
2569 caller.data().navigation.lock().unwrap().push(entry);
2570 *caller.data().current_url.lock().unwrap() = resolved_url;
2571 },
2572 )?;
2573
2574 linker.func_wrap(
2575 "oxide",
2576 "api_replace_state",
2577 |caller: Caller<'_, HostState>,
2578 state_ptr: u32,
2579 state_len: u32,
2580 title_ptr: u32,
2581 title_len: u32,
2582 url_ptr: u32,
2583 url_len: u32| {
2584 let mem = caller.data().memory.expect("memory not set");
2585 let state = read_guest_bytes(&mem, &caller, state_ptr, state_len).unwrap_or_default();
2586 let title = read_guest_string(&mem, &caller, title_ptr, title_len).unwrap_or_default();
2587 let url_arg = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
2588
2589 let resolved_url = if url_arg.is_empty() {
2590 caller.data().current_url.lock().unwrap().clone()
2591 } else {
2592 let cur = caller.data().current_url.lock().unwrap();
2593 if cur.is_empty() {
2594 url_arg
2595 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
2596 base.join(&url_arg)
2597 .map(|u| u.as_str().to_string())
2598 .unwrap_or(url_arg)
2599 } else {
2600 url_arg
2601 }
2602 };
2603
2604 let entry = crate::navigation::HistoryEntry::new(&resolved_url)
2605 .with_title(title)
2606 .with_state(state);
2607 caller
2608 .data()
2609 .navigation
2610 .lock()
2611 .unwrap()
2612 .replace_current(entry);
2613 *caller.data().current_url.lock().unwrap() = resolved_url;
2614 },
2615 )?;
2616
2617 linker.func_wrap(
2618 "oxide",
2619 "api_get_url",
2620 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
2621 let url = caller.data().current_url.lock().unwrap().clone();
2622 let bytes = url.as_bytes();
2623 let write_len = bytes.len().min(out_cap as usize);
2624 let mem = caller.data().memory.expect("memory not set");
2625 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2626 write_len as u32
2627 },
2628 )?;
2629
2630 linker.func_wrap(
2631 "oxide",
2632 "api_get_state",
2633 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> i32 {
2634 let state_bytes = {
2635 let nav = caller.data().navigation.lock().unwrap();
2636 match nav.current() {
2637 Some(entry) if !entry.state.is_empty() => Some(entry.state.clone()),
2638 _ => None,
2639 }
2640 };
2641 match state_bytes {
2642 Some(bytes) => {
2643 let write_len = bytes.len().min(out_cap as usize);
2644 let mem = caller.data().memory.expect("memory not set");
2645 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2646 write_len as i32
2647 }
2648 None => -1,
2649 }
2650 },
2651 )?;
2652
2653 linker.func_wrap(
2654 "oxide",
2655 "api_history_length",
2656 |caller: Caller<'_, HostState>| -> u32 {
2657 caller.data().navigation.lock().unwrap().len() as u32
2658 },
2659 )?;
2660
2661 linker.func_wrap(
2662 "oxide",
2663 "api_history_back",
2664 |caller: Caller<'_, HostState>| -> i32 {
2665 let mut nav = caller.data().navigation.lock().unwrap();
2666 match nav.go_back() {
2667 Some(entry) => {
2668 let url = entry.url.clone();
2669 *caller.data().current_url.lock().unwrap() = url.clone();
2670 *caller.data().pending_navigation.lock().unwrap() = Some(url);
2671 1
2672 }
2673 None => 0,
2674 }
2675 },
2676 )?;
2677
2678 linker.func_wrap(
2679 "oxide",
2680 "api_history_forward",
2681 |caller: Caller<'_, HostState>| -> i32 {
2682 let mut nav = caller.data().navigation.lock().unwrap();
2683 match nav.go_forward() {
2684 Some(entry) => {
2685 let url = entry.url.clone();
2686 *caller.data().current_url.lock().unwrap() = url.clone();
2687 *caller.data().pending_navigation.lock().unwrap() = Some(url);
2688 1
2689 }
2690 None => 0,
2691 }
2692 },
2693 )?;
2694
2695 linker.func_wrap(
2698 "oxide",
2699 "api_register_hyperlink",
2700 |caller: Caller<'_, HostState>,
2701 x: f32,
2702 y: f32,
2703 w: f32,
2704 h: f32,
2705 url_ptr: u32,
2706 url_len: u32|
2707 -> i32 {
2708 let mem = caller.data().memory.expect("memory not set");
2709 let raw_url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
2710
2711 let resolved = {
2712 let cur = caller.data().current_url.lock().unwrap();
2713 if cur.is_empty() {
2714 raw_url.clone()
2715 } else if let Ok(base) = oxide_url::OxideUrl::parse(&cur) {
2716 base.join(&raw_url)
2717 .map(|u| u.as_str().to_string())
2718 .unwrap_or(raw_url.clone())
2719 } else {
2720 raw_url.clone()
2721 }
2722 };
2723
2724 caller.data().hyperlinks.lock().unwrap().push(Hyperlink {
2725 x,
2726 y,
2727 w,
2728 h,
2729 url: resolved,
2730 });
2731 0
2732 },
2733 )?;
2734
2735 linker.func_wrap(
2736 "oxide",
2737 "api_clear_hyperlinks",
2738 |caller: Caller<'_, HostState>| {
2739 caller.data().hyperlinks.lock().unwrap().clear();
2740 },
2741 )?;
2742
2743 linker.func_wrap(
2746 "oxide",
2747 "api_url_resolve",
2748 |mut caller: Caller<'_, HostState>,
2749 base_ptr: u32,
2750 base_len: u32,
2751 rel_ptr: u32,
2752 rel_len: u32,
2753 out_ptr: u32,
2754 out_cap: u32|
2755 -> i32 {
2756 let mem = caller.data().memory.expect("memory not set");
2757 let base_str = read_guest_string(&mem, &caller, base_ptr, base_len).unwrap_or_default();
2758 let rel_str = read_guest_string(&mem, &caller, rel_ptr, rel_len).unwrap_or_default();
2759
2760 let base = match oxide_url::OxideUrl::parse(&base_str) {
2761 Ok(u) => u,
2762 Err(_) => return -1,
2763 };
2764 let resolved = match base.join(&rel_str) {
2765 Ok(u) => u,
2766 Err(_) => return -2,
2767 };
2768
2769 let bytes = resolved.as_str().as_bytes();
2770 let write_len = bytes.len().min(out_cap as usize);
2771 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2772 write_len as i32
2773 },
2774 )?;
2775
2776 linker.func_wrap(
2777 "oxide",
2778 "api_url_encode",
2779 |mut caller: Caller<'_, HostState>,
2780 input_ptr: u32,
2781 input_len: u32,
2782 out_ptr: u32,
2783 out_cap: u32|
2784 -> u32 {
2785 let mem = caller.data().memory.expect("memory not set");
2786 let input = read_guest_string(&mem, &caller, input_ptr, input_len).unwrap_or_default();
2787 let encoded = oxide_url::percent_encode(&input);
2788 let bytes = encoded.as_bytes();
2789 let write_len = bytes.len().min(out_cap as usize);
2790 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2791 write_len as u32
2792 },
2793 )?;
2794
2795 linker.func_wrap(
2796 "oxide",
2797 "api_url_decode",
2798 |mut caller: Caller<'_, HostState>,
2799 input_ptr: u32,
2800 input_len: u32,
2801 out_ptr: u32,
2802 out_cap: u32|
2803 -> u32 {
2804 let mem = caller.data().memory.expect("memory not set");
2805 let input = read_guest_string(&mem, &caller, input_ptr, input_len).unwrap_or_default();
2806 let decoded = oxide_url::percent_decode(&input);
2807 let bytes = decoded.as_bytes();
2808 let write_len = bytes.len().min(out_cap as usize);
2809 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
2810 write_len as u32
2811 },
2812 )?;
2813
2814 linker.func_wrap(
2817 "oxide",
2818 "api_mouse_position",
2819 |caller: Caller<'_, HostState>| -> u64 {
2820 let input = caller.data().input_state.lock().unwrap();
2821 let offset = caller.data().canvas_offset.lock().unwrap();
2822 let x = input.mouse_x - offset.0;
2823 let y = input.mouse_y - offset.1;
2824 ((x.to_bits() as u64) << 32) | (y.to_bits() as u64)
2825 },
2826 )?;
2827
2828 linker.func_wrap(
2829 "oxide",
2830 "api_mouse_button_down",
2831 |caller: Caller<'_, HostState>, button: u32| -> u32 {
2832 let input = caller.data().input_state.lock().unwrap();
2833 if (button as usize) < 3 && input.mouse_buttons_down[button as usize] {
2834 1
2835 } else {
2836 0
2837 }
2838 },
2839 )?;
2840
2841 linker.func_wrap(
2842 "oxide",
2843 "api_mouse_button_clicked",
2844 |caller: Caller<'_, HostState>, button: u32| -> u32 {
2845 let input = caller.data().input_state.lock().unwrap();
2846 if (button as usize) < 3 && input.mouse_buttons_clicked[button as usize] {
2847 1
2848 } else {
2849 0
2850 }
2851 },
2852 )?;
2853
2854 linker.func_wrap(
2855 "oxide",
2856 "api_key_down",
2857 |caller: Caller<'_, HostState>, key: u32| -> u32 {
2858 let input = caller.data().input_state.lock().unwrap();
2859 if input.keys_down.contains(&key) {
2860 1
2861 } else {
2862 0
2863 }
2864 },
2865 )?;
2866
2867 linker.func_wrap(
2868 "oxide",
2869 "api_key_pressed",
2870 |caller: Caller<'_, HostState>, key: u32| -> u32 {
2871 let input = caller.data().input_state.lock().unwrap();
2872 if input.keys_pressed.contains(&key) {
2873 1
2874 } else {
2875 0
2876 }
2877 },
2878 )?;
2879
2880 linker.func_wrap(
2881 "oxide",
2882 "api_scroll_delta",
2883 |caller: Caller<'_, HostState>| -> u64 {
2884 let input = caller.data().input_state.lock().unwrap();
2885 ((input.scroll_x.to_bits() as u64) << 32) | (input.scroll_y.to_bits() as u64)
2886 },
2887 )?;
2888
2889 linker.func_wrap(
2890 "oxide",
2891 "api_modifiers",
2892 |caller: Caller<'_, HostState>| -> u32 {
2893 let input = caller.data().input_state.lock().unwrap();
2894 let mut flags = 0u32;
2895 if input.modifiers_shift {
2896 flags |= 1;
2897 }
2898 if input.modifiers_ctrl {
2899 flags |= 2;
2900 }
2901 if input.modifiers_alt {
2902 flags |= 4;
2903 }
2904 flags
2905 },
2906 )?;
2907
2908 linker.func_wrap(
2914 "oxide",
2915 "api_audio_play",
2916 |caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32| -> i32 {
2917 let mem = caller.data().memory.expect("memory not set");
2918 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
2919 if data.is_empty() {
2920 return -1;
2921 }
2922
2923 let audio = caller.data().audio.clone();
2924 let mut guard = audio.lock().unwrap();
2925 if guard.is_none() {
2926 *guard = AudioEngine::try_new();
2927 }
2928 match guard.as_mut() {
2929 Some(engine) => {
2930 if engine.play_bytes_on(0, data) {
2931 console_log(
2932 &caller.data().console,
2933 ConsoleLevel::Log,
2934 "[AUDIO] Playing from bytes".into(),
2935 );
2936 0
2937 } else {
2938 console_log(
2939 &caller.data().console,
2940 ConsoleLevel::Error,
2941 "[AUDIO] Failed to decode audio data".into(),
2942 );
2943 -2
2944 }
2945 }
2946 None => {
2947 console_log(
2948 &caller.data().console,
2949 ConsoleLevel::Error,
2950 "[AUDIO] No audio device available".into(),
2951 );
2952 -3
2953 }
2954 }
2955 },
2956 )?;
2957
2958 linker.func_wrap(
2959 "oxide",
2960 "api_audio_detect_format",
2961 |caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32| -> u32 {
2962 let mem = caller.data().memory.expect("memory not set");
2963 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
2964 audio_format::sniff_audio_format(&data)
2965 },
2966 )?;
2967
2968 linker.func_wrap(
2969 "oxide",
2970 "api_audio_play_with_format",
2971 |caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32, format_hint: u32| -> i32 {
2972 let mem = caller.data().memory.expect("memory not set");
2973 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
2974 if data.is_empty() {
2975 return -1;
2976 }
2977
2978 let audio = caller.data().audio.clone();
2979 let mut guard = audio.lock().unwrap();
2980 if guard.is_none() {
2981 *guard = AudioEngine::try_new();
2982 }
2983 match guard.as_mut() {
2984 Some(engine) => {
2985 if audio_try_play(engine, 0, data, format_hint, &caller.data().console) {
2986 console_log(
2987 &caller.data().console,
2988 ConsoleLevel::Log,
2989 "[AUDIO] Playing from bytes (with format hint)".into(),
2990 );
2991 0
2992 } else {
2993 console_log(
2994 &caller.data().console,
2995 ConsoleLevel::Error,
2996 "[AUDIO] Failed to decode audio data".into(),
2997 );
2998 -2
2999 }
3000 }
3001 None => {
3002 console_log(
3003 &caller.data().console,
3004 ConsoleLevel::Error,
3005 "[AUDIO] No audio device available".into(),
3006 );
3007 -3
3008 }
3009 }
3010 },
3011 )?;
3012
3013 linker.func_wrap(
3014 "oxide",
3015 "api_audio_play_url",
3016 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
3017 let mem = caller.data().memory.expect("memory not set");
3018 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
3019
3020 console_log(
3021 &caller.data().console,
3022 ConsoleLevel::Log,
3023 format!("[AUDIO] Fetching {url}"),
3024 );
3025
3026 let (tx, rx) =
3027 std::sync::mpsc::sync_channel::<Result<(Vec<u8>, Option<String>), String>>(1);
3028 let fetch_url = url.clone();
3029 std::thread::spawn(move || {
3030 let result = (|| -> Result<(Vec<u8>, Option<String>), String> {
3031 let client = reqwest::blocking::Client::builder()
3032 .timeout(Duration::from_secs(30))
3033 .build()
3034 .map_err(|e| e.to_string())?;
3035 let resp = client
3036 .get(&fetch_url)
3037 .header(ACCEPT, audio_format::AUDIO_HTTP_ACCEPT)
3038 .send()
3039 .map_err(|e| e.to_string())?;
3040 if !resp.status().is_success() {
3041 return Err(format!("HTTP {}", resp.status()));
3042 }
3043 let ct = resp
3044 .headers()
3045 .get(CONTENT_TYPE)
3046 .and_then(|v| v.to_str().ok())
3047 .map(|s| s.to_string());
3048 let bytes = resp.bytes().map(|b| b.to_vec()).map_err(|e| e.to_string())?;
3049 Ok((bytes, ct))
3050 })();
3051 let _ = tx.send(result);
3052 });
3053
3054 let (data, content_type) = match rx.recv() {
3055 Ok(Ok(pair)) => pair,
3056 Ok(Err(e)) => {
3057 console_log(
3058 &caller.data().console,
3059 ConsoleLevel::Error,
3060 format!("[AUDIO] Fetch error: {e}"),
3061 );
3062 return -1;
3063 }
3064 Err(_) => return -1,
3065 };
3066
3067 *caller.data().last_audio_url_content_type.lock().unwrap() =
3068 content_type.clone().unwrap_or_default();
3069
3070 let sniffed = audio_format::sniff_audio_format(&data);
3071 if let Some(ref ct) = content_type {
3072 if audio_format::is_likely_non_audio_document(ct)
3073 && sniffed == audio_format::AUDIO_FORMAT_UNKNOWN
3074 {
3075 console_log(
3076 &caller.data().console,
3077 ConsoleLevel::Error,
3078 "[AUDIO] Response is not a supported audio resource (document MIME, no audio signature)"
3079 .into(),
3080 );
3081 return -4;
3082 }
3083 let mime_fmt = audio_format::mime_to_audio_format(ct);
3084 if mime_fmt != audio_format::AUDIO_FORMAT_UNKNOWN
3085 && sniffed != audio_format::AUDIO_FORMAT_UNKNOWN
3086 && mime_fmt != sniffed
3087 {
3088 console_log(
3089 &caller.data().console,
3090 ConsoleLevel::Warn,
3091 format!(
3092 "[AUDIO] Content-Type disagrees with sniffed container (MIME -> {mime_fmt}, sniff -> {sniffed})"
3093 ),
3094 );
3095 }
3096 }
3097
3098 let audio = caller.data().audio.clone();
3099 let mut guard = audio.lock().unwrap();
3100 if guard.is_none() {
3101 *guard = AudioEngine::try_new();
3102 }
3103 match guard.as_mut() {
3104 Some(engine) => {
3105 if engine.play_bytes_on(0, data) {
3106 let ct = content_type.as_deref().unwrap_or("(none)");
3107 console_log(
3108 &caller.data().console,
3109 ConsoleLevel::Log,
3110 format!("[AUDIO] Playing from URL: {url} (Content-Type: {ct})"),
3111 );
3112 0
3113 } else {
3114 console_log(
3115 &caller.data().console,
3116 ConsoleLevel::Error,
3117 "[AUDIO] Failed to decode fetched audio".into(),
3118 );
3119 -2
3120 }
3121 }
3122 None => {
3123 console_log(
3124 &caller.data().console,
3125 ConsoleLevel::Error,
3126 "[AUDIO] No audio device available".into(),
3127 );
3128 -3
3129 }
3130 }
3131 },
3132 )?;
3133
3134 linker.func_wrap(
3135 "oxide",
3136 "api_audio_last_url_content_type",
3137 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
3138 let s = caller
3139 .data()
3140 .last_audio_url_content_type
3141 .lock()
3142 .unwrap()
3143 .clone();
3144 let bytes = s.as_bytes();
3145 let write_len = bytes.len().min(out_cap as usize);
3146 let mem = caller.data().memory.expect("memory not set");
3147 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
3148 write_len as u32
3149 },
3150 )?;
3151
3152 linker.func_wrap(
3153 "oxide",
3154 "api_audio_pause",
3155 |caller: Caller<'_, HostState>| {
3156 let audio = caller.data().audio.clone();
3157 let guard = audio.lock().unwrap();
3158 if let Some(engine) = guard.as_ref() {
3159 if let Some(ch) = engine.channels.get(&0) {
3160 ch.player.pause();
3161 }
3162 }
3163 },
3164 )?;
3165
3166 linker.func_wrap(
3167 "oxide",
3168 "api_audio_resume",
3169 |caller: Caller<'_, HostState>| {
3170 let audio = caller.data().audio.clone();
3171 let guard = audio.lock().unwrap();
3172 if let Some(engine) = guard.as_ref() {
3173 if let Some(ch) = engine.channels.get(&0) {
3174 ch.player.play();
3175 }
3176 }
3177 },
3178 )?;
3179
3180 linker.func_wrap(
3181 "oxide",
3182 "api_audio_stop",
3183 |caller: Caller<'_, HostState>| {
3184 let audio = caller.data().audio.clone();
3185 let guard = audio.lock().unwrap();
3186 if let Some(engine) = guard.as_ref() {
3187 if let Some(ch) = engine.channels.get(&0) {
3188 ch.player.stop();
3189 }
3190 }
3191 },
3192 )?;
3193
3194 linker.func_wrap(
3195 "oxide",
3196 "api_audio_set_volume",
3197 |caller: Caller<'_, HostState>, level: f32| {
3198 let audio = caller.data().audio.clone();
3199 let guard = audio.lock().unwrap();
3200 if let Some(engine) = guard.as_ref() {
3201 if let Some(ch) = engine.channels.get(&0) {
3202 ch.player.set_volume(level.clamp(0.0, 2.0));
3203 }
3204 }
3205 },
3206 )?;
3207
3208 linker.func_wrap(
3209 "oxide",
3210 "api_audio_get_volume",
3211 |caller: Caller<'_, HostState>| -> f32 {
3212 let audio = caller.data().audio.clone();
3213 let guard = audio.lock().unwrap();
3214 guard
3215 .as_ref()
3216 .and_then(|e| e.channels.get(&0))
3217 .map(|ch| ch.player.volume())
3218 .unwrap_or(1.0)
3219 },
3220 )?;
3221
3222 linker.func_wrap(
3223 "oxide",
3224 "api_audio_is_playing",
3225 |caller: Caller<'_, HostState>| -> u32 {
3226 let audio = caller.data().audio.clone();
3227 let guard = audio.lock().unwrap();
3228 match guard.as_ref().and_then(|e| e.channels.get(&0)) {
3229 Some(ch) if !ch.player.is_paused() && !ch.player.empty() => 1,
3230 _ => 0,
3231 }
3232 },
3233 )?;
3234
3235 linker.func_wrap(
3236 "oxide",
3237 "api_audio_position",
3238 |caller: Caller<'_, HostState>| -> u64 {
3239 let audio = caller.data().audio.clone();
3240 let guard = audio.lock().unwrap();
3241 guard
3242 .as_ref()
3243 .and_then(|e| e.channels.get(&0))
3244 .map(|ch| ch.player.get_pos().as_millis() as u64)
3245 .unwrap_or(0)
3246 },
3247 )?;
3248
3249 linker.func_wrap(
3250 "oxide",
3251 "api_audio_seek",
3252 |caller: Caller<'_, HostState>, position_ms: u64| -> i32 {
3253 let audio = caller.data().audio.clone();
3254 let guard = audio.lock().unwrap();
3255 match guard.as_ref().and_then(|e| e.channels.get(&0)) {
3256 Some(ch) => {
3257 let pos = Duration::from_millis(position_ms);
3258 match ch.player.try_seek(pos) {
3259 Ok(_) => 0,
3260 Err(e) => {
3261 console_log(
3262 &caller.data().console,
3263 ConsoleLevel::Warn,
3264 format!("[AUDIO] Seek failed: {e}"),
3265 );
3266 -1
3267 }
3268 }
3269 }
3270 None => -1,
3271 }
3272 },
3273 )?;
3274
3275 linker.func_wrap(
3276 "oxide",
3277 "api_audio_duration",
3278 |caller: Caller<'_, HostState>| -> u64 {
3279 let audio = caller.data().audio.clone();
3280 let guard = audio.lock().unwrap();
3281 guard
3282 .as_ref()
3283 .and_then(|e| e.channels.get(&0))
3284 .map(|ch| ch.duration_ms)
3285 .unwrap_or(0)
3286 },
3287 )?;
3288
3289 linker.func_wrap(
3290 "oxide",
3291 "api_audio_set_loop",
3292 |caller: Caller<'_, HostState>, enabled: u32| {
3293 let audio = caller.data().audio.clone();
3294 let mut guard = audio.lock().unwrap();
3295 if guard.is_none() {
3296 *guard = AudioEngine::try_new();
3297 }
3298 if let Some(engine) = guard.as_mut() {
3299 engine.ensure_channel(0).looping = enabled != 0;
3300 }
3301 },
3302 )?;
3303
3304 linker.func_wrap(
3305 "oxide",
3306 "api_audio_channel_play",
3307 |caller: Caller<'_, HostState>, channel: u32, data_ptr: u32, data_len: u32| -> i32 {
3308 let mem = caller.data().memory.expect("memory not set");
3309 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
3310 if data.is_empty() {
3311 return -1;
3312 }
3313
3314 let audio = caller.data().audio.clone();
3315 let mut guard = audio.lock().unwrap();
3316 if guard.is_none() {
3317 *guard = AudioEngine::try_new();
3318 }
3319 match guard.as_mut() {
3320 Some(engine) => {
3321 if engine.play_bytes_on(channel, data) {
3322 console_log(
3323 &caller.data().console,
3324 ConsoleLevel::Log,
3325 format!("[AUDIO] Playing on channel {channel}"),
3326 );
3327 0
3328 } else {
3329 console_log(
3330 &caller.data().console,
3331 ConsoleLevel::Error,
3332 format!("[AUDIO] Failed to decode audio for channel {channel}"),
3333 );
3334 -2
3335 }
3336 }
3337 None => -3,
3338 }
3339 },
3340 )?;
3341
3342 linker.func_wrap(
3343 "oxide",
3344 "api_audio_channel_play_with_format",
3345 |caller: Caller<'_, HostState>,
3346 channel: u32,
3347 data_ptr: u32,
3348 data_len: u32,
3349 format_hint: u32|
3350 -> i32 {
3351 let mem = caller.data().memory.expect("memory not set");
3352 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
3353 if data.is_empty() {
3354 return -1;
3355 }
3356
3357 let audio = caller.data().audio.clone();
3358 let mut guard = audio.lock().unwrap();
3359 if guard.is_none() {
3360 *guard = AudioEngine::try_new();
3361 }
3362 match guard.as_mut() {
3363 Some(engine) => {
3364 if audio_try_play(engine, channel, data, format_hint, &caller.data().console) {
3365 console_log(
3366 &caller.data().console,
3367 ConsoleLevel::Log,
3368 format!("[AUDIO] Playing on channel {channel} (with format hint)"),
3369 );
3370 0
3371 } else {
3372 console_log(
3373 &caller.data().console,
3374 ConsoleLevel::Error,
3375 format!("[AUDIO] Failed to decode audio for channel {channel}"),
3376 );
3377 -2
3378 }
3379 }
3380 None => -3,
3381 }
3382 },
3383 )?;
3384
3385 linker.func_wrap(
3386 "oxide",
3387 "api_audio_channel_stop",
3388 |caller: Caller<'_, HostState>, channel: u32| {
3389 let audio = caller.data().audio.clone();
3390 let guard = audio.lock().unwrap();
3391 if let Some(engine) = guard.as_ref() {
3392 if let Some(ch) = engine.channels.get(&channel) {
3393 ch.player.stop();
3394 }
3395 }
3396 },
3397 )?;
3398
3399 linker.func_wrap(
3400 "oxide",
3401 "api_audio_channel_set_volume",
3402 |caller: Caller<'_, HostState>, channel: u32, level: f32| {
3403 let audio = caller.data().audio.clone();
3404 let guard = audio.lock().unwrap();
3405 if let Some(engine) = guard.as_ref() {
3406 if let Some(ch) = engine.channels.get(&channel) {
3407 ch.player.set_volume(level.clamp(0.0, 2.0));
3408 }
3409 }
3410 },
3411 )?;
3412
3413 linker.func_wrap(
3416 "oxide",
3417 "api_video_detect_format",
3418 |caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32| -> u32 {
3419 let mem = caller.data().memory.expect("memory not set");
3420 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
3421 video_format::sniff_video_format(&data)
3422 },
3423 )?;
3424
3425 linker.func_wrap(
3426 "oxide",
3427 "api_video_load",
3428 |caller: Caller<'_, HostState>, data_ptr: u32, data_len: u32, format_hint: u32| -> i32 {
3429 let mem = caller.data().memory.expect("memory not set");
3430 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
3431 if data.is_empty() {
3432 return -1;
3433 }
3434 let mut guard = caller.data().video.lock().unwrap();
3435 match guard.open_bytes(&data, format_hint) {
3436 Ok(()) => {
3437 console_log(
3438 &caller.data().console,
3439 ConsoleLevel::Log,
3440 "[VIDEO] Loaded from bytes".into(),
3441 );
3442 0
3443 }
3444 Err(e) => {
3445 console_log(
3446 &caller.data().console,
3447 ConsoleLevel::Error,
3448 format!("[VIDEO] Load failed: {e}"),
3449 );
3450 -2
3451 }
3452 }
3453 },
3454 )?;
3455
3456 linker.func_wrap(
3457 "oxide",
3458 "api_video_load_url",
3459 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
3460 let mem = caller.data().memory.expect("memory not set");
3461 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
3462 if url.is_empty() {
3463 return -1;
3464 }
3465 console_log(
3466 &caller.data().console,
3467 ConsoleLevel::Log,
3468 format!("[VIDEO] Opening {url}"),
3469 );
3470
3471 let client = match reqwest::blocking::Client::builder()
3472 .timeout(Duration::from_secs(90))
3473 .build()
3474 {
3475 Ok(c) => c,
3476 Err(_) => return -3,
3477 };
3478
3479 let mut ct = String::new();
3480 if let Ok(resp) = client.head(&url).send() {
3481 if let Some(h) = resp.headers().get(CONTENT_TYPE) {
3482 if let Ok(s) = h.to_str() {
3483 ct = s.to_string();
3484 }
3485 }
3486 }
3487
3488 let mut master_body: Option<String> = None;
3489 let fetch_master = url.to_ascii_lowercase().contains("m3u8")
3490 || ct.to_ascii_lowercase().contains("mpegurl")
3491 || ct.to_ascii_lowercase().contains("m3u8");
3492 if fetch_master {
3493 if let Ok(resp) = client
3494 .get(&url)
3495 .header(ACCEPT, video_format::VIDEO_HTTP_ACCEPT)
3496 .timeout(Duration::from_secs(60))
3497 .send()
3498 {
3499 if resp.status().is_success() {
3500 if let Ok(t) = resp.text() {
3501 master_body = Some(t);
3502 }
3503 }
3504 }
3505 }
3506
3507 let mut guard = caller.data().video.lock().unwrap();
3508 guard.stop();
3509 guard.last_url_content_type = ct.clone();
3510 guard.hls_base_url = url.clone();
3511 if let Some(ref body) = master_body {
3512 guard.hls_variants = video::parse_hls_master_variants(body);
3513 } else {
3514 guard.hls_variants.clear();
3515 }
3516
3517 match video::VideoPlayer::open_url(&url) {
3518 Ok(p) => {
3519 guard.player = Some(p);
3520 let ctd = ct.as_str();
3521 console_log(
3522 &caller.data().console,
3523 ConsoleLevel::Log,
3524 format!("[VIDEO] Opened URL (Content-Type: {ctd})"),
3525 );
3526 0
3527 }
3528 Err(e) => {
3529 console_log(
3530 &caller.data().console,
3531 ConsoleLevel::Error,
3532 format!("[VIDEO] Open failed: {e}"),
3533 );
3534 -2
3535 }
3536 }
3537 },
3538 )?;
3539
3540 linker.func_wrap(
3541 "oxide",
3542 "api_video_last_url_content_type",
3543 |mut caller: Caller<'_, HostState>, out_ptr: u32, out_cap: u32| -> u32 {
3544 let s = caller
3545 .data()
3546 .video
3547 .lock()
3548 .unwrap()
3549 .last_url_content_type
3550 .clone();
3551 let bytes = s.as_bytes();
3552 let write_len = bytes.len().min(out_cap as usize);
3553 let mem = caller.data().memory.expect("memory not set");
3554 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
3555 write_len as u32
3556 },
3557 )?;
3558
3559 linker.func_wrap(
3560 "oxide",
3561 "api_video_hls_variant_count",
3562 |caller: Caller<'_, HostState>| -> u32 {
3563 caller.data().video.lock().unwrap().hls_variants.len() as u32
3564 },
3565 )?;
3566
3567 linker.func_wrap(
3568 "oxide",
3569 "api_video_hls_variant_url",
3570 |mut caller: Caller<'_, HostState>, index: u32, out_ptr: u32, out_cap: u32| -> u32 {
3571 let resolved = {
3572 let g = caller.data().video.lock().unwrap();
3573 g.hls_variants
3574 .get(index as usize)
3575 .and_then(|rel| video::resolve_against_base(&g.hls_base_url, rel))
3576 .or_else(|| g.hls_variants.get(index as usize).cloned())
3577 .unwrap_or_default()
3578 };
3579 let bytes = resolved.as_bytes();
3580 let write_len = bytes.len().min(out_cap as usize);
3581 let mem = caller.data().memory.expect("memory not set");
3582 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
3583 write_len as u32
3584 },
3585 )?;
3586
3587 linker.func_wrap(
3588 "oxide",
3589 "api_video_hls_open_variant",
3590 |caller: Caller<'_, HostState>, index: u32| -> i32 {
3591 let url_opt = {
3592 let g = caller.data().video.lock().unwrap();
3593 g.hls_variants.get(index as usize).map(|rel| {
3594 video::resolve_against_base(&g.hls_base_url, rel).unwrap_or_else(|| rel.clone())
3595 })
3596 };
3597 let Some(url) = url_opt else {
3598 return -1;
3599 };
3600 let mut guard = caller.data().video.lock().unwrap();
3601 guard.hls_base_url = url.clone();
3602 guard.hls_variants.clear();
3603 match video::VideoPlayer::open_url(&url) {
3604 Ok(p) => {
3605 guard.player = Some(p);
3606 guard.reset_playback_clock();
3607 console_log(
3608 &caller.data().console,
3609 ConsoleLevel::Log,
3610 format!("[VIDEO] Opened HLS variant {index}"),
3611 );
3612 0
3613 }
3614 Err(e) => {
3615 console_log(
3616 &caller.data().console,
3617 ConsoleLevel::Error,
3618 format!("[VIDEO] Variant open failed: {e}"),
3619 );
3620 -2
3621 }
3622 }
3623 },
3624 )?;
3625
3626 linker.func_wrap(
3627 "oxide",
3628 "api_video_play",
3629 |caller: Caller<'_, HostState>| {
3630 caller.data().video.lock().unwrap().play();
3631 },
3632 )?;
3633
3634 linker.func_wrap(
3635 "oxide",
3636 "api_video_pause",
3637 |caller: Caller<'_, HostState>| {
3638 caller.data().video.lock().unwrap().pause();
3639 },
3640 )?;
3641
3642 linker.func_wrap(
3643 "oxide",
3644 "api_video_stop",
3645 |caller: Caller<'_, HostState>| {
3646 caller.data().video.lock().unwrap().stop();
3647 *caller.data().video_pip_frame.lock().unwrap() = None;
3648 },
3649 )?;
3650
3651 linker.func_wrap(
3652 "oxide",
3653 "api_video_seek",
3654 |caller: Caller<'_, HostState>, position_ms: u64| -> i32 {
3655 caller.data().video.lock().unwrap().seek(position_ms);
3656 0
3657 },
3658 )?;
3659
3660 linker.func_wrap(
3661 "oxide",
3662 "api_video_position",
3663 |caller: Caller<'_, HostState>| -> u64 {
3664 caller.data().video.lock().unwrap().current_position_ms()
3665 },
3666 )?;
3667
3668 linker.func_wrap(
3669 "oxide",
3670 "api_video_duration",
3671 |caller: Caller<'_, HostState>| -> u64 {
3672 caller.data().video.lock().unwrap().duration_ms()
3673 },
3674 )?;
3675
3676 linker.func_wrap(
3677 "oxide",
3678 "api_video_render",
3679 |caller: Caller<'_, HostState>, x: f32, y: f32, w: f32, h: f32| -> i32 {
3680 match video_render_at(
3681 &caller.data().video,
3682 &caller.data().video_pip_frame,
3683 &caller.data().video_pip_serial,
3684 &caller.data().canvas,
3685 x,
3686 y,
3687 w,
3688 h,
3689 ) {
3690 Ok(()) => 0,
3691 Err(e) => {
3692 console_log(
3693 &caller.data().console,
3694 ConsoleLevel::Error,
3695 format!("[VIDEO] Render: {e}"),
3696 );
3697 -1
3698 }
3699 }
3700 },
3701 )?;
3702
3703 linker.func_wrap(
3704 "oxide",
3705 "api_video_set_volume",
3706 |caller: Caller<'_, HostState>, level: f32| {
3707 caller.data().video.lock().unwrap().volume = level.clamp(0.0, 2.0);
3708 },
3709 )?;
3710
3711 linker.func_wrap(
3712 "oxide",
3713 "api_video_get_volume",
3714 |caller: Caller<'_, HostState>| -> f32 { caller.data().video.lock().unwrap().volume },
3715 )?;
3716
3717 linker.func_wrap(
3718 "oxide",
3719 "api_video_set_loop",
3720 |caller: Caller<'_, HostState>, enabled: u32| {
3721 caller.data().video.lock().unwrap().looping = enabled != 0;
3722 },
3723 )?;
3724
3725 linker.func_wrap(
3726 "oxide",
3727 "api_video_set_pip",
3728 |caller: Caller<'_, HostState>, enabled: u32| {
3729 caller.data().video.lock().unwrap().pip = enabled != 0;
3730 if enabled == 0 {
3731 *caller.data().video_pip_frame.lock().unwrap() = None;
3732 }
3733 },
3734 )?;
3735
3736 linker.func_wrap(
3737 "oxide",
3738 "api_subtitle_load_srt",
3739 |caller: Caller<'_, HostState>, ptr: u32, len: u32| -> i32 {
3740 let mem = caller.data().memory.expect("memory not set");
3741 let s = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
3742 caller.data().video.lock().unwrap().subtitles = subtitle::parse_srt(&s);
3743 0
3744 },
3745 )?;
3746
3747 linker.func_wrap(
3748 "oxide",
3749 "api_subtitle_load_vtt",
3750 |caller: Caller<'_, HostState>, ptr: u32, len: u32| -> i32 {
3751 let mem = caller.data().memory.expect("memory not set");
3752 let s = read_guest_string(&mem, &caller, ptr, len).unwrap_or_default();
3753 caller.data().video.lock().unwrap().subtitles = subtitle::parse_vtt(&s);
3754 0
3755 },
3756 )?;
3757
3758 linker.func_wrap(
3759 "oxide",
3760 "api_subtitle_clear",
3761 |caller: Caller<'_, HostState>| {
3762 caller.data().video.lock().unwrap().subtitles.clear();
3763 },
3764 )?;
3765
3766 linker.func_wrap(
3769 "oxide",
3770 "api_ui_button",
3771 |caller: Caller<'_, HostState>,
3772 id: u32,
3773 x: f32,
3774 y: f32,
3775 w: f32,
3776 h: f32,
3777 label_ptr: u32,
3778 label_len: u32|
3779 -> u32 {
3780 let mem = caller.data().memory.expect("memory not set");
3781 let label = read_guest_string(&mem, &caller, label_ptr, label_len).unwrap_or_default();
3782 caller
3783 .data()
3784 .widget_commands
3785 .lock()
3786 .unwrap()
3787 .push(WidgetCommand::Button {
3788 id,
3789 x,
3790 y,
3791 w,
3792 h,
3793 label,
3794 });
3795 if caller.data().widget_clicked.lock().unwrap().contains(&id) {
3796 1
3797 } else {
3798 0
3799 }
3800 },
3801 )?;
3802
3803 linker.func_wrap(
3804 "oxide",
3805 "api_ui_checkbox",
3806 |caller: Caller<'_, HostState>,
3807 id: u32,
3808 x: f32,
3809 y: f32,
3810 label_ptr: u32,
3811 label_len: u32,
3812 initial: u32|
3813 -> u32 {
3814 let mem = caller.data().memory.expect("memory not set");
3815 let label = read_guest_string(&mem, &caller, label_ptr, label_len).unwrap_or_default();
3816 let mut states = caller.data().widget_states.lock().unwrap();
3817 let entry = states
3818 .entry(id)
3819 .or_insert_with(|| WidgetValue::Bool(initial != 0));
3820 let checked = match entry {
3821 WidgetValue::Bool(b) => *b,
3822 _ => initial != 0,
3823 };
3824 drop(states);
3825 caller
3826 .data()
3827 .widget_commands
3828 .lock()
3829 .unwrap()
3830 .push(WidgetCommand::Checkbox { id, x, y, label });
3831 if checked {
3832 1
3833 } else {
3834 0
3835 }
3836 },
3837 )?;
3838
3839 linker.func_wrap(
3840 "oxide",
3841 "api_ui_slider",
3842 |caller: Caller<'_, HostState>,
3843 id: u32,
3844 x: f32,
3845 y: f32,
3846 w: f32,
3847 min: f32,
3848 max: f32,
3849 initial: f32|
3850 -> f32 {
3851 let mut states = caller.data().widget_states.lock().unwrap();
3852 let entry = states
3853 .entry(id)
3854 .or_insert_with(|| WidgetValue::Float(initial));
3855 let value = match entry {
3856 WidgetValue::Float(v) => *v,
3857 _ => initial,
3858 };
3859 drop(states);
3860 caller
3861 .data()
3862 .widget_commands
3863 .lock()
3864 .unwrap()
3865 .push(WidgetCommand::Slider {
3866 id,
3867 x,
3868 y,
3869 w,
3870 min,
3871 max,
3872 });
3873 value
3874 },
3875 )?;
3876
3877 linker.func_wrap(
3878 "oxide",
3879 "api_ui_text_input",
3880 |mut caller: Caller<'_, HostState>,
3881 id: u32,
3882 x: f32,
3883 y: f32,
3884 w: f32,
3885 init_ptr: u32,
3886 init_len: u32,
3887 out_ptr: u32,
3888 out_cap: u32|
3889 -> u32 {
3890 let mem = caller.data().memory.expect("memory not set");
3891 let text = {
3892 let mut states = caller.data().widget_states.lock().unwrap();
3893 let entry = states.entry(id).or_insert_with(|| {
3894 let init =
3895 read_guest_string(&mem, &caller, init_ptr, init_len).unwrap_or_default();
3896 WidgetValue::Text(init)
3897 });
3898 match entry {
3899 WidgetValue::Text(t) => t.clone(),
3900 _ => String::new(),
3901 }
3902 };
3903 caller
3904 .data()
3905 .widget_commands
3906 .lock()
3907 .unwrap()
3908 .push(WidgetCommand::TextInput { id, x, y, w });
3909 let bytes = text.as_bytes();
3910 let write_len = bytes.len().min(out_cap as usize);
3911 write_guest_bytes(&mem, &mut caller, out_ptr, &bytes[..write_len]).ok();
3912 write_len as u32
3913 },
3914 )?;
3915
3916 crate::media_capture::register_media_capture_functions(linker)?;
3917
3918 linker.func_wrap(
3921 "oxide",
3922 "api_gpu_create_buffer",
3923 |caller: Caller<'_, HostState>, size_lo: u32, size_hi: u32, usage: u32| -> u32 {
3924 let size = ((size_hi as u64) << 32) | (size_lo as u64);
3925 let mut gpu_lock = caller.data().gpu.lock().unwrap();
3926 let gpu = match gpu_lock.as_mut() {
3927 Some(g) => g,
3928 None => {
3929 if let Some(g) = crate::gpu::init_gpu() {
3930 *gpu_lock = Some(g);
3931 gpu_lock.as_mut().unwrap()
3932 } else {
3933 console_log(
3934 &caller.data().console,
3935 ConsoleLevel::Error,
3936 "[GPU] No suitable GPU adapter found".into(),
3937 );
3938 return 0;
3939 }
3940 }
3941 };
3942 gpu.create_buffer(size, usage)
3943 },
3944 )?;
3945
3946 linker.func_wrap(
3947 "oxide",
3948 "api_gpu_create_texture",
3949 |caller: Caller<'_, HostState>, width: u32, height: u32| -> u32 {
3950 let mut gpu_lock = caller.data().gpu.lock().unwrap();
3951 let gpu = match gpu_lock.as_mut() {
3952 Some(g) => g,
3953 None => {
3954 if let Some(g) = crate::gpu::init_gpu() {
3955 *gpu_lock = Some(g);
3956 gpu_lock.as_mut().unwrap()
3957 } else {
3958 return 0;
3959 }
3960 }
3961 };
3962 gpu.create_texture(width, height)
3963 },
3964 )?;
3965
3966 linker.func_wrap(
3967 "oxide",
3968 "api_gpu_create_shader",
3969 |caller: Caller<'_, HostState>, src_ptr: u32, src_len: u32| -> u32 {
3970 let mem = caller.data().memory.expect("memory not set");
3971 let source = read_guest_string(&mem, &caller, src_ptr, src_len).unwrap_or_default();
3972 let mut gpu_lock = caller.data().gpu.lock().unwrap();
3973 let gpu = match gpu_lock.as_mut() {
3974 Some(g) => g,
3975 None => {
3976 if let Some(g) = crate::gpu::init_gpu() {
3977 *gpu_lock = Some(g);
3978 gpu_lock.as_mut().unwrap()
3979 } else {
3980 return 0;
3981 }
3982 }
3983 };
3984 gpu.create_shader(&source)
3985 },
3986 )?;
3987
3988 linker.func_wrap(
3989 "oxide",
3990 "api_gpu_create_render_pipeline",
3991 |caller: Caller<'_, HostState>,
3992 shader: u32,
3993 vs_ptr: u32,
3994 vs_len: u32,
3995 fs_ptr: u32,
3996 fs_len: u32|
3997 -> u32 {
3998 let mem = caller.data().memory.expect("memory not set");
3999 let vs = read_guest_string(&mem, &caller, vs_ptr, vs_len).unwrap_or_default();
4000 let fs = read_guest_string(&mem, &caller, fs_ptr, fs_len).unwrap_or_default();
4001 let mut gpu_lock = caller.data().gpu.lock().unwrap();
4002 match gpu_lock.as_mut() {
4003 Some(g) => g.create_render_pipeline(shader, &vs, &fs),
4004 None => 0,
4005 }
4006 },
4007 )?;
4008
4009 linker.func_wrap(
4010 "oxide",
4011 "api_gpu_create_compute_pipeline",
4012 |caller: Caller<'_, HostState>, shader: u32, ep_ptr: u32, ep_len: u32| -> u32 {
4013 let mem = caller.data().memory.expect("memory not set");
4014 let ep = read_guest_string(&mem, &caller, ep_ptr, ep_len).unwrap_or_default();
4015 let mut gpu_lock = caller.data().gpu.lock().unwrap();
4016 match gpu_lock.as_mut() {
4017 Some(g) => g.create_compute_pipeline(shader, &ep),
4018 None => 0,
4019 }
4020 },
4021 )?;
4022
4023 linker.func_wrap(
4024 "oxide",
4025 "api_gpu_write_buffer",
4026 |caller: Caller<'_, HostState>,
4027 handle: u32,
4028 offset_lo: u32,
4029 offset_hi: u32,
4030 data_ptr: u32,
4031 data_len: u32|
4032 -> u32 {
4033 let mem = caller.data().memory.expect("memory not set");
4034 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
4035 let offset = ((offset_hi as u64) << 32) | (offset_lo as u64);
4036 let gpu_lock = caller.data().gpu.lock().unwrap();
4037 u32::from(
4038 gpu_lock
4039 .as_ref()
4040 .is_some_and(|g| g.write_buffer(handle, offset, &data)),
4041 )
4042 },
4043 )?;
4044
4045 linker.func_wrap(
4046 "oxide",
4047 "api_gpu_draw",
4048 |caller: Caller<'_, HostState>,
4049 pipeline: u32,
4050 target: u32,
4051 vertex_count: u32,
4052 instance_count: u32|
4053 -> u32 {
4054 let gpu_lock = caller.data().gpu.lock().unwrap();
4055 u32::from(
4056 gpu_lock
4057 .as_ref()
4058 .is_some_and(|g| g.draw(pipeline, target, vertex_count, instance_count)),
4059 )
4060 },
4061 )?;
4062
4063 linker.func_wrap(
4064 "oxide",
4065 "api_gpu_dispatch_compute",
4066 |caller: Caller<'_, HostState>, pipeline: u32, x: u32, y: u32, z: u32| -> u32 {
4067 let gpu_lock = caller.data().gpu.lock().unwrap();
4068 u32::from(
4069 gpu_lock
4070 .as_ref()
4071 .is_some_and(|g| g.dispatch_compute(pipeline, x, y, z)),
4072 )
4073 },
4074 )?;
4075
4076 linker.func_wrap(
4077 "oxide",
4078 "api_gpu_destroy_buffer",
4079 |caller: Caller<'_, HostState>, handle: u32| -> u32 {
4080 let mut gpu_lock = caller.data().gpu.lock().unwrap();
4081 u32::from(gpu_lock.as_mut().is_some_and(|g| g.destroy_buffer(handle)))
4082 },
4083 )?;
4084
4085 linker.func_wrap(
4086 "oxide",
4087 "api_gpu_destroy_texture",
4088 |caller: Caller<'_, HostState>, handle: u32| -> u32 {
4089 let mut gpu_lock = caller.data().gpu.lock().unwrap();
4090 u32::from(gpu_lock.as_mut().is_some_and(|g| g.destroy_texture(handle)))
4091 },
4092 )?;
4093
4094 crate::rtc::register_rtc_functions(linker)?;
4096
4097 crate::websocket::register_ws_functions(linker)?;
4099
4100 crate::midi::register_midi_functions(linker)?;
4102
4103 crate::fetch::register_fetch_functions(linker)?;
4105
4106 crate::events::register_event_functions(linker)?;
4108
4109 crate::file_picker::register_file_picker_functions(linker)?;
4111
4112 linker.func_wrap(
4115 "oxide",
4116 "api_download_data",
4117 |caller: Caller<'_, HostState>,
4118 data_ptr: u32,
4119 data_len: u32,
4120 filename_ptr: u32,
4121 filename_len: u32|
4122 -> i32 {
4123 let mem = caller.data().memory.expect("memory not set");
4124 let data = read_guest_bytes(&mem, &caller, data_ptr, data_len).unwrap_or_default();
4125 let filename =
4126 read_guest_string(&mem, &caller, filename_ptr, filename_len).unwrap_or_default();
4127 if data.is_empty() || filename.is_empty() {
4128 return -1;
4129 }
4130 match caller.data().download_manager.save_data(&data, &filename) {
4131 Ok(_) => {
4132 console_log(
4133 &caller.data().console,
4134 ConsoleLevel::Log,
4135 format!("[DOWNLOAD] Saved {} bytes to {}", data.len(), filename),
4136 );
4137 0
4138 }
4139 Err(e) => {
4140 console_log(
4141 &caller.data().console,
4142 ConsoleLevel::Error,
4143 format!("[DOWNLOAD] Failed to save {}: {e}", filename),
4144 );
4145 -1
4146 }
4147 }
4148 },
4149 )?;
4150
4151 linker.func_wrap(
4152 "oxide",
4153 "api_download_url",
4154 |caller: Caller<'_, HostState>, url_ptr: u32, url_len: u32| -> i32 {
4155 let mem = caller.data().memory.expect("memory not set");
4156 let url = read_guest_string(&mem, &caller, url_ptr, url_len).unwrap_or_default();
4157 if url.is_empty() {
4158 return -1;
4159 }
4160 caller.data().download_manager.start_download(url.clone());
4161 console_log(
4162 &caller.data().console,
4163 ConsoleLevel::Log,
4164 format!("[DOWNLOAD] Started download for {url}"),
4165 );
4166 0
4167 },
4168 )?;
4169
4170 linker.func_wrap(
4171 "oxide",
4172 "api_canvas_print_pdf",
4173 |caller: Caller<'_, HostState>, filename_ptr: u32, filename_len: u32| -> i32 {
4174 let mem = caller.data().memory.expect("memory not set");
4175 let filename =
4176 read_guest_string(&mem, &caller, filename_ptr, filename_len).unwrap_or_default();
4177 if filename.is_empty() {
4178 return -1;
4179 }
4180 let canvas = caller.data().canvas.lock().unwrap().clone();
4181 match render_canvas_to_pdf(&canvas, &filename) {
4182 Ok(_) => {
4183 console_log(
4184 &caller.data().console,
4185 ConsoleLevel::Log,
4186 format!("[PRINT] Canvas exported to PDF: {filename}"),
4187 );
4188 0
4189 }
4190 Err(e) => {
4191 console_log(
4192 &caller.data().console,
4193 ConsoleLevel::Error,
4194 format!("[PRINT] PDF export failed: {e}"),
4195 );
4196 -1
4197 }
4198 }
4199 },
4200 )?;
4201
4202 Ok(())
4203}
4204
4205fn getrandom(buf: &mut [u8]) {
4206 ::getrandom::getrandom(buf).expect("OS random number generator unavailable");
4207}