pax_chassis_web/
lib.rs

1//! Basic example of rendering in the browser
2#![allow(non_snake_case)]
3
4use js_sys::Uint8Array;
5use pax_message::ImageLoadInterruptArgs;
6use pax_runtime::api::borrow;
7use pax_runtime::api::math::Point2;
8use pax_runtime::api::use_RefCell;
9use pax_runtime::api::ButtonClick;
10use pax_runtime::api::Platform;
11use pax_runtime::api::RenderContext;
12use pax_runtime::api::TextboxChange;
13use pax_runtime::api::OS;
14use pax_runtime::DefinitionToInstanceTraverser;
15use pax_runtime_api::borrow_mut;
16use pax_runtime_api::Event;
17use pax_runtime_api::Focus;
18use pax_runtime_api::SelectStart;
19use web_time::Instant;
20use_RefCell!();
21
22use std::rc::Rc;
23use wasm_bindgen::prelude::*;
24use wasm_bindgen::JsCast;
25use web_sys::{window, HtmlCanvasElement};
26
27use piet_web::WebRenderContext;
28
29use pax_runtime::{PaxEngine, Renderer};
30
31pub use {console_error_panic_hook, console_log};
32
33use pax_message::NativeInterrupt;
34use pax_runtime::api::{
35    Clap, Click, ContextMenu, DoubleClick, Drop, KeyDown, KeyPress, KeyUp, KeyboardEventArgs,
36    ModifierKey, MouseButton, MouseDown, MouseEventArgs, MouseMove, MouseUp, Touch, TouchEnd,
37    TouchMove, TouchStart, Wheel,
38};
39use serde_json;
40
41#[cfg(any(feature = "designtime", feature = "designer"))]
42use {pax_designtime::orm::ReloadType, pax_designtime::DesigntimeManager};
43
44const USERLAND_COMPONENT_ROOT: &str = "USERLAND_COMPONENT_ROOT";
45#[cfg(any(feature = "designtime", feature = "designer"))]
46const DESIGNER_COMPONENT_ROOT: &str = "DESIGNER_COMPONENT_ROOT";
47
48#[wasm_bindgen]
49pub fn wasm_memory() -> JsValue {
50    wasm_bindgen::memory()
51}
52
53#[wasm_bindgen]
54pub struct PaxChassisWeb {
55    drawing_contexts: Renderer<WebRenderContext<'static>>,
56    engine: Rc<RefCell<PaxEngine>>,
57    #[cfg(any(feature = "designtime", feature = "designer"))]
58    userland_definition_to_instance_traverser:
59        Box<dyn pax_runtime::cartridge::DefinitionToInstanceTraverser>,
60    #[cfg(any(feature = "designtime", feature = "designer"))]
61    designtime_manager: Rc<RefCell<DesigntimeManager>>,
62}
63
64#[wasm_bindgen]
65pub struct InterruptResult {
66    pub prevent_default: bool,
67}
68
69// Two impl blocks: one for "private" functions,
70//                  the second for FFI-exposed functions
71
72impl PaxChassisWeb {
73    #[cfg(any(feature = "designtime", feature = "designer"))]
74    pub async fn new(
75        userland_definition_to_instance_traverser: Box<dyn DefinitionToInstanceTraverser>,
76        designer_definition_to_instance_traverser: Box<dyn DefinitionToInstanceTraverser>,
77    ) -> Self {
78        let (width, height, os_info, get_elapsed_millis) = Self::init_common();
79        let query_string = window()
80            .unwrap()
81            .location()
82            .search()
83            .expect("no search exists");
84
85        let main_component_instance =
86            designer_definition_to_instance_traverser.get_main_component(DESIGNER_COMPONENT_ROOT);
87        let userland_main_component_instance =
88            userland_definition_to_instance_traverser.get_main_component(USERLAND_COMPONENT_ROOT);
89
90        let designtime_manager = userland_definition_to_instance_traverser
91            .get_designtime_manager(query_string)
92            .unwrap();
93        let engine = pax_runtime::PaxEngine::new_with_designtime(
94            main_component_instance,
95            userland_main_component_instance,
96            (width, height),
97            designtime_manager.clone(),
98            Platform::Web,
99            os_info,
100            get_elapsed_millis,
101        );
102        let engine_container: Rc<RefCell<PaxEngine>> = Rc::new(RefCell::new(engine));
103        Self {
104            engine: engine_container,
105            drawing_contexts: Renderer::new(),
106            userland_definition_to_instance_traverser,
107            designtime_manager,
108        }
109    }
110
111    #[cfg(not(any(feature = "designtime", feature = "designer")))]
112    pub async fn new(
113        definition_to_instance_traverser: Box<dyn DefinitionToInstanceTraverser>,
114    ) -> Self {
115        let (width, height, os_info, get_time) = Self::init_common();
116
117        let main_component_instance =
118            definition_to_instance_traverser.get_main_component(USERLAND_COMPONENT_ROOT);
119        let engine = pax_runtime::PaxEngine::new(
120            main_component_instance,
121            (width, height),
122            Platform::Web,
123            os_info,
124            get_time,
125        );
126
127        let engine_container: Rc<RefCell<PaxEngine>> = Rc::new(RefCell::new(engine));
128
129        Self {
130            engine: engine_container,
131            drawing_contexts: Renderer::new(),
132        }
133    }
134
135    fn init_common() -> (f64, f64, OS, Box<dyn Fn() -> u128>) {
136        let window = window().unwrap();
137        let user_agent_str = window.navigator().user_agent().ok();
138        let os_info = user_agent_str
139            .and_then(|s| parse_user_agent_str(&s))
140            .unwrap_or_default();
141
142        let width = window.inner_width().unwrap().as_f64().unwrap();
143        let height = window.inner_height().unwrap().as_f64().unwrap();
144        let start = Instant::now();
145        let get_time = Box::new(move || start.elapsed().as_millis());
146        (width, height, os_info, get_time)
147    }
148}
149
150#[wasm_bindgen]
151impl PaxChassisWeb {
152    pub fn add_context(&mut self, id: usize) {
153        let window = window().unwrap();
154        let dpr = window.device_pixel_ratio();
155        let document = window.document().unwrap();
156        let canvas = document
157            .get_element_by_id(id.to_string().as_str())
158            .unwrap()
159            .dyn_into::<HtmlCanvasElement>()
160            .unwrap();
161        let context: web_sys::CanvasRenderingContext2d = canvas
162            .get_context("2d")
163            .unwrap()
164            .unwrap()
165            .dyn_into::<web_sys::CanvasRenderingContext2d>()
166            .unwrap();
167
168        let width = canvas.offset_width() as f64 * dpr;
169        let height = canvas.offset_height() as f64 * dpr;
170
171        canvas.set_width(width as u32);
172        canvas.set_height(height as u32);
173        let _ = context.scale(dpr, dpr);
174        let render_context = WebRenderContext::new(context, window.clone());
175        self.drawing_contexts.add_context(id, render_context);
176        self.engine.borrow().runtime_context.add_canvas(id);
177    }
178
179    pub fn send_viewport_update(&mut self, width: f64, height: f64) {
180        self.engine
181            .borrow()
182            .runtime_context
183            .set_all_canvases_dirty();
184        borrow_mut!(self.engine).set_viewport_size((width, height));
185    }
186    pub fn remove_context(&mut self, id: usize) {
187        self.drawing_contexts.remove_context(id);
188        self.engine.borrow().runtime_context.remove_canvas(id);
189    }
190
191    pub fn get_dirty_canvases(&self) -> Vec<usize> {
192        let ret = self.engine.borrow().runtime_context.get_dirty_canvases();
193        ret
194    }
195
196    pub fn interrupt(
197        &mut self,
198        native_interrupt: String,
199        additional_payload: &JsValue,
200    ) -> InterruptResult {
201        let x: NativeInterrupt = serde_json::from_str(&native_interrupt).unwrap();
202
203        let engine = borrow_mut!(self.engine);
204        let ctx = &engine.runtime_context;
205        let globals = ctx.globals();
206        let prevent_default = match &x {
207            NativeInterrupt::Focus(_args) => engine.global_dispatch_focus(Focus {}),
208            NativeInterrupt::DropFile(args) => {
209                let data = Uint8Array::new(additional_payload).to_vec();
210                let topmost_node = engine
211                    .runtime_context
212                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
213                let args_drop = Drop {
214                    x: args.x,
215                    y: args.y,
216                    name: args.name.clone(),
217                    mime_type: args.mime_type.clone(),
218                    data,
219                };
220                topmost_node.dispatch_drop(Event::new(args_drop), &globals, &engine.runtime_context)
221            }
222            NativeInterrupt::FormRadioSetChange(args) => {
223                let node = engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id));
224                if let Some(node) = node {
225                    borrow!(node.instance_node).handle_native_interrupt(&node, &x);
226                }
227                false
228            }
229            NativeInterrupt::FormSliderChange(args) => {
230                let node = engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id));
231                if let Some(node) = node {
232                    borrow!(node.instance_node).handle_native_interrupt(&node, &x);
233                }
234                false
235            }
236            NativeInterrupt::FormDropdownChange(args) => {
237                let node = engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id));
238                if let Some(node) = node {
239                    borrow!(node.instance_node).handle_native_interrupt(&node, &x);
240                }
241                false
242            }
243            NativeInterrupt::ChassisResizeRequestCollection(collection) => {
244                for args in collection {
245                    let node =
246                        engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id));
247                    if let Some(node) = node {
248                        node.chassis_resize_request(args.width, args.height);
249                    }
250                }
251                false
252            }
253            NativeInterrupt::Image(args) => match args {
254                ImageLoadInterruptArgs::Reference(_ref_args) => false,
255                ImageLoadInterruptArgs::Data(data_args) => {
256                    let data = Uint8Array::new(additional_payload).to_vec();
257                    self.drawing_contexts.load_image(
258                        &data_args.path,
259                        &data,
260                        data_args.width,
261                        data_args.height,
262                    );
263                    false
264                }
265            },
266            NativeInterrupt::FormButtonClick(args) => {
267                if let Some(node) =
268                    engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id))
269                {
270                    node.dispatch_button_click(
271                        Event::new(ButtonClick {}),
272                        &globals,
273                        &engine.runtime_context,
274                    )
275                } else {
276                    log::warn!(
277                        "tried to dispatch event for button click after node already removed"
278                    );
279                    false
280                }
281            }
282            NativeInterrupt::FormTextboxInput(args) => {
283                if let Some(node) =
284                    engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id))
285                {
286                    borrow!(node.instance_node).handle_native_interrupt(&node, &x);
287                } else {
288                    log::warn!(
289                        "tried to dispatch event for textbox input after node already removed"
290                    );
291                }
292                false
293            }
294            NativeInterrupt::TextInput(args) => {
295                if let Some(node) =
296                    engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id))
297                {
298                    borrow!(node.instance_node).handle_native_interrupt(&node, &x);
299                } else {
300                    log::warn!("tried to dispatch event for text input after node already removed");
301                }
302                false
303            }
304            NativeInterrupt::FormTextboxChange(args) => {
305                if let Some(node) =
306                    engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id))
307                {
308                    node.dispatch_textbox_change(
309                        Event::new(TextboxChange {
310                            text: args.text.clone(),
311                        }),
312                        &globals,
313                        &engine.runtime_context,
314                    )
315                } else {
316                    log::warn!(
317                        "tried to dispatch event for textbox change after node already removed"
318                    );
319                    false
320                }
321            }
322            NativeInterrupt::FormCheckboxToggle(args) => {
323                if let Some(node) =
324                    engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id))
325                {
326                    borrow!(node.instance_node).handle_native_interrupt(&node, &x);
327                } else {
328                    log::warn!(
329                        "tried to dispatch event for checkbox toggle after node already removed"
330                    );
331                }
332                false
333            }
334
335            NativeInterrupt::AddedLayer(_args) => false,
336            NativeInterrupt::Click(args) => {
337                let topmost_node = engine
338                    .runtime_context
339                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
340                let args_click = Click {
341                    mouse: MouseEventArgs {
342                        x: args.x,
343                        y: args.y,
344                        button: MouseButton::from(args.button.clone()),
345                        modifiers: args
346                            .modifiers
347                            .iter()
348                            .map(|x| ModifierKey::from(x))
349                            .collect(),
350                    },
351                };
352                topmost_node.dispatch_click(
353                    Event::new(args_click),
354                    &globals,
355                    &engine.runtime_context,
356                )
357            }
358            NativeInterrupt::Scrollbar(args) => {
359                let node = engine.get_expanded_node(pax_runtime::ExpandedNodeIdentifier(args.id));
360                if let Some(node) = node {
361                    borrow!(node.instance_node).handle_native_interrupt(&node, &x);
362                }
363                false
364            }
365            NativeInterrupt::Scroll(_) => false,
366            NativeInterrupt::Clap(args) => {
367                let topmost_node = engine
368                    .runtime_context
369                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
370                let args_clap = Clap {
371                    x: args.x,
372                    y: args.y,
373                };
374                topmost_node.dispatch_clap(Event::new(args_clap), &globals, &engine.runtime_context)
375            }
376            NativeInterrupt::TouchStart(args) => {
377                let first_touch = args.touches.get(0).unwrap();
378                let topmost_node = engine
379                    .runtime_context
380                    .get_topmost_element_beneath_ray(Point2::new(first_touch.x, first_touch.y));
381                let touches = args.touches.iter().map(|x| Touch::from(x)).collect();
382                let args_touch_start = TouchStart { touches };
383                topmost_node.dispatch_touch_start(
384                    Event::new(args_touch_start),
385                    &globals,
386                    &engine.runtime_context,
387                )
388            }
389            NativeInterrupt::TouchMove(args) => {
390                let first_touch = args.touches.get(0).unwrap();
391                let topmost_node = engine
392                    .runtime_context
393                    .get_topmost_element_beneath_ray(Point2::new(first_touch.x, first_touch.y));
394                let touches = args.touches.iter().map(|x| Touch::from(x)).collect();
395                let args_touch_move = TouchMove { touches };
396                topmost_node.dispatch_touch_move(
397                    Event::new(args_touch_move),
398                    &globals,
399                    &engine.runtime_context,
400                )
401            }
402            NativeInterrupt::TouchEnd(args) => {
403                let first_touch = args.touches.get(0).unwrap();
404                let topmost_node = engine
405                    .runtime_context
406                    .get_topmost_element_beneath_ray(Point2::new(first_touch.x, first_touch.y));
407                let touches = args.touches.iter().map(|x| Touch::from(x)).collect();
408                let args_touch_end = TouchEnd { touches };
409                topmost_node.dispatch_touch_end(
410                    Event::new(args_touch_end),
411                    &globals,
412                    &engine.runtime_context,
413                )
414            }
415            NativeInterrupt::KeyDown(args) => {
416                let modifiers = args
417                    .modifiers
418                    .iter()
419                    .map(|x| ModifierKey::from(x))
420                    .collect();
421                let args_key_down = KeyDown {
422                    keyboard: KeyboardEventArgs {
423                        key: args.key.clone(),
424                        modifiers,
425                        is_repeat: args.is_repeat,
426                    },
427                };
428                engine.global_dispatch_key_down(args_key_down)
429            }
430            NativeInterrupt::KeyUp(args) => {
431                let modifiers = args
432                    .modifiers
433                    .iter()
434                    .map(|x| ModifierKey::from(x))
435                    .collect();
436                let args_key_up = KeyUp {
437                    keyboard: KeyboardEventArgs {
438                        key: args.key.clone(),
439                        modifiers,
440                        is_repeat: args.is_repeat,
441                    },
442                };
443                engine.global_dispatch_key_up(args_key_up)
444            }
445            NativeInterrupt::KeyPress(args) => {
446                let modifiers = args
447                    .modifiers
448                    .iter()
449                    .map(|x| ModifierKey::from(x))
450                    .collect();
451                let args_key_press = KeyPress {
452                    keyboard: KeyboardEventArgs {
453                        key: args.key.clone(),
454                        modifiers,
455                        is_repeat: args.is_repeat,
456                    },
457                };
458                engine.global_dispatch_key_press(args_key_press)
459            }
460            NativeInterrupt::DoubleClick(args) => {
461                let topmost_node = engine
462                    .runtime_context
463                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
464                let args_double_click = DoubleClick {
465                    mouse: MouseEventArgs {
466                        x: args.x,
467                        y: args.y,
468                        button: MouseButton::from(args.button.clone()),
469                        modifiers: args
470                            .modifiers
471                            .iter()
472                            .map(|x| ModifierKey::from(x))
473                            .collect(),
474                    },
475                };
476                topmost_node.dispatch_double_click(
477                    Event::new(args_double_click),
478                    &globals,
479                    &engine.runtime_context,
480                )
481            }
482            NativeInterrupt::SelectStart(_args) => {
483                engine.global_dispatch_select_start(SelectStart {})
484            }
485            NativeInterrupt::MouseMove(args) => {
486                let topmost_node = engine
487                    .runtime_context
488                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
489                let args_mouse_move = MouseMove {
490                    mouse: MouseEventArgs {
491                        x: args.x,
492                        y: args.y,
493                        button: MouseButton::from(args.button.clone()),
494                        modifiers: args
495                            .modifiers
496                            .iter()
497                            .map(|x| ModifierKey::from(x))
498                            .collect(),
499                    },
500                };
501                topmost_node.dispatch_mouse_move(
502                    Event::new(args_mouse_move),
503                    &globals,
504                    &engine.runtime_context,
505                )
506            }
507            NativeInterrupt::Wheel(args) => {
508                let topmost_node = engine
509                    .runtime_context
510                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
511                let modifiers = args
512                    .modifiers
513                    .iter()
514                    .map(|x| ModifierKey::from(x))
515                    .collect();
516                let args_wheel = Wheel {
517                    x: args.x,
518                    y: args.y,
519                    delta_x: args.delta_x,
520                    delta_y: args.delta_y,
521                    modifiers,
522                };
523                topmost_node.dispatch_wheel(
524                    Event::new(args_wheel),
525                    &globals,
526                    &engine.runtime_context,
527                )
528            }
529            NativeInterrupt::MouseDown(args) => {
530                let topmost_node = engine
531                    .runtime_context
532                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
533                let args_mouse_down = MouseDown {
534                    mouse: MouseEventArgs {
535                        x: args.x,
536                        y: args.y,
537                        button: MouseButton::from(args.button.clone()),
538                        modifiers: args
539                            .modifiers
540                            .iter()
541                            .map(|x| ModifierKey::from(x))
542                            .collect(),
543                    },
544                };
545                topmost_node.dispatch_mouse_down(
546                    Event::new(args_mouse_down),
547                    &globals,
548                    &engine.runtime_context,
549                )
550            }
551            NativeInterrupt::MouseUp(args) => {
552                let topmost_node = engine
553                    .runtime_context
554                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
555                let args_mouse_up = MouseUp {
556                    mouse: MouseEventArgs {
557                        x: args.x,
558                        y: args.y,
559                        button: MouseButton::from(args.button.clone()),
560                        modifiers: args
561                            .modifiers
562                            .iter()
563                            .map(|x| ModifierKey::from(x))
564                            .collect(),
565                    },
566                };
567                topmost_node.dispatch_mouse_up(
568                    Event::new(args_mouse_up),
569                    &globals,
570                    &engine.runtime_context,
571                )
572            }
573            NativeInterrupt::ContextMenu(args) => {
574                let topmost_node = engine
575                    .runtime_context
576                    .get_topmost_element_beneath_ray(Point2::new(args.x, args.y));
577                let args_context_menu = ContextMenu {
578                    mouse: MouseEventArgs {
579                        x: args.x,
580                        y: args.y,
581                        button: MouseButton::from(args.button.clone()),
582                        modifiers: args
583                            .modifiers
584                            .iter()
585                            .map(|x| ModifierKey::from(x))
586                            .collect(),
587                    },
588                };
589                topmost_node.dispatch_context_menu(
590                    Event::new(args_context_menu),
591                    &globals,
592                    &engine.runtime_context,
593                )
594            }
595        };
596
597        InterruptResult { prevent_default }
598    }
599
600    pub fn deallocate(&mut self, slice: MemorySlice) {
601        let layout = std::alloc::Layout::from_size_align(slice.len(), 1).unwrap();
602        unsafe {
603            std::alloc::dealloc(slice.ptr() as *mut u8, layout);
604        }
605    }
606
607    #[cfg(any(feature = "designtime", feature = "designer"))]
608    pub fn update_userland_component(&mut self) {
609        let current_manifest_version =
610            borrow!(self.designtime_manager).get_last_written_manifest_version();
611        let reload_queue = borrow_mut!(self.designtime_manager).take_reload_queue();
612
613        if current_manifest_version.get()
614            != self
615                .designtime_manager
616                .borrow()
617                .get_last_rendered_manifest_version()
618                .get()
619        {
620            for reload_type in reload_queue {
621                match reload_type {
622                    // This and FullPlay are now the same: TODO join?
623                    ReloadType::Tree => {
624                        let mut engine = borrow_mut!(self.engine);
625                        let root = self
626                            .userland_definition_to_instance_traverser
627                            .get_main_component(USERLAND_COMPONENT_ROOT)
628                            as Rc<dyn pax_runtime::InstanceNode>;
629                        engine.full_reload_userland(root);
630                    }
631                    ReloadType::Node(uni, _) => {
632                        let manifest = self
633                            .userland_definition_to_instance_traverser
634                            .get_manifest();
635                        let containing_component = manifest
636                            .components
637                            .get(&uni.get_containing_component_type_id())
638                            .unwrap();
639                        let containing_template = containing_component.template.as_ref().unwrap();
640                        let tnd = containing_template
641                            .get_node(&uni.get_template_node_id())
642                            .unwrap();
643
644                        let nodes = self
645                            .engine
646                            .borrow()
647                            .runtime_context
648                            .get_expanded_nodes_by_global_ids(&uni);
649
650                        let prior_instance_node = nodes.get(0).map(|x| {
651                            pax_runtime::ReusableInstanceNodeArgs::new(
652                                x.instance_node.borrow().base(),
653                            )
654                        });
655
656                        let pax_type = tnd.type_id.get_pax_type();
657                        let instance_node = match pax_type {
658                            pax_manifest::PaxType::If
659                            | pax_manifest::PaxType::Slot
660                            | pax_manifest::PaxType::Repeat => self
661                                .userland_definition_to_instance_traverser
662                                .build_control_flow(
663                                    &uni.get_containing_component_type_id(),
664                                    &uni.get_template_node_id(),
665                                    prior_instance_node,
666                                ),
667                            _ => self
668                                .userland_definition_to_instance_traverser
669                                .build_template_node(
670                                    &uni.get_containing_component_type_id(),
671                                    &uni.get_template_node_id(),
672                                    prior_instance_node,
673                                ),
674                        };
675                        let mut engine = borrow_mut!(self.engine);
676                        engine.partial_update_expanded_node(Rc::clone(&instance_node));
677                    }
678                }
679            }
680            self.designtime_manager
681                .borrow_mut()
682                .set_last_rendered_manifest_version(current_manifest_version.get());
683        }
684    }
685
686    #[cfg(any(feature = "designtime", feature = "designer"))]
687    pub fn handle_recv_designtime(&mut self) {
688        borrow_mut!(self.designtime_manager)
689            .handle_recv()
690            .expect("couldn't handle recv");
691    }
692
693    #[cfg(any(feature = "designtime", feature = "designer"))]
694    pub fn designtime_tick(&mut self) {
695        self.handle_recv_designtime();
696        self.update_userland_component();
697    }
698
699    pub fn tick(&mut self) -> MemorySlice {
700        #[cfg(any(feature = "designtime", feature = "designer"))]
701        self.designtime_tick();
702
703        let message_queue = borrow_mut!(self.engine).tick();
704
705        // Serialize data to a JSON string
706        let json_string = serde_json::to_string(&message_queue).unwrap();
707
708        // Convert the string into bytes
709        let bytes = json_string.as_bytes();
710
711        // Allocate space in the WebAssembly memory
712        let layout = std::alloc::Layout::from_size_align(bytes.len(), 1).unwrap();
713        let ptr = unsafe { std::alloc::alloc(layout) as *mut u8 };
714
715        // Copy the data into the WebAssembly memory
716        unsafe {
717            std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
718        }
719
720        MemorySlice {
721            ptr: ptr as *const u8,
722            len: bytes.len(),
723        }
724    }
725
726    pub fn render(&mut self) {
727        borrow_mut!(self.engine).render((&mut self.drawing_contexts) as &mut dyn RenderContext);
728        self.engine
729            .borrow()
730            .runtime_context
731            .clear_all_dirty_canvases();
732    }
733
734    pub fn image_loaded(&mut self, path: &str) -> bool {
735        self.drawing_contexts.image_loaded(path)
736    }
737}
738
739// parsing of user_agent strings could most likely be done more robustly, possibly copy some of the logic
740// used in https://crates.io/crates/woothee (used server side normally, to large dep?)
741// list of common user agent strings: https://deviceatlas.com/blog/list-of-user-agent-strings
742fn parse_user_agent_str(user_agent: &str) -> Option<OS> {
743    // example:
744    //              /-----------we are cutting out this part------------\
745    // Mozilla/5.0 (Linux; Android 12; SM-X906C Build/QP1A.190711.020; wv) AppleWebKit/537.36 (KHTML, like Gecko)
746    // Version/4.0 Chrome/80.0.3987.119 Mobile Safari/537.36
747    let platform_start = user_agent.find('(')?;
748    let platform_end = platform_start + user_agent[platform_start..].find(')')?;
749    let platform_str = user_agent.get(platform_start + 1..platform_end - 1)?;
750
751    // NOTE: the ordering here is important: Android/iOS can contain Linux/MacOS strings
752    const STR_PLATFORM_PAIRS: &[(&str, OS)] = &[
753        ("Android", OS::Android),
754        ("iPhone", OS::IPhone),
755        ("Windows", OS::Windows),
756        ("Mac", OS::Mac),
757        ("Linux", OS::Linux),
758    ];
759    for (needle, plat) in STR_PLATFORM_PAIRS {
760        if platform_str.contains(needle) {
761            return Some(*plat);
762        }
763    }
764    None
765}
766
767#[wasm_bindgen]
768pub struct MemorySlice {
769    ptr: *const u8,
770    len: usize,
771}
772
773#[wasm_bindgen]
774impl MemorySlice {
775    pub fn ptr(&self) -> *const u8 {
776        self.ptr
777    }
778
779    pub fn len(&self) -> usize {
780        self.len
781    }
782}