maia_wasm/
lib.rs

1//! maia-wasm is part of Maia SDR. It is a web application that serves as the UI
2//! of Maia SDR. It renders the waterfall using WebGL2 and gives a UI composed
3//! of HTML elements that interacts with the maia-httpd RESTful API.
4
5#![warn(missing_docs)]
6
7use std::cell::RefCell;
8use std::rc::Rc;
9use wasm_bindgen::{JsCast, prelude::*};
10use web_sys::{Document, HtmlCanvasElement, Window};
11
12use crate::render::RenderEngine;
13use crate::ui::Ui;
14use crate::waterfall::Waterfall;
15use crate::waterfall_interaction::WaterfallInteraction;
16use crate::websocket::WebSocketClient;
17
18pub mod array_view;
19pub mod colormap;
20pub mod pointer;
21pub mod render;
22pub mod ui;
23pub mod version;
24pub mod waterfall;
25pub mod waterfall_interaction;
26pub mod websocket;
27
28/// Initialize the wasm module.
29///
30/// This function is set to run as soon as the wasm module is instantiated. It
31/// applies some settings that are needed for all kinds of usage of
32/// `maia-wasm`. For instance, it sets a panic hook using the
33/// [`console_error_panic_hook`] crate.
34#[wasm_bindgen(start)]
35pub fn start() -> Result<(), JsValue> {
36    std::panic::set_hook(Box::new(console_error_panic_hook::hook));
37    Ok(())
38}
39
40/// Starts the maia-wasm web application.
41///
42/// This function starts the maia-wasm application. It should be called from
43/// JavaScript when the web page is loaded. It sets up all the objects and
44/// callbacks that keep the application running.
45#[wasm_bindgen]
46pub fn maia_wasm_start() -> Result<(), JsValue> {
47    let (window, document) = get_window_and_document()?;
48    let canvas = Rc::new(
49        document
50            .get_element_by_id("canvas")
51            .ok_or("unable to get #canvas element")?
52            .dyn_into::<web_sys::HtmlCanvasElement>()?,
53    );
54
55    let (render_engine, waterfall, mut waterfall_interaction) =
56        new_waterfall(&window, &document, &canvas)?;
57    WebSocketClient::start(&window, Rc::clone(&waterfall))?;
58    let ui = Ui::new(
59        Rc::clone(&window),
60        Rc::clone(&document),
61        Rc::clone(&render_engine),
62        Rc::clone(&waterfall),
63    )?;
64    waterfall_interaction.set_ui(ui);
65
66    setup_render_loop(render_engine, waterfall);
67
68    Ok(())
69}
70
71/// Returns the [`Window`] and [`Document`] objects.
72///
73/// These are returned inside an [`Rc`] so that their ownership can be shared.
74pub fn get_window_and_document() -> Result<(Rc<Window>, Rc<Document>), JsValue> {
75    let window = Rc::new(web_sys::window().ok_or("unable to get window")?);
76    let document = Rc::new(window.document().ok_or("unable to get document")?);
77    Ok((window, document))
78}
79
80/// Creates a [`Waterfall`] and associated objects.
81///
82/// This function creates a waterfall, the associated WebGL2 [`RenderEngine`],
83/// and the [`WaterfallInteraction`] object, which is used to control the
84/// waterfall based on user input.
85#[allow(clippy::type_complexity)]
86pub fn new_waterfall(
87    window: &Rc<Window>,
88    document: &Document,
89    canvas: &Rc<HtmlCanvasElement>,
90) -> Result<
91    (
92        Rc<RefCell<RenderEngine>>,
93        Rc<RefCell<Waterfall>>,
94        WaterfallInteraction,
95    ),
96    JsValue,
97> {
98    let render_engine = Rc::new(RefCell::new(RenderEngine::new(
99        Rc::clone(canvas),
100        Rc::clone(window),
101        document,
102    )?));
103    let waterfall = Rc::new(RefCell::new(Waterfall::new(
104        &mut render_engine.borrow_mut(),
105        window.performance().ok_or("unable to get performance")?,
106    )?));
107    let waterfall_interaction = WaterfallInteraction::new(
108        Rc::clone(window),
109        Rc::clone(canvas),
110        Rc::clone(&render_engine),
111        Rc::clone(&waterfall),
112    )?;
113    Ok((render_engine, waterfall, waterfall_interaction))
114}
115
116/// Sets up a render loop for the waterfall.
117///
118/// This function sets up a render loop using `requestAnimationFrame()`. Each
119/// time the the callback triggers, the waterfall is prepared for rendering and
120/// the render engine is called. Then, the rendering of the next frame is
121/// scheduled using `requestAnimationFrame()`.
122pub fn setup_render_loop(
123    render_engine: Rc<RefCell<RenderEngine>>,
124    waterfall: Rc<RefCell<Waterfall>>,
125) {
126    let f = Rc::new(RefCell::new(None));
127    let g = f.clone();
128    *g.borrow_mut() = Some(Closure::new(move |dt| {
129        let mut render_engine = render_engine.borrow_mut();
130        if let Err(e) = waterfall
131            .borrow_mut()
132            .prepare_render(&mut render_engine, dt)
133        {
134            web_sys::console::error_1(&e);
135            return;
136        }
137        if let Err(e) = render_engine.render() {
138            web_sys::console::error_1(&e);
139            return;
140        }
141        // Schedule ourselves for another requestAnimationFrame callback.
142        request_animation_frame(f.borrow().as_ref().unwrap());
143    }));
144    // Initial requestAnimationFrame callback.
145    request_animation_frame(g.borrow().as_ref().unwrap());
146}
147
148fn request_animation_frame(f: &Closure<dyn FnMut(f32)>) {
149    web_sys::window()
150        .unwrap()
151        .request_animation_frame(f.as_ref().unchecked_ref())
152        .unwrap();
153}