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}