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}