+
{ ctx.props().children.iter().nth(self.selected_idx) }
>
diff --git a/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs b/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs
new file mode 100644
index 0000000000..15a8ec7f15
--- /dev/null
+++ b/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs
@@ -0,0 +1,55 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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 yew::*;
+
+use crate::clone;
+
+#[derive(Properties)]
+pub struct TrapDoorPanelProps {
+ pub id: Option<&'static str>,
+ pub class: Option<&'static str>,
+ pub children: Children,
+}
+
+impl PartialEq for TrapDoorPanelProps {
+ fn eq(&self, _other: &Self) -> bool {
+ false
+ }
+}
+
+/// A simple panel with an invisible inner `
` which stretches to fit the
+/// width of the container, but will not shrink (unless the state is reset).
+#[function_component(TrapDoorPanel)]
+pub fn trap_door_panel(props: &TrapDoorPanelProps) -> Html {
+ let sizer = use_node_ref();
+ let width = use_state_eq(|| 0.0);
+ use_effect({
+ clone!(width, sizer);
+ move || {
+ width.set(
+ sizer
+ .cast::
()
+ .unwrap()
+ .get_bounding_client_rect()
+ .width(),
+ )
+ }
+ });
+
+ html! {
+
+ { props.children.clone() }
+
+
+ }
+}
diff --git a/rust/perspective-viewer/src/rust/components/form/debug.rs b/rust/perspective-viewer/src/rust/components/form/debug.rs
new file mode 100644
index 0000000000..61390a52f0
--- /dev/null
+++ b/rust/perspective-viewer/src/rust/components/form/debug.rs
@@ -0,0 +1,237 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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 std::rc::Rc;
+
+use yew::prelude::*;
+
+use self::js::PerspectiveValidationError;
+use crate::components::containers::trap_door_panel::TrapDoorPanel;
+use crate::components::form::code_editor::CodeEditor;
+use crate::components::style::LocalStyle;
+use crate::js::{copy_to_clipboard, paste_from_clipboard, MimeType};
+use crate::model::*;
+use crate::presentation::*;
+use crate::renderer::*;
+use crate::session::*;
+use crate::utils::*;
+use crate::*;
+
+#[derive(Properties, Clone, PartialEq)]
+pub struct DebugPanelProps {
+ pub session: Session,
+ pub renderer: Renderer,
+ pub presentation: Presentation,
+}
+
+derive_model!(Presentation, Renderer, Session for DebugPanelProps);
+
+impl DebugPanelProps {
+ fn set_text(&self, setter: UseStateSetter>) {
+ let props = self.clone();
+ ApiFuture::spawn(async move {
+ let task = props.get_viewer_config();
+ let config = task.await?;
+ let json = JsValue::from_serde_ext(&config)?;
+ let js_string =
+ js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?;
+
+ setter.set(Rc::new(js_string.as_string().unwrap()));
+ Ok(())
+ });
+ }
+
+ fn reset_callback(
+ &self,
+ text: UseStateSetter>,
+ error: UseStateSetter>,
+ modified: UseStateSetter,
+ ) -> impl Fn(()) {
+ let props = self.clone();
+ move |_| {
+ error.set(None);
+ props.set_text(text.clone());
+ modified.set(false);
+ }
+ }
+}
+
+fn on_save(
+ props: &DebugPanelProps,
+ text: &Rc,
+ error: &UseStateHandle>,
+ modified: &UseStateHandle,
+) {
+ clone!(props, text, error, modified);
+ ApiFuture::spawn(async move {
+ match serde_json::from_str(&text) {
+ Ok(config) => {
+ match props.restore_and_render(config, async { Ok(()) }).await {
+ Ok(_) => {
+ modified.set(false);
+ },
+ Err(e) => {
+ modified.set(true);
+ error.set(Some(PerspectiveValidationError {
+ error_message: JsValue::from(e)
+ .as_string()
+ .unwrap_or_else(|| "Failed to validate viewer config".to_owned()),
+ line: 0_i32,
+ column: 0,
+ }));
+ },
+ }
+ Ok(())
+ },
+ Err(err) => {
+ modified.set(true);
+ error.set(Some(PerspectiveValidationError {
+ error_message: err.to_string(),
+ line: err.line() as i32 - 1,
+ column: err.column() as i32 - 1,
+ }));
+
+ Ok(())
+ },
+ }
+ });
+}
+
+#[function_component(DebugPanel)]
+pub fn debug_panel(props: &DebugPanelProps) -> Html {
+ let expr = use_state_eq(|| Rc::new("".to_string()));
+ let error = use_state_eq(|| Option::::None);
+ let select_all = use_memo((), |()| PubSub::default());
+ let modified = use_state_eq(|| false);
+ use_effect_with((expr.setter(), props.clone()), {
+ clone!(error, modified);
+ move |(text, props)| {
+ props.set_text(text.clone());
+ error.set(None);
+ let sub1 = props.renderer().style_changed.add_listener({
+ props.reset_callback(text.clone(), error.setter(), modified.setter())
+ });
+
+ let sub2 = props.renderer().reset_changed.add_listener({
+ props.reset_callback(text.clone(), error.setter(), modified.setter())
+ });
+
+ let sub3 = props.session().view_config_changed.add_listener({
+ props.reset_callback(text.clone(), error.setter(), modified.setter())
+ });
+
+ || {
+ drop(sub1);
+ drop(sub2);
+ drop(sub3);
+ }
+ }
+ });
+
+ let oninput = use_callback(expr.setter(), {
+ clone!(modified);
+ move |x, expr| {
+ modified.set(true);
+ expr.set(x)
+ }
+ });
+
+ let onsave = use_callback((expr.clone(), error.clone(), props.clone()), {
+ clone!(modified);
+ move |_, (text, error, props)| on_save(props, text, error, &modified)
+ });
+
+ let oncopy = use_callback(
+ (expr.clone(), select_all.callback()),
+ move |_, (text, select_all)| {
+ select_all.emit(());
+ let mut options = web_sys::BlobPropertyBag::new();
+ options.type_("text/plain");
+ let blob_txt = (JsValue::from((***text).clone())).clone();
+ let blob_parts = js_sys::Array::from_iter([blob_txt].iter());
+ let blob = web_sys::Blob::new_with_str_sequence_and_options(&blob_parts, &options);
+ ApiFuture::spawn(copy_to_clipboard(
+ async move { Ok(blob?) },
+ MimeType::TextPlain,
+ ));
+ },
+ );
+
+ let onapply = use_callback((expr.clone(), error.clone(), props.clone()), {
+ clone!(modified);
+ move |_, (text, error, props)| on_save(props, text, error, &modified)
+ });
+
+ let onreset = use_callback((expr.setter(), error.clone(), props.clone()), {
+ clone!(modified);
+ move |_, (text, error, props)| {
+ props.set_text(text.clone());
+ error.set(None);
+ modified.set(false);
+ }
+ });
+
+ let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), {
+ clone!(modified);
+ move |_, (text, error, props)| {
+ clone!(text, error, props, modified);
+ ApiFuture::spawn(async move {
+ if let Some(x) = paste_from_clipboard().await {
+ let x = Rc::new(x);
+ modified.set(true);
+ error.set(None);
+ text.set(x.clone());
+ on_save(&props, &x, &error, &modified);
+ }
+
+ Ok(())
+ });
+ }
+ });
+
+ html! {
+ <>
+
+
+
+
+
+
+
+
+
+ { "Apply" }
+ { "Reset" }
+ { "Copy" }
+ { "Paste" }
+
+
+
+ >
+ }
+}
diff --git a/rust/perspective-viewer/src/rust/components/form/mod.rs b/rust/perspective-viewer/src/rust/components/form/mod.rs
index 1e5c7bd6c2..cdf35a7177 100644
--- a/rust/perspective-viewer/src/rust/components/form/mod.rs
+++ b/rust/perspective-viewer/src/rust/components/form/mod.rs
@@ -18,6 +18,7 @@
pub mod code_editor;
pub mod color_range_selector;
pub mod color_selector;
+pub mod debug;
mod highlight;
pub mod number_field;
pub mod number_input;
diff --git a/rust/perspective-viewer/src/rust/components/viewer.rs b/rust/perspective-viewer/src/rust/components/viewer.rs
index 8d76328978..e7bb4fa0be 100644
--- a/rust/perspective-viewer/src/rust/components/viewer.rs
+++ b/rust/perspective-viewer/src/rust/components/viewer.rs
@@ -19,6 +19,7 @@ use yew::prelude::*;
use super::column_selector::ColumnSelector;
use super::containers::split_panel::SplitPanel;
use super::font_loader::{FontLoader, FontLoaderProps, FontLoaderStatus};
+use super::form::debug::DebugPanel;
use super::plugin_selector::PluginSelector;
use super::render_warning::RenderWarning;
use super::status_bar::StatusBar;
@@ -125,6 +126,7 @@ pub enum PerspectiveViewerMsg {
Reset(bool, Option>),
ToggleSettingsInit(Option, Option>>),
ToggleSettingsComplete(SettingsUpdate, Sender<()>),
+ ToggleDebug,
PreloadFontsUpdate,
RenderLimits(Option<(usize, usize, Option, Option)>),
SettingsPanelSizeUpdate(Option),
@@ -141,6 +143,7 @@ pub struct PerspectiveViewer {
on_rendered: Option>,
fonts: FontLoaderProps,
settings_open: bool,
+ debug_open: bool,
/// The column which will be opened in the ColumnSettingsSidebar
selected_column: Option,
selected_column_is_active: bool, // TODO: should we use a struct?
@@ -217,6 +220,7 @@ impl Component for PerspectiveViewer {
on_rendered: None,
fonts: FontLoaderProps::new(&elem, callback),
settings_open: false,
+ debug_open: false,
selected_column: None,
selected_column_is_active: false,
on_resize: Default::default(),
@@ -266,6 +270,15 @@ impl Component for PerspectiveViewer {
needs_update
},
+ PerspectiveViewerMsg::ToggleDebug => {
+ self.debug_open = !self.debug_open;
+ clone!(ctx.props().renderer, ctx.props().session);
+ ApiFuture::spawn(async move {
+ renderer.draw(session.validate().await?.create_view()).await
+ });
+
+ true
+ },
PerspectiveViewerMsg::ToggleSettingsInit(Some(SettingsUpdate::Missing), None) => false,
PerspectiveViewerMsg::ToggleSettingsInit(
Some(SettingsUpdate::Missing),
@@ -335,8 +348,8 @@ impl Component for PerspectiveViewer {
.as_ref()
.map(|l| l.is_active(&ctx.props().session))
.unwrap_or_default();
- self.selected_column_is_active = is_active;
+ self.selected_column_is_active = is_active;
if toggle && self.selected_column == locator {
self.selected_column = None;
(false, None)
@@ -415,6 +428,7 @@ impl Component for PerspectiveViewer {
.link()
.callback(|()| PerspectiveViewerMsg::ToggleSettingsInit(None, None));
+ let on_toggle_debug = ctx.link().callback(|_| PerspectiveViewerMsg::ToggleDebug);
let mut class = classes!("settings-closed");
if ctx.props().is_title() {
class.push("titled");
@@ -448,6 +462,10 @@ impl Component for PerspectiveViewer {
on_close_sidebar={&on_close_settings}
/>
}
+
if self.settings_open {
-
- { settings_panel }
- { main_panel }
-
+ if self.debug_open {
+
+
+ { settings_panel }
+ { main_panel }
+
+ } else {
+
+ { settings_panel }
+ { main_panel }
+
+ }
} else {
ApiFuture<()> {
tracing::info!("Restoring ViewerConfig");
global::document().blur_active_element();
- clone!(self.session, self.renderer, self.root, self.presentation);
+ let this = self.clone();
ApiFuture::new(async move {
let decoded_update = ViewerConfigUpdate::decode(&update)?;
-
- let ViewerConfigUpdate {
- plugin,
- plugin_config,
- columns_config,
- settings,
- theme: theme_name,
- title,
- mut view_config,
- ..//version
- } = decoded_update;
-
- if !session.has_table() {
- if let OptionalUpdate::Update(x) = settings {
- presentation.set_settings_attribute(x);
- }
- }
-
- if let OptionalUpdate::Update(title) = title {
- presentation.set_title(Some(title));
- } else if matches!(title, OptionalUpdate::SetDefault) {
- presentation.set_title(None);
- }
-
- let needs_restyle = match theme_name {
- OptionalUpdate::SetDefault => {
- let current_name = presentation.get_selected_theme_name().await;
- if current_name.is_some() {
- presentation.set_theme_name(None).await?;
- true
- } else {
- false
- }
- },
- OptionalUpdate::Update(x) => {
- let current_name = presentation.get_selected_theme_name().await;
- if current_name.is_some() && current_name.as_ref().unwrap() != &x {
- presentation.set_theme_name(Some(&x)).await?;
- true
- } else {
- false
- }
- },
- _ => false,
- };
-
- let plugin_changed = renderer.update_plugin(&plugin)?;
- if plugin_changed {
- session.set_update_column_defaults(&mut view_config, &renderer.metadata());
- }
-
- session.update_view_config(view_config);
- let draw_task = renderer.draw(async {
- let task = root
+ let settings = decoded_update.settings.clone();
+ let root = this.root.clone();
+ this.restore_and_render(decoded_update, async move {
+ let result = root
.borrow()
.as_ref()
- .ok_or("Already deleted")?
+ .into_apierror()?
.send_message_async(move |x| {
PerspectiveViewerMsg::ToggleSettingsComplete(settings, x)
});
- let internal_task = async {
- let plugin = renderer.get_active_plugin()?;
- let plugin_update = if let Some(x) = plugin_config {
- JsValue::from_serde_ext(&x).unwrap()
- } else {
- plugin.save()
- };
- presentation.update_columns_configs(columns_config);
- let columns_config = presentation.all_columns_configs();
- plugin.restore(&plugin_update, Some(&columns_config));
- session.validate().await?.create_view().await
- }
- .await;
-
- task.await?;
- internal_task
- });
-
- draw_task.await?;
-
- // TODO this should be part of the API for `draw()` above, such that
- // the plugin need not render twice when a theme is provided.
- if needs_restyle {
- let view = session.get_view().into_apierror()?;
- renderer.restyle_all(&view).await?;
- }
-
+ Ok(result.await?)
+ })
+ .await?;
Ok(())
})
}
diff --git a/rust/perspective-viewer/src/rust/js/clipboard.rs b/rust/perspective-viewer/src/rust/js/clipboard.rs
index cf6f526571..5dff13f08a 100644
--- a/rust/perspective-viewer/src/rust/js/clipboard.rs
+++ b/rust/perspective-viewer/src/rust/js/clipboard.rs
@@ -16,12 +16,20 @@ use std::rc::Rc;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
+use wasm_bindgen_futures::JsFuture;
use super::mimetype::*;
use crate::js::clipboard_item::*;
use crate::utils::*;
use crate::*;
+pub async fn paste_from_clipboard() -> Option {
+ JsFuture::from(global::clipboard().read_text())
+ .await
+ .ok()
+ .and_then(|x| x.as_string())
+}
+
/// Copy a `JsPerspectiveView` to the clipboard as a CSV.
pub fn copy_to_clipboard(
view: impl Future>,
diff --git a/rust/perspective-viewer/src/rust/model/mod.rs b/rust/perspective-viewer/src/rust/model/mod.rs
index 59b2a3ea30..81f3c4652c 100644
--- a/rust/perspective-viewer/src/rust/model/mod.rs
+++ b/rust/perspective-viewer/src/rust/model/mod.rs
@@ -75,6 +75,7 @@ mod intersection_observer;
mod is_invalid_drop;
mod plugin_column_styles;
mod resize_observer;
+mod restore_and_render;
mod structural;
mod update_and_render;
@@ -87,5 +88,6 @@ pub use self::intersection_observer::*;
pub use self::is_invalid_drop::*;
pub use self::plugin_column_styles::*;
pub use self::resize_observer::*;
+pub use self::restore_and_render::*;
pub use self::structural::*;
pub use self::update_and_render::*;
diff --git a/rust/perspective-viewer/src/rust/model/restore_and_render.rs b/rust/perspective-viewer/src/rust/model/restore_and_render.rs
new file mode 100644
index 0000000000..388deb91d9
--- /dev/null
+++ b/rust/perspective-viewer/src/rust/model/restore_and_render.rs
@@ -0,0 +1,107 @@
+// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
+// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
+// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
+// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
+// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
+// ┃ 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 futures::Future;
+
+use super::structural::*;
+use crate::config::{OptionalUpdate, ViewerConfigUpdate};
+use crate::utils::*;
+use crate::*;
+
+pub trait RestoreAndRender: HasRenderer + HasSession + HasPresentation {
+ /// Apply a `ViewConfigUpdate` to the current `View` and render.
+ fn restore_and_render(
+ &self,
+ ViewerConfigUpdate {
+ plugin,
+ plugin_config,
+ columns_config,
+ settings,
+ theme: theme_name,
+ title,
+ mut view_config,
+ ..
+ }: crate::config::ViewerConfigUpdate,
+ task: impl Future> + 'static,
+ ) -> ApiFuture<()> {
+ clone!(self.session(), self.renderer(), self.presentation());
+ ApiFuture::new(async move {
+ if !session.has_table() {
+ if let OptionalUpdate::Update(x) = settings {
+ presentation.set_settings_attribute(x);
+ }
+ }
+
+ if let OptionalUpdate::Update(title) = title {
+ presentation.set_title(Some(title));
+ } else if matches!(title, OptionalUpdate::SetDefault) {
+ presentation.set_title(None);
+ }
+
+ let needs_restyle = match theme_name {
+ OptionalUpdate::SetDefault => {
+ let current_name = presentation.get_selected_theme_name().await;
+ if current_name.is_some() {
+ presentation.set_theme_name(None).await?;
+ true
+ } else {
+ false
+ }
+ },
+ OptionalUpdate::Update(x) => {
+ let current_name = presentation.get_selected_theme_name().await;
+ if current_name.is_some() && current_name.as_ref().unwrap() != &x {
+ presentation.set_theme_name(Some(&x)).await?;
+ true
+ } else {
+ false
+ }
+ },
+ _ => false,
+ };
+
+ let plugin_changed = renderer.update_plugin(&plugin)?;
+ if plugin_changed {
+ session.set_update_column_defaults(&mut view_config, &renderer.metadata());
+ }
+
+ session.update_view_config(view_config);
+ let draw_task = renderer.draw(async {
+ task.await?;
+ let plugin = renderer.get_active_plugin()?;
+ let plugin_update = if let Some(x) = plugin_config {
+ JsValue::from_serde_ext(&x).unwrap()
+ } else {
+ plugin.save()
+ };
+
+ presentation.update_columns_configs(columns_config);
+ let columns_config = presentation.all_columns_configs();
+ plugin.restore(&plugin_update, Some(&columns_config));
+ session.validate().await?.create_view().await
+ });
+
+ draw_task.await?;
+
+ // TODO this should be part of the API for `draw()` above, such that
+ // the plugin need not render twice when a theme is provided.
+ if needs_restyle {
+ let view = session.get_view().into_apierror()?;
+ renderer.restyle_all(&view).await?;
+ }
+
+ Ok(())
+ })
+ }
+}
+
+impl RestoreAndRender for T {}
diff --git a/rust/perspective-viewer/src/rust/session.rs b/rust/perspective-viewer/src/rust/session.rs
index a2bbf0374d..694444f85b 100644
--- a/rust/perspective-viewer/src/rust/session.rs
+++ b/rust/perspective-viewer/src/rust/session.rs
@@ -181,13 +181,18 @@ impl Session {
self.borrow().config.is_column_expression_in_use(name)
}
+ /// Is this column currently being used or not
pub fn is_column_active(&self, name: &str) -> bool {
- self.borrow().config.columns.iter().any(|maybe_col| {
+ let config = Ref::map(self.borrow(), |x| &x.config);
+ config.columns.iter().any(|maybe_col| {
maybe_col
.as_ref()
.map(|col| col == name)
.unwrap_or_default()
- })
+ }) || config.group_by.iter().any(|col| col == name)
+ || config.split_by.iter().any(|col| col == name)
+ || config.filter.iter().any(|col| col.0 == name)
+ || config.sort.iter().any(|col| col.0 == name)
}
pub fn create_drag_drop_update(
diff --git a/rust/perspective-viewer/src/rust/utils/browser/selection.rs b/rust/perspective-viewer/src/rust/utils/browser/selection.rs
index 50c1c59b43..f4d3e2b878 100644
--- a/rust/perspective-viewer/src/rust/utils/browser/selection.rs
+++ b/rust/perspective-viewer/src/rust/utils/browser/selection.rs
@@ -19,11 +19,22 @@ use crate::*;
/// but `Deref` makes them fall through, so it is important that this method
/// be called on the correct struct type!
pub trait CaretPosition {
+ fn select_all(&self) -> ApiResult<()>;
fn set_caret_position(&self, offset: usize) -> ApiResult<()>;
fn get_caret_position(&self) -> Option;
}
impl CaretPosition for web_sys::HtmlElement {
+ fn select_all(&self) -> ApiResult<()> {
+ let range = global::document().create_range()?;
+ let selection = global::window().get_selection()?.into_apierror()?;
+ range.set_start(self, 0_u32)?;
+ range.set_end(self, 10000000_u32)?;
+ selection.remove_all_ranges()?;
+ selection.add_range(&range)?;
+ Ok(())
+ }
+
fn set_caret_position(&self, offset: usize) -> ApiResult<()> {
let range = global::document().create_range()?;
let selection = global::window().get_selection()?.into_apierror()?;
@@ -50,8 +61,13 @@ impl CaretPosition for web_sys::HtmlElement {
}
impl CaretPosition for web_sys::HtmlTextAreaElement {
+ fn select_all(&self) -> ApiResult<()> {
+ self.set_selection_start(Some(0_u32))?;
+ self.set_selection_end(Some(1000000000_u32))?;
+ Ok(())
+ }
+
fn set_caret_position(&self, offset: usize) -> ApiResult<()> {
- self.focus().unwrap();
self.set_selection_end(Some(offset as u32))?;
self.set_selection_start(Some(offset as u32))?;
Ok(())
diff --git a/rust/perspective-viewer/src/svg/drawer-tab-invert-hover.svg b/rust/perspective-viewer/src/svg/drawer-tab-invert-hover.svg
new file mode 100644
index 0000000000..d950b6f6c9
--- /dev/null
+++ b/rust/perspective-viewer/src/svg/drawer-tab-invert-hover.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/rust/perspective-viewer/src/svg/drawer-tab-invert.svg b/rust/perspective-viewer/src/svg/drawer-tab-invert.svg
new file mode 100644
index 0000000000..5f6026db18
--- /dev/null
+++ b/rust/perspective-viewer/src/svg/drawer-tab-invert.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/rust/perspective-viewer/src/themes/intl.less b/rust/perspective-viewer/src/themes/intl.less
index 8a2d83c8ec..6c88aa7bf1 100644
--- a/rust/perspective-viewer/src/themes/intl.less
+++ b/rust/perspective-viewer/src/themes/intl.less
@@ -94,4 +94,5 @@ perspective-dropdown {
// Tabs
--style-tab-label--content: "Style";
--attributes-tab-label--content: "Attributes";
+ --debug-tab-label--content: "Debug JSON";
}
diff --git a/rust/perspective-viewer/src/themes/intl/de.less b/rust/perspective-viewer/src/themes/intl/de.less
index dc55fe0687..a644d8cee5 100644
--- a/rust/perspective-viewer/src/themes/intl/de.less
+++ b/rust/perspective-viewer/src/themes/intl/de.less
@@ -94,4 +94,5 @@ perspective-dropdown {
// Tabs
--style-tab-label--content: "Stil";
--attributes-tab-label--content: "Attribute";
+ --debug-tab-label--content: "Debug JSON";
}
diff --git a/rust/perspective-viewer/src/themes/intl/es.less b/rust/perspective-viewer/src/themes/intl/es.less
index 1c92088ce2..6ad041837a 100644
--- a/rust/perspective-viewer/src/themes/intl/es.less
+++ b/rust/perspective-viewer/src/themes/intl/es.less
@@ -94,4 +94,5 @@ perspective-dropdown {
// Tabs
--style-tab-label--content: "Estilo";
--attributes-tab-label--content: "Atributos";
+ --debug-tab-label--content: "Debug JSON";
}
diff --git a/rust/perspective-viewer/src/themes/intl/fr.less b/rust/perspective-viewer/src/themes/intl/fr.less
index 36f5472716..0457c23616 100644
--- a/rust/perspective-viewer/src/themes/intl/fr.less
+++ b/rust/perspective-viewer/src/themes/intl/fr.less
@@ -94,4 +94,5 @@ perspective-dropdown {
// Tabs
--style-tab-label--content: "Style";
--attributes-tab-label--content: "Les attributs";
+ --debug-tab-label--content: "Debug JSON";
}
diff --git a/rust/perspective-viewer/src/themes/intl/ja.less b/rust/perspective-viewer/src/themes/intl/ja.less
index 51024d8310..ab5868e622 100644
--- a/rust/perspective-viewer/src/themes/intl/ja.less
+++ b/rust/perspective-viewer/src/themes/intl/ja.less
@@ -94,4 +94,5 @@ perspective-dropdown {
// Tabs
--style-tab-label--content: "スタイル";
--attributes-tab-label--content: "属性";
+ --debug-tab-label--content: "Debug JSON";
}
diff --git a/rust/perspective-viewer/src/themes/intl/pt.less b/rust/perspective-viewer/src/themes/intl/pt.less
index 3f1ee25210..8c0abf287d 100644
--- a/rust/perspective-viewer/src/themes/intl/pt.less
+++ b/rust/perspective-viewer/src/themes/intl/pt.less
@@ -94,4 +94,5 @@ perspective-dropdown {
// Tabs
--style-tab-label--content: "Estilo";
--attributes-tab-label--content: "Atributos";
+ --debug-tab-label--content: "Debug JSON";
}
diff --git a/rust/perspective-viewer/src/themes/intl/zh.less b/rust/perspective-viewer/src/themes/intl/zh.less
index 2030f17bed..014a7609f2 100644
--- a/rust/perspective-viewer/src/themes/intl/zh.less
+++ b/rust/perspective-viewer/src/themes/intl/zh.less
@@ -94,4 +94,5 @@ perspective-dropdown {
// Tabs
--style-tab-label--content: "风格";
--attributes-tab-label--content: "属性";
+ --debug-tab-label--content: "Debug JSON";
}