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