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