diff --git a/docs/js/index.js b/docs/js/index.js index 48b19b0b1f..3df2fb4f95 100644 --- a/docs/js/index.js +++ b/docs/js/index.js @@ -244,7 +244,7 @@ window.addEventListener("DOMContentLoaded", async function() { container.style.display = "flex"; await psp1.restore(config_defaults(EXAMPLES[key].config)); await psp1.toggleConfig(true); - await psp1.notifyResize(); + await psp1.notifyResize(true); container.style.opacity = 1; container.style.pointerEvents = ""; }); diff --git a/packages/perspective-jupyterlab/src/js/psp_widget.js b/packages/perspective-jupyterlab/src/js/psp_widget.js index 6dda81070d..2ddfc4efca 100644 --- a/packages/perspective-jupyterlab/src/js/psp_widget.js +++ b/packages/perspective-jupyterlab/src/js/psp_widget.js @@ -86,20 +86,10 @@ export class PerspectiveWidget extends Widget { */ onAfterShow(msg) { - this.notifyResize(); + this.viewer.notifyResize(true); super.onAfterShow(msg); } - /** - * Lumino: widget resize - * - */ - - onResize(msg) { - this.notifyResize(); - super.onResize(msg); - } - onActivateRequest(msg) { if (this.isAttached) { this.viewer.focus(); @@ -107,10 +97,6 @@ export class PerspectiveWidget extends Widget { super.onActivateRequest(msg); } - async notifyResize() { - await this.viewer.notifyResize(); - } - async toggleConfig() { await this.viewer.toggleConfig(); } @@ -332,20 +318,7 @@ export class PerspectiveWidget extends Widget { div.style.setProperty("display", "flex"); div.style.setProperty("flex-direction", "row"); node.appendChild(div); - if (!viewer.notifyResize) { - console.warn("Warning: not bound to real element"); - } else { - const resize_observer = new MutationObserver((mutations) => { - if (mutations.some((x) => x.attributeName === "style")) { - viewer.notifyResize.call(viewer); - } - }); - - resize_observer.observe(node, { - attributes: true, - }); - viewer.toggleConfig(); - } + viewer.toggleConfig(true); return viewer; } diff --git a/packages/perspective-jupyterlab/test/js/resize.spec.js b/packages/perspective-jupyterlab/test/js/resize.spec.js index 33eae27acd..c3a7b13588 100644 --- a/packages/perspective-jupyterlab/test/js/resize.spec.js +++ b/packages/perspective-jupyterlab/test/js/resize.spec.js @@ -41,7 +41,7 @@ utils.with_server({}, () => { "position:absolute;top:0;left:0;width:300px;height:300px"; await document .querySelector("perspective-viewer") - .notifyResize(); + .notifyResize(true); }); return await page.evaluate(async () => { @@ -49,7 +49,7 @@ utils.with_server({}, () => { "position:absolute;top:0;left:0;width:800px;height:600px"; await document .querySelector("perspective-viewer") - .notifyResize(); + .notifyResize(true); return window.__WIDGET__.viewer.innerHTML; }); } diff --git a/packages/perspective-workspace/src/js/workspace/tabbar.js b/packages/perspective-workspace/src/js/workspace/tabbar.js index 7c734002db..c78ad4a31d 100644 --- a/packages/perspective-workspace/src/js/workspace/tabbar.js +++ b/packages/perspective-workspace/src/js/workspace/tabbar.js @@ -168,7 +168,7 @@ export class PerspectiveTabBar extends TabBar { _addEventListeners() { this.tabActivateRequested.connect(() => { - this.currentTitle.owner.notifyResize(); + this.currentTitle.owner.viewer.notifyResize(true); }); this.node.addEventListener("dblclick", this); this.node.addEventListener("contextmenu", this); diff --git a/packages/perspective-workspace/src/js/workspace/widget.js b/packages/perspective-workspace/src/js/workspace/widget.js index b1f9e3fa6f..e5fd5a5cd7 100644 --- a/packages/perspective-workspace/src/js/workspace/widget.js +++ b/packages/perspective-workspace/src/js/workspace/widget.js @@ -148,15 +148,4 @@ export class PerspectiveViewerWidget extends Widget { } await this.viewer.delete(); } - - onResize(msg) { - this.notifyResize(); - super.onResize(msg); - } - - async notifyResize() { - if (this.isVisible) { - await this.viewer.notifyResize(); - } - } } diff --git a/packages/perspective-workspace/src/js/workspace/workspace.js b/packages/perspective-workspace/src/js/workspace/workspace.js index fd14dc3c58..f17ae8fcd3 100644 --- a/packages/perspective-workspace/src/js/workspace/workspace.js +++ b/packages/perspective-workspace/src/js/workspace/workspace.js @@ -472,7 +472,6 @@ export class PerspectiveWorkspace extends DiscreteSplitPanel { this._maximizedWidget = widget; this.dockpanel.mode = "single-document"; this.dockpanel.activateWidget(widget); - widget.notifyResize(); } _unmaximize() { diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index 48812a11a0..0fc1353520 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -12,6 +12,7 @@ use crate::custom_elements::expression_editor::ExpressionEditorElement; use crate::dragdrop::*; use crate::js::perspective::*; use crate::js::plugin::JsPerspectiveViewerPlugin; +use crate::js::resize_observer::*; use crate::renderer::*; use crate::session::Session; use crate::utils::*; @@ -35,6 +36,73 @@ use wasm_bindgen_futures::JsFuture; use web_sys::*; use yew::prelude::*; +struct ResizeObserverHandle { + elem: HtmlElement, + observer: ResizeObserver, + _callback: Closure, +} + +impl ResizeObserverHandle { + fn new(elem: &HtmlElement, renderer: &Renderer) -> ResizeObserverHandle { + let mut state = ResizeObserverState { + elem: elem.clone(), + renderer: renderer.clone(), + width: elem.offset_width(), + height: elem.offset_height(), + }; + + 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); + ResizeObserverHandle { + 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, +} + +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 { + let renderer = self.renderer.clone(); + let _ = promisify_ignore_view_delete( + async move { renderer.resize().await }, + ); + } + + self.width = content_width; + self.height = content_height; + } + } +} + /// A `customElements` external API. #[wasm_bindgen] #[derive(Clone)] @@ -45,6 +113,7 @@ pub struct PerspectiveViewerElement { renderer: Renderer, subscriptions: Rc<[Subscription; 4]>, expression_editor: Rc>>, + resize_handle: Rc>>, } #[wasm_bindgen] @@ -105,6 +174,7 @@ impl PerspectiveViewerElement { renderer.on_limits_changed.add_listener(callback) }; + let resize_handle = ResizeObserverHandle::new(&elem, &renderer); PerspectiveViewerElement { elem, root, @@ -112,6 +182,7 @@ impl PerspectiveViewerElement { renderer, expression_editor: Rc::new(RefCell::new(None)), subscriptions: Rc::new([plugin_sub, update_sub, limit_sub, view_sub]), + resize_handle: Rc::new(RefCell::new(Some(resize_handle))), } } @@ -245,6 +316,7 @@ impl PerspectiveViewerElement { session .set_update_column_defaults(&mut view_config, &renderer.metadata()); } + session.update_view_config(view_config); let settings = Some(settings.clone()); let draw_task = renderer.draw(async { @@ -367,11 +439,33 @@ impl PerspectiveViewerElement { } /// Recalculate the viewer's dimensions and redraw. - pub fn js_resize(&self) -> js_sys::Promise { + pub fn js_resize(&self, force: bool) -> js_sys::Promise { + if !force && self.resize_handle.borrow().is_some() { + let msg: JsValue = "`notifyResize(false)` called, disabling auto-size. It can be re-enabled with `setAutoSize(true)`.".into(); + web_sys::console::warn_1(&msg); + *self.resize_handle.borrow_mut() = None; + } + let renderer = self.renderer.clone(); promisify_ignore_view_delete(async move { renderer.resize().await }) } + /// 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. + pub fn js_set_auto_size(&mut self, autosize: bool) { + if autosize { + let handle = Some(ResizeObserverHandle::new(&self.elem, &self.renderer)); + *self.resize_handle.borrow_mut() = handle; + } else { + *self.resize_handle.borrow_mut() = None; + } + } + /// Get this viewer's edit port for the currently loaded `Table`. pub fn js_get_edit_port(&self) -> Result { self.session diff --git a/rust/perspective-viewer/src/rust/js/mod.rs b/rust/perspective-viewer/src/rust/js/mod.rs index 7c8e968125..f61104db58 100644 --- a/rust/perspective-viewer/src/rust/js/mod.rs +++ b/rust/perspective-viewer/src/rust/js/mod.rs @@ -10,3 +10,4 @@ pub mod monaco; pub mod perspective; // pub mod perspective_viewer; pub mod plugin; +pub mod resize_observer; diff --git a/rust/perspective-viewer/src/rust/js/resize_observer.rs b/rust/perspective-viewer/src/rust/js/resize_observer.rs new file mode 100644 index 0000000000..d4642d735e --- /dev/null +++ b/rust/perspective-viewer/src/rust/js/resize_observer.rs @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2018, the Perspective Authors. +// +// This file is part of the Perspective library, distributed under the terms +// of the Apache License 2.0. The full license can be found in the LICENSE +// file. + +// `rustfmt` removes `async` from extern blocks in rust stable +// [issue](https://github.com/rust-lang/rustfmt/issues/4288) + +use wasm_bindgen::prelude::*; +// use web_sys::HtmlElement; + +#[wasm_bindgen(inline_js = "export default ResizeObserver")] +extern "C" { + pub type ResizeObserver; + + #[wasm_bindgen(constructor, js_class = "default")] + pub fn new(callback: &js_sys::Function) -> ResizeObserver; + + #[wasm_bindgen(method)] + pub fn observe(this: &ResizeObserver, elem: &web_sys::HtmlElement); + + #[wasm_bindgen(method)] + pub fn unobserve(this: &ResizeObserver, elem: &web_sys::HtmlElement); + + pub type ResizeObserverEntry; + + #[wasm_bindgen(method, getter, js_name = "contentRect")] + pub fn content_rect(this: &ResizeObserverEntry) -> web_sys::DomRectReadOnly; + +} diff --git a/rust/perspective-viewer/src/rust/renderer.rs b/rust/perspective-viewer/src/rust/renderer.rs index 38bd333b9a..29e9a5ba13 100644 --- a/rust/perspective-viewer/src/rust/renderer.rs +++ b/rust/perspective-viewer/src/rust/renderer.rs @@ -212,8 +212,10 @@ impl Renderer { pub async fn resize(&self) -> Result { let draw_mutex = self.draw_lock(); + let timer = self.render_timer(); draw_mutex .debounce(async { + set_timeout(timer.get_avg()).await?; let jsplugin = self.get_active_plugin()?; jsplugin.resize().await?; Ok(JsValue::from(true)) diff --git a/rust/perspective-viewer/src/ts/viewer.ts b/rust/perspective-viewer/src/ts/viewer.ts index 6d4490d618..3ced62fb88 100644 --- a/rust/perspective-viewer/src/ts/viewer.ts +++ b/rust/perspective-viewer/src/ts/viewer.ts @@ -121,24 +121,49 @@ export class PerspectiveViewerElement extends HTMLElement { /** * Redraw this `` and plugin when its dimensions or - * visibility have been updated. This method _must_ be called in these - * cases, and will not by default respond to dimension or style changes to - * its parent container. `notifyResize()` does not recalculate the current - * `View`, but all plugins will re-request the data window (which itself - * may be smaller or larger due to resize). + * visibility has been updated. By default, `` will + * auto-size when its own dimensions change, so this method need not be + * called; when disabled via `setAutoSize(false)` however, this method + * _must_ be called, and will not respond to dimension or style changes to + * its parent container otherwise. `notifyResize()` does not recalculate + * the current `View`, but all plugins will re-request the data window + * (which itself may be smaller or larger due to resize). * * @category Util + * @param force Whether to re-render, even if the dimenions have not + * changed. When set to `false` and auto-size is enabled (the defaults), + * calling this method will automatically disable auto-size. * @returns A `Promise` which resolves when this resize event has * finished rendering. * @example Bind `notfyResize()` to browser dimensions * ```javascript * const viewer = document.querySelector("perspective-viewer"); + * viewer.setAutoSize(false); * window.addEventListener("resize", () => viewer.notifyResize()); * ``` */ - async notifyResize(): Promise { + async notifyResize(force = false): Promise { await this.load_wasm(); - await this.instance.js_resize(); + await this.instance.js_resize(force); + } + + /** + * Determines the auto-size behavior. When `true` (the default), this + * element will re-render itself whenever its own dimensions change, + * utilizing a `ResizeObserver`; when `false`, you must explicitly call + * `notifyResize()` when the element's dimensions have changed. + * + * @category Util + * @param autosize Whether to re-render when this element's dimensions + * change. + * @example Disable auto-size + * ```javascript + * await viewer.setAutoSize(false); + * ``` + */ + async setAutoSize(autosize = true): Promise { + await this.load_wasm(); + await this.instance.js_set_auto_size(autosize); } /** diff --git a/rust/perspective-viewer/test/js/simple_tests.js b/rust/perspective-viewer/test/js/simple_tests.js index c6349fab77..2bcc6ac394 100644 --- a/rust/perspective-viewer/test/js/simple_tests.js +++ b/rust/perspective-viewer/test/js/simple_tests.js @@ -96,7 +96,8 @@ exports.default = function (get_contents = get_contents_default) { column_pivots: ["Category", "Sub-Category"], settings: true, }); - await viewer.notifyResize(); + + await viewer.notifyResize(true); }); return await get_contents(page); diff --git a/tools/perspective-test/src/js/index.js b/tools/perspective-test/src/js/index.js index 3daab9d72c..e139202854 100644 --- a/tools/perspective-test/src/js/index.js +++ b/tools/perspective-test/src/js/index.js @@ -469,8 +469,7 @@ test.capture = function capture( const viewer = document.querySelector("perspective-viewer"); if (viewer) { - viewer.restore(x); - await viewer.notifyResize?.(); + await viewer.restore(x); await viewer.toggleConfig?.(false); } }