diff --git a/default.profraw b/default.profraw new file mode 100644 index 0000000000..11a6b7d4ac Binary files /dev/null and b/default.profraw differ diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index 9eb51f6269..2b3526fd01 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -33,78 +33,6 @@ use crate::session::Session; use crate::utils::*; use crate::*; -struct ResizeObserverHandle { - elem: HtmlElement, - observer: ResizeObserver, - _callback: Closure, -} - -impl ResizeObserverHandle { - fn new(elem: &HtmlElement, renderer: &Renderer, root: &AppHandle) -> Self { - let on_resize = root.callback(|()| PerspectiveViewerMsg::Resize); - let mut state = ResizeObserverState { - elem: elem.clone(), - renderer: renderer.clone(), - width: elem.offset_width(), - height: elem.offset_height(), - on_resize, - }; - - let _callback = (move |xs| state.on_resize(&xs)).into_closure_mut(); - let func = _callback.as_ref().unchecked_ref::(); - let observer = ResizeObserver::new(func); - observer.observe(elem); - Self { - elem: elem.clone(), - _callback, - observer, - } - } -} - -impl Drop for ResizeObserverHandle { - fn drop(&mut self) { - self.observer.unobserve(&self.elem); - } -} - -struct ResizeObserverState { - elem: HtmlElement, - renderer: Renderer, - width: i32, - height: i32, - on_resize: Callback<()>, -} - -impl ResizeObserverState { - fn on_resize(&mut self, entries: &js_sys::Array) { - let is_visible = self - .elem - .offset_parent() - .map(|x| !x.is_null()) - .unwrap_or(false); - - for y in entries.iter() { - let entry: ResizeObserverEntry = y.unchecked_into(); - let content = entry.content_rect(); - let content_width = content.width().floor() as i32; - let content_height = content.height().floor() as i32; - let resized = self.width != content_width || self.height != content_height; - if resized && is_visible { - clone!(self.on_resize, self.renderer); - ApiFuture::spawn(async move { - renderer.resize().await?; - on_resize.emit(()); - Ok(()) - }); - } - - self.width = content_width; - self.height = content_height; - } - } -} - /// A `customElements` class which encapsulates both the `` /// public API, as well as the Rust component state. /// @@ -131,6 +59,7 @@ pub struct PerspectiveViewerElement { elem: HtmlElement, root: Rc>>>, resize_handle: Rc>>, + intersection_handle: Rc>>, session: Session, renderer: Renderer, presentation: Presentation, @@ -192,6 +121,7 @@ impl PerspectiveViewerElement { renderer, presentation, resize_handle: Rc::new(RefCell::new(Some(resize_handle))), + intersection_handle: Rc::new(RefCell::new(None)), _events: events, _subscriptions: Rc::new(update_sub), } @@ -485,13 +415,12 @@ impl PerspectiveViewerElement { ApiFuture::new(async move { renderer.resize().await }) } - /// Sets the auto-size behavior of this component. When `true`, this + /// Sets the auto-size behavior of this component. When `true`, this /// `` will register a `ResizeObserver` on itself and /// call `resize()` whenever its own dimensions change. /// /// # Arguments - /// - `autosize` Whether to register a `ResizeObserver` on this element or - /// not. + /// - `autosize` Whether to enable `auto-size` behavior or not. #[wasm_bindgen(js_name = "setAutoSize")] pub fn set_auto_size(&mut self, autosize: bool) { if autosize { @@ -506,6 +435,26 @@ impl PerspectiveViewerElement { } } + /// Sets the auto-pause behavior of this component. When `true`, this + /// `` will register an `IntersectionObserver` on + /// itself and call `pause()` whenever its viewport visibility changes. + /// + /// # Arguments + /// - `autopause` Whether to enable `auto-pause` behavior or not. + #[wasm_bindgen(js_name = "setAutoPause")] + pub fn set_auto_pause(&mut self, autopause: bool) { + if autopause { + let handle = Some(IntersectionObserverHandle::new( + &self.elem, + &self.session, + &self.renderer, + )); + *self.intersection_handle.borrow_mut() = handle; + } else { + *self.intersection_handle.borrow_mut() = None; + } + } + /// Get this viewer's edit port for the currently loaded `Table`. #[wasm_bindgen(js_name = "getEditPort")] pub fn get_edit_port(&self) -> Result { diff --git a/rust/perspective-viewer/src/rust/js/intersection_observer.rs b/rust/perspective-viewer/src/rust/js/intersection_observer.rs new file mode 100644 index 0000000000..05325a3893 --- /dev/null +++ b/rust/perspective-viewer/src/rust/js/intersection_observer.rs @@ -0,0 +1,32 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(inline_js = "export const IntersectionObserver = window.IntersectionObserver")] +extern "C" { + pub type IntersectionObserver; + + #[wasm_bindgen(constructor, js_class = "IntersectionObserver")] + pub fn new(callback: &js_sys::Function) -> IntersectionObserver; + + #[wasm_bindgen(method)] + pub fn observe(this: &IntersectionObserver, elem: &web_sys::HtmlElement); + + #[wasm_bindgen(method)] + pub fn unobserve(this: &IntersectionObserver, elem: &web_sys::HtmlElement); + + pub type IntersectionObserverEntry; + + #[wasm_bindgen(method, getter, js_name = "isIntersecting")] + pub fn is_intersecting(this: &IntersectionObserverEntry) -> bool; +} diff --git a/rust/perspective-viewer/src/rust/js/mod.rs b/rust/perspective-viewer/src/rust/js/mod.rs index bd47315abc..26abe24cdd 100644 --- a/rust/perspective-viewer/src/rust/js/mod.rs +++ b/rust/perspective-viewer/src/rust/js/mod.rs @@ -16,6 +16,7 @@ mod clipboard; pub mod clipboard_item; +mod intersection_observer; mod mimetype; pub mod perspective; pub mod plugin; @@ -26,6 +27,7 @@ mod testing; mod tests; pub use self::clipboard::*; +pub use self::intersection_observer::*; pub use self::mimetype::*; pub use self::perspective::*; pub use self::plugin::*; diff --git a/rust/perspective-viewer/src/rust/model/intersection_observer.rs b/rust/perspective-viewer/src/rust/model/intersection_observer.rs new file mode 100644 index 0000000000..b0a6f3f8e2 --- /dev/null +++ b/rust/perspective-viewer/src/rust/model/intersection_observer.rs @@ -0,0 +1,83 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use web_sys::*; + +use crate::config::*; +use crate::js::*; +use crate::model::*; +use crate::renderer::*; +use crate::session::Session; +use crate::utils::*; +use crate::*; + +pub struct IntersectionObserverHandle { + elem: HtmlElement, + observer: IntersectionObserver, + _callback: Closure, +} + +impl IntersectionObserverHandle { + pub fn new(elem: &HtmlElement, session: &Session, renderer: &Renderer) -> Self { + clone!(session, renderer); + let _callback = (move |xs: js_sys::Array| { + let intersect = xs + .get(0) + .unchecked_into::() + .is_intersecting(); + + clone!(session, renderer); + let state = IntersectionObserverState { session, renderer }; + ApiFuture::spawn(state.set_pause(intersect)); + }) + .into_closure_mut(); + + let func = _callback.as_ref().unchecked_ref::(); + let observer = IntersectionObserver::new(func); + observer.observe(elem); + Self { + elem: elem.clone(), + _callback, + observer, + } + } +} + +impl Drop for IntersectionObserverHandle { + fn drop(&mut self) { + self.observer.unobserve(&self.elem); + } +} + +struct IntersectionObserverState { + session: Session, + renderer: Renderer, +} + +impl IntersectionObserverState { + async fn set_pause(self, intersect: bool) -> ApiResult<()> { + if intersect { + if self.session.set_pause(false) { + tracing::error!("Shellac-ed"); + self.update_and_render(ViewConfigUpdate::default()).await?; + } + } else { + self.session.set_pause(true); + }; + + Ok(()) + } +} + +derive_model!(Renderer, Session for IntersectionObserverState); diff --git a/rust/perspective-viewer/src/rust/model/mod.rs b/rust/perspective-viewer/src/rust/model/mod.rs index 1b27f05e80..0c7b259162 100644 --- a/rust/perspective-viewer/src/rust/model/mod.rs +++ b/rust/perspective-viewer/src/rust/model/mod.rs @@ -70,7 +70,9 @@ mod copy_export; mod export_app; mod export_method; mod get_viewer_config; +mod intersection_observer; mod plugin_config; +mod resize_observer; mod structural; mod update_and_render; @@ -78,6 +80,8 @@ pub use self::columns_iter_set::*; pub use self::copy_export::*; pub use self::export_method::*; pub use self::get_viewer_config::*; +pub use self::intersection_observer::*; pub use self::plugin_config::*; +pub use self::resize_observer::*; pub use self::structural::*; pub use self::update_and_render::*; diff --git a/rust/perspective-viewer/src/rust/model/resize_observer.rs b/rust/perspective-viewer/src/rust/model/resize_observer.rs new file mode 100644 index 0000000000..04001326c7 --- /dev/null +++ b/rust/perspective-viewer/src/rust/model/resize_observer.rs @@ -0,0 +1,98 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use web_sys::*; +use yew::prelude::*; + +use crate::components::viewer::{PerspectiveViewer, PerspectiveViewerMsg}; +use crate::js::*; +use crate::renderer::*; +use crate::utils::*; +use crate::*; + +pub struct ResizeObserverHandle { + elem: HtmlElement, + observer: ResizeObserver, + _callback: Closure, +} + +impl ResizeObserverHandle { + pub fn new( + elem: &HtmlElement, + renderer: &Renderer, + root: &AppHandle, + ) -> Self { + let on_resize = root.callback(|()| PerspectiveViewerMsg::Resize); + let mut state = ResizeObserverState { + elem: elem.clone(), + renderer: renderer.clone(), + width: elem.offset_width(), + height: elem.offset_height(), + on_resize, + }; + + let _callback = (move |xs| state.on_resize(&xs)).into_closure_mut(); + let func = _callback.as_ref().unchecked_ref::(); + let observer = ResizeObserver::new(func); + observer.observe(elem); + Self { + elem: elem.clone(), + _callback, + observer, + } + } +} + +impl Drop for ResizeObserverHandle { + fn drop(&mut self) { + self.observer.unobserve(&self.elem); + } +} + +struct ResizeObserverState { + elem: HtmlElement, + renderer: Renderer, + width: i32, + height: i32, + on_resize: Callback<()>, +} + +impl ResizeObserverState { + fn on_resize(&mut self, entries: &js_sys::Array) { + let is_visible = self + .elem + .offset_parent() + .map(|x| !x.is_null()) + .unwrap_or(false); + + for y in entries.iter() { + let entry: ResizeObserverEntry = y.unchecked_into(); + let content = entry.content_rect(); + let content_width = content.width().floor() as i32; + let content_height = content.height().floor() as i32; + let resized = self.width != content_width || self.height != content_height; + if resized && is_visible { + clone!(self.on_resize, self.renderer); + ApiFuture::spawn(async move { + renderer.resize().await?; + on_resize.emit(()); + Ok(()) + }); + } + + self.width = content_width; + self.height = content_height; + } + } +} diff --git a/rust/perspective-viewer/src/rust/session.rs b/rust/perspective-viewer/src/rust/session.rs index a806df2496..907cfb86b3 100644 --- a/rust/perspective-viewer/src/rust/session.rs +++ b/rust/perspective-viewer/src/rust/session.rs @@ -67,6 +67,7 @@ pub struct SessionData { view_sub: Option, stats: Option, is_clean: bool, + is_paused: bool, } impl Deref for Session { @@ -144,6 +145,20 @@ impl Session { Ok(JsValue::UNDEFINED) } + pub fn set_pause(&self, pause: bool) -> bool { + self.borrow_mut().is_clean = false; + if pause == self.borrow().is_paused { + false + } else if pause { + self.borrow_mut().view_sub = None; + self.borrow_mut().is_paused = true; + true + } else { + self.borrow_mut().is_paused = false; + true + } + } + pub async fn await_table(&self) -> ApiResult<()> { if self.js_get_table().is_none() { self.table_loaded.listen_once().await?; @@ -477,7 +492,7 @@ impl<'a> ValidSession<'a> { /// the original `&Session`. pub async fn create_view(&self) -> Result<&'a Session, ApiError> { let js_config = self.0.borrow().config.as_jsvalue()?; - if !self.0.reset_clean() { + if !self.0.reset_clean() && !self.0.borrow().is_paused { let table = self .0 .borrow() diff --git a/rust/perspective-viewer/src/ts/viewer.ts b/rust/perspective-viewer/src/ts/viewer.ts index 94dad2831c..a120cfd9d9 100644 --- a/rust/perspective-viewer/src/ts/viewer.ts +++ b/rust/perspective-viewer/src/ts/viewer.ts @@ -109,6 +109,23 @@ export interface IPerspectiveViewerElement { */ setAutoSize(autosize): void; + /** + * Determines the auto-pause behavior. When `true` (default `false`), this + * element will enter paused state (deleting it's `View` and ignoring + * render calls) whenever it is not visible in the browser's viewport, + * utilizing an `IntersectionObserver`. + * + * @category Util + * @param autopause Whether to re-render when this element's dimensions + * change. + * @example Disable auto-size + * + * ```javascript + * await viewer.setAutoPause(true); + * ``` + */ + setAutoPause(autopause): void; + /** * Returns the `perspective.Table()` which was supplied to `load()` *