perspective_viewer/custom_elements/
viewer.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13#![allow(non_snake_case)]
14
15use std::cell::RefCell;
16use std::rc::Rc;
17use std::str::FromStr;
18
19use ::perspective_js::utils::global;
20use ::perspective_js::{Table, View};
21use futures::future::join;
22use js_sys::*;
23use perspective_client::config::ViewConfigUpdate;
24use perspective_js::JsViewWindow;
25use wasm_bindgen::JsCast;
26use wasm_bindgen::prelude::*;
27use wasm_bindgen_futures::JsFuture;
28use web_sys::*;
29use yew::prelude::*;
30
31use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg, PerspectiveViewerProps};
32use crate::config::*;
33use crate::custom_events::*;
34use crate::dragdrop::*;
35use crate::js::*;
36use crate::model::*;
37use crate::presentation::*;
38use crate::renderer::*;
39use crate::session::Session;
40use crate::utils::*;
41use crate::*;
42
43/// The `<perspective-viewer>` custom element.
44///
45/// # JavaScript Examples
46///
47/// Create a new `<perspective-viewer>`:
48///
49/// ```javascript
50/// const viewer = document.createElement("perspective-viewer");
51/// window.body.appendChild(viewer);
52/// ```
53#[derive(Clone)]
54#[wasm_bindgen]
55pub struct PerspectiveViewerElement {
56    elem: HtmlElement,
57    root: Rc<RefCell<Option<AppHandle<PerspectiveViewer>>>>,
58    resize_handle: Rc<RefCell<Option<ResizeObserverHandle>>>,
59    intersection_handle: Rc<RefCell<Option<IntersectionObserverHandle>>>,
60    session: Session,
61    renderer: Renderer,
62    presentation: Presentation,
63    _events: CustomEvents,
64    _subscriptions: Rc<Subscription>,
65}
66
67derive_model!(Renderer, Session, Presentation for PerspectiveViewerElement);
68
69impl CustomElementMetadata for PerspectiveViewerElement {
70    const CUSTOM_ELEMENT_NAME: &'static str = "perspective-viewer";
71    const STATICS: &'static [&'static str] = ["registerPlugin", "getExprTKCommands"].as_slice();
72}
73
74#[wasm_bindgen]
75impl PerspectiveViewerElement {
76    #[doc(hidden)]
77    #[wasm_bindgen(constructor)]
78    pub fn new(elem: web_sys::HtmlElement) -> Self {
79        tracing::debug!("Creating <perspective-viewer>");
80        let init = web_sys::ShadowRootInit::new(web_sys::ShadowRootMode::Open);
81        let shadow_root = elem
82            .attach_shadow(&init)
83            .unwrap()
84            .unchecked_into::<web_sys::Element>();
85
86        // Application State
87        let session = Session::default();
88        let renderer = Renderer::new(&elem);
89        let presentation = Presentation::new(&elem);
90        let events = CustomEvents::new(&elem, &session, &renderer, &presentation);
91
92        // Create Yew App
93        let props = yew::props!(PerspectiveViewerProps {
94            elem: elem.clone(),
95            session: session.clone(),
96            renderer: renderer.clone(),
97            presentation: presentation.clone(),
98            dragdrop: DragDrop::default(),
99            custom_events: events.clone(),
100            weak_link: WeakScope::default(),
101        });
102
103        let root = yew::Renderer::with_root_and_props(shadow_root, props).render();
104
105        // Create callbacks
106        let update_sub = session.table_updated.add_listener({
107            clone!(renderer, session);
108            move |_| {
109                clone!(renderer, session);
110                ApiFuture::spawn(async move { renderer.update(&session).await })
111            }
112        });
113
114        let resize_handle = ResizeObserverHandle::new(&elem, &renderer, &root);
115        Self {
116            elem,
117            root: Rc::new(RefCell::new(Some(root))),
118            session,
119            renderer,
120            presentation,
121            resize_handle: Rc::new(RefCell::new(Some(resize_handle))),
122            intersection_handle: Rc::new(RefCell::new(None)),
123            _events: events,
124            _subscriptions: Rc::new(update_sub),
125        }
126    }
127
128    #[doc(hidden)]
129    #[wasm_bindgen(js_name = "connectedCallback")]
130    pub fn connected_callback(&self) {
131        tracing::debug!("Connected <perspective-viewer>");
132    }
133
134    /// Loads a [`Table`] (or rather, a Javascript `Promise` which returns a
135    /// [`Table`]) in this viewer.
136    ///
137    /// When [`PerspectiveViewerElement::load`] resolves, the first frame of the
138    /// UI + visualization is guaranteed to have been drawn. Awaiting the result
139    /// of this method in a `try`/`catch` block will capture any errors
140    /// thrown during the loading process, or from the [`Table`] `Promise`
141    /// itself.
142    ///
143    /// A [`Table`] can be created using the
144    /// [`@finos/perspective`](https://www.npmjs.com/package/@finos/perspective)
145    /// library from NPM (see [`perspective_js`] documentation for details).
146    ///
147    /// # JavaScript Examples
148    ///
149    /// ```javascript
150    /// import perspective from "@finos/perspective";
151    ///
152    /// const worker = await perspective.worker();
153    /// viewer.load(worker.table("x,y\n1,2"));
154    /// ```
155    pub fn load(&self, table: JsValue) -> ApiFuture<()> {
156        tracing::info!("Loading Table");
157        self.session.invalidate();
158        let promise = table
159            .clone()
160            .dyn_into::<js_sys::Promise>()
161            .unwrap_or_else(|_| js_sys::Promise::resolve(&table));
162
163        self.session.reset_stats();
164        let delete_task = self.session.reset(true);
165        let mut config = ViewConfigUpdate {
166            columns: Some(self.session.get_view_config().columns.clone()),
167            ..ViewConfigUpdate::default()
168        };
169
170        self.session
171            .set_update_column_defaults(&mut config, &self.renderer.metadata());
172
173        let update_task = self.session.update_view_config(config);
174        clone!(self.renderer, self.session);
175        ApiFuture::new(async move {
176            let task = async {
177                update_task?;
178                let jstable = JsFuture::from(promise).await?;
179
180                if let Some(table) =
181                    wasm_bindgen_derive::try_from_js_option::<perspective_js::Table>(jstable)?
182                {
183                    tracing::debug!(
184                        "Successfully loaded {:.0} rows from Table",
185                        table.size().await?
186                    );
187
188                    session.set_table(table.get_table().clone()).await?;
189                    session.validate().await?.create_view().await
190                } else {
191                    Err(ApiError::new("Invalid Table"))
192                }
193            };
194
195            renderer.set_throttle(None);
196            let (draw, delete) = join(renderer.draw(task), delete_task).await;
197            let result = draw.and(delete);
198            if let Err(e) = result.clone() {
199                session.set_error(e.to_string()).await?;
200            }
201
202            result
203        })
204    }
205
206    /// Delete the internal [`View`] and all associated state, rendering this
207    /// `<perspective-viewer>` unusable and freeing all associated resources.
208    /// Does not delete the supplied [`Table`] (as this is constructed by the
209    /// callee).
210    ///
211    /// <div class="warning">
212    ///
213    /// Allowing a `<perspective-viewer>` to be garbage-collected
214    /// without calling [`PerspectiveViewerElement::delete`] will leak WASM
215    /// memory!
216    ///
217    /// </div>
218    ///
219    /// # JavaScript Examples
220    ///
221    /// ```javascript
222    /// await viewer.delete();
223    /// ```
224    pub fn delete(&self) -> ApiFuture<()> {
225        clone!(self.renderer, self.session, self.root);
226        ApiFuture::new(self.renderer.clone().with_lock(async move {
227            renderer.delete()?;
228            session.delete().await?;
229            root.borrow_mut()
230                .take()
231                .ok_or("Already deleted!")?
232                .destroy();
233
234            tracing::info!("Deleted <perspective-viewer>");
235            Ok(())
236        }))
237    }
238
239    /// Get the underlying [`View`] for this viewer.
240    ///
241    /// Use this method to get promgrammatic access to the [`View`] as currently
242    /// configured by the user, for e.g. serializing as an
243    /// [Apache Arrow](https://arrow.apache.org/) before passing to another
244    /// library.
245    ///
246    /// The [`View`] returned by this method is owned by the
247    /// [`PerspectiveViewerElement`] and may be _invalidated_ by
248    /// [`View::delete`] at any time. Plugins which rely on this [`View`] for
249    /// their [`HTMLPerspectiveViewerPluginElement::draw`] implementations
250    /// should treat this condition as a _cancellation_ by silently aborting on
251    /// "View already deleted" errors from method calls.
252    ///
253    /// # JavaScript Examples
254    ///
255    /// ```javascript
256    /// const view = await viewer.getView();
257    /// ```
258    #[wasm_bindgen]
259    pub fn getView(&self) -> ApiFuture<View> {
260        let session = self.session.clone();
261        ApiFuture::new(async move { Ok(session.get_view().ok_or("No table set")?.into()) })
262    }
263
264    /// Get the underlying [`Table`] for this viewer (as passed to
265    /// [`PerspectiveViewerElement::load`]).
266    ///
267    /// # Arguments
268    ///
269    /// - `wait_for_table` - whether to wait for
270    ///   [`PerspectiveViewerElement::load`] to be called, or fail immediately
271    ///   if [`PerspectiveViewerElement::load`] has not yet been called.
272    ///
273    /// # JavaScript Examples
274    ///
275    /// ```javascript
276    /// const table = await viewer.getTable();
277    /// ```
278    #[wasm_bindgen]
279    pub fn getTable(&self, wait_for_table: Option<bool>) -> ApiFuture<Table> {
280        let session = self.session.clone();
281        ApiFuture::new(async move {
282            match session.get_table() {
283                Some(table) => Ok(table.into()),
284                None if !wait_for_table.unwrap_or_default() => Err("No table set".into()),
285                None => {
286                    session.table_loaded.listen_once().await?;
287                    Ok(session.get_table().ok_or("No table set")?.into())
288                },
289            }
290        })
291    }
292
293    /// Get render statistics. Some fields of the returned stats object are
294    /// relative to the last time [`PerspectiveViewerElement::getRenderStats`]
295    /// was called, ergo calling this method resets these fields.
296    ///
297    /// # JavaScript Examples
298    ///
299    /// ```javascript
300    /// const {virtual_fps, actual_fps} = await viewer.getRenderStats();
301    /// ```
302    #[wasm_bindgen]
303    pub fn getRenderStats(&self) -> ApiResult<JsValue> {
304        Ok(JsValue::from_serde_ext(
305            &self.renderer.render_timer().get_stats(),
306        )?)
307    }
308
309    /// Flush any pending modifications to this `<perspective-viewer>`.  Since
310    /// `<perspective-viewer>`'s API is almost entirely `async`, it may take
311    /// some milliseconds before any user-initiated changes to the [`View`]
312    /// affects the rendered element.  If you want to make sure all pending
313    /// actions have been rendered, call and await [`Self::flush`].
314    ///
315    /// [`Self::flush`] will resolve immediately if there is no [`Table`] set.
316    ///
317    /// # JavaScript Examples
318    ///
319    /// In this example, [`Self::restore`] is called without `await`, but the
320    /// eventual render which results from this call can still be awaited by
321    /// immediately awaiting [`Self::flush`] instead.
322    ///
323    /// ```javascript
324    /// viewer.restore(config);
325    /// await viewer.flush();
326    /// ```
327    pub fn flush(&self) -> ApiFuture<()> {
328        clone!(self.renderer);
329        ApiFuture::new(async move {
330            request_animation_frame().await;
331            renderer.with_lock(async { Ok(()) }).await
332        })
333    }
334
335    /// Restores this element from a full/partial
336    /// [`perspective_js::JsViewConfig`].
337    ///
338    /// One of the best ways to use [`Self::restore`] is by first configuring
339    /// a `<perspective-viewer>` as you wish, then using either the `Debug`
340    /// panel or "Copy" -> "config.json" from the toolbar menu to snapshot
341    /// the [`Self::restore`] argument as JSON.
342    ///
343    /// # Arguments
344    ///
345    /// - `update` - The config to restore to, as returned by [`Self::save`] in
346    ///   either "json", "string" or "arraybuffer" format.
347    ///
348    /// # JavaScript Examples
349    ///
350    /// Apply a `group_by` to the current [`View`], without modifying/resetting
351    /// other fields:
352    ///
353    /// ```javascript
354    /// await viewer.restore({group_by: ["State"]});
355    /// ```
356    pub fn restore(&self, update: JsValue) -> ApiFuture<()> {
357        tracing::info!("Restoring ViewerConfig");
358        global::document().blur_active_element();
359        let this = self.clone();
360        ApiFuture::new(async move {
361            let decoded_update = ViewerConfigUpdate::decode(&update)?;
362            let root = this.root.clone();
363            let settings = decoded_update.settings.clone();
364            let result = root
365                .borrow()
366                .as_ref()
367                .into_apierror()?
368                .send_message_async(move |x| {
369                    PerspectiveViewerMsg::ToggleSettingsComplete(settings, x)
370                });
371
372            this.restore_and_render(decoded_update, async move { Ok(result.await?) })
373                .await?;
374            Ok(())
375        })
376    }
377
378    pub fn resetError(&self) -> ApiFuture<()> {
379        self.session.invalidate();
380        let this = self.clone();
381        ApiFuture::new(async move {
382            this.update_and_render(ViewConfigUpdate::default())?.await?;
383            Ok(())
384        })
385    }
386
387    /// Save this element to serialized state object, one which can be restored
388    /// via the [`Self::restore`] method.
389    ///
390    /// # Arguments
391    ///
392    /// - `format` - Supports "json" (default), "arraybuffer" or "string".
393    ///
394    /// # JavaScript Examples
395    ///
396    /// Get the current `group_by` setting:
397    ///
398    /// ```javascript
399    /// const {group_by} = await viewer.restore();
400    /// ```
401    ///
402    /// Reset workflow attached to an external button `myResetButton`:
403    ///
404    /// ```javascript
405    /// const token = await viewer.save();
406    /// myResetButton.addEventListener("clien", async () => {
407    ///     await viewer.restore(token);
408    /// });
409    /// ```
410    pub fn save(&self, format: Option<String>) -> ApiFuture<JsValue> {
411        let this = self.clone();
412        ApiFuture::new(async move {
413            let viewer_config_task = this.get_viewer_config();
414            let format = format
415                .as_ref()
416                .map(|x| ViewerConfigEncoding::from_str(x))
417                .transpose()?;
418
419            let viewer_config = viewer_config_task.await?;
420            viewer_config.encode(&format)
421        })
422    }
423
424    /// Download this viewer's internal [`View`] data as a `.csv` file.
425    ///
426    /// # Arguments
427    ///
428    /// - `flat` - Whether to use the current [`perspective_js::JsViewConfig`]
429    ///   to generate this data, or use the default.
430    ///
431    /// # JavaScript Examples
432    ///
433    /// ```javascript
434    /// myDownloadButton.addEventListener("click", async () => {
435    ///     await viewer.download();
436    /// })
437    /// ```
438    pub fn download(&self, flat: Option<bool>) -> ApiFuture<()> {
439        let session = self.session.clone();
440        ApiFuture::new(async move {
441            let val = session
442                .csv_as_jsvalue(flat.unwrap_or_default(), None)
443                .await?
444                .as_blob()?;
445
446            // TODO name.as_deref().unwrap_or("untitled.csv")
447            download("untitled.csv", &val)
448        })
449    }
450
451    /// Copy this viewer's `View` or `Table` data as CSV to the system
452    /// clipboard.
453    ///
454    /// # Arguments
455    ///
456    /// - `method` - The `ExportMethod` (serialized as a `String`) to use to
457    ///   render the data to the Clipboard.
458    ///
459    /// # JavaScript Examples
460    ///
461    /// ```javascript
462    /// myDownloadButton.addEventListener("click", async () => {
463    ///     await viewer.copy();
464    /// })
465    /// ```
466    pub fn copy(&self, method: Option<JsString>) -> ApiFuture<()> {
467        let this = self.clone();
468        ApiFuture::new(async move {
469            let method = if let Some(method) = method
470                .map(|x| x.unchecked_into())
471                .map(serde_wasm_bindgen::from_value)
472            {
473                method?
474            } else {
475                ExportMethod::Csv
476            };
477
478            let js_task = this.export_method_to_jsvalue(method);
479            copy_to_clipboard(js_task, MimeType::TextPlain).await
480        })
481    }
482
483    /// Reset the viewer's `ViewerConfig` to the default.
484    ///
485    /// # Arguments
486    ///
487    /// - `reset_all` - If set, will clear expressions and column settings as
488    ///   well.
489    ///
490    /// # JavaScript Examples
491    ///
492    /// ```javascript
493    /// await viewer.reset();
494    /// ```
495    pub fn reset(&self, reset_all: Option<bool>) -> ApiFuture<()> {
496        tracing::info!("Resetting config");
497        let root = self.root.clone();
498        let all = reset_all.unwrap_or_default();
499        ApiFuture::new(async move {
500            let task = root
501                .borrow()
502                .as_ref()
503                .ok_or("Already deleted")?
504                .send_message_async(move |x| PerspectiveViewerMsg::Reset(all, Some(x)));
505
506            Ok(task.await?)
507        })
508    }
509
510    /// Recalculate the viewer's dimensions and redraw.
511    ///
512    /// Use this method to tell `<perspective-viewer>` its dimensions have
513    /// changed when auto-size mode has been disabled via [`Self::setAutoSize`].
514    /// [`Self::resize`] resolves when the resize-initiated redraw of this
515    /// element has completed.
516    ///
517    /// # Arguments
518    ///
519    /// - `force` - If [`Self::resize`] is called with `false` or without an
520    ///   argument, and _auto-size_ mode is enabled via [`Self::setAutoSize`],
521    ///   [`Self::resize`] will log a warning and auto-disable auto-size mode.
522    ///
523    /// # JavaScript Examples
524    ///
525    /// ```javascript
526    /// await viewer.resize(true)
527    /// ```
528    #[wasm_bindgen]
529    pub fn resize(&self, force: Option<bool>) -> ApiFuture<()> {
530        if !force.unwrap_or_default() && self.resize_handle.borrow().is_some() {
531            let msg: JsValue = "`resize(false)` called, disabling auto-size.  It can be \
532                                re-enabled with `setAutoSize(true)`."
533                .into();
534            web_sys::console::warn_1(&msg);
535            *self.resize_handle.borrow_mut() = None;
536        }
537
538        let renderer = self.renderer.clone();
539        ApiFuture::new(async move { renderer.resize().await })
540    }
541
542    /// Sets the auto-size behavior of this component.
543    ///
544    /// When `true`, this `<perspective-viewer>` will register a
545    /// `ResizeObserver` on itself and call [`Self::resize`] whenever its own
546    /// dimensions change. However, when embedded in a larger application
547    /// context, you may want to call [`Self::resize`] manually to avoid
548    /// over-rendering; in this case auto-sizing can be disabled via this
549    /// method. Auto-size behavior is enabled by default.
550    ///
551    /// # Arguments
552    ///
553    /// - `autosize` - Whether to enable `auto-size` behavior or not.
554    ///
555    /// # JavaScript Examples
556    ///
557    /// Disable auto-size behavior:
558    ///
559    /// ```javascript
560    /// viewer.setAutoSize(false);
561    /// ```
562    #[wasm_bindgen]
563    pub fn setAutoSize(&self, autosize: bool) {
564        if autosize {
565            let handle = Some(ResizeObserverHandle::new(
566                &self.elem,
567                &self.renderer,
568                self.root.borrow().as_ref().unwrap(),
569            ));
570            *self.resize_handle.borrow_mut() = handle;
571        } else {
572            *self.resize_handle.borrow_mut() = None;
573        }
574    }
575
576    /// Sets the auto-pause behavior of this component.
577    ///
578    /// When `true`, this `<perspective-viewer>` will register an
579    /// `IntersectionObserver` on itself and subsequently skip rendering
580    /// whenever its viewport visibility changes. Auto-pause is enabled by
581    /// default.
582    ///
583    /// # Arguments
584    ///
585    /// - `autopause` Whether to enable `auto-pause` behavior or not.
586    ///
587    /// # JavaScript Examples
588    ///
589    /// Disable auto-size behavior:
590    ///
591    /// ```javascript
592    /// viewer.setAutoPause(false);
593    /// ```
594    #[wasm_bindgen]
595    pub fn setAutoPause(&self, autopause: bool) {
596        if autopause {
597            let handle = Some(IntersectionObserverHandle::new(
598                &self.elem,
599                &self.session,
600                &self.renderer,
601            ));
602            *self.intersection_handle.borrow_mut() = handle;
603        } else {
604            *self.intersection_handle.borrow_mut() = None;
605        }
606    }
607
608    /// Return a [`perspective_js::JsViewWindow`] for the currently selected
609    /// region.
610    #[wasm_bindgen]
611    pub fn getSelection(&self) -> Option<JsViewWindow> {
612        self.renderer.get_selection().map(|x| x.into())
613    }
614
615    /// Set the selection [`perspective_js::JsViewWindow`] for this element.
616    #[wasm_bindgen]
617    pub fn setSelection(&self, window: Option<JsViewWindow>) -> ApiResult<()> {
618        let window = window.map(|x| x.into_serde_ext()).transpose()?;
619        self.renderer.set_selection(window);
620        Ok(())
621    }
622
623    /// Get this viewer's edit port for the currently loaded [`Table`] (see
624    /// [`Table::update`] for details on ports).
625    #[wasm_bindgen]
626    pub fn getEditPort(&self) -> Result<f64, JsValue> {
627        self.session
628            .metadata()
629            .get_edit_port()
630            .ok_or_else(|| "No `Table` loaded".into())
631    }
632
633    /// Restyle all plugins from current document.
634    ///
635    /// <div class="warning">
636    ///
637    /// [`Self::restyleElement`] _must_ be called for many runtime changes to
638    /// CSS properties to be reflected in an already-rendered
639    /// `<perspective-viewer>`.
640    ///
641    /// </div>
642    ///
643    /// # JavaScript Examples
644    ///
645    /// ```javascript
646    /// viewer.style = "--icon--color: red";
647    /// await viewer.restyleElement();
648    /// ```
649    #[wasm_bindgen]
650    pub fn restyleElement(&self) -> ApiFuture<JsValue> {
651        clone!(self.renderer, self.session);
652        ApiFuture::new(async move {
653            let view = session.get_view().into_apierror()?;
654            renderer.restyle_all(&view).await
655        })
656    }
657
658    /// Set the available theme names available in the status bar UI.
659    ///
660    /// Calling [`Self::resetThemes`] may cause the current theme to switch,
661    /// if e.g. the new theme set does not contain the current theme.
662    ///
663    /// # JavaScript Examples
664    ///
665    /// Restrict `<perspective-viewer>` theme options to _only_ default light
666    /// and dark themes, regardless of what is auto-detected from the page's
667    /// CSS:
668    ///
669    /// ```javascript
670    /// viewer.resetThemes(["Pro Light", "Pro Dark"])
671    /// ```
672    #[wasm_bindgen]
673    pub fn resetThemes(&self, themes: Option<Box<[JsValue]>>) -> ApiFuture<JsValue> {
674        clone!(self.renderer, self.session, self.presentation);
675        ApiFuture::new(async move {
676            let themes: Option<Vec<String>> = themes
677                .unwrap_or_default()
678                .iter()
679                .map(|x| x.as_string())
680                .collect();
681
682            let theme_name = presentation.get_selected_theme_name().await;
683            let mut changed = presentation.reset_available_themes(themes).await;
684            let reset_theme = presentation
685                .get_available_themes()
686                .await?
687                .iter()
688                .find(|y| theme_name.as_ref() == Some(y))
689                .cloned();
690
691            changed = presentation.set_theme_name(reset_theme.as_deref()).await? || changed;
692            if changed {
693                if let Some(view) = session.get_view() {
694                    return renderer.restyle_all(&view).await;
695                }
696            }
697
698            Ok(JsValue::UNDEFINED)
699        })
700    }
701
702    /// Determines the render throttling behavior. Can be an integer, for
703    /// millisecond window to throttle render event; or, if `None`, adaptive
704    /// throttling will be calculated from the measured render time of the
705    /// last 5 frames.
706    ///
707    /// # Arguments
708    ///
709    /// - `throttle` - The throttle rate in milliseconds (f64), or `None` for
710    ///   adaptive throttling.
711    ///
712    /// # JavaScript Examples
713    ///
714    /// Only draws at most 1 frame/sec:
715    ///
716    /// ```rust
717    /// viewer.setThrottle(1000);
718    /// ```
719    #[wasm_bindgen]
720    pub fn setThrottle(&self, val: Option<f64>) {
721        self.renderer.set_throttle(val);
722    }
723
724    /// Toggle (or force) the config panel open/closed.
725    ///
726    /// # Arguments
727    ///
728    /// - `force` - Force the state of the panel open or closed, or `None` to
729    ///   toggle.
730    ///
731    /// # JavaScript Examples
732    ///
733    /// ```javascript
734    /// await viewer.toggleConfig();
735    /// ```
736    #[wasm_bindgen]
737    pub fn toggleConfig(&self, force: Option<bool>) -> ApiFuture<JsValue> {
738        global::document().blur_active_element();
739        let root = self.root.clone();
740        ApiFuture::new(async move {
741            let force = force.map(SettingsUpdate::Update);
742            let task = root
743                .borrow()
744                .as_ref()
745                .into_apierror()?
746                .send_message_async(|x| PerspectiveViewerMsg::ToggleSettingsInit(force, Some(x)));
747
748            task.await.map_err(|_| JsValue::from("Cancelled"))?
749        })
750    }
751
752    /// Get an `Array` of all of the plugin custom elements registered for this
753    /// element. This may not include plugins which called
754    /// [`registerPlugin`] after the host has rendered for the first time.
755    #[wasm_bindgen]
756    pub fn getAllPlugins(&self) -> Array {
757        self.renderer.get_all_plugins().iter().collect::<Array>()
758    }
759
760    /// Gets a plugin Custom Element with the `name` field, or get the active
761    /// plugin if no `name` is provided.
762    ///
763    /// # Arguments
764    ///
765    /// - `name` - The `name` property of a perspective plugin Custom Element,
766    ///   or `None` for the active plugin's Custom Element.
767    #[wasm_bindgen]
768    pub fn getPlugin(&self, name: Option<String>) -> ApiResult<JsPerspectiveViewerPlugin> {
769        match name {
770            None => self.renderer.get_active_plugin(),
771            Some(name) => self.renderer.get_plugin(&name),
772        }
773    }
774
775    /// Create a new JavaScript Heap reference for this model instance.
776    #[doc(hidden)]
777    #[allow(clippy::use_self)]
778    #[wasm_bindgen]
779    pub fn get_model(&self) -> PerspectiveViewerElement {
780        self.clone()
781    }
782
783    /// Asynchronously opens the column settings for a specific column.
784    /// When finished, the `<perspective-viewer>` element will emit a
785    /// "perspective-toggle-column-settings" CustomEvent.
786    /// The event's details property has two fields: `{open: bool, column_name?:
787    /// string}`. The CustomEvent is also fired whenever the user toggles the
788    /// sidebar manually.
789    #[wasm_bindgen]
790    pub fn toggleColumnSettings(&self, column_name: String) -> ApiFuture<()> {
791        clone!(self.session, self.root);
792        ApiFuture::new(async move {
793            let locator = session.metadata().get_column_locator(Some(column_name));
794            let task = root
795                .borrow()
796                .as_ref()
797                .into_apierror()?
798                .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
799                    locator,
800                    sender: Some(sender),
801                    toggle: true,
802                });
803            task.await.map_err(|_| ApiError::from("Cancelled"))
804        })
805    }
806
807    /// Force open the settings for a particular column. Pass `null` to close
808    /// the column settings panel. See [`Self::toggleColumnSettings`] for more.
809    #[wasm_bindgen]
810    pub fn openColumnSettings(
811        &self,
812        column_name: Option<String>,
813        toggle: Option<bool>,
814    ) -> ApiFuture<()> {
815        clone!(self.session, self.root);
816        ApiFuture::new(async move {
817            let locator = session.metadata().get_column_locator(column_name);
818            let task = root
819                .borrow()
820                .as_ref()
821                .into_apierror()?
822                .send_message_async(|sender| PerspectiveViewerMsg::OpenColumnSettings {
823                    locator,
824                    sender: Some(sender),
825                    toggle: toggle.unwrap_or_default(),
826                });
827            task.await.map_err(|_| ApiError::from("Cancelled"))
828        })
829    }
830}