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