@perspective-dev/viewer 4.4.1 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/perspective-viewer.js +1 -2
- package/dist/cdn/perspective-viewer.js.map +4 -4
- package/dist/css/botanical.css +1 -1
- package/dist/css/dracula.css +1 -1
- package/dist/css/gruvbox-dark.css +1 -1
- package/dist/css/gruvbox.css +1 -1
- package/dist/css/icons.css +1 -1
- package/dist/css/intl/de.css +1 -1
- package/dist/css/intl/es.css +1 -1
- package/dist/css/intl/fr.css +1 -1
- package/dist/css/intl/ja.css +1 -1
- package/dist/css/intl/pt.css +1 -1
- package/dist/css/intl/zh.css +1 -1
- package/dist/css/intl.css +1 -1
- package/dist/css/monokai.css +1 -1
- package/dist/css/phosphor.css +1 -1
- package/dist/css/pro-dark.css +1 -1
- package/dist/css/pro.css +1 -1
- package/dist/css/solarized-dark.css +1 -1
- package/dist/css/solarized.css +1 -1
- package/dist/css/themes.css +1 -1
- package/dist/css/vaporwave.css +1 -1
- package/dist/esm/bootstrap.d.ts +2 -1
- package/dist/esm/column-format.d.ts +51 -0
- package/dist/esm/extensions.d.ts +2 -0
- package/dist/esm/perspective-viewer.d.ts +3 -1
- package/dist/esm/perspective-viewer.inline.js +1 -2
- package/dist/esm/perspective-viewer.inline.js.map +4 -4
- package/dist/esm/perspective-viewer.js +1 -2
- package/dist/esm/perspective-viewer.js.map +4 -4
- package/dist/esm/perspective-viewer.worker.d.ts +2 -0
- package/dist/esm/plugin.d.ts +16 -72
- package/dist/esm/ts-rs/ColumnSelectMode.d.ts +1 -0
- package/dist/esm/ts-rs/PluginStaticConfig.d.ts +77 -0
- package/dist/esm/ts-rs/ViewerConfig.d.ts +6 -3
- package/dist/esm/ts-rs/ViewerConfigUpdate.d.ts +7 -4
- package/dist/wasm/perspective-viewer.d.ts +77 -18
- package/dist/wasm/perspective-viewer.js +293 -144
- package/dist/wasm/perspective-viewer.wasm +0 -0
- package/dist/wasm/perspective-viewer.wasm.d.ts +20 -15
- package/package.json +24 -2
- package/src/css/column-selector.css +3 -2
- package/src/css/column-settings-panel.css +35 -6
- package/src/css/column-style.css +27 -2
- package/src/css/containers/scroll-panel.css +2 -1
- package/src/css/containers/tabs.css +8 -52
- package/src/css/dom/checkbox.css +0 -4
- package/src/css/form/code-editor.css +1 -0
- package/src/css/form/debug.css +3 -10
- package/src/css/plugin-selector.css +33 -0
- package/src/css/plugin-settings-panel.css +99 -0
- package/src/css/viewer.css +65 -3
- package/src/rust/components/column_dropdown.rs +3 -1
- package/src/rust/components/column_selector/active_column.rs +13 -19
- package/src/rust/components/column_selector/config_selector.rs +20 -20
- package/src/rust/components/column_selector/filter_column.rs +14 -14
- package/src/rust/components/column_selector/inactive_column.rs +9 -15
- package/src/rust/components/column_selector/pivot_column.rs +7 -7
- package/src/rust/components/column_selector/sort_column.rs +7 -7
- package/src/rust/components/column_selector.rs +55 -37
- package/src/rust/components/column_settings_sidebar/style_tab/agg_depth_selector.rs +15 -7
- package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +394 -0
- package/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +15 -6
- package/src/rust/components/column_settings_sidebar/style_tab.rs +267 -136
- package/src/rust/components/column_settings_sidebar.rs +43 -49
- package/src/rust/components/containers/dragdrop_list.rs +5 -5
- package/src/rust/components/containers/mod.rs +0 -1
- package/src/rust/components/containers/scroll_panel.rs +21 -7
- package/src/rust/components/containers/sidebar.rs +8 -6
- package/src/rust/components/containers/split_panel.rs +3 -3
- package/src/rust/components/containers/tab_list.rs +3 -9
- package/src/rust/components/copy_dropdown.rs +2 -3
- package/src/rust/components/datetime_column_style.rs +19 -81
- package/src/rust/components/editable_header.rs +2 -3
- package/src/rust/components/export_dropdown.rs +2 -3
- package/src/rust/components/expression_editor.rs +29 -17
- package/src/rust/components/filter_dropdown.rs +2 -1
- package/src/rust/components/form/color_range_selector.rs +14 -7
- package/src/rust/components/form/debug.rs +47 -37
- package/src/rust/components/main_panel.rs +24 -65
- package/src/rust/components/mod.rs +2 -1
- package/src/rust/components/number_series_style.rs +161 -0
- package/src/rust/components/plugin_tab.rs +221 -0
- package/src/rust/components/settings_panel.rs +181 -59
- package/src/rust/components/status_bar.rs +140 -173
- package/src/rust/components/status_indicator.rs +15 -22
- package/src/rust/components/string_column_style.rs +20 -82
- package/src/rust/components/style_controls/number_string_format.rs +14 -30
- package/src/rust/components/viewer.rs +92 -131
- package/src/rust/config/column_config_schema.rs +195 -0
- package/src/rust/config/columns_config.rs +4 -97
- package/src/rust/config/datetime_column_style.rs +0 -5
- package/src/rust/config/mod.rs +8 -2
- package/src/rust/config/number_series_style.rs +79 -0
- package/src/rust/config/plugin_static_config.rs +144 -0
- package/src/rust/config/string_column_style.rs +0 -5
- package/src/rust/config/viewer_config.rs +5 -6
- package/src/rust/custom_elements/copy_dropdown.rs +30 -18
- package/src/rust/custom_elements/debug_plugin.rs +1 -3
- package/src/rust/custom_elements/export_dropdown.rs +26 -18
- package/src/rust/custom_elements/viewer.rs +62 -73
- package/src/rust/custom_events.rs +181 -224
- package/src/rust/js/plugin.rs +45 -117
- package/src/rust/lib.rs +34 -5
- package/src/rust/presentation/drag_helpers.rs +206 -0
- package/src/rust/presentation/props.rs +8 -0
- package/src/rust/presentation.rs +256 -41
- package/src/rust/{tasks → queries}/column_locator.rs +17 -73
- package/src/rust/queries/column_values.rs +59 -0
- package/src/rust/{tasks → queries}/columns_iter_set.rs +11 -18
- package/src/rust/queries/exports.rs +96 -0
- package/src/rust/queries/fetch_column_stats.rs +94 -0
- package/src/rust/queries/get_viewer_config.rs +54 -0
- package/src/rust/queries/mod.rs +44 -0
- package/src/rust/queries/plugin_column_styles.rs +101 -0
- package/src/rust/{engines.rs → queries/validate_expression.rs} +26 -15
- package/src/rust/renderer/activate.rs +1 -0
- package/src/rust/renderer/limits.rs +9 -4
- package/src/rust/renderer/plugin_store.rs +12 -0
- package/src/rust/renderer/props.rs +28 -3
- package/src/rust/renderer/registry.rs +40 -15
- package/src/rust/renderer.rs +640 -51
- package/src/rust/session/column_defaults_update.rs +20 -28
- package/src/rust/session/drag_drop_update.rs +10 -10
- package/src/rust/session/metadata.rs +31 -16
- package/src/rust/session/props.rs +15 -6
- package/src/rust/session/view_subscription.rs +10 -0
- package/src/rust/session.rs +109 -147
- package/src/rust/tasks/copy_export.rs +178 -158
- package/src/rust/tasks/{structural.rs → dismiss_render_warning.rs} +20 -40
- package/src/rust/tasks/edit_expression.rs +68 -88
- package/src/rust/tasks/eject.rs +25 -22
- package/src/rust/tasks/intersection_observer.rs +8 -21
- package/src/rust/tasks/mod.rs +19 -21
- package/src/rust/tasks/reset_all.rs +78 -0
- package/src/rust/tasks/resize_observer.rs +11 -33
- package/src/rust/tasks/restore_and_render.rs +117 -90
- package/src/rust/tasks/{get_viewer_config.rs → send_column_config.rs} +38 -35
- package/src/rust/tasks/send_plugin_config.rs +32 -33
- package/src/rust/tasks/update_and_render.rs +66 -47
- package/src/rust/{components/containers/trap_door_panel.rs → tasks/update_theme.rs} +34 -33
- package/src/rust/tasks/validate_expression.rs +61 -0
- package/src/rust/utils/browser/selection.rs +4 -4
- package/src/rust/utils/mod.rs +0 -63
- package/src/svg/mega-menu-icons-density.svg +23 -0
- package/src/svg/mega-menu-icons-map-density.svg +24 -0
- package/src/svg/mega-menu-icons-map-line.svg +19 -0
- package/src/themes/botanical.css +27 -53
- package/src/themes/defaults.css +24 -36
- package/src/themes/dracula.css +36 -54
- package/src/themes/gruvbox-dark.css +39 -59
- package/src/themes/gruvbox.css +16 -28
- package/src/themes/icons.css +3 -0
- package/src/themes/intl/de.css +42 -6
- package/src/themes/intl/es.css +42 -6
- package/src/themes/intl/fr.css +42 -6
- package/src/themes/intl/ja.css +42 -6
- package/src/themes/intl/pt.css +42 -6
- package/src/themes/intl/zh.css +42 -6
- package/src/themes/intl.css +37 -4
- package/src/themes/monokai.css +45 -61
- package/src/themes/phosphor.css +20 -29
- package/src/themes/pro-dark.css +25 -34
- package/src/themes/solarized-dark.css +21 -36
- package/src/themes/solarized.css +13 -23
- package/src/themes/vaporwave.css +40 -74
- package/src/ts/bootstrap.ts +14 -3
- package/src/ts/column-format.ts +162 -0
- package/src/ts/extensions.ts +4 -0
- package/src/ts/perspective-viewer.ts +9 -1
- package/src/{rust/components/column_settings_sidebar/style_tab/stub.rs → ts/perspective-viewer.worker.ts} +2 -22
- package/src/ts/plugin.ts +25 -101
- package/src/ts/ts-rs/{FormatUnit.ts → ColumnSelectMode.ts} +1 -1
- package/src/ts/ts-rs/PluginStaticConfig.ts +78 -0
- package/src/ts/ts-rs/ViewerConfig.ts +1 -2
- package/src/ts/ts-rs/ViewerConfigUpdate.ts +2 -3
- package/dist/esm/ts-rs/ColumnConfigValues.d.ts +0 -31
- package/dist/esm/ts-rs/CustomDatetimeFormat.d.ts +0 -1
- package/dist/esm/ts-rs/CustomDatetimeStyleConfig.d.ts +0 -15
- package/dist/esm/ts-rs/CustomNumberFormatConfig.d.ts +0 -18
- package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
- package/dist/esm/ts-rs/DatetimeFormatType.d.ts +0 -6
- package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
- package/dist/esm/ts-rs/FormatUnit.d.ts +0 -1
- package/dist/esm/ts-rs/NumberBackgroundMode.d.ts +0 -1
- package/dist/esm/ts-rs/NumberForegroundMode.d.ts +0 -1
- package/dist/esm/ts-rs/PluginConfig.d.ts +0 -2
- package/dist/esm/ts-rs/RoundingMode.d.ts +0 -1
- package/dist/esm/ts-rs/RoundingPriority.d.ts +0 -1
- package/dist/esm/ts-rs/SignDisplay.d.ts +0 -1
- package/dist/esm/ts-rs/SimpleDatetimeFormat.d.ts +0 -1
- package/dist/esm/ts-rs/SimpleDatetimeStyleConfig.d.ts +0 -6
- package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
- package/dist/esm/ts-rs/TrailingZeroDisplay.d.ts +0 -1
- package/dist/esm/ts-rs/UseGrouping.d.ts +0 -1
- package/src/rust/components/number_column_style.rs +0 -491
- package/src/rust/config/number_column_style.rs +0 -136
- package/src/rust/dragdrop.rs +0 -481
- package/src/rust/tasks/plugin_column_styles.rs +0 -98
- package/src/ts/ts-rs/ColumnConfigValues.ts +0 -14
- package/src/ts/ts-rs/CustomDatetimeFormat.ts +0 -3
- package/src/ts/ts-rs/CustomDatetimeStyleConfig.ts +0 -5
- package/src/ts/ts-rs/CustomNumberFormatConfig.ts +0 -8
- package/src/ts/ts-rs/DatetimeColorMode.ts +0 -3
- package/src/ts/ts-rs/DatetimeFormatType.ts +0 -8
- package/src/ts/ts-rs/FormatMode.ts +0 -3
- package/src/ts/ts-rs/NumberBackgroundMode.ts +0 -3
- package/src/ts/ts-rs/NumberForegroundMode.ts +0 -3
- package/src/ts/ts-rs/PluginConfig.ts +0 -4
- package/src/ts/ts-rs/RoundingMode.ts +0 -3
- package/src/ts/ts-rs/RoundingPriority.ts +0 -3
- package/src/ts/ts-rs/SignDisplay.ts +0 -3
- package/src/ts/ts-rs/SimpleDatetimeFormat.ts +0 -3
- package/src/ts/ts-rs/SimpleDatetimeStyleConfig.ts +0 -4
- package/src/ts/ts-rs/StringColorMode.ts +0 -3
- package/src/ts/ts-rs/TrailingZeroDisplay.ts +0 -3
- package/src/ts/ts-rs/UseGrouping.ts +0 -3
- /package/dist/wasm/snippets/{perspective-viewer-d924246f0b4a3dce → perspective-viewer-39ab7da3ca157861}/inline0.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-d924246f0b4a3dce → perspective-viewer-39ab7da3ca157861}/inline1.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-d924246f0b4a3dce → perspective-viewer-39ab7da3ca157861}/inline2.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-d924246f0b4a3dce → perspective-viewer-39ab7da3ca157861}/inline3.js +0 -0
- /package/dist/wasm/snippets/{perspective-viewer-d924246f0b4a3dce → perspective-viewer-39ab7da3ca157861}/inline4.js +0 -0
- /package/src/rust/{tasks → config}/export_method.rs +0 -0
- /package/src/rust/{tasks → queries}/export_app.rs +0 -0
- /package/src/rust/{tasks → queries}/is_invalid_drop.rs +0 -0
package/src/rust/renderer.rs
CHANGED
|
@@ -24,18 +24,20 @@ mod props;
|
|
|
24
24
|
mod registry;
|
|
25
25
|
mod render_timer;
|
|
26
26
|
|
|
27
|
-
use std::cell::{
|
|
27
|
+
use std::cell::{Cell, RefCell};
|
|
28
28
|
use std::collections::HashMap;
|
|
29
29
|
use std::future::Future;
|
|
30
30
|
use std::ops::Deref;
|
|
31
31
|
use std::pin::Pin;
|
|
32
32
|
use std::rc::Rc;
|
|
33
33
|
|
|
34
|
-
use futures::future::
|
|
34
|
+
use futures::future::select_all;
|
|
35
|
+
use perspective_client::config::ViewConfig;
|
|
35
36
|
use perspective_client::utils::*;
|
|
36
37
|
use perspective_client::{View, ViewWindow};
|
|
37
38
|
use perspective_js::json;
|
|
38
|
-
use perspective_js::utils::{ApiResult, ResultTApiErrorExt};
|
|
39
|
+
use perspective_js::utils::{ApiResult, JsValueSerdeExt, ResultTApiErrorExt};
|
|
40
|
+
use serde_json::Value;
|
|
39
41
|
use wasm_bindgen::prelude::*;
|
|
40
42
|
use web_sys::*;
|
|
41
43
|
use yew::html::ImplicitClone;
|
|
@@ -50,9 +52,25 @@ pub use self::registry::*;
|
|
|
50
52
|
use self::render_timer::*;
|
|
51
53
|
use crate::config::*;
|
|
52
54
|
use crate::js::plugin::*;
|
|
53
|
-
use crate::
|
|
55
|
+
use crate::session::Session;
|
|
54
56
|
use crate::utils::*;
|
|
55
57
|
|
|
58
|
+
/// A per-column config map. Each inner [`serde_json::Map`] is a flat collection
|
|
59
|
+
/// of plugin-defined JSON keys whose shape is dictated by the active plugin's
|
|
60
|
+
/// [`crate::config::ColumnConfigSchema`].
|
|
61
|
+
pub type ColumnConfigMap = HashMap<String, serde_json::Map<String, serde_json::Value>>;
|
|
62
|
+
|
|
63
|
+
/// Per-plugin config bucket. Holds the per-column style map and the
|
|
64
|
+
/// plugin-level config map for one plugin. Buckets are keyed by plugin
|
|
65
|
+
/// name in [`RendererMutData::plugin_states`], so foreign keys from a
|
|
66
|
+
/// different plugin physically cannot appear in the active plugin's
|
|
67
|
+
/// bucket.
|
|
68
|
+
#[derive(Clone, Debug, Default)]
|
|
69
|
+
pub struct PluginScopedConfig {
|
|
70
|
+
pub columns: ColumnConfigMap,
|
|
71
|
+
pub plugin: serde_json::Map<String, serde_json::Value>,
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
/// Immutable state
|
|
57
75
|
pub struct RendererData {
|
|
58
76
|
plugin_data: RefCell<RendererMutData>,
|
|
@@ -60,22 +78,36 @@ pub struct RendererData {
|
|
|
60
78
|
pub plugin_changed: PubSub<JsPerspectiveViewerPlugin>,
|
|
61
79
|
pub style_changed: PubSub<()>,
|
|
62
80
|
pub reset_changed: PubSub<()>,
|
|
81
|
+
pub selection_changed: PubSub<Option<ViewWindow>>,
|
|
82
|
+
|
|
83
|
+
/// Fires after a column-style edit lands in the active plugin's
|
|
84
|
+
/// bucket.
|
|
85
|
+
pub column_style_changed: PubSub<ColumnConfigMap>,
|
|
86
|
+
|
|
87
|
+
/// Fires after a plugin-level config edit lands in the active
|
|
88
|
+
/// plugin's bucket.
|
|
89
|
+
pub plugin_config_changed: PubSub<serde_json::Map<String, serde_json::Value>>,
|
|
63
90
|
|
|
64
|
-
///
|
|
65
|
-
///
|
|
66
|
-
|
|
91
|
+
/// `true` while the active plugin's "rendering N of M" warning is
|
|
92
|
+
/// dismissable.
|
|
93
|
+
pub render_warning: Cell<bool>,
|
|
94
|
+
|
|
95
|
+
/// Fires after every draw/update with the computed render limits.
|
|
67
96
|
pub on_render_limits_changed: RefCell<Option<Callback<RenderLimits>>>,
|
|
68
97
|
}
|
|
69
98
|
|
|
70
99
|
/// Mutable state
|
|
71
100
|
pub struct RendererMutData {
|
|
72
101
|
viewer_elem: HtmlElement,
|
|
73
|
-
metadata:
|
|
102
|
+
metadata: Rc<PluginStaticConfig>,
|
|
74
103
|
plugin_store: PluginStore,
|
|
75
104
|
plugins_idx: Option<usize>,
|
|
76
105
|
timer: MovingWindowRenderTimer,
|
|
77
106
|
selection: Option<ViewWindow>,
|
|
78
107
|
pending_plugin: Option<usize>,
|
|
108
|
+
|
|
109
|
+
/// Per-plugin config buckets, keyed by plugin name.
|
|
110
|
+
plugin_states: HashMap<String, PluginScopedConfig>,
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
/// The state object responsible for the active [`JsPerspectiveViewerPlugin`].
|
|
@@ -117,17 +149,22 @@ impl Renderer {
|
|
|
117
149
|
Self(Rc::new(RendererData {
|
|
118
150
|
plugin_data: RefCell::new(RendererMutData {
|
|
119
151
|
viewer_elem: viewer_elem.clone(),
|
|
120
|
-
metadata:
|
|
152
|
+
metadata: Rc::new(PluginStaticConfig::default()),
|
|
121
153
|
plugin_store: PluginStore::default(),
|
|
122
154
|
plugins_idx: None,
|
|
123
155
|
selection: None,
|
|
124
156
|
timer: MovingWindowRenderTimer::default(),
|
|
125
157
|
pending_plugin: None,
|
|
158
|
+
plugin_states: HashMap::default(),
|
|
126
159
|
}),
|
|
127
160
|
draw_lock: Default::default(),
|
|
128
161
|
plugin_changed: Default::default(),
|
|
129
162
|
style_changed: Default::default(),
|
|
130
163
|
reset_changed: Default::default(),
|
|
164
|
+
selection_changed: Default::default(),
|
|
165
|
+
column_style_changed: Default::default(),
|
|
166
|
+
plugin_config_changed: Default::default(),
|
|
167
|
+
render_warning: Cell::new(true),
|
|
131
168
|
on_render_limits_changed: Default::default(),
|
|
132
169
|
}))
|
|
133
170
|
}
|
|
@@ -153,13 +190,442 @@ impl Renderer {
|
|
|
153
190
|
Ok(())
|
|
154
191
|
}
|
|
155
192
|
|
|
156
|
-
pub fn metadata(&self) ->
|
|
157
|
-
|
|
193
|
+
pub fn metadata(&self) -> Rc<PluginStaticConfig> {
|
|
194
|
+
self.borrow().metadata.clone()
|
|
158
195
|
}
|
|
159
196
|
|
|
160
197
|
pub fn is_chart(&self) -> bool {
|
|
161
|
-
|
|
162
|
-
|
|
198
|
+
self.metadata().name.as_str() != "Datagrid"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// Whether the active plugin opts into per-column style controls.
|
|
202
|
+
pub fn can_render_column_styles(&self) -> bool {
|
|
203
|
+
self.metadata().can_render_column_styles
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// Name of the currently-active plugin (used as the key into
|
|
207
|
+
/// `plugin_states`). Returns `None` when no plugin has been
|
|
208
|
+
/// activated yet.
|
|
209
|
+
fn active_plugin_name(&self) -> Option<String> {
|
|
210
|
+
Some(self.borrow().metadata.name.clone()).filter(|n| !n.is_empty())
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Per-column config (active plugin's bucket) ───────────────────
|
|
214
|
+
|
|
215
|
+
/// Snapshot of the active plugin's per-column config map.
|
|
216
|
+
pub fn all_columns_configs(&self) -> ColumnConfigMap {
|
|
217
|
+
self.active_plugin_name()
|
|
218
|
+
.and_then(|n| {
|
|
219
|
+
self.borrow()
|
|
220
|
+
.plugin_states
|
|
221
|
+
.get(&n)
|
|
222
|
+
.map(|b| b.columns.clone())
|
|
223
|
+
})
|
|
224
|
+
.unwrap_or_default()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Restore-prep snapshot: like [`Self::all_columns_configs`], but
|
|
228
|
+
/// for each column also materializes any `ControlSpec::Number`
|
|
229
|
+
/// fields the schema declares with `include: true` that aren't
|
|
230
|
+
/// already in the bucket entry. The materialized value is the
|
|
231
|
+
/// schema's `default`, which the schema computes from cached
|
|
232
|
+
/// column stats (via [`Self::query_column_config_schema`]).
|
|
233
|
+
///
|
|
234
|
+
/// The bucket itself stays minimal (user edits + `include: true`
|
|
235
|
+
/// values the user *explicitly set*); this helper produces the
|
|
236
|
+
/// fully-realized payload the plugin's `restore` is expected to
|
|
237
|
+
/// receive. Every restore-prep site should call this rather than
|
|
238
|
+
/// `all_columns_configs` directly — otherwise widgets that gate
|
|
239
|
+
/// `include` fields off other fields (e.g. Datagrid's
|
|
240
|
+
/// `fg_gradient` revealed when `number_fg_mode = "bar"`) will
|
|
241
|
+
/// reach the plugin without their default value populated.
|
|
242
|
+
pub fn all_columns_configs_materialized(
|
|
243
|
+
&self,
|
|
244
|
+
view_config: &ViewConfig,
|
|
245
|
+
session: &Session,
|
|
246
|
+
) -> ColumnConfigMap {
|
|
247
|
+
let mut configs = self.all_columns_configs();
|
|
248
|
+
for (col, entry) in &mut configs {
|
|
249
|
+
let Ok(schema) =
|
|
250
|
+
self.query_column_config_schema(view_config, session, col, Some(entry))
|
|
251
|
+
else {
|
|
252
|
+
continue;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
for field in &schema.fields {
|
|
256
|
+
let ControlSpec::Number {
|
|
257
|
+
key,
|
|
258
|
+
default,
|
|
259
|
+
include: Some(true),
|
|
260
|
+
..
|
|
261
|
+
} = field
|
|
262
|
+
else {
|
|
263
|
+
continue;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
if entry.contains_key(key) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let Some(num) = serde_json::Number::from_f64(*default) else {
|
|
271
|
+
continue;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
entry.insert(key.clone(), serde_json::Value::Number(num));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
configs
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// Clear the active plugin's per-column config map.
|
|
282
|
+
pub fn reset_columns_configs(&self) {
|
|
283
|
+
if let Some(n) = self.active_plugin_name() {
|
|
284
|
+
self.borrow_mut()
|
|
285
|
+
.plugin_states
|
|
286
|
+
.entry(n)
|
|
287
|
+
.or_default()
|
|
288
|
+
.columns
|
|
289
|
+
.clear();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// Clone of the active plugin's per-column entry for `column_name`,
|
|
294
|
+
/// or `None` if no value is stored.
|
|
295
|
+
pub fn get_columns_config(
|
|
296
|
+
&self,
|
|
297
|
+
column_name: &str,
|
|
298
|
+
) -> Option<serde_json::Map<String, serde_json::Value>> {
|
|
299
|
+
let n = self.active_plugin_name()?;
|
|
300
|
+
self.borrow()
|
|
301
|
+
.plugin_states
|
|
302
|
+
.get(&n)?
|
|
303
|
+
.columns
|
|
304
|
+
.get(column_name)
|
|
305
|
+
.cloned()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Wholesale update the active plugin's per-column config map
|
|
309
|
+
/// (e.g. from a `restore()` call). Each incoming column entry is
|
|
310
|
+
/// schema-stripped before insertion — values matching the
|
|
311
|
+
/// schema-declared default are dropped so the bucket converges to
|
|
312
|
+
/// the "empty ⇒ reads-default" invariant, mirroring
|
|
313
|
+
/// [`Self::update_plugin_config`]. `ControlSpec::Number` fields
|
|
314
|
+
/// declared with `include: true` survive the strip (their default
|
|
315
|
+
/// is data-dependent, so a literal default value is preserved as
|
|
316
|
+
/// the user's explicit choice). Column entries that become empty
|
|
317
|
+
/// after stripping are removed from the bucket entirely.
|
|
318
|
+
pub fn update_columns_configs(
|
|
319
|
+
&self,
|
|
320
|
+
view_config: &ViewConfig,
|
|
321
|
+
session: &Session,
|
|
322
|
+
update: ColumnConfigUpdate,
|
|
323
|
+
) -> bool {
|
|
324
|
+
let Some(n) = self.active_plugin_name() else {
|
|
325
|
+
return false;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
match update {
|
|
329
|
+
OptionalUpdate::SetDefault => {
|
|
330
|
+
let mut st = self.borrow_mut();
|
|
331
|
+
let bucket = st.plugin_states.entry(n).or_default();
|
|
332
|
+
let was_nonempty = !bucket.columns.is_empty();
|
|
333
|
+
bucket.columns.clear();
|
|
334
|
+
was_nonempty
|
|
335
|
+
},
|
|
336
|
+
OptionalUpdate::Missing => false,
|
|
337
|
+
OptionalUpdate::Update(map) => {
|
|
338
|
+
// Strip per-column before borrowing the bucket mutably:
|
|
339
|
+
// the schema query takes an immutable borrow via
|
|
340
|
+
// `self.metadata()`, which would alias-conflict with
|
|
341
|
+
// `borrow_mut` below.
|
|
342
|
+
let stripped: Vec<(String, serde_json::Map<String, serde_json::Value>)> = map
|
|
343
|
+
.into_iter()
|
|
344
|
+
.map(|(col, mut cfg)| {
|
|
345
|
+
if let Ok(schema) =
|
|
346
|
+
self.query_column_config_schema(view_config, session, &col, Some(&cfg))
|
|
347
|
+
{
|
|
348
|
+
strip_default_values(&schema, &mut cfg);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
(col, cfg)
|
|
352
|
+
})
|
|
353
|
+
.collect();
|
|
354
|
+
|
|
355
|
+
let mut st = self.borrow_mut();
|
|
356
|
+
let bucket = st.plugin_states.entry(n).or_default();
|
|
357
|
+
let mut changed = false;
|
|
358
|
+
for (col, cfg) in stripped {
|
|
359
|
+
if cfg.is_empty() {
|
|
360
|
+
if bucket.columns.remove(&col).is_some() {
|
|
361
|
+
changed = true;
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
match bucket.columns.insert(col, cfg.clone()) {
|
|
365
|
+
None => changed = true,
|
|
366
|
+
Some(old) if old != cfg => changed = true,
|
|
367
|
+
_ => {},
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
changed
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// Apply a single schema-field update from the column-style UI to
|
|
378
|
+
/// the active plugin's bucket. Clears the keys the field owns,
|
|
379
|
+
/// then splices in the partial new sub-state. Drops empty
|
|
380
|
+
/// entries.
|
|
381
|
+
///
|
|
382
|
+
/// The schema-strip is defense-in-depth: widget callbacks (e.g.
|
|
383
|
+
/// `NumberFieldPrimitive`) already pre-strip default values, so
|
|
384
|
+
/// for live edits this strip pass is a no-op. It closes the hole
|
|
385
|
+
/// for programmatic callers that construct a
|
|
386
|
+
/// `ColumnConfigFieldUpdate` directly without going through the
|
|
387
|
+
/// widget (where `include: true` would otherwise be ignored).
|
|
388
|
+
pub fn update_columns_config_field(
|
|
389
|
+
&self,
|
|
390
|
+
view_config: &ViewConfig,
|
|
391
|
+
session: &Session,
|
|
392
|
+
column_name: String,
|
|
393
|
+
mut update: ColumnConfigFieldUpdate,
|
|
394
|
+
) {
|
|
395
|
+
let Some(n) = self.active_plugin_name() else {
|
|
396
|
+
return;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// Take the schema query before the mutable borrow — same
|
|
400
|
+
// RefCell aliasing reason as in `update_columns_configs`.
|
|
401
|
+
let current_value = self.get_columns_config(&column_name);
|
|
402
|
+
if let Ok(schema) = self.query_column_config_schema(
|
|
403
|
+
view_config,
|
|
404
|
+
session,
|
|
405
|
+
&column_name,
|
|
406
|
+
current_value.as_ref(),
|
|
407
|
+
) {
|
|
408
|
+
strip_default_values(&schema, &mut update.value);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let mut st = self.borrow_mut();
|
|
412
|
+
let bucket = st.plugin_states.entry(n).or_default();
|
|
413
|
+
let entry = bucket.columns.entry(column_name.clone()).or_default();
|
|
414
|
+
for k in &update.keys {
|
|
415
|
+
entry.remove(k);
|
|
416
|
+
}
|
|
417
|
+
for (k, v) in update.value {
|
|
418
|
+
if update.keys.contains(&k) {
|
|
419
|
+
entry.insert(k, v);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if entry.is_empty() {
|
|
423
|
+
bucket.columns.remove(&column_name);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─── Plugin-level config (active plugin's bucket) ─────────────────
|
|
428
|
+
|
|
429
|
+
/// Snapshot of the active plugin's plugin-level config map.
|
|
430
|
+
pub fn get_plugin_config(&self) -> serde_json::Map<String, serde_json::Value> {
|
|
431
|
+
self.active_plugin_name()
|
|
432
|
+
.and_then(|n| {
|
|
433
|
+
self.borrow()
|
|
434
|
+
.plugin_states
|
|
435
|
+
.get(&n)
|
|
436
|
+
.map(|b| b.plugin.clone())
|
|
437
|
+
})
|
|
438
|
+
.unwrap_or_default()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// Clear the active plugin's plugin-level config map.
|
|
442
|
+
pub fn reset_plugin_config(&self) {
|
|
443
|
+
if let Some(n) = self.active_plugin_name() {
|
|
444
|
+
self.borrow_mut()
|
|
445
|
+
.plugin_states
|
|
446
|
+
.entry(n)
|
|
447
|
+
.or_default()
|
|
448
|
+
.plugin
|
|
449
|
+
.clear();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/// Synchronously query the active plugin's
|
|
454
|
+
/// [`ColumnConfigSchema`] used to gate plugin-config strip logic.
|
|
455
|
+
/// Inlined here (rather than calling `queries::get_plugin_config_schema`)
|
|
456
|
+
/// to keep `renderer` from back-importing the `queries` module.
|
|
457
|
+
fn query_plugin_config_schema(
|
|
458
|
+
&self,
|
|
459
|
+
view_config: &ViewConfig,
|
|
460
|
+
) -> ApiResult<ColumnConfigSchema> {
|
|
461
|
+
let plugin = self.get_active_plugin()?;
|
|
462
|
+
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
463
|
+
let raw = plugin._plugin_config_schema(&view_config_js)?;
|
|
464
|
+
serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/// Per-column counterpart of [`query_plugin_config_schema`]. Used by
|
|
468
|
+
/// the columns-config write paths (strip-on-write) and the
|
|
469
|
+
/// restore-prep snapshot (materialize-on-read).
|
|
470
|
+
///
|
|
471
|
+
/// Reads the cached `ColumnStats` (populated by the StyleTab's
|
|
472
|
+
/// `fetch_column_abs_max` task and cleared on `view_config_changed`)
|
|
473
|
+
/// so plugins emit gradient defaults against the column's current
|
|
474
|
+
/// `abs_max` instead of falling back to `0`. Cache cold ⇒ stats
|
|
475
|
+
/// pass through as missing and the plugin uses its `?? 0` fallback;
|
|
476
|
+
/// `include: true` fields are still preserved via
|
|
477
|
+
/// [`matches_declared_default`]'s short-circuit.
|
|
478
|
+
fn query_column_config_schema(
|
|
479
|
+
&self,
|
|
480
|
+
view_config: &ViewConfig,
|
|
481
|
+
session: &Session,
|
|
482
|
+
column_name: &str,
|
|
483
|
+
current_value: Option<&serde_json::Map<String, serde_json::Value>>,
|
|
484
|
+
) -> ApiResult<ColumnConfigSchema> {
|
|
485
|
+
let plugin = self.get_active_plugin()?;
|
|
486
|
+
let plugin_config = self.metadata();
|
|
487
|
+
let names = &plugin_config.config_column_names;
|
|
488
|
+
let group = view_config
|
|
489
|
+
.columns
|
|
490
|
+
.iter()
|
|
491
|
+
.position(|maybe_s| maybe_s.as_deref() == Some(column_name))
|
|
492
|
+
.and_then(|idx| names.get(idx))
|
|
493
|
+
.map(|s| s.as_str());
|
|
494
|
+
|
|
495
|
+
let Some(view_type) = session.metadata().get_column_view_type(column_name) else {
|
|
496
|
+
return Ok(ColumnConfigSchema { fields: vec![] });
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
let current_js = JsValue::from_serde_ext(¤t_value).unwrap_or(JsValue::NULL);
|
|
500
|
+
let view_config_js = JsValue::from_serde_ext(view_config).unwrap_or(JsValue::NULL);
|
|
501
|
+
|
|
502
|
+
// Pull the column's cached stats from the session. The StyleTab
|
|
503
|
+
// pre-warms this via `fetch_column_abs_max` whenever the user
|
|
504
|
+
// opens column settings; the cache is invalidated on every
|
|
505
|
+
// `view_config_changed`, so freshness is bounded.
|
|
506
|
+
let stats = session.get_column_stats(column_name).unwrap_or_default();
|
|
507
|
+
let stats_json = serde_json::json!({
|
|
508
|
+
"abs_max": stats.abs_max,
|
|
509
|
+
});
|
|
510
|
+
let stats_js = JsValue::from_serde_ext(&stats_json).unwrap_or(JsValue::NULL);
|
|
511
|
+
|
|
512
|
+
let raw = plugin._column_config_schema(
|
|
513
|
+
&view_type.to_string(),
|
|
514
|
+
group,
|
|
515
|
+
column_name,
|
|
516
|
+
¤t_js,
|
|
517
|
+
&view_config_js,
|
|
518
|
+
&stats_js,
|
|
519
|
+
)?;
|
|
520
|
+
|
|
521
|
+
serde_wasm_bindgen::from_value(raw).map_err(|e| e.into())
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/// Wholesale update the active plugin's plugin-level config map.
|
|
525
|
+
/// Entries whose value equals the schema-declared default are
|
|
526
|
+
/// treated as "reset this key" — the corresponding bucket entry
|
|
527
|
+
/// is cleared rather than the default being stored literally.
|
|
528
|
+
/// Keys absent from the incoming map are left alone (merge
|
|
529
|
+
/// semantics for the non-default subset).
|
|
530
|
+
pub fn update_plugin_config(
|
|
531
|
+
&self,
|
|
532
|
+
view_config: &ViewConfig,
|
|
533
|
+
update: PluginConfigUpdate,
|
|
534
|
+
) -> bool {
|
|
535
|
+
let Some(n) = self.active_plugin_name() else {
|
|
536
|
+
return false;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
let schema = self.query_plugin_config_schema(view_config).ok();
|
|
540
|
+
let mut st = self.borrow_mut();
|
|
541
|
+
let bucket = st.plugin_states.entry(n).or_default();
|
|
542
|
+
match update {
|
|
543
|
+
OptionalUpdate::SetDefault => {
|
|
544
|
+
let changed = !bucket.plugin.is_empty();
|
|
545
|
+
bucket.plugin.clear();
|
|
546
|
+
changed
|
|
547
|
+
},
|
|
548
|
+
OptionalUpdate::Missing => false,
|
|
549
|
+
OptionalUpdate::Update(mut map) => {
|
|
550
|
+
let mut changed = false;
|
|
551
|
+
if let Some(s) = &schema {
|
|
552
|
+
// Default-valued entries in a restore payload
|
|
553
|
+
// semantically reset the key — strip from the
|
|
554
|
+
// map AND clear any existing override in the
|
|
555
|
+
// bucket so the wholesale-restore path matches
|
|
556
|
+
// the live-edit path (where the widget emits an
|
|
557
|
+
// empty value to clear).
|
|
558
|
+
map.retain(|key, value| {
|
|
559
|
+
let is_default = s
|
|
560
|
+
.fields
|
|
561
|
+
.iter()
|
|
562
|
+
.any(|spec| matches_declared_default(spec, key, value));
|
|
563
|
+
if is_default {
|
|
564
|
+
if bucket.plugin.remove(key).is_some() {
|
|
565
|
+
changed = true;
|
|
566
|
+
}
|
|
567
|
+
false
|
|
568
|
+
} else {
|
|
569
|
+
true
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (k, v) in map {
|
|
575
|
+
let prev = bucket.plugin.insert(k, v.clone());
|
|
576
|
+
if prev.as_ref() != Some(&v) {
|
|
577
|
+
changed = true;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
changed
|
|
582
|
+
},
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/// Apply a single schema-field update from the plugin-settings UI
|
|
587
|
+
/// to the active plugin's bucket. Clear-then-insert semantics
|
|
588
|
+
/// mirror [`Self::update_columns_config_field`]. Entries in
|
|
589
|
+
/// `update.value` whose value equals the schema default are
|
|
590
|
+
/// stripped before applying so default picks reset the key
|
|
591
|
+
/// rather than store the default literally.
|
|
592
|
+
pub fn update_plugin_config_field(
|
|
593
|
+
&self,
|
|
594
|
+
view_config: &ViewConfig,
|
|
595
|
+
mut update: ColumnConfigFieldUpdate,
|
|
596
|
+
) -> bool {
|
|
597
|
+
let Some(n) = self.active_plugin_name() else {
|
|
598
|
+
return false;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
if let Ok(schema) = self.query_plugin_config_schema(view_config) {
|
|
602
|
+
strip_default_values(&schema, &mut update.value);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let mut st = self.borrow_mut();
|
|
606
|
+
let bucket = st.plugin_states.entry(n).or_default();
|
|
607
|
+
let mut changed = false;
|
|
608
|
+
|
|
609
|
+
for k in &update.keys {
|
|
610
|
+
if let Some(v) = update.value.get(k) {
|
|
611
|
+
let prev = bucket.plugin.insert(k.to_string(), v.clone());
|
|
612
|
+
if prev.as_ref() != Some(v) {
|
|
613
|
+
changed = true;
|
|
614
|
+
}
|
|
615
|
+
} else if bucket.plugin.remove(k).is_some() {
|
|
616
|
+
changed = true;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
changed
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// Whether the active plugin's render warning is currently armed
|
|
624
|
+
/// (i.e. an oversized view will be capped). Becomes `false` once
|
|
625
|
+
/// the user clicks "Render all points"; resets to `true` on the
|
|
626
|
+
/// next plugin change.
|
|
627
|
+
pub fn is_render_warning_enabled(&self) -> bool {
|
|
628
|
+
self.0.render_warning.get()
|
|
163
629
|
}
|
|
164
630
|
|
|
165
631
|
/// Return all plugin instances, whether they are active or not. Useful
|
|
@@ -173,6 +639,13 @@ impl Renderer {
|
|
|
173
639
|
self.0.borrow_mut().plugin_store.plugin_records().clone()
|
|
174
640
|
}
|
|
175
641
|
|
|
642
|
+
/// Cached `PluginStaticConfig`s for every registered plugin, in
|
|
643
|
+
/// registration (priority) order. Mirrors `get_all_plugins()`
|
|
644
|
+
/// element-for-element.
|
|
645
|
+
pub fn get_all_plugin_configs(&self) -> Vec<Rc<PluginStaticConfig>> {
|
|
646
|
+
self.0.borrow_mut().plugin_store.plugin_configs().clone()
|
|
647
|
+
}
|
|
648
|
+
|
|
176
649
|
/// Gets the currently active plugin. Calling this method before a plugin
|
|
177
650
|
/// has been selected will cause the default (first) plugin to be
|
|
178
651
|
/// selected, and doing so when no plugins have been registered is an
|
|
@@ -206,16 +679,17 @@ impl Renderer {
|
|
|
206
679
|
}
|
|
207
680
|
|
|
208
681
|
pub async fn restyle_all(&self, view: &perspective_client::View) -> ApiResult<JsValue> {
|
|
209
|
-
let
|
|
210
|
-
let
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
.
|
|
217
|
-
.
|
|
218
|
-
|
|
682
|
+
let plugin = self.get_active_plugin()?;
|
|
683
|
+
let meta = self.metadata();
|
|
684
|
+
plugin.restyle();
|
|
685
|
+
let mut limits =
|
|
686
|
+
get_row_and_col_limits(view, &meta, self.is_render_warning_enabled()).await?;
|
|
687
|
+
limits.is_update = false;
|
|
688
|
+
plugin
|
|
689
|
+
.draw(view.clone().into(), limits.max_cols, limits.max_rows, false)
|
|
690
|
+
.await?;
|
|
691
|
+
|
|
692
|
+
Ok(JsValue::UNDEFINED)
|
|
219
693
|
}
|
|
220
694
|
|
|
221
695
|
pub fn set_throttle(&self, val: Option<f64>) {
|
|
@@ -223,7 +697,12 @@ impl Renderer {
|
|
|
223
697
|
}
|
|
224
698
|
|
|
225
699
|
pub fn set_selection(&self, window: Option<ViewWindow>) {
|
|
226
|
-
self.
|
|
700
|
+
if self.borrow().selection == window {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
self.borrow_mut().selection = window.clone();
|
|
705
|
+
self.selection_changed.emit(window);
|
|
227
706
|
}
|
|
228
707
|
|
|
229
708
|
pub fn get_selection(&self) -> Option<ViewWindow> {
|
|
@@ -231,14 +710,13 @@ impl Renderer {
|
|
|
231
710
|
}
|
|
232
711
|
|
|
233
712
|
pub fn disable_active_plugin_render_warning(&self) {
|
|
234
|
-
self.
|
|
235
|
-
self.get_active_plugin().unwrap().set_render_warning(false);
|
|
713
|
+
self.0.render_warning.set(false);
|
|
236
714
|
}
|
|
237
715
|
|
|
238
716
|
pub fn get_next_plugin_metadata(
|
|
239
717
|
&self,
|
|
240
718
|
update: &PluginUpdate,
|
|
241
|
-
) -> Option<
|
|
719
|
+
) -> Option<Rc<PluginStaticConfig>> {
|
|
242
720
|
let default_plugin_name = PLUGIN_REGISTRY.default_plugin_name();
|
|
243
721
|
let name = match update {
|
|
244
722
|
PluginUpdate::Missing => return None,
|
|
@@ -254,9 +732,12 @@ impl Renderer {
|
|
|
254
732
|
|
|
255
733
|
if changed {
|
|
256
734
|
self.borrow_mut().pending_plugin = Some(idx);
|
|
257
|
-
self.
|
|
258
|
-
.
|
|
259
|
-
.
|
|
735
|
+
self.0
|
|
736
|
+
.borrow_mut()
|
|
737
|
+
.plugin_store
|
|
738
|
+
.plugin_configs()
|
|
739
|
+
.get(idx)
|
|
740
|
+
.cloned()
|
|
260
741
|
} else {
|
|
261
742
|
None
|
|
262
743
|
}
|
|
@@ -271,10 +752,7 @@ impl Renderer {
|
|
|
271
752
|
);
|
|
272
753
|
|
|
273
754
|
if changed {
|
|
274
|
-
self.
|
|
275
|
-
let plugin: JsPerspectiveViewerPlugin = self.get_active_plugin()?;
|
|
276
|
-
self.borrow_mut().metadata = plugin.get_requirements()?;
|
|
277
|
-
self.plugin_changed.emit(plugin);
|
|
755
|
+
self.commit_plugin_idx(idx)?;
|
|
278
756
|
}
|
|
279
757
|
|
|
280
758
|
Ok(changed)
|
|
@@ -301,15 +779,49 @@ impl Renderer {
|
|
|
301
779
|
);
|
|
302
780
|
|
|
303
781
|
if changed {
|
|
304
|
-
self.
|
|
305
|
-
let plugin: JsPerspectiveViewerPlugin = self.get_active_plugin()?;
|
|
306
|
-
self.borrow_mut().metadata = plugin.get_requirements()?;
|
|
307
|
-
self.plugin_changed.emit(plugin);
|
|
782
|
+
self.commit_plugin_idx(idx)?;
|
|
308
783
|
}
|
|
309
784
|
|
|
310
785
|
Ok(changed)
|
|
311
786
|
}
|
|
312
787
|
|
|
788
|
+
/// Shared tail of `apply_pending_plugin` / `set_plugin`: switch the
|
|
789
|
+
/// active plugin to `idx`, swap in its cached `PluginStaticConfig`,
|
|
790
|
+
/// reset the per-plugin render-warning flag, and fire
|
|
791
|
+
/// `plugin_changed`.
|
|
792
|
+
fn commit_plugin_idx(&self, idx: usize) -> ApiResult<()> {
|
|
793
|
+
self.borrow_mut().plugins_idx = Some(idx);
|
|
794
|
+
let config = self
|
|
795
|
+
.0
|
|
796
|
+
.borrow_mut()
|
|
797
|
+
.plugin_store
|
|
798
|
+
.plugin_configs()
|
|
799
|
+
.get(idx)
|
|
800
|
+
.cloned()
|
|
801
|
+
.ok_or("No Plugin")?;
|
|
802
|
+
|
|
803
|
+
self.borrow_mut().metadata = config.clone();
|
|
804
|
+
self.0.render_warning.set(true);
|
|
805
|
+
let plugin: JsPerspectiveViewerPlugin = self.get_active_plugin()?;
|
|
806
|
+
|
|
807
|
+
// Push the newly-activated plugin's stored bucket through
|
|
808
|
+
// `plugin.restore` so the swap immediately reflects any
|
|
809
|
+
// viewer-owned per-column and plugin-level config.
|
|
810
|
+
let bucket = self
|
|
811
|
+
.borrow()
|
|
812
|
+
.plugin_states
|
|
813
|
+
.get(&config.name)
|
|
814
|
+
.cloned()
|
|
815
|
+
.unwrap_or_default();
|
|
816
|
+
let token = JsValue::from_serde_ext(&bucket.plugin).unwrap_or(JsValue::NULL);
|
|
817
|
+
if let Err(e) = plugin.restore(&token, Some(&bucket.columns)) {
|
|
818
|
+
tracing::warn!("plugin.restore on swap failed: {:?}", e);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
self.plugin_changed.emit(plugin);
|
|
822
|
+
Ok(())
|
|
823
|
+
}
|
|
824
|
+
|
|
313
825
|
pub async fn with_lock<T>(self, task: impl Future<Output = ApiResult<T>>) -> ApiResult<T> {
|
|
314
826
|
let draw_mutex = self.draw_lock();
|
|
315
827
|
draw_mutex.lock(task).await
|
|
@@ -397,8 +909,9 @@ impl Renderer {
|
|
|
397
909
|
|
|
398
910
|
async fn draw_view(&self, view: &perspective_client::View, is_update: bool) -> ApiResult<()> {
|
|
399
911
|
let plugin = self.get_active_plugin()?;
|
|
400
|
-
let meta = self.metadata()
|
|
401
|
-
let mut limits =
|
|
912
|
+
let meta = self.metadata();
|
|
913
|
+
let mut limits =
|
|
914
|
+
get_row_and_col_limits(view, &meta, self.is_render_warning_enabled()).await?;
|
|
402
915
|
limits.is_update = is_update;
|
|
403
916
|
if let Some(cb) = self.0.on_render_limits_changed.borrow().as_ref() {
|
|
404
917
|
cb.emit(limits);
|
|
@@ -501,12 +1014,20 @@ impl Renderer {
|
|
|
501
1014
|
|
|
502
1015
|
fn find_plugin_idx(&self, name: &str) -> Option<usize> {
|
|
503
1016
|
let short_name = make_short_name(name);
|
|
504
|
-
self.0
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1017
|
+
let mut borrowed = self.0.borrow_mut();
|
|
1018
|
+
let configs = borrowed.plugin_store.plugin_configs();
|
|
1019
|
+
// Prefer an exact (normalised) match so e.g. `"Y Line"` doesn't
|
|
1020
|
+
// substring-resolve to `"X/Y Line"` just because it was registered
|
|
1021
|
+
// first. Falls back to `contains` so short/abbreviated names
|
|
1022
|
+
// (`restore({ plugin: "scat" })` → `"scatter"`) still work.
|
|
1023
|
+
let short_names: Vec<String> = configs.iter().map(|c| make_short_name(&c.name)).collect();
|
|
1024
|
+
if let Some(i) = short_names.iter().position(|n| n == &short_name) {
|
|
1025
|
+
return Some(i);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
short_names
|
|
508
1029
|
.iter()
|
|
509
|
-
.position(|
|
|
1030
|
+
.position(|n: &String| n.contains(&short_name))
|
|
510
1031
|
}
|
|
511
1032
|
}
|
|
512
1033
|
|
|
@@ -531,28 +1052,96 @@ impl Renderer {
|
|
|
531
1052
|
// later will never be found.
|
|
532
1053
|
let has_plugin = self.0.borrow().plugins_idx.is_some();
|
|
533
1054
|
if has_plugin {
|
|
534
|
-
let
|
|
535
|
-
let
|
|
1055
|
+
let config = self.metadata();
|
|
1056
|
+
let plugin_name = Some(config.name.clone());
|
|
1057
|
+
let is_chart = config.name.as_str() != "Datagrid";
|
|
536
1058
|
let available_plugins = self
|
|
537
|
-
.
|
|
1059
|
+
.get_all_plugin_configs()
|
|
538
1060
|
.into_iter()
|
|
539
|
-
.map(|
|
|
1061
|
+
.map(|c| c.name.clone())
|
|
540
1062
|
.collect::<Vec<_>>()
|
|
541
1063
|
.into();
|
|
1064
|
+
let plugin_config = PtrEqRc::new(self.get_plugin_config());
|
|
542
1065
|
|
|
543
1066
|
RendererProps {
|
|
544
1067
|
plugin_name,
|
|
545
|
-
|
|
1068
|
+
config,
|
|
546
1069
|
render_limits,
|
|
547
1070
|
available_plugins,
|
|
1071
|
+
is_chart,
|
|
1072
|
+
plugin_config,
|
|
548
1073
|
}
|
|
549
1074
|
} else {
|
|
550
1075
|
RendererProps {
|
|
551
1076
|
plugin_name: None,
|
|
552
|
-
|
|
1077
|
+
config: Rc::new(PluginStaticConfig::default()),
|
|
553
1078
|
render_limits,
|
|
554
1079
|
available_plugins: PtrEqRc::new(vec![]),
|
|
1080
|
+
is_chart: false,
|
|
1081
|
+
plugin_config: PtrEqRc::default(),
|
|
555
1082
|
}
|
|
556
1083
|
}
|
|
557
1084
|
}
|
|
558
1085
|
}
|
|
1086
|
+
|
|
1087
|
+
/// Drop entries from `map` whose value matches the schema-declared
|
|
1088
|
+
/// default for that key. Used by both the plugin-config and
|
|
1089
|
+
/// columns-config write paths to converge buckets to the
|
|
1090
|
+
/// "empty ⇒ reads-default" invariant. For `ControlSpec::Number`
|
|
1091
|
+
/// entries marked `include: Some(true)`,
|
|
1092
|
+
/// [`matches_declared_default`] short-circuits so the value survives
|
|
1093
|
+
/// (used when the declared default is data-dependent and unreliable —
|
|
1094
|
+
/// e.g. Datagrid's `fg_gradient`, whose default is the column's
|
|
1095
|
+
/// `abs_max`).
|
|
1096
|
+
fn strip_default_values(
|
|
1097
|
+
schema: &ColumnConfigSchema,
|
|
1098
|
+
map: &mut serde_json::Map<String, serde_json::Value>,
|
|
1099
|
+
) {
|
|
1100
|
+
map.retain(|key, value| {
|
|
1101
|
+
!schema
|
|
1102
|
+
.fields
|
|
1103
|
+
.iter()
|
|
1104
|
+
.any(|spec| matches_declared_default(spec, key, value))
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/// Does `value` for `key` match the `default` declared by `spec`?
|
|
1109
|
+
/// Composite variants (`NumberSeriesStyle`, `DatetimeFormat`, etc.) own
|
|
1110
|
+
/// nested defaults that don't have a single comparable scalar — the
|
|
1111
|
+
/// widget is responsible for emitting empty values when the user
|
|
1112
|
+
/// resets composite controls, so this helper returns `false` for them.
|
|
1113
|
+
fn matches_declared_default(spec: &ControlSpec, key: &str, value: &Value) -> bool {
|
|
1114
|
+
match spec {
|
|
1115
|
+
ControlSpec::Enum {
|
|
1116
|
+
key: k, default, ..
|
|
1117
|
+
} if k == key => value.as_str() == Some(default.as_str()),
|
|
1118
|
+
ControlSpec::Bool {
|
|
1119
|
+
key: k, default, ..
|
|
1120
|
+
} if k == key => value.as_bool() == Some(*default),
|
|
1121
|
+
ControlSpec::Number {
|
|
1122
|
+
key: k,
|
|
1123
|
+
include: Some(true),
|
|
1124
|
+
..
|
|
1125
|
+
} if k == key => false,
|
|
1126
|
+
ControlSpec::Number {
|
|
1127
|
+
key: k, default, ..
|
|
1128
|
+
} if k == key => value.as_f64() == Some(*default),
|
|
1129
|
+
ControlSpec::String {
|
|
1130
|
+
key: k, default, ..
|
|
1131
|
+
} if k == key => value.as_str() == Some(default.as_str()),
|
|
1132
|
+
ControlSpec::Color {
|
|
1133
|
+
key: k, default, ..
|
|
1134
|
+
} if k == key => value.as_str() == Some(default.as_str()),
|
|
1135
|
+
ControlSpec::ColorRange {
|
|
1136
|
+
key_pos,
|
|
1137
|
+
default_pos,
|
|
1138
|
+
..
|
|
1139
|
+
} if key_pos == key => value.as_str() == Some(default_pos.as_str()),
|
|
1140
|
+
ControlSpec::ColorRange {
|
|
1141
|
+
key_neg,
|
|
1142
|
+
default_neg,
|
|
1143
|
+
..
|
|
1144
|
+
} if key_neg == key => value.as_str() == Some(default_neg.as_str()),
|
|
1145
|
+
_ => false,
|
|
1146
|
+
}
|
|
1147
|
+
}
|